Kaynağa Gözat

Merge pull request #362 from httpswift/faster-request-reading

Faster reading of request body and headers
Victor Sigler 7 yıl önce
ebeveyn
işleme
56a73562d5

+ 2 - 4
Sources/HttpParser.swift

@@ -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] {

+ 54 - 10
Sources/Socket.swift

@@ -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 = ""

+ 69 - 15
XCode/SwifterTestsCommon/SwifterTestsHttpParser.swift

@@ -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()
         
@@ -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"))