Socket communication best practices and examples?

I'm currently implementing a library that I'd like to use to communicate with a UNIX domain socket (/var/run/usbmuxd) - I had thought that NIO might be a good fit here, as it seems to wrap the required functionality that I'd need to spin up a socket connection and leave it alive for continued notifications and communication from that socket.

I can't find any great examples of how I would do this (or I'm misunderstanding the examples), and the docs assume a level of understanding that I don't yet have. I've had a look at things like the NIOChatClient, and that's great for sending a single request/response, but I can't work out how to keep the client running for continued communication.

Is this an appropriate use of NIO, or am I bending it into a shape it doesn't fit to? Can anyone offer advice on where I can look for more examples or info?

This should absolutely work. However, it is probable that there isn’t a trivial example to use here. With that said, we can glue together a few.

Let’s start with NIOChatClient. The core infrastructure required to get a connection to a Unix socket up and running, removing all extraneous detail, looks like this:

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let bootstrap = ClientBootstrap(group: group)
let channel = try bootstrap.connect(unixDomainSocketPath: path).wait()

This will give you a Channel that encapsulates a connection on that unix domain socket. There is no lifetime management of this Channel the connection will stay up until either you close or, or the server closes it.

Next, you’ll want at least one ChannelHandler. The ChannelHandler will be where your logic lives: it’s where you send and receive messages. You said that this example is great for a single request/response, but I don’t know know why you said that: the NIOChatClient connection is long lived. What difficulty are you having here?

I just want to make sure that we’re all on the same page with regards the supported status of /var/run/usbmuxd, namely that it’s not a supported API. Feel free to play around but please do not ship a product that relies on it.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[…] namely that it’s not a supported API […]

Thanks, Quinn. Yeah, I know… I currently use MobileDevice.framework for the same purpose. I'm honestly not sure what's worse, but I don't see a reasonable, shipping alternative to achieve what we need for establishing a link to iDevices over a USB connection.

You said that this example is great for a single request/response, but I don’t know know why you said that: the NIOChatClient connection is long lived. What difficulty are you having here?

As I said earlier, my knowledge here is not great — I assumed based on the calls to close() within the handler that the whole channel gets closed up (and that's what I'm seeing from my own channel/handler combo when I try to run it all under XCTest). I'm probably very much missing how a bunch of things in these APIs interact and work together.

The only call to close in the handler is here:

    public func errorCaught(context: ChannelHandlerContext, error: Error) {
        print("error: ", error)

        // As we are not really interested getting notified on success or failure we just pass nil as promise to
        // reduce allocations.
        context.close(promise: nil)
    }

This is a standard NIO pattern, to have a single ChannelHandler be responsible for dealing with unhandled errors by closing the Channel. You're right that close does close the entire Channel, but unless you're hitting errors you wouldn't expect to see that behaviour.

To begin with debugging this I'd propose using the following very simple ChannelHandler:

final class LoggingHandler: ChannelInboundHandler {
    typealias InboundIn = Any

    func channelActive(context: ChannelHandlerContext) {
        print("Activated")
    }

    func channelInactive(context: ChannelHandlerContext) {
        print("Deactivated")
    }
}

This handler will do nothing but print when the connection becomes active, and when it gets closed. Based on which of those prints get hit you can work out how far we're getting.

1 Like