Developing a basic Swift Echo Server using Swift-NIO

Swift-NIO: a port of Netty

I am not a Java or JVM type of developer. That’s probably one of the reasons I never felt the need to try Netty framework. I have been developing all my high-performance server code in Erlang, Elixir or Go and was happy with the tooling.
However, Apple recently published Swift-NIO, a new library and framework to develop cross-platform server and client applications.

I am more a scalable back-end developer than a front-end developer. That said, I love the Swift programming language and have followed the progress since Apple released it in 2014. That’s why this new framework caught my attention. How good is it to write server-side software?

As Swift-NIO is a port of Netty, made by a team led by a prominent Netty contributor, Norman Maurer, I have first looked at Netty design to better understand how Swift-NIO is working. I like what I read about it.

Netty’s concepts provide a very nice generic abstraction that is common to good networking applications. It is a reference framework, used in Java world to build a lot of very advanced server and client tools.

The concepts map very well with Swift programming language. It is a good fit that makes me thinks that this could indeed accelerate Swift server-side development and cross-platform reach.

I bet it could have a big impact and help Swift continue its fast rise as one of the top programming languages.

Principles

Swift-NIO relies on non-blocking IO. It means that you can have a relatively small number of threads managing a very large number of network connections by having an intermediate layer dispatching the network operations that are ready to process to the worker threads.

Network operations are thus non-blocking from the processing thread perspective. They can fully use the CPU, as they can share network operations for a large number of sockets, without the wait.

In Swift-NIO wording, blocking operations are delegated to channels. Channels triggers events on network operations to the event loops in charge of managing the channel. Developers provide the logic of the server or client to the event loop as handlers. Handlers are pieces of code that implement the operation to perform when networking events are triggered.

They can be combined in handler pipelines for extra flexibility. This adds an extra layer for decoupling and makes handlers more reusable.

Implementing a basic server

In client-server world, the ‘Hello World’ application is generally an “Echo” server. The server takes what the client sends and send it back to the client.

This is very easy to implement with Swift-NIO. Let’s see how it could be done.

Please, note that the following steps were tested on MacOS, but they should work on Linux as well if you have Swift installed.

Create Swift project

You can bootstrap your project with Swift command-line:

$ mkdir EchoServer
$ cd EchoServer/
$ swift package init --type executable
Creating executable package: EchoServer
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/EchoServer/main.swift
Creating Tests/

Add Swift-NIO as dependency

To change your project configuration, you will need to edit the Package.swift file.

You can add the Swift-NIO repository in the general package dependency list:

dependencies: [
   .package(url: "https://github.com/apple/swift-nio.git", from: "1.1.0"),
 ],

and add NIO in your EchoServer target dependencies:

.target(
              name: "EchoServer",
              dependencies: ["NIO"]),

The full Package.swift file is as follow:

// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "EchoServer",
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-nio.git", from: "1.1.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "EchoServer",
            dependencies: ["NIO"]),
    ]
)

Finally, you can use the command swift package resolve to download the dependencies:

$ swift package resolve
Fetching https://github.com/apple/swift-nio.git
Fetching https://github.com/apple/swift-nio-zlib-support.git
Cloning https://github.com/apple/swift-nio.git
Resolving https://github.com/apple/swift-nio.git at 1.1.0
Cloning https://github.com/apple/swift-nio-zlib-support.git
Resolving https://github.com/apple/swift-nio-zlib-support.git at 1.0.0

The EchoServer code

The server code is implemented in Sources/EchoServer/main.swift.

The ChannelInboundHandler

The EchoServer code is very simple. The first part if about creating the ChannelInboundHandler implementation, that will implement the server logic.

In our case, we will implement only five callback methods:

  • channelRegistered: invoked on client connection.
  • channelUnregistered: invoked on client disconnect.
  • channelRead: invoked when data are received from the client.
  • channelReadComplete: invoked when channelRead as processed all the read event in the current read operation. We use this to ensure we flush the data to the socket.
  • errorCaught: invoked when an error occurs.

The other important thing to note is that each callback method receives a context that can be used to get information on the session. It contains information about the client IP address, for example. The context also provides methods, for example, to write data back to a given client.

private final class EchoHandler: ChannelInboundHandler {
    public typealias InboundIn = ByteBuffer
    public typealias OutboundOut = ByteBuffer

    // Keep tracks of the number of message received in a single session
    // (an EchoHandler is created per client connection).
    private var count: Int = 0

    // Invoked on client connection
    public func channelRegistered(ctx: ChannelHandlerContext) {
        print("channel registered:", ctx.remoteAddress ?? "unknown")
    }

    // Invoked on client disconnect
    public func channelUnregistered(ctx: ChannelHandlerContext) {
        print("we have processed \(count) messages")
    }

