Vapor(五)网络篇

  1. Route
  2. URL-Encoded
  3. Multipart
  4. HTTP
  5. WebSocket

这篇主要内容是再介绍一下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: &params)) // 42
print(router.route(path: ["foo"], parameters: &params)) // 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: &params)

// 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

请求消息,主要的参数是methodurl

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定义两个便利的协议HTTPMessageEncoderHTTPMessageDecoder去使用Codable,这两个编解码协议可以编解码自定义的Codable类型的数据到HTTP的消息体中,并设置适合的Content Type到消息头。

HTTP默认是提供了JSONEncoderJSONDecoder的实现,但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" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏

宝贝回家