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ù)
- 最后一個(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)。
愉快的使用它吧。