Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

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 scrambled text.

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:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now