Universal Bootstrap NIOTS with WebSockets

I am developing a WebSocket swift package that is written on top of NIOTS. I am running into a problem where the client connects to the server, but then immediately disconnects in the defer block. I am not sure what is going on, but basically the application is as follows.

```
public func connect() throws {
    var group: EventLoopGroup? = nil
    #if canImport(Network)
    if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
        group = NIOTSEventLoopGroup()
    } else {
        print("Sorry, your OS is too old for Network.framework.")
        exit(0)
    }
    #else
    group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    print("Sorry, no Network.framework on your OS.")
    #endif
    
    defer {
        try? group?.syncShutdownGracefully()
    }
    
    //Map the Event Loop Group to our Provider
    let provider: EventLoopGroupManager.Provider = group.map { .shared($0)} ?? .createNew
    guard let upgradePromise = group?.next().makePromise(of: Void.self) else {
        throw MyErrors.optionalELGError("Our Event Loop Group was nil")
    }
    guard let url = URL(string: self.url) else { return }
    
    //Initialize our NIOTSHandler
    let niotsHandler = NIOTSHandler(url: url, tls: tls, group: group, groupProvider: provider, upgradePromise: upgradePromise)
    
            //We get shutdown here as soon as we are connected
    defer {
        niotsHandler.disconnect()
    }
    _ = try niotsHandler.connect()
}
```

Inside of niotsHandler we grab our clientBootstrap that contains a WebSocketHandler that handles our web socket with the HTTPInitialRequestHandler, NIOWebSocketClientUpgrader, and NIOHTTPClientUpgradeConfiguration and start the connection

    func connect() throws -> EventLoopFuture<Void> {
        let connection = try clientBootstrap()
            .connect(host: host, port: port)
            .map { channel -> () in
                self.channel = channel
            }
        connection.cascadeFailure(to: upgradePromise)
        return connection.flatMap { channel in
            return self.upgradePromise.futureResult
        }
    }
    

I call the code like so

var myniows = MyNIOWS(url: "ws://echo.websocket.org", tls: false)
 _ = try? myniows.connect()

So My question is why would the connection close as soon as I open it? Is my code faulty? Any help and suggestions is appreciated

Thanks

I recommend zooming in on the end of your code block, which you wrote as:

    //Initialize our NIOTSHandler
    let niotsHandler = NIOTSHandler(url: url, tls: tls, group: group, groupProvider: provider, upgradePromise: upgradePromise)
    
            //We get shutdown here as soon as we are connected
    defer {
        niotsHandler.disconnect()
    }
    _ = try niotsHandler.connect()

Consider the flow here. At step 1, we initialise our NIOTSHandler. Step 2 we call connect, which returns a future that you throw away. Immediately after that returns, you call niotsHandler.disconnect() in your defer block.

You need to wait for your connection attempt to succeed and then let the connection actually run, rather than just calling disconnect. NIO is asynchronous, so the call to connect does not block.

Thank you Luksa, So if I call wait() on _ = try niotsHandler.connect() and it still disconnects after waiting then would that indicate that the future failed in my connect() throws -> EvenLoopFuture<Void> {} ?

I have written code similar to this before, but I am not sure if introducing promises has thrown me off somewhere.

No, if you wait and the future failed then your wait will throw. defer is going to execute immediately after the function ends. What you need is something you can wait on until you want to disconnect.

@lukasa Ok that makes sense. So I am connecting to the Handler however I still get disconnected right away however now the upgrade is not failing after the disconnect which is progress, but still I need to keep the connection open and then upgrade, If I understand what needs to happen correctly.

I wait like this

let connect = try niotsHandler.connect()
try connect.wait()

So is it correct to assume that it is because the connection completes and finishes before the WebSocket Handler's promise actually succeeds or fails?

Here are the logs for when I connect

Connect
2021-07-24 14:38:26.946535-0500 NetworkTesting[64517:979347] [connection] nw_endpoint_handler_set_adaptive_read_handler [C1.1 174.129.224.73:80 ready socket-flow (satisfied (Path is satisfied), viable, interface: en0, ipv4, ipv6, dns)] unregister notification for read_timeout failed
2021-07-24 14:38:26.946582-0500 NetworkTesting[64517:979347] [connection] nw_endpoint_handler_set_adaptive_write_handler [C1.1 174.129.224.73:80 ready socket-flow (satisfied (Path is satisfied), viable, interface: en0, ipv4, ipv6, dns)] unregister notification for write_timeout failed
Upgraders Completion Handler______ NIO.ChannelHandlerContext
HTTP handler removed.
Disconnected from the server
Channel on upgrade______ NIOTransportServices.NIOTSConnectionChannel
Request Headers on upgrade_____ HTTPResponseHead { version: HTTP/1.1, status: switchingProtocols, headers: [("Connection", "Upgrade"), ("Date", "Sat, 24 Jul 2021 19:38:23 GMT"), ("Sec-WebSocket-Accept", "3R5vTtCdj1BU3vPdKnkx5HZVOwU="), ("Server", "Kaazing Gateway"), ("Upgrade", "websocket")] }

If the connect.wait call completes it means the TCP connection is up. I don't know what's going on in your web socket handler so I can't easily diagnose from your logs what's happening.

