采用 SwiftNIO 实现一个类似 Express 的 Web 框架

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)
复制代码

实现这样一个网络应用,我们要做以下这些组件:

  1. 一个 Express 实例类,用于运行服务
  2. 请求(IncomingMessage)与响应(ServerResponse) 对象
  3. 中间件(Middleware)和路由(Router
  4. 采用 Codable 对 JSON 对象进行处理

Step 0: 准备 Xcode 工程项目

安装相应的swift-xcode-nio

brew install swiftxcode/swiftxcode/swift-xcode-nio
swift xcode link-templates
复制代码

创建一个新项目,选中Swift-NIO模板

创建新项目-SwiftNIO

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 框架了,整体写下来,对平时使用的诸多现成框架也有了更深刻的理解。

更多

猜你喜欢

转载自juejin.im/post/5b6c10fcf265da0fa42d057d