聊天室(上篇)GatewayWorker 基础
前言
本文的目的是基于 GatewayWorker 官方手册,梳理一次 GatewayWorker,并在实践中与 MVC 框架整合的思路(附最终的项目源码)。如果你已经理解了整合这一块儿的知识,那么就可以关掉这个网页了。时间蛮宝贵的~
这篇是上篇,梳理 GatewayWorker 基础,下篇是 GatewayWorker 与 Laravel 整合聊天室。如果你具备了 GatewayWorker 基础,请直接阅读下篇。
很久以前就想做一个聊天室了。查了下 "php 通信",找到了可用的东西:Socket、WebSocket、 Workerman 以及 GatewayWorker。Socket(接口)提供了一组端到端互相通信的接口,作为通信的核心功能。Websocket(协议)定义了通信中数据的封装和显示的格式,而且最大的特点是它支持服务端向客户端的主动推送,这一点是 HTTP 做不到的。而 Workerman (框架)将这两者很好地整合在了一起(当然不仅仅于此)。GatewayWorker(框架)是在 Workerman 的基础上开发的 TCP 长连接应用框架,提供了单发、群发和广播等接口,还可以客户端和客户端通信。
所以最终我选择了 GatewayWorker 作为 Socket 监听的服务端,Laravel 作为 HTTP 请求的业务处理框架,完成一个响应式的在线聊天室(项目地址在下一篇文章最后)。
GatewayWorker工作原理
先理解一下工作原理,可以对 GatewayWorker 有个整体的把握。这一块儿其实手册里已经详细不啰嗦地解释清楚了。我这里再理一下:
1、Register、Gateway、BusinessWorker 3 种进程依次启动(因为支持多进程,所以我说“种”,而不是“个”)
2、Gateway 进程和 BusinessWorker 进程启动后向 Register 服务进程发起长连接注册自身。
3、Register 服务进程收到 Gateway 的注册后,把所有 Gateway 进程的通讯地址写入内存。
4、Register 服务进程收到 BusinessWorker 的注册后,把内存中的 Gateway 进程通讯地址发给所有 BusinessWorker 进程。
5、BusinessWorker 进程收到所有 Gateway 进程的通讯地址后,尝试连接 Gateway。
6、至此,所有 Gateway 和 BusinessWorker 进程就通过 Register 服务进程建立了长连接。
如果期间有新的 Gateway 注册到 Register(一般是分布式部署加机器),新 Gateway 的通讯地址会被广播给所有 BusinessWorker,BusinessWorker 收到通知后建立新连接。
如果期间有 Gateway 下线,Register 会收到通知、删除这个 Gateway 的内部通讯地址,并将新的内部通讯地址列表广播给所有 BusinessWorker,BusinessWorker 不再连接下线的 Gateway。
7、客户端的连接事件和连接上的数据会经由 Gateway 转发给 BusinessWorker,BusinessWorker 默认调用 Events.php 中 Events 类的 onConnect、onMessage、onClose 事件回调处理业务逻辑。
8、BusinessWorker 负责运行所有的业务逻辑,实际的处理逻辑默认在 Events.php 中实现。
GatewayWorker进程模型
GatewayWorker 是以进程的形式进驻内存的,了解了它的工作原理之后,有必要理解一下它的进程模型。
GatewayWorker 主要有 3 种进程:Register 进程、Gateway 进程和 BusinessWorker 进程。这 3 种进程分别对应了内核源码中的 Register 类、Gateway 类和 BusinessWorker 类,并且它们都是基于 Workerman 框架的 Worker 类开发的,所以这 3 种进程都有一些公共的属性,比如 name、count、onWorkerStart、onWorkerStop 等等。可以说,GatewayWorker 里所有的进程都是 Worker 进程。
1、Register进程
Register 进程主要负责 Gateway 进程 与 BusinessWorker 进程建立连接并内部通讯。
该进程由 Register 类实例化,并随进程启动进驻内存。
它可定制的只有实例化时指定自身所在的服务进程地址。包括 IP 和端口,并且目前只支持 text 协议。text 协议是 Workerman 框架定义的一种文本协议(协议格式为:数据包 + 换行符)。
2、Gateway进程
Gateway 进程主要负责客户端的连接以及连接上的数据,并将所有的请求转发给 BusinessWorker 进程进行处理。BusinessWorker 进程的所有处理结果都经由 Gateway 进程转发给客户端。
该进程由 Gateway 类实例化,并随进程启动进驻内存。
它可定制的有:
(1)实例化。指定协议、IP 和端口。
协议:目前支持的有 Websocket 协议、text 协议、Frame 协议、自定义通讯协议和 裸 TCP 协议(不推荐,见通讯协议作用),不支持监听 HTTP 协议。
IP:"0.0.0.0" 表示监听本机所有网卡;"127.0.0.1"表示仅允许本机通过 127.0.0.1 访问该进程;内网 IP 如 "192.168.11.2" 表示只允许该 IP 访问;外网 IP 如 "110.110.110.110" 表示只允许该 IP 访问。
端口:大于 1024 小于等于 65535。小于 1024 时需要 root 权限运行该进程。
(2)name:Gateway 进程名。以便在 Bash 等终端里查看区分。
(3)count:Gateway 进程数。充分利用多 CPU 资源。默认为 1。如何设置进程数,请参考这里。
(4)lanIp:Gateway 进程所在服务器的内网 IP,默认填写 "127.0.0.1" 即可。多服务器分布式部署 时要填写真实 IP。无论如何都不能填写 "0.0.0.0"。
(5)startPort:Gateway 进程启动后监听的起始端口(本机端口),用来给 BusinessWorker 进程提供连接服务,然后两者通过这个端口建立通讯。假设进程数 count 为 4,起始端口 startPort 为 2003,则 会启动 4 个 Gateway进程,各进程分别监听 2003、2004、2005、2006 端口。
(6)registerAddress:向 Register 进程的注册地址,格式为"IP + 端口",如 "127.0.0.1:1236"。和 BusinessWorker 进程指定的注册地址要保持一致。
(7)心跳设置:为了防止长时间不通讯被路由节点强行断开或断电断网等极端事件,必须加心跳。相关属性有 pingInterval、pingNotResponseLimit、pingInterval。详细心跳设置请参考服务端到客户端的心跳检测。
pingInterval:心跳间隔,单位秒,0 表示不发送心跳检测。
pingNotResponseLimit:客户端连续 $pingNotResponseLimit * $pingInterval 秒内不回应心跳则断开连接。
pingData:心跳数据,可任意,客户端能识别就行。
(8)onWorkerStart:Gateway 进程启动后的回调函数。
(9)onWorkerStop:Gateway 进程关闭的回调函数。
(10)onConnect:当有客户端连接上来时触发。与 Events::onConnect 的区别是 Events::onConnect 方法运行在 BusinessWorker 进程上。而 Gateway::onConnect 方法是运行在Gateway 进程上,无法使用 \GatewayWorker\Lib\Gateway 类提供的接口。
(11)onClose:当有客户端连接关闭时触发。同样与Events::onClose的区别是 Gateway::onClose 方法是运行在 Gateway 进程上,无法使用 \GatewayWorker\Lib\Gateway 类提供的接口。
3、BusinessWorker进程
BusinessWorker 进程负责运行业务逻辑。BusinessWorker 进程收到 Gateway 进程转发来的事件和请求时,会默认调用 Events.php 中的 onConnect、onMessage、onClose 方法处理事件和数据。
该进程由 BusinessWorker 类实例化,并随进程启动进驻内存。
它可定制的有:
(1)name:BusinessWorker 进程名。以便在 Bash 等终端里查看区分。
(2)count:BusinessWorker 进程数。充分利用多 CPU 资源。默认为 1。如何设置进程数,请参考这里。
(3)registerAddress:向 Register 进程的注册地址,格式为"IP + 端口",如 "127.0.0.1:1236"。和 Gateway 进程指定的注册地址要保持一致。
(4)onWorkerStart:BusinessWorker 进程启动后的回调函数
(5)onWorkerStop:BusinessWorker 进程关闭的回调函数。
(6)eventHandler:指定 BusinessWorker 进程里实际处理业务逻辑的类,默认是 Events。也就是默认使用 Events.php 中的 Events 类来处理业务。业务类至少要实现onMessage 静态方法,onConnect 和 onClose 静态方法可以不用实现。(如果使用了命名空间,建议填写完全限定名称的命名空间。)
。
Events.php
上面提到了 Events.php,它是实际处理业务逻辑的类 Events 所在的文件。我们在实际的开发中,只需要关注这一个文件。
Events 里有 5 个事件回调的处理方法,按照发生顺序,依次是
- onWorkerStart (Worker $businessWorker):当 BusinessWorker 进程启动时触发。每个进程生命周期内只触发一次。
-
onConnect (string $client_id):当客户端连接上 Gateway 进程时触发(TCP 三层握手)。
- onMesssge (string $client_id, mixed $recv_data):当客户端发来数据,也就是 Gateway 进程收到数据后触发。
-
onClose (string client_id):当客户端连接断开时触发。无论是客户端还是服务端主动断开,都会触发。
- onWorkerStop (Worker $businessWorker):当 BusinessWorker 进程退出时触发。每个进程生命周期内只触发一次。
这里面我们常用到的是 onMessage 和 onClose 回调,其他比较少用。
上面的回调事件里有一个比较重要的参数:$client_id。client_id 是 20 个字符的定长字符串,用来全局标识一个 Socket 连接。每个客户端连接都会被分配一个全局唯一的 client_id。客户端关闭连接时,对应的 client_id 会失效。当客户端再次打开一个 Socket 连接时,会被分配一个新的 client_id。
Lib\Gateway类提供的接口
既然(默认)在 Events.php 中处理实际的业务逻辑,回调的事件我们已经知道了。那么怎么向客户端发送消息呢?
命名空间 \GatewayWorker\Lib\Gateway 指向的这个 Gateway 类,提供了一组单发、群发和广播的接口,在 Events.php 中向客户端发信的时候就可以使用这个类。它提供的接口非常丰富:
Gateway::sendToAll($data); // 向所有客户端发送数据 Gateway::sendToClient($client_id, $data); // 向某个客户端发送数据 Gateway::closeClient($client_id); // 关闭某个客户端 Gateway::isOnline($client_id); // 判断某客户端连接是否在线 Gateway::bindUid($client_id, $uid); // 绑定 uid 与 client_id Gateway::unbindUid($client_id, $uid); // 取消 uid 与 某个 client_id 的绑定 Gateway::isUidOnline($uid); // 某个 uid 是否在线 Gateway::GetClientIdByUid(); // 获取与 uid 绑定的 client_id 列表(一对多) Gateway::sendToUid($uid, $data); // 向所有 uid 发送 Gateway::joinGroup($client_id, $group); // 把该 client_id 加入群组 Gateway::leaveGroup($client_id, $group); // 将 client_id 离开群组 Gateway::sendToGroup($group, $data); // 向某群组 group 发送 Gateway::getClientCountByGroup($group); // 获取某个组的在线连接数 Gateway::getClientSessionsByGroup($group); // 获取某个组的连接信息 Gateway::getClientInfoByGroup($group); // getClientSessionsByGroup 的别名 Gateway::getAllClientCount(); // 获取所有的在线连接数 Gateway::getAllClientSessions(); // 获取所有在线用户的 session Gateway::getAllClientInfo(); // getAllClientSessions 的别名 Gateway::setSession($client_id, $session); // 设置 session,原 session 值会被覆盖 Gateway::updateSession($client_id, $session); // 更新 session,实际上是与旧的session合并 Gateway::getSession($client_id); // 获取某个 client_id的 session
这里面比较重要的是 GatewayWorker 的超全局数组 $_SESSION。每个客户端连接对应一个 Session 会话,并由 Gateway 进程存储在内存里。示例如下,在收到客户端消息时,打印所有在线连接的 Session:
use \GatewayWorker\Lib\Gateway; class Events { ... public onMessage($client_id, $message) { $_SESSION['name'] = $message['name']; // 操作当前用户的 Session 时,直接使用 $_SESSION 即可 var_export(Gateway::getAllClientSessions()); } ... } 打印出的数据类似如下: array( '7f00000108fc00000008' => array('name' => 'Tom'), '7f00000108fc00000009' => array('name' => 'Joan') )
注意上面的注释,操作当前连接上的 Session 时,直接使用 $_SESSION['xx'] = 'xxx'; 的方式赋值即可,操作其他用户的 Session 时用 Gateway::setSession 接口。
此外,如果你在 GatewayWorker 的进程模型里需要获取客户端、服务端的 IP,请使用 $_SERVER 数组。它由 Workerman 框架定义,内置了 5 个数组成员,数组 key 分别如下,详细请参考文档。
REMOTE_ADDR // 客户端IP(如果客户端处于局域网,则是客户端所在局域网的出口IP) REMOTE_PORT // 客户端端口(如果客户端处于局域网,则是客户端所在局域网的出口端口) GATEWAY_ADDR // Gateway 进程所在服务器的 IP GATEWAY_PORT // Geteway 监听的端口,这对于多端口应用中在 Events.php 里区分客户端连的是哪个端口非常有用。 GATEWAY_CLIENT_ID // 全局唯一的客户端 IP
好的。有关 GatewayWorker 框架的基础暂时就梳理这么多。更多 GatewayWorker 开发和部署的细节或问题,比如心跳检测、设置定时器、合理选择多进程、分布式部署、定制通讯协议、启用 wss 协议等等,都在文档里有详细的介绍。车在下面。
我感觉这一篇有点长了,所以将在下一篇开始梳理 GatewayWorker 与 Laravel 框架的整合。
相关链接
GatewayWorker 在线文档:http://doc2.workerman.net/326102
Workerman 在线文档:http://doc.workerman.net/
Workerman 官网:https://www.workerman.net/