Send TCP command and await response

Hello all,

I'm used to working with events in C# to handle responses from sent TCP commands, and I'm having a bit of difficulty switching to a more Swift way of doing things. I think I'm right in thinking that NIO is the framework to use for async network applications, but I'm having a bit of trouble getting to grips with futures and promises.

Specifically,

  • I have a method which sends a TCP command to a server.
  • Within this method, but before the command is sent, I want to send a different TCP command to the same server, which returns some data necessary for the first command.
  • While the server sends other responses, these are tagged, so I can differentiate between them on the client device.

I was hoping to use an async/await method that awaits the response of a writeAndFlush, but as far as I can see, incoming messages are handled by the channelRead function.
I've got the following function in my TCP Handler which I've added to the pipeline in my main TCP client file. Am I on the right path or is there a better way of doing things? How would I call this function from the main app?

public func sendWithResponse(context: ChannelHandlerContext, data: NIOAny) -> EventLoopFuture<String> {
        var buffer = context.channel.allocator.buffer(capacity: data.utf8.count)
        buffer.write(data)
        let responsePromise = channel.eventLoop.makePromise(of: String.self)
        context.writeAndFlush(wrapOutboundOut(buffer, responsePromise), promise: nil)
        return responsePromise.futureResult
    }

If you want to solve the request/response problem in SwiftNIO's pipeline, then I would recommend starting with a ChannelHandler that keeps track of the requests sent and the responses received. swift-nio-extras actually ships a RequestResponseHandler which implements one variant of this.

RequestResponseHandler however assumes that the responses arrive in the same order as the requests that were sent (much like in HTTP/1.x). From how you describe your question it seems that in your case the responses can come in arbitrary order but carry some kind of tag that tells you which response if for which request. If that's the case, you could make a new handler which is like RequestResponseHandler but instead of storing the response promises in a var promiseBuffer: CircularBuffer<EventLoopPromise<Response>> it would store them in a dictionary, say var promiseBuffer: [RequestTag: EventLoopPromise<Response>]. This would also make a really great addition to swift-nio-extras and the tag could probably use the Identifiable protocol.

Once that problem is solved you need to make a nice API for your network protocol which today means Swift Concurrency.

At this point, SwiftNIO doesn't ship a ton of nice utilities for that yet but it really should I think (CC @fabianfett / @lukasa / @georgebarnett / @dnadoba WDYT?). For now, you could get inspiration from SwiftNIO's NIOAsyncAwaitDemo example, specifically AsyncChannelIO which creates a simple async/await API for a HTTP pipeline using RequestResponseHandler.

Thank you for the reply and the useful information.
I had actually come across the RequestResponseHandler but like you suggest, I figured that it probably wouldn't be quite right for this particular use case.

I'm usually very happy to go through documentation, but this time I didn't really know where to start looking, so thank you for the leads. I'll have a proper read through the swift-nio-extras and NIOAsyncAwaitDemo code and the documentation for the Identifiable protocol and see what I can do.

I'm moving a few device control apps over to iPad from Windows, and with Microsoft taking so long to get MAUI out, Swift seems to be the best option. It would definitely be nice to have more in the way of utilities in SwiftNIO! Thank you once again.

Yes, we know we need nice abstractions here. We're working on it, but we're swamped with a bunch of other requirements right now.

1 Like

Hi again,

While it needs a lot of cleanup, I've got a lot of it working and just had some follow-up questions.

Firstly, I'm wondering about the best way of opening a persistent TCP connection so I can call writeAndFlush on the same channel from separate functions. I tried a number of things, but if I terminate the connection at the server end and then try to reconnect, it leads to a Fatal error: leaking promise.
I think I've answered this one for myself. See edit below.

This leads me to my second question, which is how best to use the fireChannelInactive method to trigger a reconnection automatically? I think I found one solution on Stack Overflow written by you @lukasa, but I couldn't quite get it working.

Finally, how does one go about accessing an instance of a client from other views? Can it be done the same way as passing instances of other classes with EnvironmentObjects?

It's a lot to answer, but if you could help me with even one question, I'd be extremely grateful.

Edit: Looking through the Netty documentation, I found that a channel can't be reopened once Channel.getCloseFuture() is called. I think this answers my first question, and that instead of trying to reuse the original channel, a new channel should be created.

It looks like I can't edit the original post any more, but I've got it all working smoothly. If anyone came looking for answers as to how to do this, please feel free to message me.

1 Like

FWIW, the suggested RequestResponseHandler that can keep track of out-of-order responses (by the virtue of a requestID) is now available in swift-nio-extras 1.19.0 as RequestResponseWithIDHandler: