WebSocket
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
为什么需要 WebSocket?
需求是:用户停留页面 15 分钟,且没有任何操作,则弹出登陆窗口,让用户重新登陆。
一般这样的需求实现多为长连接轮询,会有浏览器的卡顿、服务端消耗及不容易维护等问题。
后来发现 websocket 这样的通讯方式,主要有以下优点:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
目标已经选定,那么如何实现呢?
PHP 已经有非常好用的异步网络通信框架 swoole,节省了自己实现 websocket 服务的时间。我的使用的是 laravel 框架,最终选择了 laravel-swoole 扩展。
安装配置
引入 laravel-swoole 扩展包 wiki 。启用 websocket.enabled
及其他相应的配置,通过下面的命名可以非常方便的管理服务:
php artisan swoole:http {start|stop|restart|reload|infos}
修改配置文件中的默认 handler
配置为自定义的类:主要是为了自定义 websocket
的生命周期中的一些回调。
/*
|--------------------------------------------------------------------------
| Websocket handler for onOpen and onClose callback
| Replace this handler if you want to customize your websocket handler
|--------------------------------------------------------------------------
*/
'handler' => \App\Listeners\Swoole\WebsocketHandler::class,
/*
|--------------------------------------------------------------------------
| Default frame parser
| Replace it if you want to customize your websocket payload
|--------------------------------------------------------------------------
*/
'parser' => SwooleTW\Http\Websocket\SocketIO\SocketIOParser::class,
当配置完成后,会在 routes
目录中添加一个名为 websocket.php
的文件。可以非常方便像定义 laravel 路由一样,定义各种事件。例如:
//Websocket::on('open', function ($websocket, Request $request) {
// Log::info('websocket','open 111 +' . $websocket->getSender());
//});
//
//Websocket::on('connect', function ($websocket, Request $request) {
// Log::info('websocket','Connected ++ 222' . $websocket->getSender());
// // called while socket on connect.
//});
//
//Websocket::on('disconnect', function ($websocket) {
// Log::info('websocket','Disconnected ++ 333' . $websocket->getSender());
// // called while socket on disconnect
//});
// 在 UserController 中的 checkLogin 方法上会带有$websocket, $data这两个参数。
Websocket::on('loginCheck', "App\Http\Controllers\Api\UserController@checkLogin");
Websocket::on('logout', "App\Http\Controllers\Api\UserController@sendLogout");
使用
控制器:
public function checkLogin($websocket, $data)
{
if (empty($data['holding'])) {
$websocket->emit('message', ['code' => self::HTTP_UNPROCESSABLE_ENTITY, 'message' => "参数错误"]);
return false;
}
$flag = true;
$step = 1;
while ($flag) {
$step++;
if ( ! $this->validateLoginStatus($data['holding'])) {
$websocket->emit('message', ['code' => self::HTTP_UNAUTHORIZED, 'message' => "登陆超时"]);
unset($step);
$flag = false;
}else {
if($step === 1) {
$websocket->emit('message', ['code' => self::HTTP_OK, 'message' => "success"]);
}
}
sleep(1);
}
}
前端调用
这里一定要注意数据包的结构,之前就踩了比较多的坑,API docs 才找到正确的结构:
var websocket = new WebSocket("ws://127.0.0.1:1215");
websocket.onopen = function (evt) {
console.log("已连接websocket服务器");
// 这里比较关键,通道建立后,可以进非常方便的进行轮询
setInterval(function() {
if (websocket.bufferedAmount == 0)
var data = {"holding": "eyJLQNDqj0y473pCJ6zjMTUyOTk5NzU1MgnVMQ==$d84XkeMCv7umajhMRiU"};
websocket.send(encodeMessage('loginCheck', data));
}, 50);
};
// 监听消息体
websocket.onmessage = function (evt) {
console.log(decodeMessage(evt.data))
};
// 监听关闭消息
websocket.onclose = function (evt) {
console.log("websocket close");
};
//监听连接错误信息
websocket.onerror = function (evt) {
console.log(evt);
};
function decodeMessage(str) {
return JSON.parse(str.substring(2))[1] || [];
}
function encodeMessage(event, data) {
return JSON.stringify([
event,
data
])
}
Swoole扩展安装
因为 swoole 的安装依赖 php
的 sockets
模块的开启。
- 安装 swoole
中间报错,需要安装以下依赖:
yum -y install gcc postgresql-devel gcc-c++
下载 swoole 扩展源码,安装 安装步骤 进行安装即可。
性能监控
查看当前
$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
常用的三个状态是:ESTABLISHED
表示正在通信,TIME_WAIT
表示主动关闭,CLOSE_WAIT
表示被动关闭。
删除进程
查看进程数
$ ps -eaf |grep "swoole" | grep -v "grep"| awk '{print $2}'|wc -l
批量删除进程:
$ ps -eaf |grep "swoole" | grep -v "grep"| awk '{print $2}'|xargs kill -9
重启服务。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。