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