基于swiftNIO 实现一个swift版本的 express

swift5 即将发布 又该学习一门新语言了是吧。正好这段时间 apple 在 try?swift 会上发布了 新的服务端基础组件 SwiftNIO 可以说是良心之作,官方定位就是在于java的Netty。这里 我们基于swiftNIO来开发我们自己的express 服务框架。
截止3.14 siwft NIO 已经支持多种协议,TCP、UDP、 HTTP1.1、 HTTPS 、Websocket 。HTTP2大礼包也在路上,待HTTP2发布后会支持grpc 这样 微服务那套也可以上手,可谓是良心之作了,再加上swift5 发布 语言层面上支持协程,async await 操作,可以说是会吸一大波粉了。

我们这里实现这样的效果
let app = Express()
app.use(querystring)
app.use { (req, res, next) in
    print("1",req.userInfo)
    next()
}

app.get("/var") { (req, res, next) in
    res.send("fuck your")
}

let r = Router()
r.get("/router") { (req, res, next) in

    res.send("router is ok")
}


r.post("hi") { (req, res, next) in
    res.send("hello")
}
app.use("/s", router: r)

app.listen(8989)
  • 支持路由 Router
  • 支持中间件 Middleware
用swift package manager 创建我们的项目

swift package init --type executable 创建一个可执行项目。 如果选 library 为创建一个库
package.swift 文件中加入依赖。在target 中 我们要依赖两个库,为 NIO,NIOHTTP1,否则spm不会将文件依赖打包

let package = Package(
    name: "swift-express",
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0")
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "swift-express",
            dependencies: ["NIO","NIOHTTP1"]),
    ]
)

首先 我们要知道 什么是swift NIO

官方介绍
看不懂英文的点我
这里我们知道它是一个底层的高性能网络应用,基于 事件驱动模型 无I/O 阻塞。

第一步 我们想实现如下 一个最基础的服务

let app = Express()

app.listen(8989)

Express.swift

import Foundation
import NIO
import NIOHTTP1

open class Express {
  
  override public init() {}
  
  let loopGroup = 
        MultiThreadedEventLoopGroup(numThreads: 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.addHTTPServerHandlers()
        
        // this is where the action is going to be!
      }
      
      .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: "localhost", port: port)
                         .wait()
      print("Server running on:", serverChannel.localAddress!)
      
      try serverChannel.closeFuture.wait() // runs forever
    }
    catch {
      fatalError("failed to start server: \(error)")
    }
  }
}

xcode run

Server running on: [IPv6]::1:8989

讨论下
第一步创建了MultiThreadedEventLoopGroup

let loopGroup = 
      MultiThreadedEventLoopGroup(numThreads: System.coreCount)

swiftNIO中的EventLoop 有点类似于DispatchQueue,它处理IO事件,可以异步处理多任务,你可以设置一个时间 就像 DispatchQueue.asyncAfter。
MultiThreadedEventLoopGroup 就像一个并发队列,他会在他工作的时候去使用多线程(好羡慕go 的协程)。

第二步 listen函数

它使用了ServerBootstrap 对象去设置 server channel,bootstrap 就是一个初始化 channel 的辅助对象,对象设置完成之后 channel也就完成了。

swiftNIO中的channel 有点类似于swift 中的 FileHandle。包裹了文件描述以及在他之上提供了一些操作。

channels维护了一个channelPipline (管道),他们可以按顺序执行,并且操作传入传出的数据。
最后 我们调用了 channel.pipeline.addHTTPServerHandlers(),浙江处理管道中传入的数据转化为高级的http对象 即为 请求对象,并且输出字节到客户端中。

添加我们自己的NIO 处理函数

因为这个函数也就只有在Express中有用 所以我们可以直接在Express类中去完成它

open class Express {
  // other code
  
  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
      }
    }
  }
}

修改之前的初始化bootstrap代码

open class Express {
  ...
      .childChannelInitializer { channel in
        channel.pipeline.addHTTPServerHandlers().then {
          channel.pipeline.add(handler: HTTPHandler())
        }
      }
  ...
}

为什么会有个then方法?这里就需要读者去翻阅NIO源码了

至此 我们完成了此部分。可以调用 listen 来 接受请求

第二步 构建我们自己的 Request/Response 对象

2.1 IncomingMessage 顾名思义 是对于请求对象的封装

服务端收到请求都有这么几个特征
请求头
用户参数
对此 很容易可以写出这个类

import NIOHTTP1

open class IncomingMessage {

