项目中用到了websocket长链接, 记录下结合swoole如何实现这个功能
项目中之所以要用websocket主要是想实现用户在回收设备上扫码投递瓶子之后,将投递的瓶子数据推送到用户小程序端进行同步展示, 这样用户在设备上投递完瓶子后, 在小程序上就能同时看到相应变化, 给用户一个更好的使用体验
面向过程风格代码
//引入redis
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('***'); //redis密码
$redis->select(15);//选择使用的redis库
//创建websocket服务端
$server = new Swoole\Server('0.0.0.0', 9502, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$server->set(array(
'ssl_cert_file' => __DIR__.'/config/fullchain.pem',
'ssl_key_file' => __DIR__.'/config/privkey.pem',
'ssl_verify_peer' => false,
'ssl_allow_self_signed' => true,
'log_file' => __DIR__ . '/cert/' . date('Ymd') . '.log',
));
//监听WebSocket连接打开事件。
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
$returnData = ['fid' => $request->fd];
$ws->push($request->fd, json_encode($returnData));
});
//监听WebSocket消息事件。
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "Message: {$frame->data}\n";
$ws->push($frame->fd, "server: {$frame->data}");
$data = json_decode($frame->data, true);
//用redis存储小程序用户id与wesocket的链接标识fd的对应关系
if (isset($data['uid']) && $data['uid']) {
$oldFid = $redis->hGet('user:links', $data['uid']);
if ($oldFid != $frame->fd) {
$redis->hSet('user:links', $data['uid'], $frame->fd);
}
$returnData = [
'uid' => $data['uid'],
'fid' => $frame->fd
];
//给对应链接推送个消息
$ws->push($frame->fd, json_encode($returnData));
}
});
//监听WebSocket连接关闭事件。
$server->on('close', function ($server, $fd) {
echo "client {$fd} closed\n";
});
//设置onRequest回调,WebSocket\Server 也可以同时作为 HTTP 服务器,
//这样就可以通过接收HTTP请求来触发webSocket的推送, 这样就可以在程序中主动触发推送了
$server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
global $server;//调用外部的server
$post = $request->post ?: [];//获取post请求传递的数据
if (isset($post['fd']) && $ws->isEstablished(intval($request->post['fd']))) {
$ws->push($request->post['fd'], $request->post['message']);
}
//可以通过$server->connections 遍历所有websocket连接用户的fd,给所有用户推送
});
$server->start();
面向对象风格代码
//声明一个WebSocketServer 服务类
class WebSocketServer
{
public $server;
public function __construct()
{
$this->server = new Swoole\WebSocket\Server("0.0.0.0", 9502);
$this->server->on('open', function (Swoole\WebSocket\Server $server, $request) {
echo "server: handshake success with fd{$request->fd}\n";
});
$this->server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
$server->push($frame->fd, "this is server");
});
$this->server->on('close', function ($ser, $fd) {
echo "client {$fd} closed\n";
});
$this->server->on('request', function (Swoole\Http\Request $request, Swoole\Http\Response $response) {
// 接收http请求从get获取message参数的值,给用户推送
// $this->server->connections 遍历所有websocket连接用户的fd,给所有用户推送
foreach ($this->server->connections as $fd) {
// 需要先判断是否是正确的websocket连接,否则有可能会push失败
if ($this->server->isEstablished($fd)) {
$this->server->push($fd, $request->get['message']);
}
}
});
$this->server->start();
}
}
//实例化这个服务类,这样就启动了websocket服务了
new WebSocketServer();
上面分别用了"面向过程风格"与"面向对象风格"演示了如何通过swoole创建一websocket服务端, 有下面几点需要注意的:
- 通过设置onRequest回调, 在创建websocket服务端的时候同时内部也起了一个http服务器,监听9502端口, 这样在程序中如果想给某个用户主动推送数据的时候, 就可以借助这个9502端口的http服务, 发送post请求, 参数传递要发送请求的websocket链接标识fd以及要传递信息, 这样就可以主动在程序中向用户推送数据了
- Swoole\Http\Request $request的post属性中存储的是HTTP请求的POST参数,格式为数组
//post属性
Swoole\Http\Request->post: array
//示例
echo $request->post['hello'];
// 获取所有POST参数
var_dump($request->post);
//get属性
//存的是HTTP请求的GET参数, 相当于PHP中的$_GET, 格式为数组
Swoole\Http\Request->get: array
// 如:index.php?hello=123
echo $request->get['hello'];
// 获取所有GET参数
var_dump($request->get);
- 讲几个配置项
ssl_cert_file / ssl_key_file
设置SSL隧道加密, 设置值为一个文件名字符串, 指定cert证书和key私钥的路径信息.
wss应用中, 发起WebSocket连接的页面必须使用HTTPS, 且HTTPS应用浏览器必须信任证书才能浏览网页, 浏览器如果不信任sll证书将无法使用wss.log_file
指定Swoole错误日志文件. 在 Swoole 运行期发生的异常信息会记录到这个文件中,默认会打印到屏幕。开启守护进程模式后 (daemonize => true),标准输出将会被重定向到 log_file。在 PHP 代码中 echo/var_dump/print 等打印到屏幕的内容会写入到 log_file 文件。
debug_mode 调试模式
设置日志模式为 debug 调试模式,只有编译时开启了 --enable-debug 才有作用。
$server->set([ 'debug_mode' => true ])daemonize 守护进程化(默认false)
设置 daemonize => true 时,程序将转入后台作为守护进程运行。长时间运行的服务器端程序必须启用此项。如果不启用守护进程,当 ssh 终端退出后,程序将被终止运行。
- 启用守护进程后,标准输入和输出会被冲定向到log_file.
- 如果没有设置log_file, 将重定向到/dev/null, 所有打印屏幕的信息都会被丢弃
- 使用supervisord或者systemd管理Swoole服务的时候, 请勿设置daemonize=true. 主要因为两者机制不同
实际测试看看效果
在宝塔面板中通过supervisord进行守护进程管理:
守护进程启动后,就可以进行websocket链接了
连接成功之后就可以给websocket服务端发送消息了
如果这个时候想测试通过服务器给某个用户推送数据,就可以借助启动的HTTP服务
// 写一个测试方法,
public function testPush()
{
$userId = $this->request->param('user_id',3075);
$redis = RedisService::getInstance();
$redis->select(15);
$fd = $redis->hGet('user:links', $userId);
if (!$fd) {
return frontReturn(0, 'websockt未连接');
}
$domain = 'https://***.demo-domain.com';
$port = 9502;
$listNew = ['test' => 'data'];
Http::post("{$domain}:{$port}", [
'fd' => $fd,
'message' => json_encode(['list_new' => $listNew])
]);
return frontReturn(1, 'ok',['list_new' => $listNew]);
}
然后请求接口如下:
接口请求完成之后,再看刚刚的websocket连接界面,就会收到一条新的消息:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。