How to use SwiftNIO

Issue #1067

SwiftNIO is Apple’s networking framework for building servers and clients that move a lot of data without blocking threads. It powers Vapor, the AWS SDK for Swift, and gRPC Swift, and you can also reach for it directly when you need socket-level control that URLSession does not give you. This guide walks through the smallest useful thing you can build with it: a TCP echo server.

Adding the dependency

SwiftNIO ships as a set of small modules rather than one big framework, so you only pull in what you actually use. Add the package to Package.swift, then depend on NIOCore for the core types and NIOPosix for the POSIX event loop implementation.

swift
targets: [
    .executableTarget(
        name: "EchoServer",
        dependencies: [
            .product(name: "NIOCore", package: "swift-nio"),
            .product(name: "NIOPosix", package: "swift-nio"),
        ]
    )
]

Event loops, not threads

Everything in SwiftNIO runs on an EventLoopGroup, a small pool of threads that each run their own loop and multiplex many connections without blocking on any single one. You create one group for the lifetime of your process and hand it to a bootstrap, which describes how each incoming connection should be configured. This is the same model Netty uses on the JVM, and it is why SwiftNIO scales to thousands of connections on a handful of threads instead of one thread per connection.

A basic echo server

The current way to write server code in SwiftNIO uses Swift concurrency instead of callback-based channel handlers. NIOAsyncChannel wraps a raw Channel and exposes its inbound data as an AsyncSequence, so you read and write with for try await instead of implementing a handler class.

swift
import NIOCore
import NIOPosix

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
defer { 
  try? group.syncShutdownGracefully() 
}

let server = try await ServerBootstrap(group: group)
    .bind(host: "127.0.0.1", port: 9999) { channel in
        channel.eventLoop.makeCompletedFuture {
            try NIOAsyncChannel<ByteBuffer, ByteBuffer>(synchronouslyWrapping: channel)
        }
    }

print("Listening on 127.0.0.1:9999")

try await withThrowingDiscardingTaskGroup { group in
    try await server.executeThenClose { connections in
        for try await connection in connections {
            group.addTask {
                try? await connection.executeThenClose { inbound, outbound in
                    for try await buffer in inbound {
                        try await outbound.write(buffer)
                    }
                }
            }
        }
    }
}

ServerBootstrap.bind opens the listening socket and calls its trailing closure once per inbound connection, wrapping each one as a NIOAsyncChannel<ByteBuffer, ByteBuffer>. The outer loop iterates over accepted connections as an async sequence, and for each one spawns a child task in a discarding task group, so a slow or long-lived client cannot block new connections from being accepted. Inside that task, a second loop reads every ByteBuffer the client sends and writes it straight back out.

You can test this with netcat. Run the server, then in another terminal type nc 127.0.0.1 9999. Anything you type should echo back immediately.

Where the channel pipeline fits in

A lot of existing SwiftNIO code, including most of Vapor’s internals, predates Swift concurrency and instead builds a pipeline of ChannelInboundHandler and ChannelOutboundHandler objects that process data as it flows through a connection, similar to middleware. That pattern still works and is worth recognizing if you read or extend an older codebase, but for new code the NIOAsyncChannel approach above needs less boilerplate and reads closer to ordinary Swift.

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation