0

I'm trying to make a client application in Swift and I've chosen to follow this tutorial as an inspiration: https://www.raywenderlich.com/157128/real-time-communication-streams-tutorial-ios

I send binary data to the output stream and my server gets the request successfully and answers back. Unfortunately, the Swift client application's StreamDelegate is never triggered.

I've developed in the Playgrounds a simple example of a HTTP request similar to what my application is doing and the same problem occurs:

import UIKit

class WebServerConnection: NSObject, StreamDelegate {
    let host: String
    var inputStream: InputStream!
    var outputStream: OutputStream!
    var responseHandler:((String) -> Void)?

    init(_ host:String) {
        self.host = host
    }

    func setup(responseHandler:((String) -> Void)?) {
        self.responseHandler = responseHandler
        var readStream: Unmanaged<CFReadStream>?
        var writeStream: Unmanaged<CFWriteStream>?
        CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, host as CFString, 80, &readStream, &writeStream)
        inputStream = readStream!.takeRetainedValue()
        outputStream = writeStream!.takeRetainedValue()
        inputStream.delegate = self
        inputStream.schedule(in: .current, forMode: .commonModes)
        outputStream.schedule(in: .current, forMode: .commonModes)
        inputStream.open()
        outputStream.open()
    }

    func query(_ path:String = "/") {
        let data = "GET \(path) HTTP/1.1\r\nhost: \(host)\r\n\r\n".data(using: .utf8)!
        _ = data.withUnsafeBytes { outputStream.write($0, maxLength: data.count) }
        print("sent!")
    }

    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        switch eventCode {
        case Stream.Event.hasBytesAvailable:
            printAvailableBytes(stream: aStream as! InputStream)
        case Stream.Event.endEncountered:
            print("<End Encountered>")
        case Stream.Event.errorOccurred:
            print("<Error occurred>")
        default:
            print("<Some other event>")
        }
    }

    func printAvailableBytes(stream: InputStream) {
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 4096)
        while stream.hasBytesAvailable {
            let numberOfBytesRead = inputStream.read(buffer, maxLength: 4096)
            if numberOfBytesRead < 0 {
                if stream.streamError != nil {
                    break
                }
            }

            if let message = String(bytesNoCopy: buffer, length: numberOfBytesRead, encoding: .utf8, freeWhenDone: true) {
                responseHandler?(message)
            }
        }
    }

    func close() {
        inputStream.close()
        outputStream.close()
    }
}

let google = WebServerConnection("google.com")
google.setup() { message in
    print(message)
    google.close()
}
google.query()

I get the "sent!" message in the console, but nothing else! I thought with the input.delegate = self, the stream function would be called with the response. What am I missing?

Béatrice Cassistat
  • 1,048
  • 12
  • 37

1 Answers1

1

As @martin-r figured out, I was missing:

import UIKit
import PlaygroundSupport

...

google.query()

PlaygroundPage.current.needsIndefiniteExecution = true

Edit: If somebody is having the same issue as me (StreamDelegate never triggered), maybe it has something to do with your RunLoop. In the raywenderlich.com example and in the example I've made in the playground, the function to setup network connections was called from the main thread, while in my case, it was not called from another thread without a run loop. I replaced:

inputStream.schedule(in: .current, forMode: .commonModes)
outputStream.schedule(in: .current, forMode: .commonModes)

with:

inputStream.schedule(in: .main, forMode: .commonModes)
outputStream.schedule(in: .main, forMode: .commonModes)

And it started to work seamlessly.

Béatrice Cassistat
  • 1,048
  • 12
  • 37
  • By default thread's associated run loop only runs if someone `runs` it. If you create a thread, but don't run an associated run loop, it won't run. Use `RunLoop.current.run()` to start it – Borzh May 16 '22 at 21:58