Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster reading of request body and headers #362

Merged
merged 1 commit into from
Jan 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Sources/HttpParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,9 @@ public class HttpParser {
return c + [(name, value)]
}
}

private func readBody(_ socket: Socket, size: Int) throws -> [UInt8] {
var body = [UInt8]()
for _ in 0..<size { body.append(try socket.read()) }
return body
return try socket.read(length: size)
}

private func readHeaders(_ socket: Socket) throws -> [String: String] {
Expand Down
64 changes: 54 additions & 10 deletions Sources/Socket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,21 +112,65 @@ open class Socket: Hashable, Equatable {
}
}

/// Read a single byte off the socket. This method is optimized for reading
/// a single byte. For reading multiple bytes, use read(length:), which will
/// pre-allocate heap space and read directly into it.
///
/// - Returns: A single byte
/// - Throws: SocketError.recvFailed if unable to read from the socket
open func read() throws -> UInt8 {
var buffer = [UInt8](repeating: 0, count: 1)
#if os(Linux)
let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), Int32(MSG_NOSIGNAL))
#else
let next = recv(self.socketFileDescriptor as Int32, &buffer, Int(buffer.count), 0)
#endif
if next <= 0 {
var byte: UInt8 = 0
let count = Darwin.read(self.socketFileDescriptor as Int32, &byte, 1)
guard count > 0 else {
throw SocketError.recvFailed(Errno.description())
}
return buffer[0]
return byte
}

/// Read up to `length` bytes from this socket
///
/// - Parameter length: The maximum bytes to read
/// - Returns: A buffer containing the bytes read
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
open func read(length: Int) throws -> [UInt8] {
var buffer = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: length)

let bytesRead = try read(into: &buffer, length: length)

let rv = [UInt8](buffer[0..<bytesRead])
buffer.deallocate()
return rv
}

static let kBufferLength = 1024

/// Read up to `length` bytes from this socket into an existing buffer
///
/// - Parameter into: The buffer to read into (must be at least length bytes in size)
/// - Parameter length: The maximum bytes to read
/// - Returns: The number of bytes read
/// - Throws: SocketError.recvFailed if unable to read bytes from the socket
func read(into buffer: inout UnsafeMutableBufferPointer<UInt8>, length: Int) throws -> Int {
var offset = 0
guard let baseAddress = buffer.baseAddress else { return 0 }

while offset < length {
// Compute next read length in bytes. The bytes read is never more than kBufferLength at once.
let readLength = offset + Socket.kBufferLength < length ? Socket.kBufferLength : length - offset

let bytesRead = Darwin.read(self.socketFileDescriptor as Int32, baseAddress + offset, readLength)
guard bytesRead > 0 else {
throw SocketError.recvFailed(Errno.description())
}

offset += bytesRead
}

return offset
}

private static let CR = UInt8(13)
private static let NL = UInt8(10)
private static let CR: UInt8 = 13
private static let NL: UInt8 = 10

public func readLine() throws -> String {
var characters: String = ""
Expand Down
84 changes: 69 additions & 15 deletions XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,46 @@
//

import XCTest
import Swifter
@testable import Swifter

class SwifterTestsHttpParser: XCTestCase {

/// A specialized Socket which creates a linked socket pair with a pipe, and
/// immediately writes in fixed data. This enables tests to static fixture
/// data into the regular Socket flow.
class TestSocket: Socket {
var content = [UInt8]()
var offset = 0

init(_ content: String) {
super.init(socketFileDescriptor: -1)
self.content.append(contentsOf: [UInt8](content.utf8))
}

override func read() throws -> UInt8 {
if offset < content.count {
let value = self.content[offset]
offset = offset + 1
return value
/// Create an array to hold the read and write sockets that pipe creates
var fds = [Int32](repeating: 0, count: 2)
fds.withUnsafeMutableBufferPointer { ptr in
let rv = pipe(ptr.baseAddress!)
guard rv >= 0 else { fatalError("Pipe error!") }
}

// Extract the read and write handles into friendly variables
let fdRead = fds[0]
let fdWrite = fds[1]

// Set non-blocking I/O on both sockets. This is required!
_ = fcntl(fdWrite, F_SETFL, O_NONBLOCK)
_ = fcntl(fdRead, F_SETFL, O_NONBLOCK)

// Push the content bytes into the write socket.
_ = content.withCString { stringPointer in
// Count will be either >=0 to indicate bytes written, or -1
// if the bytes will be written later (non-blocking).
let count = write(fdWrite, stringPointer, content.lengthOfBytes(using: .utf8) + 1)
guard count != -1 || errno == EAGAIN else { fatalError("Write error!") }
}
throw SocketError.recvFailed("")

// Close the write socket immediately. The OS will add an EOF byte
// and the read socket will remain open.
Darwin.close(fdWrite) // the super instance will close fdRead in deinit!

super.init(socketFileDescriptor: fdRead)
}
}

func testParser() {
let parser = HttpParser()

Expand Down Expand Up @@ -89,6 +106,43 @@ class SwifterTestsHttpParser: XCTestCase {
let _ = try parser.readHttpRequest(TestSocket("GET / HTTP/1.0\nContent-Length: 10\r\n\n"))
XCTAssert(false, "Parser should throw an error if request' body is too short.")
} catch { }

do { // test payload less than 1 read segmant
let contentLength = Socket.kBufferLength - 128
let bodyString = [String](repeating: "A", count: contentLength).joined(separator: "")

let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
let request = try parser.readHttpRequest(TestSocket(payload))

XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")

let unicodeBytes = bodyString.utf8.map { return $0 }
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
} catch { }

do { // test payload equal to 1 read segmant
let contentLength = Socket.kBufferLength
let bodyString = [String](repeating: "B", count: contentLength).joined(separator: "")
let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
let request = try parser.readHttpRequest(TestSocket(payload))

XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")

let unicodeBytes = bodyString.utf8.map { return $0 }
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
} catch { }

do { // test very large multi-segment payload
let contentLength = Socket.kBufferLength * 4
let bodyString = [String](repeating: "C", count: contentLength).joined(separator: "")
let payload = "GET / HTTP/1.0\nContent-Length: \(contentLength)\n\n".appending(bodyString)
let request = try parser.readHttpRequest(TestSocket(payload))

XCTAssert(bodyString.lengthOfBytes(using: .utf8) == contentLength, "Has correct request size")

let unicodeBytes = bodyString.utf8.map { return $0 }
XCTAssert(request.body == unicodeBytes, "Request body must be correct")
} catch { }

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

Expand Down