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

swift5 即將發(fā)布 又該學(xué)習(xí)一門新語言了是吧珠十。正好這段時間 apple 在 try?swift 會上發(fā)布了 新的服務(wù)端基礎(chǔ)組件 SwiftNIO 可以說是良心之作挖诸,官方定位就是在于java的Netty仰剿。這里 我們基于swiftNIO來開發(fā)我們自己的express 服務(wù)框架日川。
截止3.14 siwft NIO 已經(jīng)支持多種協(xié)議寓盗,TCP、UDP望侈、 HTTP1.1印蔬、 HTTPS 、Websocket 脱衙。HTTP2大禮包也在路上侥猬,待HTTP2發(fā)布后會支持grpc 這樣 微服務(wù)那套也可以上手,可謂是良心之作了捐韩,再加上swift5 發(fā)布 語言層面上支持協(xié)程退唠,async await 操作,可以說是會吸一大波粉了荤胁。

我們這里實(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)建一個可執(zhí)行項(xiàng)目铜邮。 如果選 library 為創(chuàng)建一個庫
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

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

第一步 我們想實(shí)現(xiàn)如下 一個最基礎(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)類似于DispatchQueue,它處理IO事件,可以異步處理多任務(wù)运褪,你可以設(shè)置一個時間 就像 DispatchQueue.asyncAfter惊楼。
MultiThreadedEventLoopGroup 就像一個并發(fā)隊(duì)列,他會在他工作的時候去使用多線程(好羨慕go 的協(xié)程)秸讹。

第二步 listen函數(shù)

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

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

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

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

因?yàn)檫@個函數(shù)也就只有在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源碼了

至此 我們完成了此部分∈叱洌可以調(diào)用 listen 來 接受請求

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

2.1 IncomingMessage 顧名思義 是對于請求對象的封裝

服務(wù)端收到請求都有這么幾個特征
請求頭
用戶參數(shù)
對此 很容易可以寫出這個類

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 會把我們需要發(fā)給客戶端的信息 通過相關(guān)的Channel 發(fā)送牧抵。然后發(fā)出適當(dāng)?shù)男盘?(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ā)送信息給客戶端 下面給出詳細(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() 寫入header
2 寫入body數(shù)據(jù)

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

我們再對響應(yīng)添加擴(kuò)展犀变,使其可以以下標(biāo)方式訪問或?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

順手再寫個發(fā)送json給客戶端的函數(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)
需要如下類型 簽名函數(shù)

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

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

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

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

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

有了中間件 我們希望去用它做點(diǎn)事情。那么正好把路由也一起做了
一個路由 應(yīng)該有一個中間件數(shù)組弛作,每次調(diào)用這個路由 的uri 我們將會去遍歷每個中間件涕蜂,依次調(diào)用 (中間件就是一個閉包,等待調(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)用 會依次調(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)
    }
}

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

愉快的使用它吧萨西。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末有鹿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谎脯,更是在濱河造成了極大的恐慌葱跋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異娱俺,居然都是意外死亡稍味,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門荠卷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來模庐,“玉大人,你說我怎么就攤上這事油宜〉嗉睿” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵慎冤,是天一觀的道長疼燥。 經(jīng)常有香客問我,道長蚁堤,這世上最難降的妖魔是什么醉者? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮违寿,結(jié)果婚禮上湃交,老公的妹妹穿的比我還像新娘。我一直安慰自己藤巢,他們只是感情好搞莺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掂咒,像睡著了一般才沧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绍刮,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天温圆,我揣著相機(jī)與錄音,去河邊找鬼孩革。 笑死岁歉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膝蜈。 我是一名探鬼主播锅移,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼饱搏!你這毒婦竟也來了非剃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤推沸,失蹤者是張志新(化名)和其女友劉穎备绽,沒想到半個月后券坞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肺素,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年恨锚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倍靡。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡眠冈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出菌瘫,到底是詐尸還是另有隱情,我是刑警寧澤布卡,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布雨让,位于F島的核電站,受9級特大地震影響忿等,放射性物質(zhì)發(fā)生泄漏栖忠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一贸街、第九天 我趴在偏房一處隱蔽的房頂上張望庵寞。 院中可真熱鬧,春花似錦薛匪、人聲如沸捐川。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽古沥。三九已至,卻和暖如春娇跟,著一層夾襖步出監(jiān)牢的瞬間岩齿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工苞俘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盹沈,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓吃谣,卻偏偏與公主長得像乞封,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子基协,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容