Getting Started with SwiftNIO
You might have heard a few rumblings a while back about SwiftNIO (pronounced Swift Neo, like the guy from the Matrix ;-)). I was pretty excited when I first read about it and wanted to check it out and see how it differed from Netty in Java.
So far, if you are familiar with Netty, you should feel right at home with SwiftNIO. If you aren’t, keep reading.
Why Server-Side Swift?
Why not? If you are already an iOS developer and love writing Swift, why not try to build your backend in Swift? Compared to other languages like Java, C#, Ruby, Go and Node, Swift does not have nearly the same amount of packages available for you to reuse on the backend. However, all those communities had to start somewhere. If you look at newer languages like Go and Javascript on the backend, it’s people like you that are interested in the language and solving problems that help push the community forward.
Why SwiftNIO?
There are already some great server-side Swift frameworks like Vapor, Kitura and Perfect that you can get started in to build a web app. Vapor in fact uses SwiftNIO for their networking layer. What I want to do is introduce you to some lower level basics to better understand the higher level abstractions.
In this post, we are going to look at SwiftNIO. Taken from their Github page:
SwiftNIO is a cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
It’s like Netty, but written for Swift.
SwiftNIO Architecture
First, let’s get a brief overview of SwiftNIO’s components.
A Channel
is a construct that is responsible for handling inbound and outbound events. It connects to the underlying networking socket.
A Channel
has a ChannelPipeline
which is a doubly linked list of ChannelHandler
objects.
The ChannelHandlers
typically contain your application logic. ChannelHandler
methods are triggered when networking events occur and are processed in order. In essence, the ChannelPipeline
is a pipeline of data processing events.
Getting started
This tutorial is for Mac, but the steps for Linux should be quite similar. Since we are on the server, we are going to use Swift Package Manager (SPM). There are two parts:
- create a TCP client
- create a TCP server
Part 1: Create Client
We are going to run a few commands to create our project structure and generate our Xcode project. Open up your Terminal and run these commands:
$ mkdir TCPClient
$ cd TCPClient
$ swift package init --type executable
$ swift package generate-xcodeproj
$ open TCPClient.xcodeproj/
What this does is:
- create a new directory for your project called TCPClient
- change directory to TCPClient
- have SPM create a command line application rather than a framework
- have SPM generate an Xcode project for you
- open the newly created TCPClient Xcode project
In your Package.swift
file, we need to pull in SwiftNIO as a dependency. At the time of this article, we are pulling in version 1.5.1
. Your Package.swift
should look like:
// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(
name: "TCPClient",
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", .exact("1.5.1"))
],
targets: [
.target(
name: "TCPClient",
dependencies: ["NIO"]),
]
)
Note: We are using an exact dependency for this article, but you alternatively can take advantage of SPM’s semantic versioning and use:
.package(url: "https://github.com/apple/swift-nio.git", from: "1.5.1")
This way we depend on the minor version that we require and still get all the latest and greatest fixes and performance enhancements from the great team at Apple and other contributors to SwiftNIO!
Installing dependencies
After you add your dependencies, have SPM update your dependencies for your project and re-generate the Xcode project.
$ swift package update
$ swift package generate-xcodeproj
TCPClientHandler
The first class we are going to create is TCPClientHandler.
The channelActive
method is the first method we override in our subclass of ChannelInboundHandler
. This method creates a message of “SwiftNIO rocks!”. Within a ChannelPipeline
, when a ChannelHandler
is added, a ChannelHandlerContext
is created to interact with the ChannelHandlers
in the ChannelPipeline
. A ChannelHandlerContext
has a reference to the previous and next ChannelHandler
. By using this ChannelHandlerContext
, we get a reference to the Channel
and use its ByteBufferAllocator
to allocate a ByteBuffer
, SwiftNIO’s container for bytes. We write our message to the buffer and call writeAndFlush
on the ChannelHandlerContext
. This enqueues the data to be written on the next flush
event, which writes the data to the socket. We pass nil
to the promise since we are not interested in getting notified when the data has been flushed.
The channelRead
method unwraps the incoming data into a buffer where we unwrap the string and print it to the console. When there are no more bytes to be received, we close the channel.
The errorCaught
method closes the channel if an error is received.
TCPClient
The second class we are going to create is TCPClient.
An EventLoop
is an object that waits for incoming events (e.g. data received) and fires a callback when they do. An EventLoopGroup
handles multiple EventLoops
. Both EventLoop
and EventLoopGroup
are protocols and MultiThreadedEventLoopGroup
is an implementation of EventLoopGroup
. Because this is a simple client, we can use just one thread for our client.
The client also needs to connect to a host and port, so we set those in our initializer.
We have a variable called bootstrap
that is of type ClientBootstrap
. This is what we use to connect to the server in the start
method.
Main
The last file is the main.swift
. This is what actually starts our TCPClient.
Here we create an instance of our TCPClient, passing in the location of our server, localhost
, and the port 3010
to connect to and run the start method. Next, let’s look at creating a server to connect to.
Part 2: Create Server
We are going to follow the same steps as in part 1 and create a new Xcode project for our server.
$ mkdir EchoServer
$ cd EchoServer
$ swift package init --type executable
$ swift package generate-xcodeproj
$ open EchoServer.xcodeproj/
Our Package.swift
should look like:
// swift-tools-version:4.0// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(
name: "EchoServer",
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git",
.exact("1.5.1"))
],
targets: [
.target(
name: "EchoServer",
dependencies: ["NIO"]),
]
)
Now you are ready to install your dependencies again.
$ swift package update
$ swift package generate-xcodeproj
Creating the server is very similar to creating the client. We are going to need a handler to handle the data and a way to bootstrap our server. Let’s start with handling our data first.
EchoHandler
The EchoHandler
is going to echo what it receives back to the client. Like in the TCPClientHandler
, we subclass ChannelInboundHandler
.
In our channelRead
method, we unwrap the incoming data into the buffer, read the amount of readable bytes, print that to the console, and write that data. When the read is complete, it flushes that data to the socket.
EchoServer
In our EchoServer, we again need a MultiThreadedEventLoopGroup
, a host and a port.
Now because we are on the server, we give the MultiThreadedEventLoopGroup
more cores. Also, SwiftNIO gives us ServerBootstrap
to use on the server instead of using ClientBootstrap
.
In the childChannelInitializer
closure, we add an instance of our EchoHandler
.
Main
Our main.swift
for our server is very similar to our TCPClient main.swift
. We create an instance of EchoServer on the host, localhost
and port 3010
and run start
.
Run it
Now that we have both a client and server, let’s give this a go. One of the nice things about Swift on the server is you can still use Xcode.
Server:
- choose the EchoServer scheme to run on My Mac and hit Run
- once you see a message in the Xcode console that you are connected on port
3010
, run the client
Client:
- choose the TCPClient scheme to run on My Mac and hit Run
Now you should see “SwiftNIO rocks!” in both the console of the server and client. The client connects to the server on port 3010
and sends the message “SwiftNIO rocks!”. The server reads those bytes, prints to its own console, and writes back out to the channel
. The client then receives that message, prints to its own console, and closes the channel since there are no more bytes to read.
For the full projects:
Conclusion
Now you’ve created a client and server with SPM, SwiftNIO, and Xcode. We looked at some of the basic constructs of SwiftNIO and were able to send and receive a message through TCP. Next time we’ll look at another backend technology with Swift. Stay tuned!