30.
WebSockets
Written by Logan Wright
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.
Enter the following commands:
cd share-touch-server
open Package.swift
This navigates into the share-touch-server directory and opens the project in Xcode.
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)
}
}
Here’s what this does:
- Create a WebSocket route handler for the echo endpoint.
- Log a message to the console when a client connects.
- Create a listener that fires each time the endpoint receives text.
- Log the received text to the console.
- Echo the received text back to the sender after prepending **echo: **.
In Xcode’s scheme selector, choose the ShareTouchServer scheme and My Mac as the destination. Build and run. In your browser, open https://websocketking.com and enter ws://localhost:8080/echo in the URL field, then press Connect. You should see in the logs something like:
Connected to ws://localhost:8080/echo
Connecting to ws://localhost:8080/echo
Check the Xcode console and you’ll see ws connected.
Enter a message in WebSocketKing, and you’ll see your server respond with an appropriate 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.
You’ll be using an in-memory manager. This means if your application were to scale up to multiple servers, you’d need a more complex management system that assigns various users to various servers. For now, you can assume a single session for all your users and a single server is enough.
Here’s the basic architecture you’ll use:
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.
For your purposes, a relative point uses a 0-1.0 scale representing the visible area of a screen. This allows you to translate touches between various screen sizes.
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.
Upon a client’s successful connection, the server will immediately notify that client of all current participants by sending a joined message.
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.
Now that you understand the states and messages used by the app, it’s time to begin implementing.
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)")
}
}
Here’s what your new code does:
- Add a WebSocket endpoint for /session.
- When you receive text, print it to the console.
Run your server application and leave it running. Then, open the iOS project.
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}
Awesome! Your server is communicating with the iOS app via a WebSocket!
This is good! It means your app is sending data successfully to the server, and the server is successfully receiving it. Return to the server application to build out more of the session management logic.
Note: If you try to run the iOS app on a device, you’ll need to change the URL in ShareTouchApp.swift to locate your computer’s IP address over WiFi. If you’re looking to test remote devices and tunnel them to your computer’s server, checkout ngrok! It’s a great tool and makes it easy to setup domains that forward to your computer’s server.
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)")
}
This is what your new code does:
- Get the color and position from the request’s query.
- If you can’t decode the color or position, close the WebSocket with an “unacceptable data” status.
- Print the color and position to the console.
Build and run and then return to the iOS simulator and press BEGIN. You should see the server logging the color you selected. Select a different color and notice how the components are changed.
Next, you need to set the user up with TouchSessionManager. Still in WebSockets.swift, find:
print("new user joined with: \(color) at \(position)")
and add the following below it:
let newId = UUID().uuidString
TouchSessionManager.default
.insert(id: newId, color: color, at: position, on: ws)
This creates a new ID for the user, using UUID, and inserts the user into TouchSessionManager using the color and position from earlier.
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.
Below TouchSessionManager.default.insert(id: newId, color: color, at: position, on: ws) add the following:
// 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)")
}
}
This code does the following:
- Create an
onText(_:)listener to run when the WebSocket receives some text. - Decode the received text to
RelativePointfrom JSON. - Update the user in
TouchSessionManagerwith the user’s new point. - If the decoding fails, return a message to the client.
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)
}
Here’s what the final part does:
- Register a
onClosehandler for the WebSocket.always(_:)triggers the closure on any WebSocket close event. - Remove the user from
TouchSessionManagerusing the ID created earlier.
Build and run the server and return to the simulator to start a new session. Drag the circle around and notice the logs on the server. You should see logs from the TrackingSessionManager, but it’s not yet implemented.
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
Here’s what the new code does:
- Create a
SharedTouchandMessagefrom the new user’s details. - Send the message to all existing users.
- Loop through each current user and create a new join
Message. Send the messages to the new user, which allows tracking all existing users. - Store the new user’s session to respond to future events.
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)
This new code does the following:
- Update the position of the participant using the provided
position. - Create a new
Messagewith the user’s ID and updated point. - Send the message to all active sessions.
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)
Here’s what this does:
- Remove the associated reference from the backing dictionary.
- Create a new
Messagefor the user to remove. Use.leftto notify other users this user has left. - Send the message to all the remaining active sessions.
Build and run the server, leave it running, then return to the ShareApp iOS Xcode project. Run the project on any two simulators. Xcode can only host one debugging session at a time. However, if you open the second simulator and select the ShareTouch app, you can run two sessions.
Select a color on each simulator and drag the circles around to see the updates. You can even run a third simulator (or more, if your computer can handle it).
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:
- Upgrade the server and client to transmit raw binary data as opposed to text for a bit of a performance boost.
- Add a way for users to see more information about active sessions, such as how many sessions are active and how long they’ve been active.
- Maintain some sort of historical record from touch to lift and recreate movements
- Try hosting your basic application on a remote server. Make sure to update
shareSessionURLin ShareTouchApp.swift in the iOS project.