Vapor(五)网络篇
这篇主要内容是再介绍一下Route的通用方法,网络传输数据的编码格式,Vapor中HTTP服务端和客户端的详细使用说明,以及WebSocket。
Route
Routing (vapor/routing)是像HTTP请求一样寻找某些东西路径的一个库,可以通过使用嵌套、动态路径组件的路由器注册或查找路由。
收集动态组件值的路由请求如下(组件之间是独立的关系,代表有users和comments两个组件,也可以分别单独请求)
/users/:user_id/comments/:comment_id
通常会使用像HTTP响应这类型作为TriRouter的泛型,这里为简单演示使用了Double。
// Create a router that stores Doubles
let router = TrieRouter(Double.self)
// Register some routes and values to the router
router.register(route: Route(path: ["funny", "meaning_of_universe"], output: 42))
router.register(route: Route(path: ["funny", "leet"], output: 1337))
router.register(route: Route(path: ["math", "pi"], output: 3.14))
// Create empty Parameters to hold dynamic params (none yet)
var params = Parameters()
// Test fetching some routes
print(router.route(path: ["fun", "meaning_of_universe"], parameters: ¶ms)) // 42
print(router.route(path: ["foo"], parameters: ¶ms)) // nil
使用register(…)给路由器注册路由,使用route(…)抓取回该路由注册时设定返回的内容。 TrieRouter 使用了二叉树查找法定位路由的路径。
注册一个动态的路径组件,下面展示会变化路径的其中一部分的请求
/users/:user_id
路由设置
// Create a route for /users/:user_id
let user = Route(path: [.constant("users"), .parameter("user_id")], output: ...)
// Create a router and register our route
let router = TrieRouter(...)
router.register(route: user)
// Create empty Parameters to hold dynamic values
var params = Parameters()
// Route the path /users/42
_ = router.route(path: ["users", "42"], parameters: ¶ms)
// The params contains our dynamic value!
print(params) // ["user_id": "42"]
.parameter(…) 设定的字符串就是用来从Parameters中抓取值的键。
注意这里演示所用的路由器是自定义的,应与应用App中默认的EngineRouter.default()区分开来。
URL-Encoded
URL-Encoded是Web里面的一种广泛的编码方式,能序将Web表单序列化后通过POST请求发送出去,也能用于发送结构数据或URL的查询,但只对发送小量的数据时有效,数据也需要满足percent-encoded,而上传文件则参照下一点的Multipart。
解码Body
解码 application/x-www-form-urlencoded
的请求
form-urlencoded的请求格式
POST /users HTTP/1.1
Content-Type: application/x-www-form-urlencoded
name=Vapor&age=3&luckyNumbers[]=5&luckyNumbers[]=7
[]
是用来编码数组的。
对应的表单格式
<form method="POST" action="/users">
<input type="text" name="name">
<input type="text" name="age">
<input type="text" name="luckyNumbers[]">
<input type="text" name="luckyNumbers[]">
</form>
对应模型
import Vapor
struct User: Content {
var name: String
var age: Int
var luckyNumbers: [Int]
}
获取数据
router.post("users") { req -> Future<HTTPStatus> in
return try req.content.decode(User.self).map(to: HTTPStatus.self) { user in
print(user.name) // "Vapor"
print(user.age) // 3
print(user.luckyNumbers) // [5, 7]
return .ok
}
}
编码Body
router.get("multipart") { req -> User in
let res = req.makeResponse()
let user = User(name: "Vapor", age: 3, luckyNumbers: [5, 7])
res.content.encode(user, as: .urlEncodedForm)
return user
}
Multipart
将表单设为multipart/form-data,同时加入文件域,而后通过HTTP协议将文件内容发送到服务器,服务器端读取这个分段(multipart)的数据信息,就能将其中的文件内容提取出来。
POST /users HTTP/1.1
Content-Type: multipart/form-data; boundary=123
--123
Content-Disposition: form-data; name="name"
Vapor
--123
Content-Disposition: form-data; name="age"
3
--123
Content-Disposition: form-data; name="image"; filename="droplet.png"
<contents of image>
--123--
这里使用标识123
作为边界
上面的multipart请求格式对应的表单格式为
<form method="POST" action="/users" enctype="multipart/form-data">
<input type="text" name="name">
<input type="text" name="age">
<input type="file" name="image">
</form>
对应的模型格式为
import Vapor
struct User: Content {
var name: String
var age: Int
var image: Data
}
获取数据
router.post("users") { req -> Future<HTTPStatus> in
return try req.content.decode(User.self).map(to: HTTPStatus.self) { user in
print(user.name) // "Vapor"
print(user.age) // 3
print(user.image) // Raw image data
return .ok
}
}
这样,POST这个表单到/users时,就会接收到对应的数据。
若需要用到编码时,可以这样
router.get("multipart") { req -> User in
let res = req.makeResponse()
let user = User(name: "Vapor", age: 3, image: Data(...))
res.content.encode(user, as: .formData)
return user
}
HTTP
Client
抓取Vapor的主页
// Connect a new client to the supplied hostname.
let client = try HTTPClient.connect(hostname: "vapor.codes", on: ...).wait()
print(client) // HTTPClient
// Create an HTTP request: GET /
let httpReq = HTTPRequest(method: .GET, url: "/")
// Send the HTTP request, fetching a response
let httpRes = try client.send(httpReq).wait()
print(httpRes) // HTTPResponse
可以看到首先需要连接到主域名来创建client
,然后再指定请求的方式和路径,最后通过client
发出该请求从而得到响应。发送请求前Vapor会自动解析域名与路径的关系。
Server
创建响应者HTTPServerResponder
,它能直接响应连入的请求。注意,这里是一个Echo示例,直接将请求的内容作为响应回给客户端,实际会根据具体情况响应具体内容
/// Echoes the request as a response.
struct EchoResponder: HTTPServerResponder {
/// See `HTTPServerResponder`.
func respond(to req: HTTPRequest, on worker: Worker) -> Future<HTTPResponse> {
// Create an HTTPResponse with the same body as the HTTPRequest
let res = HTTPResponse(body: req.body)
// We don't need to do any async work here, we can just
// se the Worker's event-loop to create a succeeded future.
return worker.eventLoop.newSucceededFuture(result: res)
}
}
创建服务器HTTPServer,绑定主域名、端口、响应者
// Create an EventLoopGroup with an appropriate number
// of threads for the system we are running on.
let group = MultiThreadedEventLoopGroup(numThreads: System.coreCount)
// Make sure to shutdown the group when the application exits.
defer { try! group.syncShutdownGracefully() }
// Start an HTTPServer using our EchoResponder
// We are fine to use `wait()` here since we are on the main thread.
let server = try HTTPServer.start(
hostname: "localhost",
port: 8123,
responder: EchoResponder(),
on: group
).wait()
// Wait for the server to close (indefinitely).
try server.onClose.wait()
start(…) 方法会异步返回一个HTTPServer的future对象,当服务器完成启动起来后future就会完成,否则会抛出异常。
在等待服务器关闭的future完成时,应用能一直在激活状态。一般情况下服务器不会自己关闭。
message
请求消息,主要的参数是method
和url
let httpReq = HTTPRequest(method: .GET, url: "/hello")
var httpReq: HTTPRequest = ...
httpReq.method = .POST
httpReq.url = URL(...)
//output
GET /hello HTTP/1.1
Content-Length: 0
响应消息,主要参数时status
let httpRes = HTTPResponse(status: .ok, body: "hello")
var httpRes: HTTPResponse = ...
httpRes.status = .notFound
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain
hello
status的类型可以参考httpstatuses.com
消息头,至少需要有Content-Length
或者 Transfer-Encoding
来定义消息体有多长,Content-Type
来解释消息体的数据类型
Content-Length: 5
Content-Type: text/plain
var message: HTTPMessage ...
message.headers.firstValue(for: .contentLength) // 5
消息体,设置消息体会自动更新Content-Length
或者 Transfer-Encoding
的内容
var message: HTTPMessage = ...
message.body = HTTPBody(string: "Hello, world!")
message.contentType = .plainText
var message: HTTPMessage = ...
message.body = HTTPBody(string: """
{"message": "Hello, world!"}
""")
message.contentType = .json
可编码(Codable)
HTTP定义两个便利的协议HTTPMessageEncoder
和HTTPMessageDecoder
去使用Codable,这两个编解码协议可以编解码自定义的Codable类型的数据到HTTP的消息体中,并设置适合的Content Type
到消息头。
HTTP默认是提供了JSONEncoder
和JSONDecoder
的实现,但Vapor也包含了很多类型的编码方式
struct Greeting: Codable {
var message: String
}
// Create an instance of Greeting
let greeting = Greeting(message: "Hello, world!")
// Create a 200 OK response
var httpRes = HTTPResponse(status: .ok)
// Encode the greeting to the response
try JSONEncoder().encode(greeting, to: &httpRes, on: ...)
WebSocket
与HTTP不同,WebSockets可以一个开放、可交互的方式通讯,能发送文本或二进制格式的消息。客户端和服务端可以在同一时间发送多条消息而无需等待响应。但它还是基于HTTP发起设置的(在响应头中带有状态101的交换协议)。
WebSocket服务器可能同时连接着一个或多个WebSocket客户端,WebSocket服务器建立在HTTP服务器之上,并使用HTTP的upgrade机制。
WebSocket servers are built on top of HTTP servers using the HTTP upgrade mechanism.
// First, create an HTTPProtocolUpgrader
let ws = HTTPServer.webSocketUpgrader(shouldUpgrade: { req in
// Returning nil in this closure will reject upgrade
if req.url.path == "/deny" { return nil }
// Return any additional headers you like, or just empty
return [:]
}, onUpgrade: { ws, req in
// This closure will be called with each new WebSocket client
ws.send("Connected")
ws.onText { ws, string in
ws.send(string.reversed())
}
})
// Next, create your server, adding the WebSocket upgrader
let server = try HTTPServer.start(
...
upgraders: [ws],
...
).wait()
// Run the server.
try server.onClose.wait()
shouldUpgrade
在收到HTTP的升级请求后回调,它决定了upgrade是否能完成,返回nil则代表拒绝upgrade。onUpgrade
在每一个WebSocket客户端连接创建时回调。
注意,upgrade的闭包可能会被多个事件循环调用,如果必须访问外部变量时则需要避免资源竞争。
客户端连接到WebSocket服务端,就像WebSocket服务端使用HTTP服务端一样,WebSocket客户端也可以使用HTTP客户端。
// Create a new WebSocket connected to echo.websocket.org
let ws = try HTTPClient.webSocket(hostname: "echo.websocket.org", on: ...).wait()
// Set a new callback for receiving text formatted data.
ws.onText { ws, text in
print("Server echo: \(text)")
}
// Send a message.
ws.send("Hello, world!")
// Wait for the Websocket to closre.
try ws.onClose.wait()
Vapor的便利WebSocket方法
Vapor的WebSocket服务端具有路由请求的能力
// Create a new NIO websocket server
let wss = NIOWebSocketServer.default()
// Add WebSocket upgrade support to GET /echo
wss.get("echo") { ws, req in
// Add a new on text callback
ws.onText { ws, text in
// Simply echo any received text
ws.send(text)
}
}
// Register our server
services.register(wss, as: WebSocketServer.self)
启动服务器后,就能够运行一个WebSocket的upgrade,通过GET /echo
,可以通过命令行工具wsta
来测试
$ wsta ws://localhost:8080/echo
Connected to ws://localhost:8080/echo
hello, world!
hello, world!
获取参数
// Add WebSocket upgrade support to GET /chat/:name
wss.get("chat", String.parameter) { ws, req in
let name = try req.parameters.next(String.self)
ws.send("Welcome, \(name)!")
// ...
}
$ wsta ws://localhost:8080/chat/Vapor
Connected to ws://localhost:8080/chat/Vapor
Welcome, Vapor!
Vapor同样支持作为客户端去连接WebSocket服务器,最简单的方法是使用客户端的webSocket(...)
// connect to echo.websocket.org
let done = try app.client().webSocket("ws://echo.websocket.org").flatMap { ws -> Future<Void> in
// setup an on text callback that will print the echo
ws.onText { ws, text in
print("rec: \(text)")
// close the websocket connection after we recv the echo
ws.close()
}
// when the websocket first connects, send message
ws.send("hello, world!")
// return a future that will complete when the websocket closes
return ws.onClose
}
print(done) // Future<Void>
// wait for the websocket to close
try done.wait()
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:Vapor(五)网络篇
文章字数:2.6k
本文作者:Mingfung
发布时间:2018-08-17, 20:30:00
最后更新:2018-08-28, 22:32:19
原始链接:http://blog.ifungfay.com/后端/Vapor(五)网络篇/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。