Okay thank you. So my basic flow is initialize NIOTSHandler. NIOTSHandler contains this method. On the NIOHTTPClientUpgradeConfiguration we succeed the promise and then remove the HTTPHandler, at this point the client disconnects from the server and then the WebSocket is added.


func makeWebSocket(channel: NIO.Channel) -> EventLoopFuture<Void> {
let httpHandler = HTTPInitialRequestHandler(host: host host, path: path, headers: headers, upgradePromise: upgradePromise)

       let websocketUpgrader = NIOWebSocketClientUpgrader(requestKey: someRequestKey,
                                                          automaticErrorHandling: true,
                                                          upgradePipelineHandler: { channel, req in
                                                           return channel.pipeline.addHandler(WebSocket(channel: channel, type: .client))
                                                          })
                                                          })
       let config: NIOHTTPClientUpgradeConfiguration = (
           upgraders: [ websocketUpgrader ],
           completionHandler: { _ in
               self.upgradePromise.succeed(())
//Disconnects after httpHandler is removed
               channel.pipeline.removeHandler(httpHandler, promise: nil)
           })
       
       return channel.pipeline.addHTTPClientHandlers(
           leftOverBytesStrategy: .forwardBytes,
           withClientUpgrade: config
       ).flatMap {
           channel.pipeline.addHandler(httpHandler)
       }
}
}

I then add it to the bootstrap

private func clientBootstrap() throws -> NIOClientTCPBootstrap {
        let bootstrap: NIOClientTCPBootstrap
        
        if scheme != "wss" {
            bootstrap = try groupManager.makeBootstrap(hostname: host, useTLS: false)
        } else {
            bootstrap = try groupManager.makeBootstrap(hostname: host, useTLS: true)
        }
       return bootstrap
        .connectTimeout(.hours(1))
        .channelOption(ChannelOptions.socket(SocketOptionLevel(IPPROTO_TCP), TCP_NODELAY), value: 1)
        .channelInitializer { channel in
            self.makeWebSocket(channel: channel)
        }
    }

from there I create the connection method and call it as I previously mentioned

    internal func connect() throws -> EventLoopFuture<Void> {
        let connection = try clientBootstrap()
            .connect(host: host, port: port)
            .map { channel -> () in
                self.channel = channel
            }
        connection.cascadeFailure(to: upgradePromise)
        return connection.flatMap { channel in
            return self.upgradePromise.futureResult
        }
    }

So basically the completion block looks like it is finishing before the WebSocket is initialized. Do I need to wait for the WebSocket to finish initializing some how?

So I removed the upgradePromise.succeed(()) from the configuration where we were disconnecting and I mapped it after the WebSocket Init() like so

 return channel.pipeline.addHandler(WebSocket(channel: channel, type: .client)).map {
                                self.upgradePromise.succeed(())
         }

and the disconnect is now occurring after the WebSocket has been initialized how

Connect
2021-07-24 14:38:26.946535-0500 NetworkTesting[64517:979347] [connection] nw_endpoint_handler_set_adaptive_read_handler [C1.1 174.129.224.73:80 ready socket-flow (satisfied (Path is satisfied), viable, interface: en0, ipv4, ipv6, dns)] unregister notification for read_timeout failed
2021-07-24 14:38:26.946582-0500 NetworkTesting[64517:979347] [connection] nw_endpoint_handler_set_adaptive_write_handler [C1.1 174.129.224.73:80 ready socket-flow (satisfied (Path is satisfied), viable, interface: en0, ipv4, ipv6, dns)] unregister notification for write_timeout failed
Upgraders Completion Handler______ NIO.ChannelHandlerContext
HTTP handler removed.
Channel on upgrade______ NIOTransportServices.NIOTSConnectionChannel
Request Headers on upgrade_____ HTTPResponseHead { version: HTTP/1.1, status: switchingProtocols, headers: [("Connection", "Upgrade"), ("Date", "Sat, 24 Jul 2021 19:38:23 GMT"), ("Sec-WebSocket-Accept", "3R5vTtCdj1BU3vPdKnkx5HZVOwU="), ("Server", "Kaazing Gateway"), ("Upgrade", "websocket")] }
Disconnected from the server

However why would the Client disconnect from the server without me telling it to?

When I run my unit test It says

Restarting after unexpected exit, crash, or test timeout in MyNIOWSTests.testMYNIOWSConnection(); summary will include totals from previous launches.

``

Are you calling close at any point? The client will not auto-disconnect: it will either be hitting an error or you will be closing it.

No the defer that is inside of public func connect() throws {} is what is still causing the disconnect

        defer {
            niotsHandler.disconnect()
        }

Right, so the core issue here is that you're immediately disconnecting. You need something else to wait for to know your work is done.

Alright thank you Cory. Much appreciated. I have 2 Questions,

  1. My problem was that I needed to call try self.channel?.closeFuture.wait() after I connect, so why do we need to call this on WebSockets and not on pure TCP connections?

    and

  2. In my app that I am using the WebSocket Client in I needed to run it on the background thread i.e. DispatchQueue.global(qos: .background).async {}, Is there a recommended nio way to let the WebSocket Library do that instead of the library consumer?

You don't need to call this for web sockets only, if you want to wait for a pure TCP connection to exit you have to do the same thing.

You can dispatch yourself to a background thread, NIO doesn't have specific recommendations. Note that NIO owns its own threads and never runs them on the main thread.

1 Like