Printed on: January 24, 2023
In iOS 13, we gained the power to simply ship and obtain knowledge utilizing internet sockets by URLSession
. With async/await, we gained the power to fetch knowledge from servers utilizing the await
key phrase and we will iterate over asynchronous sequences utilizing async for
loops.
We will even learn knowledge from a URL
one line at a time by calling the strains
property on URL
:
let url = URL(string: "https://donnywals.com")!
for strive await line in url.strains {
// use line
}
Whereas that is actually cool and permits us to construct apps that ingest knowledge in actual time if the server helps streaming our bodies, we can not use the strains
property to arrange an internet socket connection and pay attention for incoming messages and probably ship messages over the identical connection too.
On this put up, you’ll be taught all the things it is advisable to find out about constructing your personal mechanism to conveniently iterate over messages from an internet socket asynchronously. We are going to leverage some present performance from URLSessionWebSocketTask
and AsyncThrowingStream
to construct our personal AsyncSequence
that conveniently wraps our URLSessionWebSocketTask
.
Be aware that the ensuing code has solely had comparatively restricted testing executed so I can not assure that the supplied answer might be 100% appropriate for all the things you throw at it. When you discover any points with the ultimate code, be at liberty to contact me. Bonus factors for those who’re in a position to present some concepts for a possible repair.
Utilizing an internet socket with out async / await
Earlier than we get began, let’s rapidly overview the best way to use an internet socket with out async/await. The code particulars are outlined in this put up. Make sure you learn it if you wish to be taught extra about utilizing internet sockets in your apps.
let url = URL(string: "ws://127.0.0.1:8080")!
let socketConnection = URLSession.shared.webSocketTask(with: url)
socketConnection.resume()
func setReceiveHandler() {
socketConnection.obtain { lead to
defer { self.setReceiveHandler() }
do {
let message = strive end result.get()
change message {
case let .string(string):
print(string)
case let .knowledge(knowledge):
print(knowledge)
@unknown default:
print("unkown message acquired")
}
} catch {
// deal with the error
print(error)
}
}
}
setReceiveHandler()
Discover how, to obtain messages from the socket, I need to name obtain
with a completion handler. This technique solely permits me to obtain a single incoming message, so I need to re-set my handler after receiving a message to robotically start listening for the subsequent message.
This can be a nice instance of a state of affairs the place an async for loop similar to for strive await message in socketConnection
would make loads of sense. Sadly, this isn’t potential out of the field. Nevertheless, URLSessionWebSocketTask
supplies some type of assist for async / await so we’re not totally out of luck.
A primary implementation of internet sockets with async / await
Whereas URLSessionWebSocketTask
doesn’t expose an AsyncSequence
that emits incoming messages out of the field, it does include an async model of the obtain
technique you noticed earlier.
This enables us to rewrite the instance above as an async technique as follows:
func setReceiveHandler() async {
do {
let message = strive await socketConnection.obtain()
change message {
case let .string(string):
print(string)
case let .knowledge(knowledge):
print(knowledge)
@unknown default:
print("unkown message acquired")
}
} catch {
print(error)
}
await setReceiveHandler()
}
This code works simply nice, besides we don’t actually have a method to cease the recursion right here. The code you noticed earlier really has the very same concern; there’s no situation to cease listening for internet socket messages even when the net socket connection has already been closed.
We might enhance our code by solely recursing if:
- We didn’t encounter any errors
- The socket connection continues to be energetic
This is able to look a bit as follows:
func setReceiveHandler() async {
guard socketConnection.closeCode == .invalid else {
return
}
do {
let message = strive await socketConnection.obtain()
change message {
case let .string(string):
print(string)
case let .knowledge(knowledge):
print(knowledge)
@unknown default:
print("unkown message acquired")
}
await setReceiveHandler()
} catch {
print(error)
}
}
An open internet socket’s closed code is all the time stated to invalid
to sign that the connection has not (but) been closed. We will leverage this to examine that our connection continues to be energetic earlier than ready for the subsequent message to be acquired.
That is significantly better already as a result of we respect closed sockets and failures a lot nicer now, however we might enhance the readability of this code a tiny bit by leveraging a whereas
loop as a substitute of recursively calling the setReceiveHandler
operate:
func setReceiveHandler() async {
var isActive = true
whereas isActive && socketConnection.closeCode == .invalid {
do {
let message = strive await socketConnection.obtain()
change message {
case let .string(string):
print(string)
case let .knowledge(knowledge):
print(knowledge)
@unknown default:
print("unkown message acquired")
}
} catch {
print(error)
isActive = false
}
}
}
To me, this model of the code is barely simpler to learn however which may not be the case for you. It’s functionally equal so you’ll be able to select to make use of whichever possibility fits you greatest.
Whereas this code works, I’m not fairly proud of the place we’ve landed proper now. There’s loads of logic on this operate and I would like to separate dealing with the incoming values from the calls to socketConnection.obtain()
one way or the other. Ideally, I ought to have the ability to write the next:
do {
for strive await message in socketConnection {
change message {
case let .string(string):
print(string)
case let .knowledge(knowledge):
print(knowledge)
@unknown default:
print("unkown message acquired")
}
} catch {
// deal with error
}
That is a lot, a lot nicer from a call-site perspective and it could permit us to place the ugly bits elsewhere.
To do that, we will leverage the facility of AsyncStream
which permits us to construct a customized async sequence of values.
Utilizing AsyncStream to emit internet socket messages
Given our finish purpose, there are just a few methods for us to get the place we need to be. The simplest manner can be to write down a operate in an extension on URLSessionWebSocketTask
that will encapsulate the whereas
loop you noticed earlier. This implementation would look as follows:
typealias WebSocketStream = AsyncThrowingStream<URLSessionWebSocketTask.Message, Error>
public extension URLSessionWebSocketTask {
var stream: WebSocketStream {
return WebSocketStream { continuation in
Process {
var isAlive = true
whereas isAlive && closeCode == .invalid {
do {
let worth = strive await obtain()
continuation.yield(worth)
} catch {
continuation.end(throwing: error)
isAlive = false
}
}
}
}
}
}
To make the code a bit bit simpler to learn, I’ve outlined a typealias
for my AsyncThrowingStream
so we don’t have to have a look at the identical lengthy kind signature everywhere.
The code above creates an occasion of AsyncThrowingStream
that asynchronously awaits new values from the net socket so long as the net socket is taken into account energetic and hasn’t been closed. To emit incoming messages and potential errors, the continuation’s yield
and end
strategies are used. These strategies will both emit a brand new worth (yield
) or finish the stream of values with an error (end
).
This code works nice in lots of conditions, however there’s one concern. If we resolve to shut the net socket connection from the app’s facet by calling cancel(with:purpose:)
on our socketConnection
, our WebSocketStream
doesn’t finish. As a substitute, it will likely be caught ready for messages, and the decision website might be caught too.
Process {
strive await Process.sleep(for: .seconds(5))
strive await socketConnection.cancel(with: .goingAway, purpose: nil)
}
Process {
do {
for strive await message in socketConnection.stream {
// deal with incoming messages
}
} catch {
// deal with error
}
print("this is able to by no means be printed")
}
If all the things works as anticipated, our internet socket connection will shut after 5 seconds. At that time, our for loop ought to finish and our print assertion ought to execute, because the asynchronous stream is now not energetic. Sadly, this isn’t the case, so we have to discover a higher solution to mannequin our stream.
URLSessionWebSocketTask
doesn’t present a manner for us to detect cancellation. So, I’ve discovered that it’s best to make use of an object that wraps the URLSessionWebSocketTask
, and to cancel the duty by that object. This enables us to each finish the async stream we’re offering to callers and shut the net socket reference to one technique name.
Right here’s what that object seems to be like:
class SocketStream: AsyncSequence {
typealias AsyncIterator = WebSocketStream.Iterator
typealias Component = URLSessionWebSocketTask.Message
non-public var continuation: WebSocketStream.Continuation?
non-public let process: URLSessionWebSocketTask
non-public lazy var stream: WebSocketStream = {
return WebSocketStream { continuation in
self.continuation = continuation
Process {
var isAlive = true
whereas isAlive && process.closeCode == .invalid {
do {
let worth = strive await process.obtain()
continuation.yield(worth)
} catch {
continuation.end(throwing: error)
isAlive = false
}
}
}
}
}()
init(process: URLSessionWebSocketTask) {
self.process = process
process.resume()
}
deinit {
continuation?.end()
}
func makeAsyncIterator() -> AsyncIterator {
return stream.makeAsyncIterator()
}
func cancel() async throws {
process.cancel(with: .goingAway, purpose: nil)
continuation?.end()
}
}
There’s a bunch of code right here, however it’s not too dangerous. The primary few strains are all about establishing some kind aliases and properties for comfort. The lazy var stream
is basically the very same code that you just’ve already within the URLSessionWebSocketTask
extension from earlier than.
When our SocketStream
‘s deinit
is known as we be sure that we finish our stream. There’s additionally a cancel
technique that closes the socket connection in addition to the stream. As a result of SocketStream
conforms to AsyncSequence
we should present an Iterator
object that’s used once we attempt to iterate over our SocketStream
s. We merely ask our inner stream
object to make an iterator and use that as our return worth.
Utilizing the code above seems to be as follows:
let url = URL(string: "ws://127.0.0.1:8080")!
let socketConnection = URLSession.shared.webSocketTask(with: url)
let stream = SocketStream(process: socketConnection)
Process {
do {
for strive await message in stream {
// deal with incoming messages
}
} catch {
// deal with error
}
print("this might be printed as soon as the stream ends")
}
To cancel our stream after 5 seconds identical to earlier than, you’ll be able to run the next process in parallel with our iterating process:
Process {
strive await Process.sleep(for: .seconds(5))
strive await stream.cancel()
}
Process {
// iterate...
}
Whereas that is fairly cool, we do have a little bit of a difficulty right here due to the next little bit of code:
non-public lazy var stream: WebSocketStream = {
return WebSocketStream { continuation in
self.continuation = continuation
Process {
var isAlive = true
whereas isAlive && process.closeCode == .invalid {
do {
let worth = strive await process.obtain()
continuation.yield(worth)
} catch {
continuation.end(throwing: error)
isAlive = false
}
}
}
}
}()
The duty that we run our whereas
loop in received’t finish until we finish our stream from inside our catch
block. If we manually shut the net socket connection utilizing the cancel
technique we write earlier, the decision to obtain()
won’t ever obtain an error nor a worth which signifies that it will likely be caught without end.
Probably the most dependable solution to repair that is to return to the callback primarily based model of obtain
to drive your async stream:
non-public lazy var stream: WebSocketStream = {
return WebSocketStream { continuation in
self.continuation = continuation
waitForNextValue()
}
}()
non-public func waitForNextValue() {
guard process.closeCode == .invalid else {
continuation?.end()
return
}
process.obtain(completionHandler: { [weak self] lead to
guard let continuation = self?.continuation else {
return
}
do {
let message = strive end result.get()
continuation.yield(message)
self?.waitForNextValue()
} catch {
continuation.end(throwing: error)
}
})
}
With this strategy we don’t have any lingering duties, and our name website is as clear and concise as ever; we’ve solely modified a few of our inner logic.
In Abstract
Swift Concurrency supplies many helpful options for writing higher code, and Apple rapidly adopted async / await for present APIs. Nevertheless, some APIs that will be helpful are lacking, similar to iterating over internet socket messages.
On this put up, you discovered the best way to use async streams to create an async sequence that emits internet socket messages. You first noticed a totally async / await model that was neat, however had reminiscence and process lifecycle points. Then, you noticed a model that mixes a callback-based strategy with the async stream.
The result’s a straightforward solution to iterate over incoming internet socket messages with async / await. If in case you have any questions, feedback, or enhancements for this put up, please do not hesitate to achieve out to me on Twitter.