loading...

Different flavors of WebSockets on Vapor with Swift

fassko profile image Kristaps Grinbergs ・4 min read

Swift on the server lately is getting more traction despite IBM leaving the club. One of the most popular frameworks out there is Vapor. It is built on top of Apple’s SwiftNIO and written fully in Swift programming language.

This time we will look into how to work with WebSockets using Vapor framework. We are going to see how to create a client and server using Vapor’s module WebSocketKit and Vapor framework itself.

WebSocketKit

WebSocketKit is a WebSocket client library built on SwiftNIO. Vapor framework consists of many smaller modules and this is one of them. WebSocketKit is hiding away SwiftNIO lower-level complexity and nicely abstracting the event loops. Let’s see how can we use it.

Client

When creating a WebSocket client with WebSocketKit we need to follow a couple of steps.
At first, we need to create an event loop group where we can receive the WebSocket events such as receiving a text.

  var eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)

After that, we should create a WebSocket promise within the created event loop group. Within the promise, we specify the WebSocket events. In this example, we are sending hello String value and printing out text that we receive back.

  let port: Int = 8080
  let promise = eventLoopGroup.next().makePromise(of: String.self)
  WebSocket.connect(to: "ws://localhost:\(port)", on: eventLoopGroup) { ws in
    ws.send("hello")
    ws.onText { ws, string in
      print(string)
    }
  }.cascadeFailure(to: promise)

After that, we need to wait for executing events within this promise and event loop group.

  _  = try promise.futureResult.wait()

Server

Creating a server with WebSocketKit is a bit more complicated - let’s move step by step.

Similarly, when creating the client we need to create the event loop group.

  var eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 2)

A GET connection can be transformed into WebSocket connection via upgrade dance. When using this approach, we need to configure that ourselves.

  let upgradePipelineHandler: (Channel, HTTPRequestHead) -> EventLoopFuture<Void> = { channel, req in
    WebSocket.server(on: channel) { ws in
      ws.send("You have connected to WebSocket")

      ws.onText { ws, string in
        print("received")
        ws.send(string.trimmingCharacters(in: .whitespacesAndNewlines).reversed())
      }

      ws.onBinary { ws, buffer in
        print(buffer)
      }

      ws.onClose.whenSuccess { value in
        print("onClose")
      }
    }
  }

Now as we have created the WebSocket upgrade pipeline let’s use it. To do that we need to create the promise in which we will receive the events.

  let promise = eventLoopGroup.next().makePromise(of: String.self)

  let server = try ServerBootstrap(group: eventLoopGroup).childChannelInitializer { channel in
    let webSocket = NIOWebSocketServerUpgrader(
      shouldUpgrade: { channel, req in
        return channel.eventLoop.makeSucceededFuture([:])
    },
      upgradePipelineHandler: upgradePipelineHandler
    )

    return channel.pipeline.configureHTTPServerPipeline(
      withServerUpgrade: (
        upgraders: [webSocket],
        completionHandler: { ctx in
          // complete
      })
    )
  }.bind(host: "localhost", port: port).wait()

To boot up the server we need to start waiting for the events with the freshly created promise and server object.

  _ = try promise.futureResult.wait()
  try server.close(mode: .all).wait()

Now we have a running server using WebSocketKit framework. When a client sends text message we are reversing all the characters. For example when a client sends Hello we are sending back olleH.

Vapor app approach

All this seems quite complicated, but don’t worry - by using Vapor app approach, all this complexity goes away.

Client

Creating a client is much easier within Vapor app. You just need to create a new WebSocket instance using an event loop group from app object. Then connect to the network address. Within a closure, you get WebSocket object on which you can register events you want to trigger.

  let url = "wss://echo.websocket.org"
  let _ = WebSocket.connect(to: url, on: app.eventLoopGroup) { ws in
    ws.onText { ws, text in
      print(text)
    }

    ws.send("Hello")
  }

In this example, client sends Hello to the server once connected and prints out to the console any text that is received.

Server

When creating a server we need to provide an endpoint where clients can connect to. Then within a closure, we get WebSocket object and request object.

Similarly, like with the client, we can specify what we want to do when these events are triggered. For instance, once the server receives text it will reverse it and send back.

  app.webSocket("") { request, ws in
    ws.send("You have been connected to WebSockets")

    ws.onText { ws, string in
      ws.send(string.trimmingCharacters(in: .whitespacesAndNewlines).reversed())
    }

    ws.onClose.whenComplete { result in
      switch result {
      case .success():
        print("Closed")
      case .failure(let error):
        print("Failed to close connection \(error)")
      }
    }
  }

Additional to that we can can react once client disconnects and many more.

TL;DR

Swift on the server has gained a lot of popularity especially now since iOS developers can create apps and backend services in the same language.

Most of the Swift server frameworks are built on top of SwiftNIO framework that gives a very granular way to configure WebSockets.

Using Vapor tools like WebSocketKit and app framework itself we can ease this complicated process. You can check out the code samples and start using Swift when you need to deal with WebSockets on the backend.

Links

Posted on by:

fassko profile

Kristaps Grinbergs

@fassko

Experienced Apple tech software engineer. Co-founded Qminder. Co-curator at Swift Weekly Brief. Love cycling and cats. Inclusivity. Vegan lifestyle. Animal freedom.

Discussion

markdown guide