A µTutorial on Swift NIO

In a surprise move, Apple released
swift-nio
on March 1st.
Today we are going to have a look on how to use that Swift package,
and build a tiny but useful web framework along the way.
Say hello to µExpress. Again.

We also throw in support for JSON.
And all that with just a µscopic amount of code.
The final package has a little more than 350 lines of code
(as if that would say anything).
You think you need that [insert the latest hype] framework?
Quite likely it is just monolithic bloat and you don’t.

First: What is Swift NIO?

SwiftNIO
is a cross-platform asynchronous event-driven network application
framework for rapid development of maintainable high performance protocol
servers & clients.

It is what?
Well, it is a toolkit to write Internet servers (and clients) of various kinds.
Those can be
web servers (HTTP),
mail servers (IMAP4/SMTP),
Redis servers,
IRC chat servers, etc.
It is built with a focus on very high performance and scalability.

As a regular HTTP-toolkit developer, who uses stuff along the lines of
Rails or Node, you usually do not care about directly interfacing with
Swift NIO.
It becomes relevant if you are
adding a completely new protocol,
add some common network level functionality
(like rate limiters, content compressors, XSLT renderers, etc.),
have a very performance sensitive endpoint,
or want to build an own web framework. Hey, the latter is us!

In other words: Swift NIO is a rather low level API,
somewhat similar to the Apache 2 module API.

To implement our web framework, we are going to create those components:

There is a little setup overhead before we can actually see something,
but not that much - a few files, it is µ - so stick with us.
And if you are really lazy and just want to follow along,
you can clone the
finished project at GitHub
🤓

Step 0: Prepare the Xcode Project

Instead of fiddling around with Swift Package Manager,
we use
swift xcode
to use the package directly within Xcode.
Grab Homebrew if you don’t have it yet, and install the
Swift NIO image
using:

Within Xcode, create a new project (⌘-Shift-N), and select the
“Swift-NIO” template:

Give it a name, e.g. “MicroExpress”.
Make sure that the “Generate Server Boilerplate” option is unchecked,
and the the
“Include SwiftNIO HTTP1 module” is option is checked
(do not check the µExpress option, this is for including
the finished µExpress framework, which we are about to build):

Build the project.

Step 1: Application Class

The primary purpose of the Express application class is
starting and running the HTTP server. This part (add it to the main.swift):

// File: Express.swift - create this in Sources/MicroExpressimportFoundationimportNIOimportNIOHTTP1openclassExpress{letloopGroup=MultiThreadedEventLoopGroup(numberOfThreads:System.coreCount)openfunclisten(_port:Int){letreuseAddrOpt=ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET),SO_REUSEADDR)letbootstrap=ServerBootstrap(group:loopGroup).serverChannelOption(ChannelOptions.backlog,value:256).serverChannelOption(reuseAddrOpt,value:1).childChannelInitializer{channelinchannel.pipeline.configureHTTPServerPipeline()// this is where the action is going to be!}.childChannelOption(ChannelOptions.socket(IPPROTO_TCP,TCP_NODELAY),value:1).childChannelOption(reuseAddrOpt,value:1).childChannelOption(ChannelOptions.maxMessagesPerRead,value:1)do{letserverChannel=trybootstrap.bind(host:"localhost",port:port).wait()print("Server running on:",serverChannel.localAddress!)tryserverChannel.closeFuture.wait()// runs forever}catch{fatalError("failed to start server: \(error)")}}}

Discussion

A Swift NIO
EventLoop
is pretty much the same like a
DispatchQueue.
It handles IO events,
one can queue blocks to it for later execution (like DispatchQueue.async),
you can schedule a timer (like DispatchQueue.asyncAfter).
The
MultiThreadedEventLoopGroup
is somewhat like a concurrent queue.
It is using multiple threads to distribute workload thrown at it.

The next thing is the listen function (dropping all the Java-ish boilerplate
to setup the common options):

