1

Context

  • I am trying to create a communication channel based on Bonjour NetService and URLSessionStreamTask to open the I/O stream.
  • I have the setup ready and I am able to connect with bonjour service and open I/O streams between mac and iOS app.
  • I am using swift codables for request and response, so communication is based on codable structs encoded as Data. I send a Request struct (by outputStream.write(data)) and wait for a response struct from mac app in input stream.

Problem

  • I am sending fetchUser request (after receiving response for loginUser), mac app receives the request and writes the response without write failing (-1) but I am not receiving the response in iOS app. (Although I received the response for previous loginUser request)
  • I am not able to figure out where exactly is the issue, Attaching the code snippets here.
  • PS: I am noob at IO streams handling so a detailed response will be helpful. May be some sources where I can understand more about the topic.

Mac App code

let service: NetService = NetService(domain: "local.", type: "_my-app._tcp.", name: "test", port: 0)
service.delegate = self
service.publish(options: .listenForConnections)

//Writes response to connected output stream
  func send(response: HalloResponse) {
    do {
      let data = try encoder.encode(response)
      print("HalloServer: Response: \(String(describing: String(data: data, encoding: .utf8)))")
      if serviceDelegate.dataStream?.outputSteam.write(data: data) == -1 {
        print("HalloServer: send(response: HalloResponse) Write failied")
      }
    } catch {
      print("HalloServer: Exception in send(request: Request)")
    }
  }
//NetServiceDelegate
func netService(_ sender: NetService, didAcceptConnectionWith inputStream: InputStream, outputStream: OutputStream) {
    print("NetServiceDelegate: service - \(sender.name) inputStream - \(inputStream) outputStream \(outputStream)")
    self.inputStream = inputStream
    self.outputSteam = outputSteam
    self.inputStream.delegate = self
    self.outputSteam.delegate = self

    self.inputStream.schedule(in: .main, forMode: .default)
    self.inputStream.schedule(in: .main, forMode: .default)

    self.inputStream.open()
    self.inputStream.open()
  }
// StreamDelegate
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    print("StreamDelegate: handle eventCode: \(eventCode.rawValue)")
    if inputStream == aStream {
      switch eventCode {
      case .hasBytesAvailable:
        var data = Data()
        guard inputStream.read(data: &data) > 0 else { return }
        print("HalloDataStream: Recieved data - \(String(describing: String(data: data, encoding: .utf8)))")
        let decoder = JSONDecoder()
        if let request = try? decoder.decode(Request.self, from: data) {
          delegate?.didReceive(request: request)
        }
        if let response = try? decoder.decode(HalloResponse.self, from: data) {
          delegate?.didReceive(response: response)
        }
      default: break
      }
    }
  }

iOS App code

serviceBrowser.searchForServices(ofType: "_my-app._tcp.", inDomain: "local.")

func connect(with service: NetService, completion: @escaping DeviceConnectionCompletion) {
    deviceCompletion = completion
    let config = URLSessionConfiguration.default
    config.requestCachePolicy = .reloadIgnoringLocalCacheData
    let session = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    streamTask = session.streamTask(with: service)
    streamTask?.resume()
    streamTask?.captureStreams()
  }

  func send(request: Request) {
    do {
      let data = try encoder.encode(request)
      print("HalloClient: Request: \(String(describing: String(data: data, encoding: .utf8)))")
      if dataStream?.outputSteam.write(data: data) == -1 {
        print("HalloClient: send(request: Request) Write failied")
      }
    } catch {
      print("HalloClient: Exception in send(request: Request)")
    }
  }

// URLSessionStreamDelegate
func urlSession(
    _ session: URLSession,
    streamTask: URLSessionStreamTask,
    didBecome inputStream: InputStream,
    outputStream: OutputStream
  ) {
    print("didBecomeInputStream:(NSInputStream *)inputStream outputStream: OutputStream")
    deviceCompletion?(true)
    self.inputStream = inputStream
    self.outputSteam = outputSteam
    self.inputStream.delegate = self
    self.outputSteam.delegate = self

    self.inputStream.schedule(in: .main, forMode: .default)
    self.inputStream.schedule(in: .main, forMode: .default)

    self.inputStream.open()
    self.inputStream.open()
  }

// StreamDelegate
   func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
     // code exactly same as mac app delegate
   }

Extensions on IO stream

extension InputStream {
  private var maxLength: Int { return 4096 }

  func read(data: inout Data) -> Int {
    var totalReadCount: Int = 0
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxLength)
    while hasBytesAvailable {
      let numberOfBytesRead = read(buffer, maxLength: maxLength)
      if numberOfBytesRead < 0, let error = streamError {
        print("Read Error: \(error)")
        break
      }
      data.append(buffer, count: numberOfBytesRead)
      totalReadCount += numberOfBytesRead
    }
    return totalReadCount
  }
}

extension OutputStream {
  @discardableResult
  func write(data: Data) -> Int {
    if streamStatus != .open {
      open()
    }
    let count = data.count
    let result = data.withUnsafeBytes {
      write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: count)
    }
    close()

    return result
  }
}

Would be really helpful if somebody can review my code and help me figure out the issue. I have a feeling that the issue is with stream open() and close(), Initially, nothing was working but adding open and close functions during write helped. Maybe I need a better way to fix this problem. PS: I had the same problem with CocoaAsyncSocket and I am not looking to use it or any other third party solution.

Jasveer Singh
  • 354
  • 3
  • 10
  • 2
    The only thing that jumps out with the limited code is the `close()` after `write`. This isn't normal when working with streams (in any language). Specifically in swift `Once a stream is closed, it cannot be reopened.`. https://developer.apple.com/documentation/foundation/stream – kendavidson Nov 20 '20 at 01:47
  • Thanks, I will try without it. One thing I also realized I am writing all data at once. I will try with an output buffer. My code for writing is in the Output stream extension I shared. – Jasveer Singh Nov 20 '20 at 02:36
  • 1
    That's something I found out, is that on your first write you just push the bear minimum (I don't know why OutputStreams have a `hasSpaceAvailable` but doesn't tell you how much. Anyhow, you should (1) add data to a buffer (2) attempt to write buffer (3) see how much was written (4) remove that much from the buffer (5) the _stream delegate will take over at this point. – kendavidson Nov 20 '20 at 03:15

0 Answers0