  public let header   : HTTPRequestHead // <= from NIOHTTP1
  public var userInfo = [ String : Any ]()
  
  init(header: HTTPRequestHead) {
    self.header = header
  }
}
2.2 ServerResponse

ServerResponse 会把我们需要发给客户端的信息 通过相关的Channel 发送。然后发出适当的信号 (head body end)

import NIO
import NIOHTTP1

open class ServerResponse {
  public  var status         = HTTPResponseStatus.ok
  public  var headers        = HTTPHeaders()
  public  let channel        : Channel
  public init(channel: Channel) {
    self.channel = channel
  }
  open func send(_ s: String)  {} 
}

你只要调用send 即可发送信息给客户端 下面给出详细的实现

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() }
  }
}

重点考虑send 函数
1 flushHeader() 写入header
2 写入body数据

  1. 最后一个map 调用了 end()函数 为写入end
    这里都调用了NIO的writeAndFlush函数。顾名思义 写入并且清理。
    注意 string需要转为ByteBuffer后才能输出
2.21

我们再对响应添加扩展,使其可以以下标方式访问或写入header中的数据。

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: ", ")
        }
    }
}
2.21

顺手再写个发送json给客户端的函数 锦上添花

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() }
    }
}

至此 我们已经可以很方便的拿到response 以及 request
在 express.swift 文件中修改 channelRead(ctx,data)函数为

func channelRead(ctx: ChannelHandlerContext, data: NIOAny) {
            let reqPart = self.unwrapInboundIn(data)
            
            switch reqPart {
            case .head(let header):
                
                let req = IncomingMessage(header: header)
                let res = ServerResponse(channel: ctx.channel)
//
//                // trigger Router
                
                router.handle(request: req, response: res) {
                    (items : Any...) in // the final handler
                    res.status = .notFound
                    res.send("No middleware handled the request!")
                }
                
            // ignore incoming content to keep it micro :-)
            case .body, .end: break
            }
        }

3 实现中间件

在node 的Express中 我们这样使用中间件

let app = new Express()
app.use((req,res,next) => {
})

同样我们希望在swift也如此实现
需要如下类型 签名函数

func(req:IncomingMessage,res:ServerResponse,next:()->()){
//print(req)
next() 调用next 将会执行下一个中间件 
}

所以我们最终的设计会是这样

public typealias Next = ( Any... ) -> Void

public typealias Middleware =
    ( IncomingMessage,
    ServerResponse,
    @escaping Next ) -> Void

4 实现路由

有了中间件 我们希望去用它做点事情。那么正好把路由也一起做了
一个路由 应该有一个中间件数组,每次调用这个路由 的uri 我们将会去遍历每个中间件,依次调用 (中间件就是一个闭包,等待调用)

protocol RouterProtocol {
    var middleware : [Middleware]
    func use(_ middleware:Middleware...) // 可以接受多参数
}

实现该协议即可做到

let app = new Router()
 app.use((req,res,next) => {
})

Router.swift

open class Router {
    private var part : String = ""
    /// 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)
    {
        final class State {
            var stack    : ArraySlice<Middleware>
            let request  : IncomingMessage
            let response : ServerResponse
            var next     : Next?
            
            init(_ stack    : ArraySlice<Middleware>,
                 _ request  : IncomingMessage,
                 _ response : ServerResponse,
                 _ next     : @escaping Next)
            {
                self.stack    = stack
                self.request  = request
                self.response = response
                self.next     = next
            }
            
            func step(_ args : Any...) {
                if let middleware = stack.popFirst() {
                    middleware(request, response, self.step)
                }
                else {
                    next?(); next = nil
                }
            }
        }
        
        let state = State(middleware[middleware.indices],
                          request, response, upperNext)
        state.step()
    }
}

handle 函数需要在接受响应后调用 会依次调用中间件。

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(self.part + path)
                else { return next() }
            
            middleware(req, res, next)
        }
    }
    
    public func post(_ path: String = "",
                    middleware: @escaping Middleware)
    {
        use { req, res, next in
            guard req.header.method == .POST,
                req.header.uri.hasPrefix(self.part + "/" + path)
                else { return next() }
            
            middleware(req, res, next)
        }
    }
}

public extension Router  {
    
    public func use(router:Router){
        let _ = router.middleware.map{
            self.middleware.append($0)
        }
    }
    
    public func use(_ part:String,router:Router){
        router.part = part
        use(router: router)
    }
}

我们对Router做了些扩展。这样它就可以实现我们一开始的目标。

愉快的使用它吧。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容