30
WebSockets
Written by Logan Wright
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
WebSockets, like HTTP, define a protocol used for communication between two devices. Unlike HTTP, the WebSocket protocol is designed for real-time communication. WebSockets can be a great option for things like chat or other features that require real-time behavior. Vapor provides a succinct API to create a WebSocket server or client. This chapter focuses on building a basic server.
In this chapter, you’ll build a simple client-server application that allows users to share a touch with other users and view in real-time other user’s touches on their own device.
Tools
Testing WebSockets can be a bit tricky since they can send/receive multiple messages. This makes using a simple CURL request or a browser difficult. Fortunately, there’s a great WebSocket client tool you can use to test your server at: https://www.websocketking.com. It’s important to note that, as of writing this, connections to localhost are only supported in Chrome.
A basic server
Now that your tools are ready, it’s time to set up a very basic WebSocket server. Copy this chapter’s starter project to your favorite location and open a Terminal window in that directory.
cd share-touch-server
open Package.swift
Echo server
Open WebSockets.swift and add the following to the end of sockets(_:)
to create an echo endpoint:
// 1
app.webSocket("echo") { req, ws in
// 2
print("ws connected")
// 3
ws.onText { ws, text in
// 4
print("ws received: \(text)")
// 5
ws.send("echo: " + text)
}
}
Connected to ws://localhost:8080/echo
Connecting to ws://localhost:8080/echo
Sessions
Now that you’ve verified you can communicate with your server, it’s time to add more capabilities to it. For the basic application, you’ll use a single WebSocket endpoint at /session.
Client -> Server
The connection from the client to the server can be in one of three states: joined, moved and left.
Joined
A new participant will open a WebSocket using the /session endpoint. In the opening request, you’ll include two bits of information from the user: the color to use — represented as r,g,b,a — and a starting point — represented using a relative point.
Moved
To keep things simple, after a client opens a new session, the only thing it will send the server is new relative points as the user drags the circle.
Left
This server will interpret any closure on the client’s side as leaving the room. This keeps things succinct.
Server -> Client
The server sends three different types of messages to clients: joined, moved and left.
Joined
When the server sends a joined message, it includes in the message an ID, a Color and the last known point for that participant.
Moved
Any time a participant moves, the server notifies the clients. These notifications include only an ID and a new relative point.
Left
Any time a participant disconnects from the session, the server notifies all other participants and removes that user from associated views.
Setting up “Join”
Open WebSockets.swift and add the following to the end of sockets(_:)
// 1
app.webSocket("session") { req, ws in
// 2
ws.onText { ws, text in
print("got message: \(text)")
}
}
iOS project
The materials for this chapter include a complete iOS app. You can change the URL you’d like to use in ShareTouchApp.swift. For now, it should be set to ws://localhost:8080/session
. Build and run the app in the simulator. Select a color and press BEGIN, then drag the circle around the screen. You should see logs in your server application that look similar to the following:
got message: {"x":0.62031250000000004,"y":0.60037878787878785}
got message: {"x":0.61250000000000004,"y":0.59469696969696972}
got message: {"x":0.60781249999999998,"y":0.59185606060606055}
got message: {"x":0.59999999999999998,"y":0.59469696969696972}
Finishing “Join”
As described earlier, the client will include a color and a starting position in the web socket connection request. WebSocket requests are treated as an upgraded GET request, so you’ll include the data in the query of the request. In WebSockets.swift, replace the code you added earlier for app.webSocket("session")
with the following:
app.webSocket("session") { req, ws in
// 1
let color: ColorComponents
let position: RelativePoint
do {
color = try req.query.decode(ColorComponents.self)
position = try req.query.decode(RelativePoint.self)
} catch {
// 2
_ = ws.close(code: .unacceptableData)
return
}
// 3
print("new user joined with: \(color) at \(position)")
}
print("new user joined with: \(color) at \(position)")
let newId = UUID().uuidString
TouchSessionManager.default
.insert(id: newId, color: color, at: position, on: ws)
Handling “Moved”
Next, you need to listen to messages from the client. For now, you’ll only expect to receive a stream of RelativePoint objects. In this case, you’ll use onText(_:)
. Using onText(_:)
is perhaps slightly less performant than using onBinary(_:)
and receiving data directly. However, it makes debugging easier and you can change it later.
// 1
ws.onText { ws, text in
do {
// 2
let pt = try JSONDecoder()
.decode(RelativePoint.self, from: Data(text.utf8))
// 3
TouchSessionManager.default.update(id: newId, to: pt)
} catch {
// 4
ws.send("unsupported update: \(text)")
}
}
Implementing “Left”
Finally, you need to implement the code for a WebSocket close. You’ll consider any disconnect or cancellation that leaves the socket unable to send messages as a close. Below ws.onText(_:)
, add:
// 1
_ = ws.onClose.always { result in
// 2
TouchSessionManager.default.remove(id: newId)
}
Implementing TouchSessionManager: Joined
At this point, you can successfully dispatch WebSocket events to their associated architecture event in the TouchSessionManager. Next, you need to implement the management logic. Open TouchSessionManager.swift and replace the body of insert(id:color:at:on:)
with the following:
// 1
let start = SharedTouch(
id: id,
color: color,
position: pt)
let msg = Message(
participant: id,
update: .joined(start))
// 2
send(msg)
// 3
participants.values.map {
Message(
participant: $0.touch.participant,
update: .joined($0.touch))
} .forEach { ws.send($0) }
/// store new session
// 4
let session = ActiveSession(touch: start, ws: ws)
participants[id] = session
Implementing TouchSessionManager: Moved
Next, to handle “moved” messages, replace the body of update(id:to:)
with the following code:
// 1
participants[id]?.touch.position = pt
// 2
let msg = Message(participant: id, update: .moved(pt))
// 3
send(msg)
Implementing TouchSessionManager: Left
Finally, you need to handle closes and cancellations. Replace the body of remove(id:)
with the following:
// 1
participants[id] = nil
// 2
let msg = Message(participant: id, update: .left)
// 3
send(msg)
Where to go from here?
You’ve done it. Your iOS Application communicates in real-time via WebSockets with your Swift server. Many different kinds of apps can benefit from the instantaneous communications made possible by WebSockets, including things such as chat applications, games, airplane trackers and so much more. If the app you imagine needs to respond in real time, WebSockets may be your answer!
Challenges
For more practice with WebSockets, try these challenges: