NWWebSocket: a flexible, native WebSocket client for Swift developers
I recently blogged about how I wrote a native WebSocket client to improve our Swift Channels SDK. The discovery and implementation phases for the WebSocket client actually took a while longer than I originally anticipated, in the most part due to the sparse documentation for the APIs in use. So I decided it would be useful to factor this code out for fellow Swift developers that are using WebSockets in their own applications.
You can read the full story below, but I can announce that this client is now available as its own repo called NWWebSocket, and supports installation as a dependency using Cocoapods, Swift Package Manager or Carthage. It is currently still in beta and as of today the latest release is v0.4.0.
A feature-complete WebSocket client for iOS, macOS & tvOS
There are several options available in the iOS, macOS and tvOS ecosystem for WebSocket clients. However, I decided against using any of the existing solutions. My justifications for writing my own Swift WebSocket client were:
The WebSocket protocol is fairly concise, and therefore any client implementation consists of a simple API.
iOS, macOS and tvOS now have native support for the WebSocket protocol via the Network framework.
N.B: The higher-level API URLSessionWebSocketTask could have been used if custom closure support wasn’t necessary.
Existing WebSocket client libraries for iOS, macOS and tvOS tend to suffer from at least one of:
No longer being actively maintained.
One or more third-party dependencies.
Verbose implementations due to using low-level APIs such as CFNetwork.
The Network framework from Apple is somewhere in the middle of their networking API stack for iOS, macOS and tvOS. The high-level URLSession API is the bread and butter for developers looking to bring networking features to their apps. There are several tutorials on how to useURLSessionWebSocketTask to write a concise, native WebSocket client in Swift or Objective-C. Given the simplicity and conciseness of that API, for most iOS, macOS or tvOS WebSocket applications writing your own client using this API would be my recommendation.
The complication at Pusher is that for Channels, we define several custom closure codes describing reasons why a connection could have been closed by our servers. The single omission from the URLSessionWebSocketTask API is the fact that it does not support custom closure codes. Take a look at the following URLSessionWebSocketDelegate method:
The URLSessionWebSocketTask.CloseCode enum only defines the standard closure codes. There is no way with this API to have a client close a connection with a non-standard closure code, nor to expose a custom code when a server closes a connection.
The Network framework specifies the NWProtocolWebSocket.CloseCode enum, which has the following cases:
This means that a WebSocket client can inspect any valid closure code it receives from a server (or indeed close a connection itself with any valid closure code). This is ideal for our purposes at Pusher, and is of course also applicable to any other iOS, macOS or tvOS developers that are working on WebSocket applications that use closure codes outside of the standard protocol range.
A flexible, native WebSocket client for Swift developers
The NWWebSocket library is about as concise as a Swift WebSocket client library can get. It is 100% native to the iOS, macOS and tvOS platforms, and uses no third-party dependencies. It can be configured for a variety of applications that may want to specify custom options for the WebSocket connection. It can also be configured to execute on any queue.
WebSocket connectivity is handled by the Network framework itself, but the client implementation is covered by unit tests which are run as part of a Continuous Integration pipeline for Pull Requests and merges to the master branch. This is handled by GitHub Actions.
If you are a Swift or Objective-C developer looking for a concise but fully-featured WebSocket client with zero third-party dependencies, NWWebSocket should meet your needs.
Unit testing considerations
Before moving NWWebSocket to its own repository, the unit tests were configured to use a free-to-use hosted echo server. This was a pragmatic decision in order to allow the client to be shipped as soon as possible as part of the recent v9.0.0 release of the Swift Channels SDK.
The obvious disadvantage to this approach is that those unit tests in the CI pipeline become dependent on network conditions and externally-hosted services to pass successfully. The test run time is also slightly increased due to network latency. Moving NWWebSocket to its own repository mitigated some of these concerns, since any spurious failures would not impact the Swift Channels SDK CI pipeline. However the underlying issues remained wherever the tests are run from.
The solution settled on was to replace the dependency on a hosted echo server with a localhost WebSocket echo server. Using this set up, the unit tests for the client no longer depended on any externally-hosted services. Also, latency was reduced to essentially zero since the server runs locally on the CI runner.
This localhost server is currently a private class, and therefore it is not exposed as part of the public API of the library. In the future, this may change if there is a use case for it to be made available as part of the public API.
Going forward NWWebSocket is going to be maintained by myself and the rest of the Pusher Engineering team, however we of course welcome any feature requests or bug reports. If you’re interesting in contributing, please open a Pull Request or an issue on the repo. It is in a largely feature complete state, but still in beta and the current release is v0.4.0. If you want to use NWWebSocket in your own app, you can install it as a dependency using Cocoapods, Swift Package Manager, Carthage or by directly adding the source files to your app project.