在正常业务中经?;崤龅椒衿餍枰蚩突Ф朔⑵鹬鞫扑褪莸男枨?,其实有很多实现方案,
①、客户端轮询接口
- 优点:简单
- 缺点:如果是请求频率比较高,业务场景比较常用,可能会对服务器造成比较大的压力。
②、借用第三方推送服务的静默推送(即消息模式),比如友盟,极光
- 优点:对服务器压力小,不需要自建长连接服务,移动客户端支持也比较好
- 缺点:依赖于第三方,受制于人,web版不支持
③、自建长连接服务
- 优点:提升技能点,锻炼能力,可定制化需求,自由度高
- 缺点:需要开发时间,配置socket服务,需守护进程保证服务不中断
下面我们就开始研究如何用PHP实现长连接问题:
PHP自身支持socket编程,但是比较繁琐,网上常用的轮子有两种 swoole (c 扩展) 和 workerman(PHPsocket),本文以workerman为例。
1、下载workerman包
workerman官网地址:https://www.workerman.net/workerman
支持直接下载,或者composer安装
2、测试socket连接
首先在把下载的包解压放在php项目里,在根目录建立一个start.php文件
<?php
use Workerman\Worker;
require_once 'Autoloader.php';
// 创建一个Worker监听2346端口,使用websocket协议通讯
$ws_worker = new Worker("websocket://0.0.0.0:2345");
// 启动4个进程对外提供服务
$ws_worker->count = 4;
// 当收到客户端发来的数据后返回hello $data给客户端
$ws_worker->onMessage = function($connection, $data)
{
// 向客户端发送hello $data
$connection->send('hello ' . $data);
};
// 运行
Worker::runAll();
然后建立html文件,index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>测试websocket</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://127.0.0.1:2345");
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert(received_msg);
};
ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">运行 WebSocket</a>
</div>
</body>
</html>
然后在cmd命令行窗口进入项目目录,运行
php start.php start -d
会看到
就代表服务以及启动成功了
接下里打开index.html,运行,如果能正常收到页面的alert消息,就代表通讯已经没有问题了。
3、正式业务中如何使用主动推送
上面的例子,我们只是建立好了socket连接,客户端在发送内容到服务器之后能收到返回的消息,这时候我们如何让服务器主动给客户端推送消息呢。实现的思想其实是建立一个对外监听的worker容器,再开启一个内部数据推送监听的端口,再把客户端通过uid做一个映射,通过监听内部端口的数据,来实现把数据转发到对应的映射内的客户端来实现。友盟的推送,laravel的广播功能,都是通过这种逻辑实现的。
下面分别贴一下服务器端服务代码,服务器端推送代码,客户端html代码就可以轻松看明白了。
a、服务代码 start.php
<?php
use Workerman\Worker;
require_once 'Autoloader.php';
// 初始化一个worker容器,监听1234端口
global $worker;
$worker = new Worker('websocket://0.0.0.0:1234');
// 这里进程数必须设置为1
$worker->count = 1;
// worker进程启动后建立一个内部通讯端口
$worker->onWorkerStart = function($worker)
{
// 开启一个内部端口,方便内部系统推送数据,Text协议格式 文本+换行符
$inner_text_worker = new Worker('Text://0.0.0.0:5678');
$inner_text_worker->onMessage = function($connection, $buffer)
{
global $worker;
// $data数组格式,里面有uid,表示向那个uid的页面推送数据
$data = json_decode($buffer, true);
$uid = $data['uid'];
// 通过workerman,向uid的页面推送数据
$ret = sendMessageByUid($uid, $buffer);
// 返回推送结果
$connection->send($ret ? 'ok' : 'fail');
};
$inner_text_worker->listen();
};
// 新增加一个属性,用来保存uid到connection的映射
$worker->uidConnections = array();
// 当有客户端发来消息时执行的回调函数
$worker->onMessage = function($connection, $data)use($worker)
{
// 判断当前客户端是否已经验证,既是否设置了uid
if(!isset($connection->uid))
{
// 没验证的话把第一个包当做uid(这里为了方便演示,没做真正的验证)
$connection->uid = $data;
/* 保存uid到connection的映射,这样可以方便的通过uid查找connection,
* 实现针对特定uid推送数据
*/
$worker->uidConnections[$connection->uid] = $connection;
$connection->send($data);
return;
}
};
// 当有客户端连接断开时
$worker->onClose = function($connection)use($worker)
{
global $worker;
if(isset($connection->uid))
{
// 连接断开时删除映射
unset($worker->uidConnections[$connection->uid]);
}
};
// 向所有验证的用户推送数据
function broadcast($message)
{
global $worker;
foreach($worker->uidConnections as $connection)
{
$connection->send($message);
}
}
// 针对uid推送数据
function sendMessageByUid($uid, $message)
{
global $worker;
if(isset($worker->uidConnections[$uid]))
{
$connection = $worker->uidConnections[$uid];
$connection->send($message);
return true;
}
return false;
}
// 运行所有的worker(其实当前只定义了一个)
Worker::runAll();
b、推送代码 push.php
<?php
// 建立socket连接到内部推送端口
$client = stream_socket_client('tcp://127.0.0.1:5678', $errno, $errmsg, 1);
// 推送的数据,包含uid字段,表示是给这个uid推送,这里可以通过修改uid来测试给哪个客户端发推送
$data = array('uid'=>'uid4', 'percent'=>'88%');
// 发送数据,注意5678端口是Text协议的端口,Text协议需要在数据末尾加上换行符
fwrite($client, json_encode($data)."\n");
// 读取推送结果
echo fread($client, 8192);
c、客户端HTML index.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>uid4的接收页面(修改uid即可测试给哪个客户端推送)</title>
<script type="text/javascript">
var ws = new WebSocket('ws://127.0.0.1:1234');
ws.onopen = function(){
var uid = 'uid4';
ws.send(uid);
};
ws.onmessage = function(e){
alert(e.data);
};
</script>
</head>
<body>
<div id="so">
测试页面
</div>
</body>
</html>
先启动服务,再打开网页,最后运行push.php即可测试,这里比较关键的是进程数必须设置为1,否则可能无法推送成功。一个基础的长连接推送就这样ok了。
如果需要多进程啦、服务器集群啦、就需要基于Channel组件或者GatewayWorker了,更多进阶功能可以参考官方文档http://doc.workerman.net
wss的nginx服务器配置
话不多说粘贴配置,这个放在https的配置里面
location /wss
{
proxy_pass http://127.0.0.1:2345;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
rewrite /wss/(.*) /$1 break;
proxy_redirect off;
}
接下来吧前端页面的代码做修改
var ws = new WebSocket("wss://api.pinkechuxing.com/wss");
就是这么简单就配置好了
在实际的使用中我们可能会遇到连接中断的情况,这个时候就需要发送心跳包来维持连接
var ws = new WebSocket("ws://www.goozp.com");
//连接websocket
ws.onopen = function () {
setInterval(function () {
ws.send('Hello!');
}, 10000)
};