openfunclisten(_port:Int){...letbootstrap=ServerBootstrap(group:loopGroup)....childChannelInitializer{channelinchannel.pipeline.configureHTTPServerPipeline()// this is where the action is going to be!}...letserverChannel=trybootstrap.bind(host:"localhost",port:port).wait()

It uses the
ServerBootstrap
object to setup and configure the “Server Channel”.
The Bootstrap object is just a helper to perform the setup, after it is done,
it is done.

A Swift NIO
Channel
is similar to a Swift Foundation
FileHandle.
It (usually) wraps a Unix file descriptor (socket, pipe, file, etc.),
and provides operations on top of it.

In this case we have a “Server Channel”, that is, a passive socket which
is going to accept incoming connections.
The latter are again represented as
Channel
objects and are configured in the childChannelInitializer shown above.
The channel argument is the freshly setup connection to the client.

Channels maintain a
ChannelPipeline,
which is simply a set of “handler” objects
(in a way they are not that different to Middleware).
They get executed in sequence and can transform the incoming and outgoing data,
or do other actions.

So far we call channel.pipeline.configureHTTPServerPipeline().
This adds handlers to the pipeline which:
transform the incoming data
(plain bytes) into higher level HTTP objects (i.e. requests),
and “render” outgoing HTTP objects (i.e. responses) back to bytes.
Which are then written back to the client.

Next we are going to add our own handler to that pipeline.

Step 1b: Add an own NIO Handler

Our handler is going to receive HTTP request parts (because we put
configureHTTPServerPipeline in the pipeline before us),
and it is going to send back HTTP to the client:

Discussion

Our handler is a
ChannelInboundHandler,
which means it receives incoming data from the client (web browser, curl, etc.).
The type of the data it expects is specified using the InboundIn
typealias (which refers a
generic associated type
of a Swift protocol):

typealiasInboundIn=HTTPServerRequestPart

This means that the data we are going to receive/read are
HTTPServerRequestPart
items,
a Swift enum, with the cases
.head (the HTTP request head),
.body (some body byte data) and
.end (the request was fully read).

Those items is passed into the channelRead function when new data becomes
available:

The data is passed around as NIOAny objects for efficiency reasons and
needs to be unwrapped.
Again: The data in there are HTTPServerRequestPart items, because the
NIOHTTP1 handler sits in front of our handler in the channel pipeline.
It converts (parses) the HTTP bytes into a sequence of the HTTP enum values.

As a temporary measure, lets return some “Hello World” data to the browser,
add this to the .head section of the switch (below the print):

(we are going to put the code above in a matching method of our ServerResponse
object).

A few points worth noting:

We cannot just write back bytes to the channel. Just like we receive HTTP
items, we need to send HTTP items (.head, .body and .end). The NGHTTP1
handler is going to convert those to actual byte data on the socket.

We call .write on the channel. That thing does not actually write data
out to the socket. To send it to the socket, the channel must be flushed.
Which is why we send the response .end via writeAndFlush.

When sending byte data (the content of the response), Swift NIO usually
expects us to use a
ByteBuffer.
That thing is again very similar to a Foundation
Data
object.

The final detail is that we close the channel (aka connection) after the
last writeAndFlush. But we cannot just immediately close the connection,
because the writes may not have happend yet.
Hence we attach to the
Future
returned by writeAndFlush and close the
channel after that has completed (then).

Summary: Step 1

We now have a working Hello World HTTP endpoint. We can receive requests
and return schwifty responses.
In the next step we are going to wrap that functionality in nice
IncomingMessage and ServerResponse
objects.

Our Express application object can create a server Channel
to accept incoming connections using a Bootstrap object.
We add the NIOHTTP1 handlers to the Pipeline of incoming client connections,
just before adding our own Handler to parse and emit typed HTTP protocol
items.

Step 2: Request/Response objects

2.1 IncomingMessage

When our Express.HTTPHandler receives a HTTP .head item,
it passes over a
HTTPRequestHead
struct in an associated value of the enum case.
We are going to wrap that in an own IncomingMessage class.

The primary enhancement of this class is the userInfo
storage. The storage can later be used by middleware to pass along
data to subsequent middleware.

Why are we wrapping this instead of just using the API struct?
For one, as a struct,
HTTPRequestHead
cannot be extended with
additional stored properties (yet?).
Which we need to associate more data w/ the request.
Also, we are going to pass the request around a lot.
Passing it around by reference is cheaper than copying
the struct all the time.
Finally:
HTTPRequestHead
represents just the HTTP header,
not the actual HTTP message (i.e. not the body).

This is how you get the HTTP method, the request URL, and the User-Agent:

(Feel free to add convenience properties/functions/subscripts to
IncomingMessage,
for this we want to keep it µ.)

2.2 ServerResponse

The ServerResponse incorporates the same code we
used above in our “temporary measure” to send a “Hello World”
to the client.
On creation it gets passed in the associated Channel. It then emits the
appropriate HTTP items (.head, .body and .end).

Initially the primary function is an Express-like send method,
which writes the HTTP header, the response body, and closes the
response.

// File: ServerResponse.swift - create this in Sources/MicroExpressimportNIOimportNIOHTTP1openclassServerResponse{publicvarstatus=HTTPResponseStatus.okpublicvarheaders=HTTPHeaders()publicletchannel:ChannelprivatevardidWriteHeader=falseprivatevardidEnd=falsepublicinit(channel:Channel){self.channel=channel}/// An Express like `send()` function.openfuncsend(_s:String){flushHeader()letutf8=s.utf8varbuffer=channel.allocator.buffer(capacity:utf8.count)buffer.write(bytes:utf8)letpart=HTTPServerResponsePart.body(.byteBuffer(buffer))_=channel.writeAndFlush(part).mapIfError(handleError).map{self.end()}}/// Check whether we already wrote the response header./// If not, do so.funcflushHeader(){guard!didWriteHeaderelse{return}// done alreadydidWriteHeader=truelethead=HTTPResponseHead(version:.init(major:1,minor:1),status:status,headers:headers)letpart=HTTPServerResponsePart.head(head)_=channel.writeAndFlush(part).mapIfError(handleError)}funchandleError(_error:Error){print("ERROR:",error)end()}funcend(){guard!didEndelse{return}didEnd=true_=channel.writeAndFlush(HTTPServerResponsePart.end(nil)).map{self.channel.close()}}}

When encountering errors we just log and close the socket.
You would probably want to add onError listeners here,
similar to Noze.io.

How do you use it, simple:

response.send("Hello World!")

2.3 Hook them up to the HTTPHandler

Lets hook up our new IncomingMessage and ServerResponse objects to the
Express.HTTPHandler.

We don’t use the request yet, but we can send data w/ much less effort.

Discussion

The ServerResponse uses the HTTPHeaders struct and HTTPResponseStatus enum
from
NIOHTTP1.
They do what their name says…

As mentioned we init the object with the
Channel,
this is where we are going to perform our writes on.
We discussed writes already, but lets review it again, this is what we do:

_=channel.writeAndFlush(part).mapIfError(handleError).map{self.end()}

We pass the data into the writeAndFlush as HTTPPart items -
a .head (status + headers), .body (response body) and .end (response end).
NIOHTTP1
will convert that to the actual HTTP/1.x protocol on the socket.

The writeAndFlush method works asynchronously,
it just enqueues the part to be written,
and returns a so called
Future
item.
To that Future you can attach a handler for the case
when errors happen
(mapIfError(handleError)
and for the case
when the operation was successful
(map({ self.end())}).
Either one will be invoked when the write has completed or error’ed.
In µExpress, like in Express, we tunnel the errors through a single error
handler. Which in our case just logs the error, and closes the connection. µ.

Summary: Step 2

We encapsulated the rather complicated Swift NIO operations in neat,
Express like IncomingMessage and ServerResponse classes.
Which we hooked up the the Setty HTTP handler object.

Yet the core functionality (sending “Hello World” to the world) is still
embedded deep within the Express.HTTPHandler object.

Step 3: Middleware and Routing

Step 3.1: Middleware

The term “middleware” has many meanings,
but in the context of
Connect /
Express.js
it is simply a closure/function which can opt in to handle a HTTP request
(or not).

A middleware function gets a request, a response and
another function to call if it didn’t (completely) handle
the request (next).
An example middleware function:

That’s it. There is no magic to a middleware, it is just a simple
function!

Step 3.2: Router

In real
Express
there is a little more to it
(e.g. mounting),
but for our purposes think of a router as a simple list of
middleware functions.
Middleware is added to that list using the use() function.

When handling a request, the router just steps through its list
of middleware until one of them doesn’t call next.
And by that, finishes the request handling process.

// File: Router.swift - create this in Sources/MicroExpressopenclassRouter{/// The sequence of Middleware functions.privatevarmiddleware=[Middleware]()/// Add another middleware (or many) to the listopenfuncuse(_middleware:Middleware...){self.middleware.append(contentsOf:middleware)}/// Request handler. Calls its middleware list/// in sequence until one doesn't call `next()`.funchandle(request:IncomingMessage,response:ServerResponse,nextupperNext:@escapingNext){letstack=self.middlewareguard!stack.isEmptyelse{returnupperNext()}varnext:Next?={(args:Any...)in}vari=stack.startIndexnext={(args:Any...)in// grab next item from matching middleware arrayletmiddleware=stack[i]i=stack.index(after:i)letisLast=i==stack.endIndexmiddleware(request,response,isLast?upperNext:next!)}next!()}}

Note: This leaves out Error middleware.
ExExpress
has an implementation of that,
if you want to see how you might implement that part.

This doesn’t do any actual routing yet 😀, but we’ll get to that soon!
How do you use it - as shown before:

router.use{req,res,nextinprint("We got a request:",req)next()// do not stop here}router.use{_,res,_inres.send("hello!")// response is done.}router.use{_,_,_in// we never get here, because the // middleware above did not call `next`}

Discussion

The one non-obvious thing here is the handle method of the Router.
Why not just loop through the array and call the middleware directly,
like we did in the HTTP-API version of
µExpress?
What is that next closure thing?

Our µExpress implementation of a Router can run completely asynchronously.
When a middleware runs, it does not have to call next immediately!
Which is also the reason why the next closure passed in is marked as
@escaping.
To give an example,
a middleware delaying any incoming request by 2 seconds:

app.use{_,res,nextin// run the closure/task in 2 seconds_=res.channel.eventLoop.scheduleTask(in:.seconds(2)){next()}}

Notice how our call to next “escapes” the scope. It will run at an arbitrary
later time - and that while we are stepping through the Router’s array of
middlewarez.
To solve that, our embedded next closures captures the current position
in the middleware array, as well as the array itself (in the stack variable):

The next closure - and its captured state - is shared between all invocations.
When it gets called, it advances the index attached to it, and thereby moves
forward in the middleware stack.

Important: Never do this in any asynchronous Express implementation
(or Swift NIO handler for that matter):

app.use{_,res,nextinsleep(2)// in here we block ALL CONNECTIONS on this threadnext()}

Never call blocking functions or do blocking I/O of any sorts.
The active thread is shared between many connections (Channels),
which will all block.

The attentive reader may notice that the Router itself acts like a
middleware (its handle method matches the Middleware signature).
This is how you can chain together Routers w/ little effort
(add Routers to other Routers).

Step 3.3: Hook up the Router to the App

Let the App itself be a Router

This one is easy. In Express the application object itself is also a Router,
that is, you can call app.use { ... } on it.
The only thing we have to do here, is make Router a superclass of our
existing Express app object.

Notice how we pass in a next function when we call
handle.
This next function is called when no middleware handled
the request, i.e. no middleware did not call next.
This is also known as the final handler.
We emit a 404 response.

There is one more change:
We need to pass in the router to the handler when we create it as part of the
Bootstrap.
And since the Express app object is a router itself now,
we just pass in self:

// File: Router.swift - add this to Router.swiftpublicextensionRouter{/// Register a middleware which triggers on a `GET`/// with a specific path prefix.publicfuncget(_path:String="",middleware:@escapingMiddleware){use{req,res,nextinguardreq.header.method==.GET,req.header.uri.hasPrefix(path)else{returnnext()}middleware(req,res,next)}}}

The trick here is that we embed the middleware within another middleware.
The enclosing middleware only runs the embedded one
when the HTTP method and path matches,
otherwise it just passes on using next.

Step 4: Reusable Middleware

Middleware functions can do anything you like,
but quite often reusable middleware - which is what we long for -
extracts data from the request and passes on a parsed form to the actual
“handler” middleware.
It could be some form of Auth, or JSON body parsing, or:
One thing you often want to do: parse query parameters.
Let’s do a reusable middleware for that!

// File: ServerResponse.swift - add this to the endpublicextensionServerResponse{/// A more convenient header accessor. Not correct for/// any header.publicsubscript(name:String)->String?{set{assert(!didWriteHeader,"header is out!")ifletv=newValue{headers.replaceOrAdd(name:name,value:v)}else{headers.remove(name:name)}}get{returnheaders[name].joined(separator:", ")}}}

Note:
This subscript does not work for all HTTP headers, but for a lot of simple
ones it does.

First thing we need is a model containing the data we want to deliver to the
API client.
In this case a list of todos (the real API has more fields, but it is enough
to get going):

// File: TodoModel.swift - create this in Sources/MicroExpressstructTodo:Codable{varid:Intvartitle:Stringvarcompleted:Bool}// Our fancy todo "database". Since it is// immutable it is webscale and lock free, // if not useless.lettodos=[Todo(id:42,title:"Buy beer",completed:false),Todo(id:1337,title:"Buy more beer",completed:false),Todo(id:88,title:"Drink beer",completed:true)]

Note that we are using the Swift 4
Codable
feature.
To deliver the JSON to the client, we enhance our ServerResponse object
with a json() function (similar to what Express does).
It can deliver any Codable object as JSON:

// File: ServerResponse.swift - add this to ServerResponse.swiftimportFoundationpublicextensionServerResponse{/// Send a Codable object as JSON to the client.funcjson<T:Encodable>(_model:T){// create a Data struct from the Codable objectletdata:Datado{data=tryJSONEncoder().encode(model)}catch{returnhandleError(error)}// setup JSON headersself["Content-Type"]="application/json"self["Content-Length"]="\(data.count)"// send the headers and the dataflushHeader()varbuffer=channel.allocator.buffer(capacity:data.count)buffer.write(bytes:data)letpart=HTTPServerResponsePart.body(.byteBuffer(buffer))_=channel.writeAndFlush(part).mapIfError(handleError).map{self.end()}}}

Finally, lets create a middleware which sends our todos to the client:

// File: CORS.swift - create this in Sources/MicroExpresspublicfunccors(allowOriginorigin:String)->Middleware{return{req,res,nextinres["Access-Control-Allow-Origin"]=originres["Access-Control-Allow-Headers"]="Accept, Content-Type"res["Access-Control-Allow-Methods"]="GET, OPTIONS"// we handle the optionsifreq.header.method==.OPTIONS{res["Allow"]="GET, OPTIONS"res.send("")}else{// we set the proper headersnext()}}}

To use it, add the cors middleware above your TodoMVC middleware in
main.swift,
e.g. like this:

Summary

That’s it for now.
On top of Swift NIO
we built an asynchronous micro-framework featuring
middleware, routing, JSON and CORS support
in about 350 lines of code.
Sure, it is not everything you may need yet, but it is a pretty decent way
to write Swift HTTP and JSON endpoints.

I hope we have shown that you may not need some hipster “framework”
and can accomplish a lot w/ very little code and dependencies.
In about an hour you can spin your own.
Choose independent reusable modules with as little dependencies as possible.