基于swiftNIO 實(shí)現(xiàn)一個(gè)swift版本的 express

swift5 即將發(fā)布 又該學(xué)習(xí)一門(mén)新語(yǔ)言了是吧。正好這段時(shí)間 apple 在 try?swift 會(huì)上發(fā)布了 新的服務(wù)端基礎(chǔ)組件 SwiftNIO 可以說(shuō)是良心之作,官方定位就是在于java的Netty。這里 我們基于swiftNIO來(lái)開(kāi)發(fā)我們自己的express 服務(wù)框架。
截止3.14 siwft NIO 已經(jīng)支持多種協(xié)議,TCP、UDP、 HTTP1.1、 HTTPS 、Websocket 。HTTP2大禮包也在路上,待HTTP2發(fā)布后會(huì)支持grpc 這樣 微服務(wù)那套也可以上手,可謂是良心之作了,再加上swift5 發(fā)布 語(yǔ)言層面上支持協(xié)程,async await 操作,可以說(shuō)是會(huì)吸一大波粉了。

我們這里實(shí)現(xiàn)這樣的效果
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 創(chuàng)建我們的項(xiàng)目

swift package init --type executable 創(chuàng)建一個(gè)可執(zhí)行項(xiàng)目。 如果選 library 為創(chuàng)建一個(gè)庫(kù)
package.swift 文件中加入依賴(lài)。在target 中 我們要依賴(lài)兩個(gè)庫(kù),為 NIO,NIOHTTP1,否則spm不會(huì)將文件依賴(lài)打包

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

官方介紹
看不懂英文的點(diǎn)我
這里我們知道它是一個(gè)底層的高性能網(wǎng)絡(luò)應(yīng)用,基于 事件驅(qū)動(dòng)模型 無(wú)I/O 阻塞。

第一步 我們想實(shí)現(xiàn)如下 一個(gè)最基礎(chǔ)的服務(wù)

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

討論下
第一步創(chuàng)建了MultiThreadedEventLoopGroup

let loopGroup = 
      MultiThreadedEventLoopGroup(numThreads: System.coreCount)

swiftNIO中的EventLoop 有點(diǎn)類(lèi)似于DispatchQueue,它處理IO事件,可以異步處理多任務(wù),你可以設(shè)置一個(gè)時(shí)間 就像 DispatchQueue.asyncAfter。
MultiThreadedEventLoopGroup 就像一個(gè)并發(fā)隊(duì)列,他會(huì)在他工作的時(shí)候去使用多線程(好羨慕go 的協(xié)程)。

第二步 listen函數(shù)

它使用了ServerBootstrap 對(duì)象去設(shè)置 server channel,bootstrap 就是一個(gè)初始化 channel 的輔助對(duì)象,對(duì)象設(shè)置完成之后 channel也就完成了。

swiftNIO中的channel 有點(diǎn)類(lèi)似于swift 中的 FileHandle。包裹了文件描述以及在他之上提供了一些操作。

channels維護(hù)了一個(gè)channelPipline (管道),他們可以按順序執(zhí)行,并且操作傳入傳出的數(shù)據(jù)。
最后 我們調(diào)用了 channel.pipeline.addHTTPServerHandlers(),浙江處理管道中傳入的數(shù)據(jù)轉(zhuǎn)化為高級(jí)的http對(duì)象 即為 請(qǐng)求對(duì)象,并且輸出字節(jié)到客戶(hù)端中。

添加我們自己的NIO 處理函數(shù)

因?yàn)檫@個(gè)函數(shù)也就只有在Express中有用 所以我們可以直接在Express類(lèi)中去完成它

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

為什么會(huì)有個(gè)then方法?這里就需要讀者去翻閱NIO源碼了

至此 我們完成了此部分。可以調(diào)用 listen 來(lái) 接受請(qǐng)求

第二步 構(gòu)建我們自己的 Request/Response 對(duì)象

2.1 IncomingMessage 顧名思義 是對(duì)于請(qǐng)求對(duì)象的封裝

服務(wù)端收到請(qǐng)求都有這么幾個(gè)特征
請(qǐng)求頭
用戶(hù)參數(shù)
對(duì)此 很容易可以寫(xiě)出這個(gè)類(lèi)

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 會(huì)把我們需要發(fā)給客戶(hù)端的信息 通過(guò)相關(guān)的Channel 發(fā)送。然后發(fā)出適當(dāng)?shù)男盘?hào) (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)  {} 
}

你只要調(diào)用send 即可發(fā)送信息給客戶(hù)端 下面給出詳細(xì)的實(shí)現(xiàn)

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

重點(diǎn)考慮send 函數(shù)
1 flushHeader() 寫(xiě)入header
2 寫(xiě)入body數(shù)據(jù)

  1. 最后一個(gè)map 調(diào)用了 end()函數(shù) 為寫(xiě)入end
    這里都調(diào)用了NIO的writeAndFlush函數(shù)。顧名思義 寫(xiě)入并且清理。
    注意 string需要轉(zhuǎn)為ByteBuffer后才能輸出
2.21

我們?cè)賹?duì)響應(yīng)添加擴(kuò)展,使其可以以下標(biāo)方式訪問(wèn)或?qū)懭雋eader中的數(shù)據(jù)。

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

順手再寫(xiě)個(gè)發(fā)送json給客戶(hù)端的函數(shù) 錦上添花

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

至此 我們已經(jīng)可以很方便的拿到response 以及 request
在 express.swift 文件中修改 channelRead(ctx,data)函數(shù)為

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 實(shí)現(xiàn)中間件

在node 的Express中 我們這樣使用中間件

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

同樣我們希望在swift也如此實(shí)現(xiàn)
需要如下類(lèi)型 簽名函數(shù)

func(req:IncomingMessage,res:ServerResponse,next:()->()){
//print(req)
next() 調(diào)用next 將會(huì)執(zhí)行下一個(gè)中間件 
}

所以我們最終的設(shè)計(jì)會(huì)是這樣

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

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

4 實(shí)現(xiàn)路由

有了中間件 我們希望去用它做點(diǎn)事情。那么正好把路由也一起做了
一個(gè)路由 應(yīng)該有一個(gè)中間件數(shù)組,每次調(diào)用這個(gè)路由 的uri 我們將會(huì)去遍歷每個(gè)中間件,依次調(diào)用 (中間件就是一個(gè)閉包,等待調(diào)用)

protocol RouterProtocol {
    var middleware : [Middleware]
    func use(_ middleware:Middleware...) // 可以接受多參數(shù)
}

實(shí)現(xiàn)該協(xié)議即可做到

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 函數(shù)需要在接受響應(yīng)后調(diào)用 會(huì)依次調(diào)用中間件。

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

我們對(duì)Router做了些擴(kuò)展。這樣它就可以實(shí)現(xiàn)我們一開(kāi)始的目標(biāo)。

愉快的使用它吧。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容