第30章:WebSockets
¶
WebSockets
和HTTP
一样,定义了一个用于两个设备间通信的协议。与HTTP
不同,WebSocket
协议是为实时通信设计的。对于像聊天或其他需要实时行为的功能来说,WebSockets
是一个不错的选择。Vapor
提供了一个简洁的API
来创建WebSocket
服务器或客户端。本章的重点是建立一个基本的服务器。
在本章中,你将建立一个简单的客户端-服务器应用程序,允许用户与其他用户共享触摸,并实时查看其他用户在自己设备上的触摸。
工具¶
测试WebSockets
可能有点棘手,因为它们可以发送/接收多个消息。这使得使用一个简单的CURL
请求或浏览器变得很困难。幸运的是,有一个很棒的WebSocket
客户端工具,你可以用它来测试你的服务器:https://www.websocketking.com。值得注意的是,截至本文撰写时,只有Chrome
浏览器支持与localhost
的连接。
一个基本的服务器¶
现在你的工具已经准备好了,现在是时候建立一个非常基本的WebSocket
服务器了。将本章的启动项目复制到你喜欢的位置,并在该目录下打开一个终端窗口。
输入以下命令:
cd share-touch-server
open Package.swift
这样就可以导航到share-touch-server
目录,并在Xcode
中打开该项目。
Echo
服务器¶
打开WebSockets.swift
,在sockets(_:)
的末尾添加以下内容,以创建一个Echo
端点:
// 1
app.webSocket("echo") { req, ws in
// 2
print("ws connected")
// 3
ws.onText { ws, text in
// 4
print("ws received: \(text)")
// 5
ws.send("echo: " + text)
}
}
下面是这个的作用:
- 为
echo
端点创建一个WebSocket
路由处理程序。 - 当客户端连接时,向控制台记录一条信息。
- 创建一个监听器,每次端点收到文本时都会触发。
- 将收到的文本记录到控制台。
- 将收到的文本回声给发送者,并在前面加上
echo:
。
在Xcode
的方案选择器中,选择ShareTouchServer
方案和My Mac
作为目标。建立并运行。在你的浏览器中,打开https://websocketking.com,在URL
字段中输入ws://localhost:8080/echo,然后按Connect
。你应该在日志中看到类似的内容:
Connected to ws://localhost:8080/echo
Connecting to ws://localhost:8080/echo
检查Xcode
控制台,你会看到ws connected
。
在WebSocketKing
中输入一条信息,你会看到你的服务器以适当的回声回应。
Sessions¶
Now that you’ve verified you can communicate with your server, it’s time to add more capabilities to it. For the basic application, you’ll use a single WebSocket endpoint at /session.
You’ll be using an in-memory manager. This means if your application were to scale up to multiple servers, you’d need a more complex management system that assigns various users to various servers. For now, you can assume a single session for all your users and a single server is enough.
Here’s the basic architecture you’ll use:
会话¶
现在你已经验证了你可以与你的服务器通信,现在是时候为它添加更多的功能了。对于基本的应用程序,你将在/session
使用一个单一的WebSocket
端点。
你将使用一个内存管理器。这意味着如果你的应用程序要扩展到多个服务器,你需要一个更复杂的管理系统,将各种用户分配到各种服务器上。现在,你可以假设所有的用户都有一个会话,一个服务器就足够了。
下面是你要使用的基本架构:
客户端 -> 服务器¶
从客户端到服务器的连接可以处于三种状态之一:加入、移动和离开。
已加入¶
一个新的参与者将使用/session
端点打开一个WebSocket
。在打开的请求中,你将包括来自用户的两个信息:要使用的颜色--用r,g,b,a
表示--和一个起点--用一个相对点表示。
为了你的目的,相对点使用一个0-1.0
的刻度,代表屏幕的可见区域。这允许你在不同的屏幕尺寸之间转换触摸。
移动了¶
为了简单起见,在客户端打开一个新的会话后,它将向服务器发送的唯一东西是用户拖动圆圈时的新相对点。
离开¶
这个服务器将把客户端的任何关闭解释为离开房间。这使事情保持简洁。
服务器->客户端¶
服务器向客户发送三种不同类型的信息:加入、移动和离开。
已加入¶
当服务器发送joined
消息时,它在消息中包括一个ID
,一个颜色和该参与者的最后已知点。
在客户端成功连接后,服务器将立即通过发送加入消息通知该客户端所有当前的参与者。
移动了¶
任何时候一个参与者移动,服务器都会通知客户端。这些通知只包括一个ID
和一个新的相对点。
离开¶
任何时候一个参与者从会话中断开连接,服务器都会通知所有其他参与者,并将该用户从相关的视图中删除。
现在你了解了应用程序所使用的状态和消息,是时候开始实施了。
设置"Join"
¶
打开WebSockets.swift
,在sockets(_:)
的末尾添加以下内容:
// 1
app.webSocket("session") { req, ws in
// 2
ws.onText { ws, text in
print("got message: \(text)")
}
}
以下是你的新代码的作用:
- 为
/session
添加一个WebSocket
端点。 - 当你收到文本时,将其打印到控制台。
运行你的服务器应用程序,让它继续运行。然后,打开iOS项目。
iOS
项目¶
本章的材料包括一个完整的iOS
应用程序。你可以在ShareTouchApp.swift
中改变你想使用的URL
。现在,它应该被设置为ws://localhost:8080/session
。在模拟器中构建并运行该应用程序。选择一种颜色并按BEGIN
,然后在屏幕上拖动圆圈。你应该在你的服务器应用程序中看到日志,看起来类似于以下内容:
got message: {"x":0.62031250000000004,"y":0.60037878787878785}
got message: {"x":0.61250000000000004,"y":0.59469696969696972}
got message: {"x":0.60781249999999998,"y":0.59185606060606055}
got message: {"x":0.59999999999999998,"y":0.59469696969696972}
棒极了! 你的服务器正在通过WebSocket
与iOS
应用程序进行通信!这很好。
这很好! 这意味着你的应用程序正在成功地向服务器发送数据,而服务器也在成功地接收数据。返回到服务器应用程序,建立更多的会话管理逻辑。
Note
如果你试图在设备上运行iOS
应用程序,你需要改变ShareTouchApp.swift
中的URL
,以便通过WiFi
定位你的计算机的IP
地址。如果你想测试远程设备并通过隧道将它们连接到你的计算机的服务器,请查看ngrok
! 它是一个很好的工具,可以很容易地设置转发到你的计算机服务器的域。
完成"Join"
¶
如前所述,客户端将在Web Socket
连接请求中包含一个颜色和一个起始位置。WebSocket
请求被当作一个升级的GET请求,所以你会在请求的查询中包含数据。在WebSockets.swift
中,用以下内容替换你之前为app.webSocket("session")
添加的代码:
app.webSocket("session") { req, ws in
// 1
let color: ColorComponents
let position: RelativePoint
do {
color = try req.query.decode(ColorComponents.self)
position = try req.query.decode(RelativePoint.self)
} catch {
// 2
_ = ws.close(code: .unacceptableData)
return
}
// 3
print("new user joined with: \(color) at \(position)")
}
这就是你的新代码的作用:
- 从请求的查询中获取颜色和位置。
- 如果你不能解码颜色或位置,则以
unacceptable data
状态关闭WebSocket
。 - 将颜色和位置打印到控制台。
建立并运行,然后返回到iOS
模拟器,按BEGIN
。你应该看到服务器记录了你选择的颜色。选择一个不同的颜色,注意组件是如何变化的。
接下来,你需要用TouchSessionManager
来设置用户。还是在WebSockets.swift
中,找到:
print("new user joined with: \(color) at \(position)")
并在其下方添加以下内容:
let newId = UUID().uuidString
TouchSessionManager.default
.insert(id: newId, color: color, at: position, on: ws)
这将为用户创建一个新的ID
,使用UUID
,并使用先前的颜色和位置将用户插入TouchSessionManager
中。
处理"Moved"
¶
接下来,你需要监听来自客户端的消息。现在,你只希望收到一个RelativePoint
对象的流。在这种情况下,你将使用onText(_:)
。使用onText(_:)
可能比使用onBinary(_:)
和直接接收数据的性能稍差。然而,它使调试更容易,而且你可以在以后改变它。
在TouchSessionManager.default.insert(id: newId, color: color, at: position, on: ws)
下面添加以下内容:
// 1
ws.onText { ws, text in
do {
// 2
let pt = try JSONDecoder()
.decode(RelativePoint.self, from: Data(text.utf8))
// 3
TouchSessionManager.default.update(id: newId, to: pt)
} catch {
// 4
ws.send("unsupported update: \(text)")
}
}
该代码做了以下工作:
- 创建一个
onText(_:)
监听器,在WebSocket收到一些文本时运行。 - 将收到的文本从JSON解码为
RelativePoint
。 - 用用户的新点更新
TouchSessionManager
中的用户。 - 如果解码失败,返回一个消息给客户端。
实现"Left"
¶
最后,你需要实现WebSocket关闭的代码。你将把任何使套接字无法发送消息的断开连接或取消视为关闭。在ws.onText(_:)
下面,添加:
// 1
_ = ws.onClose.always { result in
// 2
TouchSessionManager.default.remove(id: newId)
}
下面是最后部分的作用:
- 为
WebSocket
注册一个onClose
处理程序。always(_:)
会在任何WebSocket
关闭事件中触发关闭。 - 使用先前创建的
ID
从TouchSessionManager
中删除用户。
建立并运行服务器,并返回到模拟器开始一个新的会话。拖动圆圈,注意服务器上的日志。你应该看到TrackingSessionManager
的日志,但它还没有实现。
实现TouchSessionManager:Joined
¶
在这一点上,你可以成功地将WebSocket
事件分派给TouchSessionManager
中与之相关的架构事件。接下来,你需要实现管理逻辑。打开TouchSessionManager.swift
,将insert(id:color:at:on:)
的主体替换为以下内容:
// 1
let start = SharedTouch(
id: id,
color: color,
position: pt)
let msg = Message(
participant: id,
update: .joined(start))
// 2
send(msg)
// 3
participants.values.map {
Message(
participant: $0.touch.participant,
update: .joined($0.touch))
} .forEach { ws.send($0) }
/// store new session
// 4
let session = ActiveSession(touch: start, ws: ws)
participants[id] = session
以下是新代码的作用:
- 从新用户的详细信息中创建一个
SharedTouch
和Message
。 - 向所有现有用户发送消息。
- 循环浏览每个当前用户,并创建一个新的连接
消息
。发送消息给新用户,这样可以跟踪所有现有用户。 - 存储新用户的会话,以便对未来的事件作出反应。
实现TouchSessionManager:Moved
¶
接下来,为了处理"moved"
信息,用以下代码替换update(id:to:)
的主体:
// 1
participants[id]?.touch.position = pt
// 2
let msg = Message(participant: id, update: .moved(pt))
// 3
send(msg)
这部新代码做了以下工作:
- 使用提供的
position
更新参与者的位置。 - 创建一个新的
Message
,包含用户的ID
和更新的点。 - 将消息发送到所有活动的会话。
实现TouchSessionManager:Left
¶
最后,你需要处理关闭和取消的问题。将remove(id:)
的主体替换为以下内容:
// 1
participants[id] = nil
// 2
let msg = Message(participant: id, update: .left)
// 3
send(msg)
下面是这个的作用:
- 从后援字典中删除相关的引用。
- 为要删除的用户创建一个新的
Message
。使用.left
来通知其他用户这个用户已经离开。 - 向所有剩余的活动会话发送该消息。
构建并运行服务器,让它运行,然后回到ShareApp iOS Xcode
项目。在任何两个模拟器上运行该项目。Xcode
一次只能承载一个调试会话。然而,如果你打开第二个模拟器并选择ShareTouch
应用程序,你可以运行两个会话。
在每个模拟器上选择一种颜色,然后拖动周围的圆圈来查看更新。你甚至可以运行第三个模拟器(或更多,如果你的电脑可以处理)。
接下来去哪?¶
你已经做到了。你的iOS
应用程序通过WebSockets
与你的Swift
服务器进行实时通信。许多不同类型的应用程序可以从WebSockets
实现的即时通信中受益,包括诸如聊天应用程序、游戏、飞机追踪器等等。如果你想象中的应用程序需要实时响应,WebSockets
可能是你的答案!
挑战¶
为了进一步练习WebSockets
,请尝试这些挑战。
- 将服务器和客户端升级为传输原始二进制数据,而不是文本,以提高性能。
- 增加一种方法,让用户看到更多关于活动会话的信息,比如有多少会话是活动的,它们已经活动了多长时间。
- 保持某种历史记录,从触摸到举起,重新创建动作
- 尝试在远程服务器上托管你的基本应用程序。确保更新
iOS
项目中ShareTouchApp.swift
的shareSessionURL
。