Real time data exchange using web sockets in iOS 13
Apps send and receive data all the time. Some apps mostly read data from the network, others are more focussed on sending data to servers. Depending on your needs, you might need to be able to send data to a server as quickly as possible, or maybe you need to receive new data as soon as it’s available from a server. Every app has different needs and several mechanisms exist to streamline network communication.
In this post week’s blog post, I will focus on one specific use of networking in apps. We’ll look at using web sockets to receive data as soon as it becomes available on the server. We’ll explore some of the available alternatives and how you can use web sockets with URLSession
in iOS 13.
Are you excited? I know I am! Let’s get started then.
Receiving updates from a web server
The simplest and possibly most well-known way to receive updates from a server is by making a request to the server with URLSession
. While it’s well-known, it’s also quite possibly the least efficient way to get real-time updates. The server only sends your app new data when it asks for new data. This means that your app should either go to the server many times to get data as soon as possible, which means that a lot of requests might come back without new data. Or alternatively, you might make fewer requests which means that it takes longer for the new data to reach your apps. This process of retrieving new data from a server is called polling.
Another way to retrieve new data from a server with URLSession
is by implementing silent push notifications in your app. Your server can send a push notification that includes the content-available
key in its apns
payload to trigger a special AppDelegate
method in your app. You can then request new data from the server and know that you will always receive new data. This is really efficient if your app limits the number of push notifications it sends by grouping updates together. If your server processes a lot of changing data, this might mean that your app receives lots of silent push notifications, which results in lots of requests to your server. Batching the notifications that are sent to your app will limit the number of requests made, but it will also introduce a delay between data becoming available on the server and your app knowing about it.
An alternative to polling and silent push notifications is long-polling. When you implement long-polling in an app, you make a request to a web server and leave the connection open for an extended period of time. Much longer than you normally would when making a request for new data. The server won’t respond to your application’s request until it has new data. Once new data is received, you can make a new long-polling request to receive the next bit of data.
Retrieving data through long-polling has the advantage of receiving new data as soon as it’s available. A potential downside of this approach is that your server will have to maintain many open connections. A properly configured server shouldn’t have too much trouble doing this though, so for receiving data long-polling is actually a pretty decent solution. A real downside of long polling is that you can’t send data over the active connection. This means that you’ll need to send a new request every time you want to send data from your app to the server. And again, in a rapidly changing environment, this could mean that your server will receive lots of incoming requests.
What if there was a way to use a single connection that can be kept active and is used to send and receive data at the same time? Well, you’re in luck. That’s exactly what a web socket does! Let’s see how you can use web sockets in your iOS 13+ apps using URLSession
.
Using web sockets to send and receive data
In order to understand what we’re looking at in this blog post, let’s take a quick step back and learn about web sockets from a very high level. Once you have a rough overview of how web sockets work, we’ll look at using them in an app.
Understanding web sockets
Web sockets can be considered an open connection between a client and a server. They can send each other messages, typically these messages are short to make sure they’re sent and received as quickly as possible. So while it’s possible to send a complicated JSON payload with several items over a web socket connection, it’s more likely that you’re going to send each item in your payload individually.
It might seem a little bit counter-intuitive to send and receive many smaller payloads rather than a single bigger payload. After all, when you make requests to a server in a normal setting, you want to make sure you don’t make too many requests and favor a single larger request over several small requests, depending on your exact requirements, of course.
When you’re dealing with web sockets, you don’t send requests. You’re sending messages. And since you’re already connected to the server, you don’t incur any extra overhead when you send many small messages to the server. And since your ultimate goal when using a web socket is often communication with as little latency as possible, it makes sense to send any messages you have for the server as soon as possible in a package that is as small as possible so it can be sent over the network quickly.
The same is true for server-to-app communication. A server that supports web sockets will often prefer to send your app small messages with data as soon as the data becomes available rather than batching up several messages and sending them all at once.
So when we implement web sockets in an app, we need to be prepared to:
- Send messages as soon as we can and keep them as small as possible.
- Receive many messages in rapid succession.
Based on these two points, you should be able to decide whether web sockets are a good fit for the task you’re trying to accomplish. For example, uploading a large high-resolution image is not typically a good task for a web socket. The image data can be quite large and the upload might take a while. You’re probably better off using a regular data task for this.
If your app has to sync a large amount of data all at once, you might also want to consider using a separate request to your server to make sure your socket remains available for the quick and short messaging it was designed to handle. Of course, the final decision depends on your use case and trial and error might be key to finding the optimal balance between using web sockets and using regular URL requests in your app.
Now that you know a bit more about web sockets, let’s find out how they work in an iOS application, shall we?
Using web sockets in your app
Using web sockets in your app is essentially a three-step process:
- Connecting to a web socket
- Send messages over a web socket connection
- Receiving incoming messages
Let’s go over each step individually to implement a very simple connection that will send and receive simple messages as either JSON data or a string.
Connecting to a web socket
Establishing a connection to a web socket is done similar to how you make a regular request to a server. You make use of URLSession
, you need a URL
and you create a task that you must resume. When you make a regular request, the task you need is usually a dataTask
. For web sockets, you need a webSocketTask
. It’s also important that you keep a reference to the web socket task around so you can send messages using that same task at a later moment. The following code shows a simple example of this:
var socketConnection: URLSessionWebSocketTask?
func connectToSocket() {
let url = URL(string: "ws://127.0.0.1:9001")!
socketConnection = URLSession.shared.webSocketTask(with: url)
socketConnection?.resume()
}
Note that the URL
that’s used for the socket connection is prefixed with ws://
, this is the protocol that’s used to connect to a web socket. Similar to how http://
and https://
are protocols to connect to a web server when making a regular request.
After obtaining the web socket task, it must be resumed to actually connect to the web socket server.
Next up, sending a message over the web socket connection.
Sending message over a web socket connection
Depending on your app and the kind of data you wish to send over the web socket, you might want to send strings or plain data objects. Both are available on the Message
enum that is defined on URLSessionWebSocketTask
as a case with an associated value. Let’s look at sending a string first:
func sendStringMessage() {
let message = URLSessionWebSocketTask.Message.string("Hello!")
socketConnection?.send(message) { error in
if let error = error {
// handle the error
print(error)
}
}
}
The preceding code creates a message using the string
version of URLSessionWebSocketTask.Message
. The existing socket connection is then used to send the message. The send(_:)
method on URLSessionWebSocketTask
takes a closure that is called when the message is sent. The error
is an optional argument for the closure that will be nil
if the message was sent successfully.
Let’s see how sending data over the socket is different from sending a string:
func sendDataMessage() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(anEncodableObject)
let message = URLSessionWebSocketTask.Message.data(data)
socketConnection?.send(message) { error in
if let error = error {
// handle the error
print(error)
}
}
} catch {
// handle the error
print(error)
}
}
The preceding code uses a JSONEncoder
to encode an Encodable
object into a Data
object and then it creates a message using the data
case of URLSessionWebSocketTask.Message
. The rest of the code is identical to the version you saw earlier. The message is sent using the existing socket connection and the completion closure is called once the message has been delivered or if an error occurred.
This is pretty straightforward, right? Nothing fancy has to be done to send messages. We just need to wrap our message in a URLSessionWebSocketTask.Message
and send it using an existing web socket connection.
Let’s see if receiving messages is just as nice as sending messages is.
Receiving incoming messages
Any time our server has new data that we’re interested in, we want to receive this new data over the web socket connection. To do this, you must provide the web socket task with a closure that it can call whenever incoming data is received.
The web socket connection will call your receive closure with a Result
object. The Result
’s success type is URLSessionWebSocketTask.Message
and the Failure
type is Error
.
This means that we can either get a string or data depending on the contents of the message. The following code shows how you can set up the receive closure, and how you can distinguish between the two different types of messages you can receive.
func setReceiveHandler() {
socketConnection?.receive { result in
defer { self.setReceiveHandler() }
do {
let message = try result.get()
switch message {
case let .string(string):
print(string)
case let .data(data):
print(data)
@unknown default:
print("unkown message received")
}
} catch {
// handle the error
print(error)
}
}
}
Similar to sending messages, receiving them isn’t terribly complex. Since the receive closure can receive either a string or data, we must make sure that the web socket server only sends us responses we expect. It’s important to coordinate with your server team, or yourself, that you can handle unexpected messages and that you only send strings or data in a pre-defined format.
Note the defer
statement at the start of the receive handler. It calls self.setReceiveHandler()
to reset the receive handler on the socket connection to allow it to receive the next message. Currently, the receive
handler you set on a socket connection is only called once, rather than every time a message is received. By using a defer
statement, you make sure that self.setReceiveHandler
is always called before exiting the scope of the receive handler, which makes sure that you always receive the next message from your socket connection.
If you want to use a JSONDecoder
to decode that data you’ve received from the server, you need to either attempt to decode several different types of objects until an attempt succeeds, or you need to make sure that you can only receive a single type of data over a certain web socket connection.
Personally, I would recommend to always make sure that you send a single type of message over a socket connection. This allows you to write robust code that is easy to reason about.
For example, if you’re building a chat application, you’ll want to make sure that your web socket only sends and receives instances of a ChatMessage
that conforms to Codable
. You might even want to have separate sockets for each active chat a user has. Or if you’re building a stocks application, your web sockets will probably only receive StockQuote
objects that contain a stock name, timestamp, and a current price.
If you make sure that your messages are well-defined, URLSession
will make sure that you have a convenient way of using your web socket.
In Summary
Web sockets are an amazing technology that, as you now know, allow you to communicate with a server in real-time, using a single active connection that can be used to both send and receive messages. You know that the big advantage here is that there is no overhead of making requests to a server every time you want to either send or receive data.
You also saw how you can use URLSession
’s web socket task to connect to a web socket, send messages and receive them by using very straightforward and convenient APIs. Lastly, you learned that you must pre-define the messages you want to send and receive over your web socket to make sure all components involved understand what’s going on.
I personally really like web sockets, especially because you can use them to build experiences that feel like magic. I once used web sockets to build an app that could be controlled through a companion website by sending messages over a web socket, it was really cool to see the app reflect the things I was doing on the website.
If you have any questions about this blog post, have feedback or anything else, don’t hesitate to reach out to me on Twitter!
Thanks to Kristaps Grinbergs for pointing out that receive handlers are always called once 🙌🏼.