Webserver mit SwiftNIO

Am 11.09.2025 um 09:03
von ridcully, Kategorie: Blog, Tags: Swift Server Code

Es gibt einige Server-Frameworks für Swift, mit denen man komfortabel einen Webserver nutzen kann. Für viele kleine Projekte mag aber ein Framework zu viel sein und eigentlich reicht SwiftNIO aus. Dieser Code/Artikel basiert auf dem Artikel A µTutorial on SwiftNIO 2 und stellt im wesentlichen einen kleinen Webserver zur Verfügung. Inklusive Routing.

Das ganze hier unkommentiert als Swift-Code:

import Foundation
import NIO
import NIOHTTP1

// MARK: - Server
open class Server {
    public var router = Router()

    public typealias Next = (Any...) -> Void
    public typealias Middleware =
        (Server.Request, Server.Response, @escaping Next) -> Void

    private let loopGroup = MultiThreadedEventLoopGroup(
        numberOfThreads: System.coreCount)

    public init() {}

    open func listen(_ port: Int, _ host: String) {
        let reuseAddrOpt = ChannelOptions.socket(
            SocketOptionLevel(SOL_SOCKET),
            SO_REUSEADDR)
        let bootstrap = ServerBootstrap(group: loopGroup)
            .serverChannelOption(ChannelOptions.backlog, value: 256)
            .serverChannelOption(reuseAddrOpt, value: 1)
            .childChannelInitializer { channel in
                channel.pipeline.configureHTTPServerPipeline().flatMap {
                    channel.pipeline.addHandler(
                        HTTPHandler(router: self.router))
                }
            }
            .childChannelOption(
                ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY),
                value: 1)
            .childChannelOption(reuseAddrOpt, value: 1)
            .childChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)

        do {
            let serverChannel = try bootstrap.bind(host: host, port: port)
                                             .wait()
            print("Server running on:", serverChannel.localAddress!)

            try serverChannel.closeFuture.wait() // läuft ewig
        } catch {
            fatalError("failed to start server: \(error)")
        }
    }
}

// MARK: - HTTPHandler
extension Server {
    final class HTTPHandler: ChannelInboundHandler {
        typealias InboundIn = HTTPServerRequestPart

        let router: Server.Router

        var requestHeader: HTTPRequestHead?
        var requestBody = Data()


        init(router: Server.Router) {
            self.router = router
        }

        func channelRead(context: ChannelHandlerContext, data: NIOAny) {
            let reqPart = unwrapInboundIn(data)
            let response = Response(channel: context.channel)

            switch reqPart {
            case .head(let header):
                requestHeader = header
            case .body(var body):
                if let newData = body.readBytes(length: body.readableBytes) {
                    requestBody.append(contentsOf: newData)
                }
            case .end:
                guard let requestHeader = requestHeader else {
                    return
                }

                let request = Request(header: requestHeader, body: requestBody)

                // Router aufrufen
                router.handle(request: request,
                              response: response) { (_: Any...) in
                    self.router.notFoundHandler(
                        request,
                        response) { (_: Any...) in }
                    }
                }
        }
    }
}

// MARK: - Router
extension Server {
    open class Router {
        // Liste der Middleware-Handler
        private var middleware = [Middleware]()

        // Letzter handler, der ein 404-Fehler anzeigen soll
        var notFoundHandler: Middleware

        init() {
            notFoundHandler = { (_, response, _) in
                response.status = .notFound // 404 error
                response.send("404 - Not found.")
            }
        }

        open func setNotFoundHandler(handler: @escaping Middleware) {
            notFoundHandler = handler
        }

        // Fügt einen weiteren (oder mehrere) Middleware-Handler hinzu
        open func use(_ middleware: Middleware...) {
            self.middleware.append(contentsOf: middleware)
        }

        // Request handler. Ruft die Middleware-Handler in Reihe auf
        // bis einer nicht mehr next() aufruft.
        func handle(request: Server.Request,
                    response: Server.Response,
                    next upperNext: @escaping Next) {
            let stack = self.middleware
            guard !stack.isEmpty else { return upperNext() }

            var next: Next? = { (_: Any...) in }
            var index = stack.startIndex
            next = { (_: Any...) in
                // nimmt den nächsten Handler aus dem middleware array
                let middleware = stack[index]
                index = stack.index(after: index)

                let isLast = index == stack.endIndex
                middleware(request, response, isLast ? upperNext : next!)
            }

            next!()
        }
    }
}

// MARK: - Request
extension Server {
    open class Request {
        public var header: HTTPRequestHead // <= from NIOHTTP1
        public var body: Data

        init(header: HTTPRequestHead, body: Data) {
            self.header = header
            self.body = body
        }
    }
}

// MARK: - Response
extension Server {
    open class Response {
        public var status = HTTPResponseStatus.ok
        public var headers = HTTPHeaders()
        public let channel: Channel
        private var didWriteHeader = false
        private var didEnd = false

        public init(channel: Channel) {
            self.channel = channel
        }

        // Sendet einen String und beendet die Verbindung
        open func send(_ string: String) {
            chunck(string)
            end()
        }

        // Sendet Daten und beendet Verbindung
        open func send(_ data: Data) {
            chunck(data)
            end()
        }

        // Sendet einen Teil-String und hält die Verbindung offen
        open func chunck(_ string: String) {
            guard let data = string.data(using: .utf8) else {
                return handleError("Failed to encode String as Data.")
            }

            send(data)
        }

        // Sendet Teil-Daten und hält die Verbindung offen
        open func chunck(_ data: Data) {
            guard !didEnd else { return }

            flushHeader()

            var buffer = channel.allocator.buffer(capacity: data.count)
            buffer.writeBytes(data)

            let part = HTTPServerResponsePart.body(.byteBuffer(buffer))

            _ = channel.writeAndFlush(part)
                       .recover(handleError)
        }

        // MARK: - Helpers

        // Prüft ob bereits die Header gesendet wurden und sendet diese,
        // wenn das nicht der Fall ist.
        private func flushHeader() {
            guard !didWriteHeader else { return }
            didWriteHeader = true

            let head = HTTPResponseHead(version: .init(major: 1, minor: 1),
                                        status: status,
                                        headers: headers)
            let part = HTTPServerResponsePart.head(head)
            _ = channel.writeAndFlush(part).recover(handleError)
        }

        private func handleError(_ error: Error) {
            print("ERROR:", error)
            end()
        }

        // Beendet die Verbindung
        private func end() {
            guard !didEnd else { return }
            didEnd = true
            _ = channel.writeAndFlush(HTTPServerResponsePart.end(nil))
                       .map { self.channel.close() }
        }
    }
}

Build @ 2025-09-11T09:10:34+02:00