Webserver mit SwiftNIO
Am 11.09.2025 um 09:03von , Kategorie: Blog, Tags:
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() }
}
}
}