跳转至

第30章:WebSockets

WebSocketsHTTP一样,定义了一个用于两个设备间通信的协议。与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)
  }
}

下面是这个的作用:

  1. echo端点创建一个WebSocket路由处理程序。
  2. 当客户端连接时,向控制台记录一条信息。
  3. 创建一个监听器,每次端点收到文本时都会触发。
  4. 将收到的文本记录到控制台。
  5. 将收到的文本回声给发送者,并在前面加上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

img

检查Xcode控制台,你会看到ws connected

img

WebSocketKing中输入一条信息,你会看到你的服务器以适当的回声回应。

img

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)")
  }
}

以下是你的新代码的作用:

  1. /session添加一个WebSocket端点。
  2. 当你收到文本时,将其打印到控制台。

运行你的服务器应用程序,让它继续运行。然后,打开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}

棒极了! 你的服务器正在通过WebSocketiOS应用程序进行通信!这很好。

这很好! 这意味着你的应用程序正在成功地向服务器发送数据,而服务器也在成功地接收数据。返回到服务器应用程序,建立更多的会话管理逻辑。

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)")
}

这就是你的新代码的作用:

  1. 从请求的查询中获取颜色和位置。
  2. 如果你不能解码颜色或位置,则以unacceptable data状态关闭WebSocket
  3. 将颜色和位置打印到控制台。

建立并运行,然后返回到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)")
  }
}

该代码做了以下工作:

  1. 创建一个onText(_:)监听器,在WebSocket收到一些文本时运行。
  2. 将收到的文本从JSON解码为RelativePoint
  3. 用用户的新点更新TouchSessionManager中的用户。
  4. 如果解码失败,返回一个消息给客户端。

实现"Left"

最后,你需要实现WebSocket关闭的代码。你将把任何使套接字无法发送消息的断开连接或取消视为关闭。在ws.onText(_:)下面,添加:

// 1
_ = ws.onClose.always { result in
  // 2
  TouchSessionManager.default.remove(id: newId)
}

下面是最后部分的作用:

  1. WebSocket注册一个onClose处理程序。always(_:)会在任何WebSocket关闭事件中触发关闭。
  2. 使用先前创建的IDTouchSessionManager中删除用户。

建立并运行服务器,并返回到模拟器开始一个新的会话。拖动圆圈,注意服务器上的日志。你应该看到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

以下是新代码的作用:

  1. 从新用户的详细信息中创建一个SharedTouchMessage
  2. 向所有现有用户发送消息。
  3. 循环浏览每个当前用户,并创建一个新的连接消息。发送消息给新用户,这样可以跟踪所有现有用户。
  4. 存储新用户的会话,以便对未来的事件作出反应。

实现TouchSessionManager:Moved

接下来,为了处理"moved"信息,用以下代码替换update(id:to:)的主体:

// 1
participants[id]?.touch.position = pt
// 2
let msg = Message(participant: id, update: .moved(pt))
// 3
send(msg)

这部新代码做了以下工作:

  1. 使用提供的position更新参与者的位置。
  2. 创建一个新的Message,包含用户的ID和更新的点。
  3. 将消息发送到所有活动的会话。

实现TouchSessionManager:Left

最后,你需要处理关闭和取消的问题。将remove(id:)的主体替换为以下内容:

// 1
participants[id] = nil
// 2
let msg = Message(participant: id, update: .left)
// 3
send(msg)

下面是这个的作用:

  1. 从后援字典中删除相关的引用。
  2. 为要删除的用户创建一个新的Message。使用.left来通知其他用户这个用户已经离开。
  3. 向所有剩余的活动会话发送该消息。

构建并运行服务器,让它运行,然后回到ShareApp iOS Xcode项目。在任何两个模拟器上运行该项目。Xcode一次只能承载一个调试会话。然而,如果你打开第二个模拟器并选择ShareTouch应用程序,你可以运行两个会话。

在每个模拟器上选择一种颜色,然后拖动周围的圆圈来查看更新。你甚至可以运行第三个模拟器(或更多,如果你的电脑可以处理)。

接下来去哪?

你已经做到了。你的iOS应用程序通过WebSockets与你的Swift服务器进行实时通信。许多不同类型的应用程序可以从WebSockets实现的即时通信中受益,包括诸如聊天应用程序、游戏、飞机追踪器等等。如果你想象中的应用程序需要实时响应,WebSockets可能是你的答案!

挑战

为了进一步练习WebSockets,请尝试这些挑战。

  • 将服务器和客户端升级为传输原始二进制数据,而不是文本,以提高性能。
  • 增加一种方法,让用户看到更多关于活动会话的信息,比如有多少会话是活动的,它们已经活动了多长时间。
  • 保持某种历史记录,从触摸到举起,重新创建动作
  • 尝试在远程服务器上托管你的基本应用程序。确保更新iOS项目中ShareTouchApp.swiftshareSessionURL