SwiftNIO Tutorial: Practical Guide for Asynchronous Problems

In this tutorial, you’ll solve common asynchronous problems about promises and futures in SwiftNIO by building a quotations app. By Joannis Orlandos.

Leave a rating/review
Download materials
Save for later
Share

Futures and promises abstract the asynchronous creation and usage of a single value. They’re critical to creating services in SwiftNIO because they make asynchronous code more readable. However, code can become more complicated when you add a layer of abstraction.

In this SwiftNIO asynchronous tutorial, you’ll learn how to use futures and promises while still keeping your code simple and concise.

To do this, you’ll build a simple SwiftNIO service called Quotanizer, which can store quotes or serve random ones, ensuring that every quote will only appear once. In the process, you’ll learn:

  • How to hop event loops and use pipeline handlers.
  • How to solve common problems using promises and futures.
  • Methods that make futures easier to use.
Note: If you’re not familiar with SwiftNIO, async programming, or futures and promises, read our tutorial, A Simple Guide to Async on the Server before you begin.

Getting Started

To start this tutorial, download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

You’ll also need Xcode 11 or later and an HTTP client such as RESTed.

The materials archive contains a project in the starter folder, consisting of an HTTP server and a mocked database. This will get you started.

The final folder contains the same project, but after applying all the changes described in this tutorial — useful in case you want to compare it with the result of your work.

To begin, navigate to starter and double-click Package.swift to open it.

Xcode appears, and you’re ready to go! It automatically downloads all dependencies that the project needs — which, in this case, is only the SwiftNIO framework. All the code is in Sources/PromisesFutures, so be sure to expand this folder in the Project navigator.

Group containing the source code

Creating the Web Service

As mentioned before, you’ll start by creating the Quotanizer web service to server and store various quotes. This service will allow adding a quote using a HTTP POST request, and fetching a random quote and removing it from the database using a HTTP GET request.

All SwiftNIO applications depend on an event loop for network communication, which is basically an infinite loop getting “interrupted” by various events, such as network requests. You can learn more about the event loop in A Simple Guide to Async on the Server.

To create a web service, first create a MultiThreadedEventLoopGroup instance by adding the following line to your now-empty main.swift:

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

This creates as many event loops as there are CPU cores. This works well if you need to run a service with as much performance as is available. It’s a good solution for apps that need to handle a lot of load, as web services do.

You can set the numberOfThreads to a smaller number if you run a small service off the event loop. One event loop is enough for many tasks, like database drivers.

Bootstrapping an HTTP Service

To create an HTTP service in SwiftNIO, you must first create a ServerBootstrap. This bootstrap functions as a template for a server socket.

An EventLoop, selected by a round-robin approach, pairs with each new client. This balances clients over the available threads so that, on average, every thread handles the same number of clients.

To create a bootstrap for the HTTP service, add the following code to main.swift:

let bootstrap = ServerBootstrap(group: group)
  // 1
  .serverChannelOption(ChannelOptions.backlog, value: 256)
  // 2
  .serverChannelOption(ChannelOptions.socket(
    SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
  // 3
  .childChannelInitializer { channel in
    channel.pipeline.configureHTTPServerPipeline(
      withErrorHandling: true).flatMap {

      channel.pipeline.addHandler(HTTPHandler(responder: QuoteResponder()))
    }
  }

The code above:

  1. Specifies that you can have up to 256 waiting clients.
  2. Enables reusing the IP address and port so that many threads can receive clients.
  3. Configures each client socket to communicate over HTTP. After that, adds a handler to respond to HTTP requests.

Starting the Service

To use the HTTP service, you need to choose a host and port to publish it on. HTTP’s default port is 80 for unencrypted connections and 443 for encrypted connections. Note that these ports are below 1024, so you need administrative privileges to use them.

Most services will publish themselves on the host 0.0.0.0 or ::1. These addresses will publish the bootstrap on any address linked to the computer.

Still in main.swift, add the following code, and your service is ready to go!

// 1
let host = "::1"
let port = 1111

// 2
let serverChannel = try bootstrap.bind(host: host, port: port).wait()

// 3
guard serverChannel.localAddress != nil else {
  fatalError("Unable to bind to \(host) at port \(port)")
}

// 4
print("Server is running on http://localhost:1111")
try serverChannel.closeFuture.wait()

The code above consists of four steps, where you:

  1. Choose a host that allows anyone to connect to this service if they visit on the selected port.
  2. Bind a socket to this host and port, making it available.
  3. Check if the service is now available and stop the app if binding failed.
  4. Wait for the server socket to shut down. This keeps the app running.
Note: If you don’t .wait() for the server to close, the app will shut down.

Build and run the project and take a look at your Debug Console, where you should see Server is running on http://localhost:1111. Then, go to http://localhost:1111 in a browser. You should see a message saying server error. If both of these tests work well for you, it means your server is good to go!

You might’ve noticed the code above uses methods such as serverChannelOption and childChannelInitializer, as well as the fact the call to bootstrap.bind(host:port:).wait() returns a Channel object. But what is a channel?

Using Channels

In SwiftNIO, channels wrap each socket. Channels are types that can read and write data. Sockets are the most common type of channels.

Each channel has a pipeline consisting of handlers. Each handler transforms either inbound or outbound traffic. Inbound handlers receive data from the channel, usually a socket, that transforms it.

Inbound handlers work from front to back. Each step in the process is a transformation on the output of the previous data. The arrival of new data from a channel initializes these pipelines.

HTTP inbound handlers

Outbound handlers work from back to front, then send the result to the channel. The TCP channel provided by SwiftNIO requires you to send a ByteBuffer. The input that SwiftNIO’s TCP channel receives is also a ByteBuffer.

HTTP outbound handlers

Now that you have your web service set up, it’s time to start using quotes with Quotanizer.