    // Invoked when data are received from the client
    public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
        ctx.write(data, promise: nil)
        count = count + 1
    }

    // Invoked when channelRead as processed all the read event in the current read operation
    public func channelReadComplete(ctx: ChannelHandlerContext) {
        ctx.flush()
    }

    // Invoked when an error occurs
    public func errorCaught(ctx: ChannelHandlerContext, error: Error) {
        print("error: ", error)
        ctx.close(promise: nil)
    }
}

Setting up the server

The rest of the code is about setting up the server. Swift-NIO provide some “Bootstrap” helpers to make the process even simpler for a common situation.

In that example, we use the ServerBootstrap.

// Create a multi thread even loop to use all the system core for the processing
let group = MultiThreadedEventLoopGroup(numThreads: System.coreCount)

// Set up the server using a Bootstrap
let bootstrap = ServerBootstrap(group: group)
        // Define backlog and enable SO_REUSEADDR options atethe server level
        .serverChannelOption(ChannelOptions.backlog, value: 256)
        .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)

        // Handler Pipeline: handlers that are processing events from accepted Channels
        // To demonstrate that we can have several reusable handlers we start with a Swift-NIO default
        // handler that limits the speed at which we read from the client if we cannot keep up with the
        // processing through EchoHandler.
        // This is to protect the server from overload.
        .childChannelInitializer { channel in
            channel.pipeline.add(handler: BackPressureHandler()).then { v in
                channel.pipeline.add(handler: EchoHandler())
            }
        }

        // Enable common socket options at the channel level (TCP_NODELAY and SO_REUSEADDR).
        // These options are applied to accepted Channels
        .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1)
        .childChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
        // Message grouping
        .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 16)
        // Let Swift-NIO adjust the buffer size, based on actual trafic.
        .childChannelOption(ChannelOptions.recvAllocator, value: AdaptiveRecvByteBufferAllocator())
defer {
    try! group.syncShutdownGracefully()
}

// Bind the port and run the server
let channel = try bootstrap.bind(host: "::1", port: 9999).wait()

print("Server started and listening on \(channel.localAddress!)")

// Clean-up (never called, as we do not have a code to decide when to stop
// the server). We assume we will be killing it with Ctrl-C.
try channel.closeFuture.wait()

print("Server terminated")

You can check the full code of the server on Github: Sources/EchoServer/main.swift.

Running the server

You can run the server with the command:

$ swift package resolve
Fetching https://github.com/apple/swift-nio.git
Fetching https://github.com/apple/swift-nio-zlib-support.git
Cloning https://github.com/apple/swift-nio.git
Resolving https://github.com/apple/swift-nio.git at 1.1.0
Cloning https://github.com/apple/swift-nio-zlib-support.git
Resolving https://github.com/apple/swift-nio-zlib-support.git at 1.0.0
MBP-de-Mickael:EchoServer mremond$ vim Sources/EchoServer/main.swift 
MBP-de-Mickael:EchoServer mremond$ swift run 
Compile CNIODarwin shim.c
Compile CNIOLinux shim.c
Compile CNIOAtomics src/c-atomics.c
Compile Swift Module 'NIOPriorityQueue' (2 sources)
Compile Swift Module 'NIOConcurrencyHelpers' (2 sources)
Compile Swift Module 'NIO' (47 sources)
Compile Swift Module 'EchoServer' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/EchoServer
Server started and listening on [IPv6]::1:9999

Swift will compile the server and run it.

You can also build the server with swift build and run the executable from the .build directory:

$ swift build
$ .build/debug/EchoServer

Testing the server with Telnet client

You can connect to your echo server with telnet.

$ telnet localhost 9999
Trying ::1...
Connected to localhost.
Escape character is '^]'.
Hello
Hello
Bye 
Bye
^]
telnet> close
Connection closed.

As you see, all your commands are repeated, sent back from your EchoServer.

The server will print a message on each connection and the total of messages processed during the session on each disconnect:

channel registered: [IPv6]::1:62759
we have processed 2 messages

Note: On latest MacOS version, telnet is not installed as default, but you can install it easily with homebrew.

Conclusion

Swift-NIO is a very interesting framework for developing server and client applications. I think it will help spread the use of Swift on the server-side. Vapor, a well-known server-side framework, has already changed their own code to use Swift-NIO as a building brick, and this is only the beginning.

Netty is at the heart of many server-side components in the Java world (Akka, PlayFramework, Gatling, Finagle, Cassandra, Spark, Vert.x, …). Swift-NIO will play a similar role for Swift, making it an interesting contender among the wide choice of languages you can use for back-end development.

Please, send me comments and feedback to let me know if you are interested by more content on Swift-NIO, server-side Swift (or Swift in general).

Leave a Comment