An Introduction to WebSockets

Learn about WebSockets using Swift and Vapor by building a question and answer client and server app. By Jari Koopman.

4.3 (19) · 3 Reviews

Download materials
Save for later

Learn about WebSockets using Swift and Vapor by building a question and answer client and server app.

A WebSocket is a network protocol that allows two-way communication between a server and client. Unlike HTTP, which uses a request and response pattern, WebSocket peers can send messages in either direction at any point in time.

WebSockets are often used for chat-based apps and other apps that need to continuously talk between server and client.

In this WebSockets tutorial you’ll learn how to:

  • Initiate a WebSocket handshake including how the connection is upgraded to a WebSocket.
  • Send messages through the WebSocket.
  • Process received messages sent through the WebSocket.
  • Integrate with a Vapor server, iOS application and web page using WebSockets.

In this tutorial you’ll learn how to integrate a simple iOS Question and Answer application with a Vapor Server to send and receive information via WebSockets. The iOS application allows you to create questions and the Vapor application will store these questions along with their status of Unanswered or Answered.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

In the Starter directory are two folders:

  • websockets-backend: Contains the Vapor 4 back end that you’ll use as your WebSocket server.
  • websocket-ios: Contains the iOS app that will be the WebSocket client.

In the next section, you’ll set up the server part of the WebSocket and learn more about how a WebSocket works.

Creating a WebSocket Server

A WebSocket connection starts with a handshake. This is a little dance the client and server do to start the connection, like two birds doing a mating dance. :]

The client starts out with a normal HTTP request that contains two special headers: Upgrade: WebSocket and Connection: Upgrade, along with any other required request data like authentication.

The server then sends back an HTTP 101 Switching Protocols status code indication to the client, which basically says: We used to talk in HTTP, but we’ll use something else in the future. Along with this HTTP 101 response, it also sends the Upgrade: WebSocket and Connection: Upgrade headers again.

After this, the handshake is complete and the WebSocket connection is in place!

Diagram showing the flow of data between the client and server using a WebSocket

With the handshake out of the way, you can start setting up the back-end server. Navigate to Starter/websocket-backend and double-click Package.swift. This opens the project in Xcode and starts downloading the dependencies.

When Xcode finishes fetching the dependencies, open QuestionsController.swift, where you’ll add the route the WebSocket client will connect to.

Your first step is to add the following new function above index in QuestionsController:

