An Introduction to WebSockets
Learn about WebSockets using Swift and Vapor by building a question and answer client and server app. By Jari Koopman.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
An Introduction to WebSockets
30 mins
- Getting Started
- Creating a WebSocket Server
- Testing the WebSocket
- Writing WebSocket Data
- Understanding WebSocket Messages
- Setting up the UI
- Connecting to the Server
- Creating the Data Structure
- Handling the Data
- Setting up the Server Handshake
- Challenge: Extending WebSocketSendOption
- Sending the Questions
- Giving Your WebSocket Connection a Test Run
- Receiving WebSocket Data
- Storing the Data
- Connecting to the Server 2: Connect Harder
- Answering Questions
- Implementing the Answer Button
- Connecting to the Server: With a Vengeance
- Where to Go From Here?
Challenge: Extending WebSocketSendOption
In this app, you’ll only send data to single sockets. However, as a fun challenge for yourself, try extending WebSocketSendOption
to allow sending to:
- All sockets
- Multiple sockets based on IDs
Open the spoiler below to find the answer.
[spoiler title=”Solution”]
Add the following cases to WebSocketSendOption
:
case all, ids([UUID])
And add the following to switch
in send
:
case .all:
return self.sockets.values.map { $0 }
case .ids(let ids):
return self.sockets.filter { key, _ in ids.contains(key) }.map { $1 }
[/spoiler]
With that in place, it’s time to go back to the iOS project and start sending questions.
Sending the Questions
Now that the server is ready to receive questions, it’s time to get the app ready to send them.
Return to the Conference Q&A Xcode project and open WebSocketController.swift. Locate addQuestion(_:)
and replace the contents with the following:
guard let id = self.id else { return }
// 1
let message = NewQuestionMessage(id: id, content: content)
do {
// 2
let data = try encoder.encode(message)
// 3
self.socket.send(.data(data)) { (err) in
if err != nil {
print(err.debugDescription)
}
}
} catch {
print(error)
}
This code will:
- Construct
Codable
with the contents of the question and the ID that the handshake message passed. - Encode it to a
Data
instance. - Send the result to the server using the binary opcode. Here,
URLSession
takes care of the masking for you.
Great job! Now, it’s time to try it out.
Giving Your WebSocket Connection a Test Run
With that, everything is in place to give the entire process a full on test run!
First, build and run the websocket-backend project to make sure the server is ready to accept connections.
When that’s done, build and run the Conference Q&A iOS app.
In the server log messages, you’ll see:
[ INFO ] GET /socket [ INFO ] Sending QnAHandshake to socket(WebSocketKit.WebSocket)
The iOS app will log Shook the hand
. Connection established!
Now, enter a question in the app and press Return. The server will log something like this:
[ INFO ] {"type":"newQuestion","id":"1FC4044C-7E20-4332-9349-E4FDD69A10B3","content":"Awesome question!"}
Awesome, everything’s working!
Receiving WebSocket Data
With the first connection established, there are only a few things left to do. Namely:
- Store the asked questions in the database.
- Update the iOS app to use the back end as the source of questions asked.
- Add the ability to answer questions.
- Update the iOS app when the presenter answers a question.
You’ll address these issues next.
Storing the Data
To store the data, open the websocket-backend project and open WebSocketController.swift. Above onData
, add a new function called onNewQuestion
and add the following code to it:
func onNewQuestion(_ ws: WebSocket, _ id: UUID, _ message: NewQuestionMessage) {
let q = Question(content: message.content, askedFrom: id)
self.db.withConnection {
// 1
q.save(on: $0)
}.whenComplete { res in
let success: Bool
let message: String
switch res {
case .failure(let err):
// 2
self.logger.report(error: err)
success = false
message = "Something went wrong creating the question."
case .success:
// 3
self.logger.info("Got a new question!")
success = true
message = "Question created. We will answer it as soon as possible :]"
}
// 4
try? self.send(message: NewQuestionResponse(
success: success,
message: message,
id: q.requireID(),
answered: q.answered,
content: q.content,
createdAt: q.createdAt
), to: .socket(ws))
}
}
Here’s what’s going on:
- First, you create a
Question
model instance and save it to the database. - If saving fails, you log the error and indicate no success.
- On the other hand, if saving was successful, you log that a new question came in.
- Finally, you send a message to the client indicating you received the question, whether successfully or not.
Next, replace the log line in onData
with the following:
let decoder = JSONDecoder()
do {
// 1
let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
// 2
switch sinData.type {
case .newQuestion:
// 3
let newQuestionData = try decoder.decode(NewQuestionMessage.self,
from: data)
self.onNewQuestion(ws, sinData.id, newQuestionData)
default:
break
}
} catch {
logger.report(error: error)
}
This will:
- Decode
QnAMessageSinData
to see what type of message came in. - Switch on the type. Currently the server only accepts one type of incoming message.
- Decode the full
NewQuestionMessage
and pass it toonNewQuestion
.
These two functions will ensure you store all received questions in the database, and that you send a confirmation back to the clients.
Connecting to the Server 2: Connect Harder
Remember there was a local array in the iOS app that stored the asked questions? It’s time to get rid of that! Bonus points if you got the “Die Hard” reference. :]
Open WebSocketController.swift in the Conference Q&A Xcode project. At the top of WebSocketController
, add the following:
@Published var questions: [UUID: NewQuestionResponse]
This dictionary will hold your questions.
Next, you have to set an initial value in the init
. Add this to the top:
self.questions = [:]
Next, find handleQuestionResponse(_:)
and replace the TODO with the following:
// 1
let response = try decoder.decode(NewQuestionResponse.self, from: data)
DispatchQueue.main.async {
if response.success, let id = response.id {
// 2
self.questions[id] = response
let alert = Alert(title: Text("New question received!"),
message: Text(response.message),
dismissButton: .default(Text("OK")) { self.alert = nil })
self.alert = alert
} else {
// 3
let alert = Alert(title: Text("Something went wrong!"),
message: Text(response.message),
dismissButton: .default(Text("OK")) { self.alert = nil })
self.alert = alert
}
}
Here’s what this code does:
- It decodes the full
NewQuestionResponse
message. - If the response was successful, it stores the response in the
questions
dictionary and displays a positive alert. - If the response fails, it displays an error alert.
Now, your next step is to connect the UI to this new data source. Open ContentView.swift and replace List
inside body
with the following:
List(socket.questions.map { $1 }.sorted(), id: \.id) { q in
VStack(alignment: .leading) {
Text(q.content)
Text("Status: \(q.answered ? "Answered" : "Unanswered")")
.foregroundColor(q.answered ? .green : .red)
}
}
This code is mostly the same as the previous version, but it uses the socket as its data source and takes the answered
property of the question into account.
Also, make sure to remove the @State var questions
at the top of ContentView
and the temporary self.questions.append
.
With this done, build and run the server and the iOS app. The handshake will happen again, just like before.
Now, send a question. The server will no longer log the raw JSON, but instead log [ INFO ] Got a new question!
. You will also see the success alert appear in the iOS app.
Awesome!