SwiftNIO is a cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. It’s like Netty, but written for Swift.
SwiftNIO 是由苹果推动并开源的一款基于事件驱动的跨平台网络应用开发框架,用于快速开发可维护的高性能服务器与客户端应用协议。NIO 是(Non-blocking)I/O 的缩写,即为了提升性能,其采用的是非阻塞 I/O。
SwiftNIO 实际上是一个底层工具,致力于为上层框架专注提供基础 I/O 功能与协定。接下来我们就将采用其构建一个类似 Express 的小型 Web 框架。
目标:看看我们最终实现的框架能做些什么
import MicroExpress
let app = Express()
app.get("/hello") { req, res, next in
res.send("Hello, ExpressSwift")
}
app.get("/todolist") { _, res, _ in
res.json(todolist)
}
app.listen(1337)
复制代码
实现这样一个网络应用,我们要做以下这些组件:
- 一个
Express
实例类,用于运行服务 - 请求(
IncomingMessage
)与响应(ServerResponse
) 对象 - 中间件(
Middleware
)和路由(Router
) - 采用 Codable 对 JSON 对象进行处理
Step 0: 准备 Xcode 工程项目
安装相应的swift-xcode-nio
brew install swiftxcode/swiftxcode/swift-xcode-nio
swift xcode link-templates
复制代码
创建一个新项目,选中Swift-NIO
模板
Step 1: Express 实例类
- main.swift
let app = Express()
app.listen(1337)
复制代码
- Express.swift
import Foundation
import NIO
import NIOHTTP1
open class Express {
let loopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
open func listen(_ port: Int) {
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()
// this is where the action is going to be!
}
.childChannelOption(ChannelOptions.socket(
IPPROTO_TCP, TCP_NODELAY), value: 1)
.childChannelOption(reuseAddrOpt, value: 1)
.childChannelOption(ChnanelOptions.maxMessagePerRead, value: 1)
do {
let serverChannel = try bootstrap.bind(host: "localhost", port: port).wait()
print("Server running on: ", serverChannel.localAddress)
try serverChannel.closeFuture.wait() // runs forever
} catch {
fatalError("failed to start server: \(error)")
}
}
}
复制代码
讨论:
let loopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
复制代码
EventLoopPromise和EventLoopFuture
EventLoop 是 SwfitNIO 最基本的 IO 元素,它等待事件的发生,在发生事件时触发某种回调操作。在大部分 SwfitNIO 应用程序中,EventLoop 对象的数量并不多,通常每个CPU核数对应一到两个 EventLoop 对象。一般来说,EventLoop 会在应用程序的整个生命周期中存在,进行无限的事件分发。
EventLoop 可以组合成 EventLoopGroup,EventLoopGroup 提供了一种机制用于在各个EventLoop 间分发工作负载。例如,服务器在监听外部连接时,用于监听连接的 socket 会被注册到一个 EventLoop 上。但我们不希望这个 EventLoop 承担所有的连接负载,那么就可以通过 EventLoopGroup 在多个EventLoop间分摊连接负载。
目前,SwiftNIO 提供了一个 EventLoopGroup 实现(MultiThreadedEventLoopGroup)和两个 EventLoop 实现(SelectableEventLoop 和 EmbeddedEventLoop)。
MultiThreadedEventLoopGroup 会创建多个线程(使用 POSIX 的 pthreads 库),并为每个线程分配一个 SelectableEventLoop 对象。
SelectableEventLoop使用选择器(基于 kqueue 或 epoll)来管理来自文件和网络IO事件。EmbeddedEventLoop 是一个空的 EventLoop,什么事也不做,主要用于测试。
open func listen(_ port: Int) {
...
let bootstrap = ServerBootstrap(group: loopGroup)
...
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline()
// this is where the action is going to be!
}
...
let serverChannel =
try bootstrap.bind(host: "localhost", port: port)
.wait()
复制代码
Channels、ChannelHandler、ChannelPipeline 和 ChannelHandlerContext
尽管 EventLoop 非常重要,但大部分开发者并不会与它有太多的交互,最多就是用它创建 EventLoopPromise 和调度作业。开发者经常用到的是 Channel 和 ChannelHandler。
每个文件描述符对应一个 Channel,Channel 负责管理文件描述符的生命周期,并处理发生在文件描述符上的事件:每当 EventLoop 检测到一个与相应的文件描述符相关的事件,就会通知 Channel。
ChannelPipeline 由一系列 ChannelHandler 组成,ChannelHandler 负责按顺序处理 Channel 中的事件。ChannelPipeline 就像数据处理管道一样,所以才有了这个名字。
ChannelHandler 要么是 Inbound,要么是 Outbound,要么两者兼有。Inbound 的ChannelHandler 负责处理 “inbound” 事件,例如从 socket 读取数据、关闭 socket 或者其他由远程发起的事件。Outbound 的 ChannelHandler 负责处理 “outbound” 事件,例如写数据、发起连接以及关闭本地 socket。
ChannelHandler 按照一定顺序处理事件,例如,读取事件从管道的前面传到后面,而写入事件则从管道的后面传到前面。每个 ChannelHandler 都会在处理完一个事件后生成一个新的事件给下一个 ChannelHandler。
ChannelHandler 是高度可重用的组件,所以尽可能设计得轻量级,每个 ChannelHandler 只处理一种数据转换,这样就可以灵活组合各种 ChannelHandler,提升代码的可重用性和封装性。
我们可以通过 ChannelHandlerContext 来跟踪 ChannelHandler 在 ChannelPipeline 中的位置。ChannelHandlerContext 包含了当前 ChannelHandler 到上一个和下一个 ChannelHandler的引用,因此,在任何时候,只要 ChannelHandler 还在管道当中,就能触发新事件。
SwiftNIO 内置了多种 ChannelHandler,包括 HTTP 解析器。另外,SwiftNIO 还提供了一些Channel 实现,比如 ServerSocketChannel(用于接收连接)、SocketChannel(用于TCP连接)、DatagramChannel(用于UDP socket)和 EmbeddedChannel(用于测试)。
Step 1b: 添加 NIO Handler
- Express.swift
open class Express {
...
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline().then {
channel.pipeline.add(handler: HTTPHandler())
}
}
...
}
复制代码
添加真正的处理器方法
- Express.swift
open class Express {
//...
final class HTTPHandler : ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let reqPart = unwrapInboundIn(data)
switch reqPart {
case .head(let header):
print("req:", header)
// ignore incoming content to keep it micro :-)
case .body, .end: break
}
}
}
} // end of Express class
复制代码
编译并运行,通过浏览器访问 http://localhost:1337
,暂时未有响应,但在控制台中可以看到输出:
Server running on: [IPv6]::1:1337``
req: HTTPRequestHead(method: NIOHTTP1.HTTPMethod.GET, uri: "/", ...)
复制代码
在 .head
中添加以下代码
case .head(let header):
print("req:", header)
let head = HTTPResponseHead(version: header.version,
status: .ok)
let part = HTTPServerResponsePart.head(head)
_ = ctx.channel.write(part)
var buffer = ctx.channel.allocator.buffer(capacity: 42)
buffer.write(string: "Hello Schwifty World!")
let bodypart = HTTPServerResponsePart.body(.byteBuffer(buffer))
_ = ctx.channel.write(bodypart)
let endpart = HTTPServerResponsePart.end(nil)
_ = ctx.channel.writeAndFlush(endpart).then {
ctx.channel.close()
}
复制代码
现在,我们第一步就完成了,实现了一个 Express 对象,运行我们的 Web 服务。
Step 2: 请求(IncomingMessage
)与响应(ServerResponse
) 对象
- IncomingMessage.swift
import NIOHTTP1
open class IncomingMessage {
public let header : HTTPRequestHead // <= from NIOHTTP1
public var userInfo = [ String : Any ]()
init(header: HTTPRequestHead) {
self.header = header
}
}
复制代码
- ServerResponse.swift
import NIO
import NIOHTTP1
open class ServerResponse {
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
}
/// An Express like `send()` function.
open func send(_ s: String) {
flushHeader()
let utf8 = s.utf8
var buffer = channel.allocator.buffer(capacity: utf8.count)
buffer.write(bytes: utf8)
let part = HTTPServerResponsePart.body(.byteBuffer(buffer))
_ = channel.writeAndFlush(part)
.mapIfError(handleError)
.map { self.end() }
}
/// Check whether we already wrote the response header.
/// If not, do so.
func flushHeader() {
guard !didWriteHeader else { return } // done already
didWriteHeader = true
let head = HTTPResponseHead(version: .init(major:1, minor:1),
status: status, headers: headers)
let part = HTTPServerResponsePart.head(head)
_ = channel.writeAndFlush(part).mapIfError(handleError)
}
func handleError(_ error: Error) {
print("ERROR:", error)
end()
}
func end() {
guard !didEnd else { return }
didEnd = true
_ = channel.writeAndFlush(HTTPServerResponsePart.end(nil))
.map { self.channel.close() }
}
}
复制代码
在 HTTPHandler 中使用
- Express.swift
case .head(let header):
let request = IncomingMessage(header: header)
let response = ServerResponse(channel: ctx.channel)
print("req:", header.method, header.uri, request)
response.send("Way easier to send data!!!")
复制代码
Step 3: 中间件(Middleware)和路由(Router)
中间件其实就是闭包,采用 typealias 进行别名定义:
- Middleware.swift
public typealias Next = ( Any... ) -> Void
public typealias Middleware = (IncomingMessage, ServerResponse, @escaping Next ) -> Void
复制代码
- Router.swift
open class Router {
/// The sequence of Middleware functions.
private var middleware = [ Middleware ]()
/// Add another middleware (or many) to the list
open func use(_ middleware: Middleware...) {
self.middleware.append(contentsOf: middleware)
}
/// Request handler. Calls its middleware list
/// in sequence until one doesn't call `next()`.
func handle(request : IncomingMessage,
response : ServerResponse,
next upperNext : @escaping Next)
{
let stack = self.middleware
guard !stack.isEmpty else { return upperNext() }
var next : Next? = { ( args : Any... ) in }
var i = stack.startIndex
next = { (args : Any...) in
// grab next item from matching middleware array
let middleware = stack[i]
i = stack.index(after: i)
let isLast = i == stack.endIndex
middleware(request, response, isLast ? upperNext : next!)
}
next!()
}
}
复制代码
将路由类接入Express
- Express.swift
open class Express : Router { // <= make Router the superclass
...
}
// -------
final class HTTPHandler : ChannelInboundHandler {
typealias InboundIn = HTTPServerRequestPart
let router : Router
init(router: Router) {
self.router = router
}
func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
let reqPart = unwrapInboundIn(data)
switch reqPart {
case .head(let header):
let request = IncomingMessage(header: header)
let response = ServerResponse(channel: ctx.channel)
// trigger Router
router.handle(request: request, response: response) {
(items : Any...) in // the final handler
response.status = .notFound
response.send("No middleware handled the request!")
}
// ignore incoming content to keep it micro :-)
case .body, .end: break
}
}
}
// ------
...
.childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline().then {
channel.pipeline.add(
handler: HTTPHandler(router: self))
}
}
...
复制代码
在 main.swift 中使用中间件和路由
- main.swift
let app = Express()
// Logging
app.use { req, res, next in
print("\(req.header.method):", req.header.uri)
next() // continue processing
}
// Request Handling
app.use { _, res, _ in
res.send("Hello, Schwifty world!")
}
app.listen(1337)
复制代码
有了 use()
,接下来实现 get(path)
- Router.swift
public extension Router {
/// Register a middleware which triggers on a `GET`
/// with a specific path prefix.
public func get(_ path: String = "",
middleware: @escaping Middleware)
{
use { req, res, next in
guard req.header.method == .GET,
req.header.uri.hasPrefix(path)
else { return next() }
middleware(req, res, next)
}
}
}
复制代码
Step 4: 可复用的中间件
- QueryString.swift
import Foundation
fileprivate let paramDictKey =
"de.zeezide.µe.param"
/// A middleware which parses the URL query
/// parameters. You can then access them
/// using:
///
/// req.param("id")
///
public
func queryString(req : IncomingMessage,
res : ServerResponse,
next : @escaping Next)
{
// use Foundation to parse the `?a=x`
// parameters
if let queryItems = URLComponents(string: req.header.uri)?.queryItems {
req.userInfo[paramDictKey] =
Dictionary(grouping: queryItems, by: { $0.name })
.mapValues { $0.flatMap({ $0.value })
.joined(separator: ",") }
}
// pass on control to next middleware
next()
}
public extension IncomingMessage {
/// Access query parameters, like:
///
/// let userID = req.param("id")
/// let token = req.param("token")
///
func param(_ id: String) -> String? {
return (userInfo[paramDictKey]
as? [ String : String ])?[id]
}
}
复制代码
- main.swift
app.use(queryString) // parse query params
app.get { req, res, _ in
let text = req.param("text")
?? "Schwifty"
res.send("Hello, \(text) world!")
}
复制代码
Step 5: 采用 Codable 对 JSON 对象进行处理
- ServerResponse.swift
public extension ServerResponse {
/// A more convenient header accessor. Not correct for
/// any header.
public subscript(name: String) -> String? {
set {
assert(!didWriteHeader, "header is out!")
if let v = newValue {
headers.replaceOrAdd(name: name, value: v)
}
else {
headers.remove(name: name)
}
}
get {
return headers[name].joined(separator: ", ")
}
}
}
复制代码
- TodoModel.swift
struct Todo : Codable {
var id : Int
var title : String
var completed : Bool
}
// Our fancy todo "database". Since it is
// immutable it is webscale and lock free,
// if not useless.
let todos = [
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)
]
复制代码
- ServerResponse.swift
import Foundation
public extension ServerResponse {
/// Send a Codable object as JSON to the client.
func json<T: Encodable>(_ model: T) {
// create a Data struct from the Codable object
let data : Data
do {
data = try JSONEncoder().encode(model)
}
catch {
return handleError(error)
}
// setup JSON headers
self["Content-Type"] = "application/json"
self["Content-Length"] = "\(data.count)"
// send the headers and the data
flushHeader()
var buffer = channel.allocator.buffer(capacity: data.count)
buffer.write(bytes: data)
let part = HTTPServerResponsePart.body(.byteBuffer(buffer))
_ = channel.writeAndFlush(part)
.mapIfError(handleError)
.map { self.end() }
}
}
复制代码
- main.swift
app.get("/todomvc") { _, res, _ in
// send JSON to the browser
res.json(todos)
}
复制代码
总结
以上就实现了一个小型的 Express Web 框架了,整体写下来,对平时使用的诸多现成框架也有了更深刻的理解。