func webSocket(req: Request, socket: WebSocket) {
  socket.onText { _, text in print(text) }

For now, all you want to do is print any text messages you’ve received over your WebSocket connection. To ensure the client can connect to this server, add the following route to the top of boot(routes:):

routes.webSocket("socket", onUpgrade: self.webSocket)

By using routes.webSocket here instead of a usual HTTP method, Vapor takes care of all the handshaking for you… awesome!

Testing the WebSocket

Your server should work now, but it’s a good idea to be sure. To test your new WebSocket server, build and run your code. It worked successfully if the logs show the following:

[ NOTICE ] Server starting on

Now, it’s time to do some WebSocket-ing! Open your browser and navigate to

Note: You must use http here, not https, since you’re connecting to a localhost server.

Next, enter ws://localhost:8080/socket into the Location field and press the Connect button. In the Log field to the right, CONNECTED will appear and your Vapor back end will log:

[ INFO ] GET /socket

Now that you’ve set up the connection, enter some text in the Message field and hit Send. Your message will now magically appear in your Xcode console.

Congratulations, you just created your own WebSocket server!

Writing WebSocket Data

Of course, you can’t expect your users to go to this testing site and enter all their data manually. In this section, you’ll set up the iOS companion app to send WebSocket messages directly to your server. But before you do that, take a moment to learn what exactly a WebSocket message is.

Understanding WebSocket Messages

In the test above, you simply entered some text, but a lot more is going on behind the scenes. Per the WebSocket Protocol RFC: “In the WebSocket Protocol, data is transmitted using a sequence of frames.”

A WebSocket frame consists of some bits that you can compare to HTTP headers, followed by an actual message. If a message is too big, you can use multiple frames to send it.

The following frame parts are especially interesting:

  • FIN: This single bit indicates whether this is the final frame of a message or if more frames will follow.
  • opcode: These four bits indicate what type of message this is and how to handle the payload data.
  • MASK: This single bit indicates whether the message is masked. Client-to-server messages must always be masked.
  • Payload length: Use these 7, 23 or 71 bits to explain how big the payload data is in bits, much like HTTP’s Content-Length header.
  • Masking key: These four optional bits contain the key you used to mask the message. You can omit them if the MASK bit is 0.
  • Payload data: The rest of the message contains the actual payload.

There are two types of opcodes: ones for non-control frames and for control frames. The non-control frame codes are: continuation, text and binary. These deal with how to decode the payload. The control frame codes are: connection close, ping and pong. These are special types of messages.

As the RFC states, you must mask all client-to-server messages. You mask a message by generating four random bits as the mask key. Every byte in the payload data is than XORed based on the masking key. This doesn’t affect payload length.

Unmasking uses the same process as masking.

Now that you know how WebSocket messages work, it’s time to start creating some.

Setting up the UI

To exchange WebSocket messages with your server, you’ll create a simple SwiftUI app that users can use to ask questions after a conference talk.

Your first step is to set up the app’s UI to let the users send their questions.

Open the iOS project by navigating to Starter/websocket-ios and double-click Conference Q&A.xcodeproj. Build and run and you’ll see a plain “Hello, world” view. You’ll change that next!

Open ContentView.swift and replace the contents of ContentView with the following:

    // 1
    @State var newQuestion: String = ""
    // TODO: Remove following line when socket is implemented.
    @State var questions: [String] = []
    // 2
    @ObservedObject var keyboard: Keyboard = .init()
    @ObservedObject var socket: WebSocketController = .init()
    var body: some View {
      // 3
      VStack(spacing: 8) {
        Text("Your asked questions:")
        // 4
        // TODO: Update list when socket is implemented.
        List(self.questions, id: \.self) { q in
          VStack(alignment: .leading) {
            Text("Status: Unanswered")
        // 5
        TextField("Ask a new question", text: $newQuestion, onCommit: {
          guard !self.newQuestion.isEmpty else { return }
          // TODO: Remove following line when socket is implemented.
          self.newQuestion = ""
          .padding(.bottom, keyboard.height)
          .edgesIgnoringSafeArea(keyboard.height > 0 ? .bottom : [])
      // 6
      .alert(item: $socket.alertWrapper) { $0.alert }

Now, there’s quite a lot going on here:

  1. At the top are two State variables for the views. One contains the user input for new questions while the other stores those questions temporarily.
  2. Next come the ObservedObjects. Keyboard moves the text field up when the keyboard is active. WebSocketController contains all the required WebSocket code.
  3. In the body of the view, you start with a simple VStack to put all the views on top of each other.
  4. The first section of the VStack is a List containing all asked questions. You’ll connect this to the WebSocketController data later on.
  5. Next is the TextField for new questions. Once you connect the WebSocket, you’ll remove the local self.questions array and store the data in the back end.
  6. Finally, you have a placeholder for an alert that you’ll use when the WebSocket reports an error.

Build and run the app and try entering a question. Press Enter and you’ll see the question appear in the list with a status of Unanswered. Awesome!

Now that the UI is complete, you’re ready to connect to your WebSocket server.

Connecting to the Server

Your app doesn’t do much good if it can’t connect to the server to send the user’s questions. Your next step is to build that connection.

Open WebSocketController.swift and locate connect(). Replace // TODO: Implement with the following code to start the socket connection:

    self.socket = session.webSocketTask(with: 
      URL(string: "ws://localhost:8080/socket")!)

This will set self.socket to a URLSessionWebSocketTask, start your listening cycle and resume the task. If you’ve used URLSession, this will look familiar.

Next, you’ll tell URLSession to receive new messages in listen. Replace the TODO comment with:

    // 1
    self.socket.receive { [weak self] (result) in
      guard let self = self else { return }
      // 2
      switch result {
      case .failure(let error):
        // 3
        let alert = Alert(
            title: Text("Unable to connect to server!"),
            dismissButton: .default(Text("Retry")) {
              self.alert = nil
              self.socket.cancel(with: .goingAway, reason: nil)
        self.alert = alert
      case .success(let message):
        // 4
        switch message {
        case .data(let data):
        case .string(let str):
          guard let data = .utf8) else { return }
        @unknown default:
      // 5

Here’s what’s going on with this snippet:

  1. First, you add a callback to URLSessionWebSocketTask.receive. When the server receives a message, it will execute this closure. self is weakly captured so before executing the rest of the closure, the guard ensures self is not nil.
  2. The value passed into the closure is of type Result<URLSessionWebSocketTask.Message, Error>. The switch lets you handle both successful messages and errors.
  3. In case of an error, create a SwiftUI Alert, which you’ll present to the user later.
  4. In case of a successful message, pass the raw Data to your handle.
  5. URLSessionWebSocketTask.receive registers a one-time callback. So after the callback executes, re-register the callback to keep listening for new messages.

Now your server is ready to receive messages. However, you still have a bit of work to do to let the server know what kinds of messages it will receive.

Creating the Data Structure

Before handling any data, you need to define your data structure. You’ll do that now.

Start by creating a new Swift file called WebSocketTypes.swift containing the following code:

import Foundation

enum QnAMessageType: String, Codable {
  // Client to server types
  case newQuestion
  // Server to client types
  case questionResponse, handshake, questionAnswer

struct QnAMessageSinData: Codable {
  let type: QnAMessageType

This is the base layer of your data structure. QnAMessageType declares the different types of messages, both client-to.server and server-to-client.

Every message you send between your client and server will contain a type field so the receiver knows what type of message to decode.

Now, create the actual message types on top of this layer. At the bottom of WebSocketTypes.swift, add the following:

struct QnAHandshake: Codable {
  let id: UUID

struct NewQuestionMessage: Codable {
  var type: QnAMessageType = .newQuestion
  let id: UUID
  let content: String

class NewQuestionResponse: Codable, Comparable {
  let success: Bool
  let message: String
  let id: UUID?
  var answered: Bool
  let content: String
  let createdAt: Date?
  static func < (lhs: NewQuestionResponse, rhs: NewQuestionResponse) -> Bool {
    guard let lhsDate = lhs.createdAt,
      let rhsDate = rhs.createdAt else {
        return false

    return lhsDate < rhsDate
  static func == (lhs: NewQuestionResponse, rhs: NewQuestionResponse) -> Bool { ==

struct QuestionAnsweredMessage: Codable {
  let questionId: UUID

All the above types, except for NewQuestionMessage, are server-to-client types and, thus, don’t have the type field set.

Handling the Data

Now you’ve set up your data structure, it’s time to let the server know how to handle the questions it receives from connecting clients.

Open WebSocketController.swift. Now, you can now implement the rest of the methods, starting with handle(_:).

Replace the TODO with the following code:

    do {
      // 1
      let sinData = try decoder.decode(QnAMessageSinData.self, from: data)
      // 2
      switch sinData.type {
      case .handshake:
        // 3
        print("Shook the hand")
        let message = try decoder.decode(QnAHandshake.self, from: data) =
      // 4
      case .questionResponse:
        try self.handleQuestionResponse(data)
      case .questionAnswer:
        try self.handleQuestionAnswer(data)
    } catch {

This code does the following:

  1. Decodes the QnAMessageSinData type you just declared. This will only decode the type field.
  2. Switches on the decoded type field and does type-specific handling.
  3. For the handshake type, decodes the full message and stores the ID.
  4. For a question response and answer, passes the data along to specific implementation functions.

You’re making great progress! You probably want to try it out, but before you can test the full system, there are two things you need to do:

  1. The server needs to send a custom handshake to the client to identify clients later.
  2. The client needs to send new questions to the server.

Those will be the next two steps. You’ll start with the server.

Setting up the Server Handshake

In your websocket-backend project, open QuestionsController.swift and replace the contents of webSocket(req:socket:) with the following:


At this point, your route collection will no longer control the socket connection.

Now, open WebSocketController.swift to implement connect(_:). Replace the TODO comment with the following:

    // 1
    let uuid = UUID()
    self.lock.withLockVoid {
      self.sockets[uuid] = ws
    // 2
    ws.onBinary { [weak self] ws, buffer in
      guard let self = self,
        let data = buffer.getData(
          at: buffer.readerIndex, length: buffer.readableBytes) else {

      self.onData(ws, data)
    // 3
    ws.onText { [weak self] ws, text in
      guard let self = self,
        let data = .utf8) else {

      self.onData(ws, data)
    // 4
    self.send(message: QnAHandshake(id: uuid), to: .socket(ws))

This does the following:

  1. Generates a random UUID for every socket connection to identify each one, then stores the connection in a dictionary based on this UUID.
  2. When receiving binary data, it stores the data in SwiftNIO’s ByteBuffer. Using getData, you get a Data instance from the buffer, then pass it on to be handled.
  3. For text data, you use to get a Data instance and, again, pass it on to be handled.
  4. Finally, you send a QnAHandshake message to the new connection, containing the UUID you generated above.

The QnAHandshake type here is the same one you created in the iOS app earlier. Look at the server-side data model in WebSocketTypes.swift. It’s the same as the iOS side, except the types that already have their type field set are reversed.

Next, back in WebSocketController.swift, put the following code in send(message:to:):"Sending \(T.self) to \(sendOption)")
    do {
      // 1
      let sockets: [WebSocket] = self.lock.withLock {
        switch sendOption {
        case .id(let id):
          return [self.sockets[id]].compactMap { $0 }
        case .socket(let socket):
          return [socket]
      // 2
      let encoder = JSONEncoder()
      let data = try encoder.encode(message)
      // 3
      sockets.forEach {
        $0.send(raw: data, opcode: .binary)
    } catch { error)

The following happens here:

  1. Based on WebSocketSendOption, you select a socket from the connected sockets.
  2. You encode the message you want to send using JSONEncoder.
  3. You send the encoded data to all selected sockets using the binary opcode.

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 { $0 }
case .ids(let ids):
  return self.sockets.filter { key, _ in ids.contains(key) }.map { $1 }


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 = 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 {
    } catch {

This code will:

  1. Construct Codable with the contents of the question and the ID that the handshake message passed.
  2. Encode it to a Data instance.
  3. 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 $0)
    }.whenComplete { res in
      let success: Bool
      let message: String
      switch res {
      case .failure(let err):
        // 2 err)
        success = false
        message = "Something went wrong creating the question."
      case .success:
        // 3"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:

  1. First, you create a Question model instance and save it to the database.
  2. If saving fails, you log the error and indicate no success.
  3. On the other hand, if saving was successful, you log that a new question came in.
  4. 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,, newQuestionData)
    } catch { error)

This will:

  1. Decode QnAMessageSinData to see what type of message came in.
  2. Switch on the type. Currently the server only accepts one type of incoming message.
  3. Decode the full NewQuestionMessage and pass it to onNewQuestion.

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 = {
        // 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:

  1. It decodes the full NewQuestionResponse message.
  2. If the response was successful, it stores the response in the questions dictionary and displays a positive alert.
  3. 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( { $1 }.sorted(), id: \.id) { q in
        VStack(alignment: .leading) {
          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.


Answering Questions

To answer questions, you’ll use Leaf to create an admin page. Leaf is Vapor’s templating language to create HTML pages.

Open the websocket-backend project and open QuestionsController.swift. Replace index with the following:

  struct QuestionsContext: Encodable {
    let questions: [Question]

  func index(req: Request) throws -> EventLoopFuture<View> {
    // 1
    Question.query(on: req.db).all().flatMap {
      // 2
      return req.view.render("questions", QuestionsContext(questions: $0))

Here’s what this code does:

  1. Select all Question objects from the database.
  2. Returns the leaf view with the questions object set in the context to the questions pulled in step 1 above.

Before you can see the dashboard in action, you need to tell Leaf where to find your templates.

To do this, press Command-Option-R or, if you prefer not to fold your fingers into uncomfortable positions, click websocket-backend in the top-left corner, then click Edit Scheme….

This brings up the Scheme editor. Inside, navigate to the Options tab and locate the Working Directory setting.

Check Use custom working directory and make sure the working directory points to the directory containing your Package.swift. For example: /Users/lotu/Downloads/Conference_Q&A/Starter/websockets-backend.

Custom working directory

Now, build and run the server and open http://localhost:8080 in your browser. You’ll see a basic HTML table with three columns: Question, Answered and Answer. Use the iOS app to send in a question and refresh the page.

You’ll now see a new table row with your question, which has false in the Answered column and an Answer button in the last column.

However, when you click Answer, you get a 404 error at the moment. You’ll fix that next.

Implementing the Answer Button

To get the Answer button working properly, open QuestionsController.swift, find index(_:), and add the new route directly below its implementation:

  func answer(req: Request) throws -> EventLoopFuture<Response> {
    // 1
    guard let questionId = req.parameters.get("questionId"),
      let questionUid = UUID(questionId) else {
        throw Abort(.badRequest)
    // 2
    return Question.find(questionUid, on: req.db)
                   .unwrap(or: Abort(.notFound))
                   .flatMap { question in
      question.answered = true
      // 3
      return req.db).flatMapThrowing {
        // 4
        try self.wsController.send(message: 
          QuestionAnsweredMessage(questionId: question.requireID()),
          to: .id(question.askedFrom))
        // 5
        return req.redirect(to: "/")

This code will:

  1. Make sure the question ID you’re trying to answer has a valid UUID.
  2. Try to find the question in the database.
  3. Set answered to true and save the question in the database.
  4. Send an update to the client indicating you answered the question.
  5. Redirect to the index page to show the updated front end.

Finally, you need to register the route in boot like this:":questionId", "answer", use: answer)

OK, so now it’s time to see if answering questions works as you expect.

Connecting to the Server: With a Vengeance

Yes, more “Die Hard” jokes. :] Before you give answering questions a test run, you need to make sure the iOS app is ready for it.

Once again, open the Conference Q&A Xcode project and open WebSocketController.swift. Add the following code to handleQuestionAnswer(_:):

    // 1
    let response = try decoder.decode(QuestionAnsweredMessage.self, from: data)
    DispatchQueue.main.async {
      // 2
      guard let question = self.questions[response.questionId] else { return }
      question.answered = true
      self.questions[response.questionId] = question

Here’s what’s happening:

  1. You decode the full message.
  2. On the main queue, you update the question in the questions dictionary. This also updates the UI!.

Now, it’s time to make sure everything works.

Build and run the back-end server and the iOS app. From the app, enter a question and open the dashboard at http://localhost:8080. Click the Answer button and stare in awe at how the iOS app UI updates instantly!

Now as the very last step, take your right hand, move it to your left shoulder and give yourself a firm pat on the back. You just created your very own WebSocket server and client!

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of this page.

To learn more about WebSockets, read the WebSocket Protocol RFC, the NIOWebSocket documentation or Vapor’s WebSocket documentation.

If you have any questions or comments, please join the forum discussion below!