最近在把 Facebook Message 接入客服系统,因为与 Facebook Message 对接的收发消息都是经过调用 http 接口来实现的,若是想实现即时通信,还须要在中间加一个 WebSocket 来转发消息。以下图:php
其中用到了 WebSocket 协议和 IO多路复用相关的知识。在这里作一个学习记录。nginx
WebSocket 链接的初期是基于 HTTP 协议的,假如 WebSocket 的地址是这个:wss://www.xxx.com/websocket ,在链接 WebSocket 的初期浏览器首先会向这个地址发出一个 HTTP GET 请求,请求头信息截图以下:web
红色框标出的是比较重要的请求头:redis
Connection: Upgrade
告诉服务端这个链接须要升级。Upgrade: websocket
告诉服务端须要升级到 WebSocket 协议。Sec-WebSocket-Key: d97OXZzuRlSJV/6SrX+uUA==
是浏览器随机生成的一个字符串。服务端接收到这个 HTTP 请求,会做出响应,响应头的截图以下:算法
红色框标出的是比较重要的响应头:编程
HTTP/1.1 101 Switching Protocols
告诉浏览器,服务端已经成功切换了协议。Sec-WebSocket-Accept: axMY+KY1i8F9y9zyUMPhrfuYtPw=
这个是服务端拿到请求头中的 Sec-WebSocket-Key: d97OXZzuRlSJV/6SrX+uUA==
,在 d97OXZzuRlSJV/6SrX+uUA==
后面拼接一个固定的字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,对拼接后的字符串作SHA1,获得16进制表示的字符串,将每两位看成一个字节进行分隔,获得字节数组,再对这个字节数组作Base64,获得最后的结果,把最后的结果放到 Sec-WebSocket-Accept
响应头里返回。浏览器也会使用一样的算法把请求头中的 Sec-WebSocket-Key
算出一个结果,将这个结果与服务端返回的 Sec-WebSocket-Accept
作对比。就像对暗号同样,两边的暗号相同,WebSocket 链接就会被创建起来。这个过程也叫作握手,握手成功后,就能够愉快的使用这个 WebSocket 链接来收发消息了。数组
WebSocket 的通讯,实际上是利用了操做系统给咱们提供的一套 socket 编程接口。接下来,我把 Linux 系统中给咱们提供的 socket 头文件找出来,看看里面有哪些接口提供给咱们使用,以及每一个接口的做用是什么。找到 socket.h 头文件在以下位置:浏览器
打开 socket.h 文件:服务器
打开另外一个目录下的 socket.h 文件:websocket
socket 编程的流程以下:
在 socket 服务端除了用到上面流程图列出来的函数,还用到了 setsockopt() 函数,这个函数能够用来设置一些 socket 选项。好比:我在开发调试的过程当中,改完代码后须要杀掉运行中的 socket 进程,从新运行新编译出来的 socket。这时候常常会运行失败,缘由是进程是立马被杀掉了,可是原来被进程监听的那个端口会进入 TIME_WAIT 状态,而不会当即被释放出来。解决方法有两个:一、杀掉进程后等一下子,端口被释放了就能被再次使用了。二、在绑定端口以前,利用 setsockopt() 函数,给端口设置一个 SO_REUSEPORT 选项,这样杀掉这个进程后立马从新运行这个进程,也不会运行失败。
在项目中还用到了IO 多路复用:
IO 多路复用有3 种:select、poll、epoll。在项目中用到的是 epoll。接下来,我把 Linux 系统中给咱们提供的 epoll 头文件找出来,看看里面有哪些接口提供给咱们使用,以及每一个接口的做用是什么。找到 epoll.h 头文件在以下位置:
打开 epoll.h 文件:
epoll 的使用流程以下:
看到网上有文章说 redis 和 nginx 也有使用 epoll,为了验证他讲的是否是真的。咱们找 redis 和 nginx 的源码看一看:
果真 redis 和 nginx 的源码里面都有使用 epoll。
//建立WebSocket Server对象,监听0.0.0.0:9502端口 $ws = new Swoole\WebSocket\Server('0.0.0.0', 9502); //监听WebSocket链接打开事件 $ws->on('open', function ($ws, $request) { var_dump($request->fd, $request->server); $ws->push($request->fd, "hello, welcome\n"); }); //监听WebSocket消息事件 $ws->on('message', function ($ws, $frame) { echo "Message: {$frame->data}\n"; $ws->push($frame->fd, "server: {$frame->data}"); }); //监听WebSocket链接关闭事件 $ws->on('close', function ($ws, $fd) { echo "client-{$fd} is closed\n"; }); $ws->start();
想了解更多,请参考 Swoole 官方文档:https://wiki.swoole.com/#/
在学习 WebSocket 的过程当中,还发现了一个纯 PHP 实现的框架:Workerman
<?php use Workerman\Worker; require_once __DIR__ . '/Workerman/Autoloader.php'; // 注意:这里与上个例子不一样,使用的是websocket协议 $ws_worker = new Worker("websocket://0.0.0.0:2000"); // 启动4个进程对外提供服务 $ws_worker->count = 4; // 当收到客户端发来的数据后返回hello $data给客户端 $ws_worker->onMessage = function($connection, $data) { // 向客户端发送hello $data $connection->send('hello ' . $data); }; // 运行worker Worker::runAll();
想了解更多,请参考 Workerman 官方文档:http://doc.workerman.net/