Skip to main content

Swift NIO, Apple's High Performance, Event-Driven, Networking Framework

Swift NIO, Apple's High Performance, Event-Driven, Networking Framework

A few weeks ago Apple released a new open source framework on GitHub called Swift-NIO.  What is Swift-NIO? The opening line on GitHub describes the project as a cross platform, event driven, non-blocking, application framework to develop high performance networking applications in Swift.  So what does that mean? That means that Apple has developed a foundational project for any application that may need take advantage of an asynchronous networking framework, similar to that of Node.js.  As an example, Swift on the server or any other network based application, like Messages, would be the first projects that I could see taking advantage of Swift-NIO. And that is why I wanted to write this article, to briefly touch on the technology involved with Swift-NIO and to describe what has been happening with Swift on the Server and why I think this is great step forward for the Swift community.

 

Swift-NIO Under the Hood 👨‍💻

Swift-NIO is a very interesting project under the hood.  The project is based around the concept that execution is asynchronous and Futures and Promises are created to manage almost all event operations by way of notifications from the kernel.  To achieve this, the core component of Swift-NIO’s architecture is the use of event loops. The event loop is an endless loop listening for events to be dispatched from the kernel so the event loop can honor a promise and distribute data to a channel to perform work needed.  This workflow is the same for both inbound and outbound events. Providing support for reading data off of a socket or writing data back to an outbound connection. Thus providing a mechanism of non-blocking execution between the kernel and the application.

So how is non-blocking IO useful and why would I want to use this architecture? Well, the main goal for any non-blocking IO architecture, especially in networking, is that reading and writing data from a socket is not blocked by what has happened in front of the event.  Instead, handlers are setup to fulfill promises when IO comes in from the kernel.  Blocking execution relies on the premise that IO from a socket is delegated in the order it was received and can create a slow down faster if things become overloaded. So, the main reason you would want to use a non-blocking IO architecture is the performance gains you can achieve when pushing large amounts of connections through an application like this.  

To demonstrate an example, I put together a brief video of three chat clients all connected to a server and sending messages back and forth.  This demonstrates how Swift-NIO's event loop listens for both read and writes on a socket and then distributes the IO to any client listening on localhost:9899.

The chat client is pretty interesting, when I first looked at the source code it reminded me a lot of C socket programming but with Swift APIs wrapped around the methods.  For example, the code below bootstraps an .ip connection with the host and port passed in from the command line.  The bootstrap constant setups a connection using SOL_SOCKET and SO_REUSEADDR as options.  After the connection is established a ChatHandler instance is setup as a channel handler to fulfill promises to event loop.

let group = MultiThreadedEventLoopGroup(numThreads: 1)
let bootstrap = ClientBootstrap(group: group)
    // Enable SO_REUSEADDR.
    .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
    .channelInitializer { channel in
        channel.pipeline.add(handler: ChatHandler())
    }
defer {
    try! group.syncShutdownGracefully()
}
 
// First argument is the program path
let arguments = CommandLine.arguments
let arg1 = arguments.dropFirst().first
let arg2 = arguments.dropFirst().dropFirst().first
 
let defaultHost = "::1"
let defaultPort = 9999
 
enum ConnectTo {
    case ip(host: String, port: Int)
    case unixDomainSocket(path: String)
}
 
let connectTarget: ConnectTo
switch (arg1, arg1.flatMap { Int($0) }, arg2.flatMap { Int($0) }) {
case (.some(let h), _ , .some(let p)):
    /* we got two arguments, let's interpret that as host and port */
    connectTarget = .ip(host: h, port: p)
case (.some(let portString), .none, _):
    /* couldn't parse as number, expecting unix domain socket path */
    connectTarget = .unixDomainSocket(path: portString)
case (_, .some(let p), _):
    /* only one argument --> port */
    connectTarget = .ip(host: defaultHost, port: p)
default:
    connectTarget = .ip(host: defaultHost, port: defaultPort)
}
 
let channel = try { () -> Channel in
    switch connectTarget {
    case .ip(let host, let port):
        return try bootstrap.connect(host: host, port: port).wait()
    case .unixDomainSocket(let path):
        return try bootstrap.connect(unixDomainSocketPath: path).wait()
    }
}()
 
