Cole
1
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
lukasa
(Cory Benfield)
2
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.
Cole
3
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.
lukasa
(Cory Benfield)
4
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.
Cole
5
@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")] }
lukasa
(Cory Benfield)
6
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.
Cole
7
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?
Cole
8
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.
``
lukasa
(Cory Benfield)
9
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.
Cole
10
No the defer that is inside of public func connect() throws {} is what is still causing the disconnect
defer {
niotsHandler.disconnect()
}
lukasa
(Cory Benfield)
11
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.
Cole
12
Alright thank you Cory. Much appreciated. I have 2 Questions,
-
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
-
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?
lukasa
(Cory Benfield)
13
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