Skip to content

Commit 56a7356

Browse files
authored
Merge pull request #362 from httpswift/faster-request-reading
Faster reading of request body and headers
2 parents 0026096 + b4bf689 commit 56a7356

3 files changed

Lines changed: 125 additions & 29 deletions

File tree

Sources/HttpParser.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ public class HttpParser {
6464
return c + [(name, value)]
6565
}
6666
}
67-
67+
6868
private func readBody(_ socket: Socket, size: Int) throws -> [UInt8] {
69-
var body = [UInt8]()
70-
for _ in 0..<size { body.append(try socket.read()) }
71-
return body
69+
return try socket.read(length: size)
7270
}
7371

7472
private func readHeaders(_ socket: Socket) throws -> [String: String] {

Sources/Socket.swift

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,21 +112,65 @@ open class Socket: Hashable, Equatable {
112112
}
113113
}
114114

115+
/// Read a single byte off the socket. This method is optimized for reading
116+
/// a single byte. For reading multiple bytes, use read(length:), which will
117+
/// pre-allocate heap space and read directly into it.
118+
///
119+
/// - Returns: A single byte
120+
/// - Throws: SocketError.recvFailed if unable to read from the socket
115121
open func read() throws -> UInt8 {
116-
var buffer = [UInt8](repeating: 0, count: 1)
117-
#if os(Linux)
118-
let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), Int32(MSG_NOSIGNAL))
119-
#else
120-
let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), 0)
121-
#endif
122-
if next <= 0 {
122+
var byte: UInt8 = 0
123+
let count = Darwin.read(self.socketFileDescriptor as Int32, &byte, 1)
124+
guard count > 0 else {
123125
throw SocketError.recvFailed(Errno.description())
124126
}
125-
return buffer[0]
127+
return byte
128+
}
129+
130+
/// Read up to `length` bytes from this socket
131+
///
132+
/// - Parameter length: The maximum bytes to read
133+
/// - Returns: A buffer containing the bytes read
134+
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
135+
open func read(length: Int) throws -> [UInt8] {
136+
var buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: length)
137+
138+
let bytesRead = try read(into: &buffer, length: length)
139+
140+
let rv = [UInt8](buffer[0..<bytesRead])
141+
buffer.deallocate()
142+
return rv
143+
}
144+
145+
static let kBufferLength = 1024
146+
147+
/// Read up to `length` bytes from this socket into an existing buffer
148+
///
149+
/// - Parameter into: The buffer to read into (must be at least length bytes in size)
150+
/// - Parameter length: The maximum bytes to read
151+
/// - Returns: The number of bytes read
152+
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
153+
func read(into buffer: inout UnsafeMutableBufferPointer<UInt8>, length: Int) throws -> Int {
154+
var offset = 0
155+
guard let baseAddress = buffer.baseAddress else { return 0 }
156+
157+
while offset < length {
158+
// Compute next read length in bytes. The bytes read is never more than kBufferLength at once.
159+
let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset
160+
161+
let bytesRead = Darwin.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength)
162+
guard bytesRead > 0 else {
163+
throw SocketError.recvFailed(Errno.description())
164+
}
165+
166+
offset += bytesRead
167+
}
168+
169+
return offset
126170
}
127171

128-
private static let CR = UInt8(13)
129-
private static let NL = UInt8(10)
172+
private static let CR: UInt8 = 13
173+
private static let NL: UInt8 = 10
130174

131175
public func readLine() throws -> String {
132176
var characters: String = ""

XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,46 @@
66
//
77

88
import XCTest
9-
import Swifter
9+
@testable import Swifter
1010

1111
class SwifterTestsHttpParser: XCTestCase {
1212

13+
/// A specialized Socket which creates a linked socket pair with a pipe, and
14+
/// immediately writes in fixed data. This enables tests to static fixture
15+
/// data into the regular Socket flow.
1316
class TestSocket: Socket {
14-
var content = [UInt8]()
15-
var offset = 0
16-
1717
init(_ content: String) {
18-
super.init(socketFileDescriptor: -1)
19-
self.content.append(contentsOf: [UInt8](content.utf8))
20-
}
21-
22-
override func read() throws -> UInt8 {
23-
if offset < content.count {
24-
let value = self.content[offset]
25-
offset = offset + 1
26-
return value
18+
/// Create an array to hold the read and write sockets that pipe creates
19+
var fds = [Int32](repeating: 0, count: 2)
20+
fds.withUnsafeMutableBufferPointer { ptr in
21+
let rv = pipe(ptr.baseAddress!)
22+
guard rv >= 0 else { fatalError("Pipe error!") }
23+
}
24+
25+
// Extract the read and write handles into friendly variables
26+
let fdRead = fds[0]
27+
let fdWrite = fds[1]
28+
29+
// Set non-blocking I/O on both sockets. This is required!
30+
_ = fcntl(fdWrite, F_SETFL, O_NONBLOCK)
31+
_ = fcntl(fdRead, F_SETFL, O_NONBLOCK)
32+
33+
// Push the content bytes into the write socket.
34+
_ = content.withCString { stringPointer in
35+
// Count will be either >=0 to indicate bytes written, or -1
36+
// if the bytes will be written later (non-blocking).
37+
let count = write(fdWrite, stringPointer, content.lengthOfBytes(using: .utf8) + 1)
38+
guard count != -1 || errno == EAGAIN else { fatalError("Write error!") }
2739
}
28-
throw SocketError.recvFailed("")
40+
41+
// Close the write socket immediately. The OS will add an EOF byte
42+
// and the read socket will remain open.
43+
Darwin.close(fdWrite) // the super instance will close fdRead in deinit!
44+
45+
super.init(socketFileDescriptor: fdRead)
2946
}
3047
}
31-
48+
3249
func testParser() {
3350
let parser = HttpParser()
3451

@@ -89,6 +106,43 @@ class SwifterTestsHttpParser: XCTestCase {
89106
let _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 10\r\n\n"))
90107
XCTAssert(false, "Parser should throw an error if request' body is too short.")
91108
} catch { }
109+
110+
do { // test payload less than 1 read segmant
111+
let contentLength = Socket.kBufferLength - 128
112+
let bodyString = [String](repeating: "A", count: contentLength).joined(separator: "")
113+
114+
let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
115+
let request = try parser.readHttpRequest(TestSocket(payload))
116+
117+
XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
118+
119+
let unicodeBytes = bodyString.utf8.map { return $0 }
120+
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
121+
} catch { }
122+
123+
do { // test payload equal to 1 read segmant
124+
let contentLength = Socket.kBufferLength
125+
let bodyString = [String](repeating: "B", count: contentLength).joined(separator: "")
126+
let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
127+
let request = try parser.readHttpRequest(TestSocket(payload))
128+
129+
XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
130+
131+
let unicodeBytes = bodyString.utf8.map { return $0 }
132+
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
133+
} catch { }
134+
135+
do { // test very large multi-segment payload
136+
let contentLength = Socket.kBufferLength * 4
137+
let bodyString = [String](repeating: "C", count: contentLength).joined(separator: "")
138+
let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
139+
let request = try parser.readHttpRequest(TestSocket(payload))
140+
141+
XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")
142+
143+
let unicodeBytes = bodyString.utf8.map { return $0 }
144+
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
145+
} catch { }
92146

93147
var r = try? parser.readHttpRequest(TestSocket("GET /open?link=https://www.youtube.com/watch?v=D2cUBG4PnOA HTTP/1.0\nContent-Length: 10\n\n1234567890"))
94148

0 commit comments

Comments
 (0)