print("ChatClient connected to ChatServer: \(channel.remoteAddress!), happy chatting\n. Press ^D to exit.")

The ChatHandler's main goal to read data coming in off of the connection, write that data to a buffer, and then write that buffer data to STDOUT so it can be viewed in the chat window.  The ChatHandler class is setup as a very straight forward read loop. 

One more thing.  The InboundIn and the OutboundOut ByteBuffer's are setup as in this class as typealiases, but I cannot see where they are being used?  If I missed something please leave a comment and let me know!  Possibly I will put in a pull request to remove these two lines.  They might have been left there on accident while designing this class.

private final class ChatHandler: ChannelInboundHandler {
    public typealias InboundIn = ByteBuffer
    public typealias OutboundOut = ByteBuffer
 
    public func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
        var buffer = self.unwrapInboundIn(data)
        while let byte: UInt8 = buffer.readInteger() {
            fputc(Int32(byte), stdout)
        }
    }
 
    public func errorCaught(ctx: ChannelHandlerContext, error: Error) {
        print("error: ", error)
 
        // As we are not really interested getting notified on success or failure we just pass nil as promise to
        // reduce allocations.
        ctx.close(promise: nil)
    }
}

The last part of the chat client application that I wanted to mention is the write loop.  The write loop just spins waiting for data to wrote to STDIN by the user. Once the data is picked up from STDIN the write loop fills a channel buffer and the cannel writes the data to the socket connection for the downstream event loop to pick it up.

while let line = readLine(strippingNewline: false) {
    var buffer = channel.allocator.buffer(capacity: line.utf8.count)
    buffer.write(string: line)
    try! channel.writeAndFlush(buffer).wait()
}
 
// EOF, close connect
try! channel.close().wait()

 

Swift on the Server 🚀

Swift on the Server

As mentioned previously, Swift-NIO is an excellent foundational framework to develop cross platform applications or to utilize in server side development.  For example, the Swift web framework, Vapor, has now implemented Swift-NIO under the hood in their 3.0.0 RC 2 release. This is a big move for the Swift community because it shows that Apple is serious about Swift becoming a major player on the server.  Which, to me, is exciting because I come from a long background of iOS and server side development. And if I can use Swift on the server and in my iOS development that could be a big win for productivity and for the growth of the language long term.

In Summary ⌛️

In summary Swift-NIO is a major step forward for the Swift community.  It not only provides an asynchronous networking framework, but it also provides the Swift on the server community the opportunity to start making a large presence in the web development world.  I know that after reading through Swift-NIO’s source code and seeing that Vapor integrated it, I am very interested to get started with Swift on the server and am very excited to see what the future holds for Swift!

Thank you for reading and as always, if you have any questions, comments, or concerns, please leave a comment and I will get back to you as soon as possible.

Resources 📘:

Member for

3 years 9 months
Matt Eaton

Long time mobile team lead with a love for network engineering, security, IoT, oss, writing, wireless, and mobile.  Avid runner and determined health nut living in the greater Chicagoland area.

Comments

matthewj

Tue, 04/03/2018 - 04:36 AM

This is great, but for Swift to be truely considered for the backend it needs to support Windows. Unfortunetly many enterprizes are still tied to Microsoft, and they don't consider things that don't run on Windows. Personally I don't like Windows at all, and haven't dev'ed on it for the last 3 years except where the client insisted on it, but because of .net Core and .Net standard and Xamarin/Mono I can use C# for writing code on the frontend and backend and have them share code and deploy it to all desktop and mobile operating systems. This is amazing and still feals unreal and is what Microsoft should have done back when they released .Net in the first place. And this is what Swift needs to do to properly compete - all platforms (i.e. it needs to add Windows and Android) and then it could do it.

Thank you very much for the feedback, matthewj.  I agree that there is A LOT of Enterprises still tied to Windows environments.  Having worked in consulting software for the last 8 years, I have seen this first hand.  I also think there is a lot of truth to what you are saying in that Windows would be the last hurdle before Swift on the server would see major adoption all across the board.  Appreciate the response!