使用WebSocket与PHP通信的思路梳理(三)
上篇简单的写了下多进程模型的实现方式,本篇就写写IO多路复用模型的实现方式。
常用的IO多路复用模型有三种:
- select模型:早期解决方案,通过轮询方式监控客户端资源,但是在单个进程能够监视的客户端资源的数量存在最大限制。
- poll模型:解决select模型的最大限制问题,可以维持任意数量的连接。但是通过轮询方式依然会浪费系统资源。
- epoll模型:终极解决方案,不再是通过轮询方式,而且维护一个队列。既没有数量限制,又不会浪费系统资源。
其实Libevent系统库早已实现以上三种IO多路复用模型,而且还支持一大堆其他事件等。注意这里的Libevent是系统库不是PHP的扩展,当然无所不能的PHP早已出现调用该系统库的扩展:libevent扩展和event扩展。
libevent扩展目前停止更新,而且对PHP7版本支持不是很好。
event扩展则完全支持PHP7版本,还拥有更好的性能表现和更全面的API。
当然不管用哪个PHP扩展,都要保证Libevent系统库存在才行!
不过本篇要介绍的还是PHP的socket扩展,因为它同样实现select模型的功能:
socket_select(array &$read, array &$write, array &$except, int $tv_sec [, int $tv_usec = 0]): int
参数一$read数组中保存着服务端与客户端所有的Socket资源,该函数将对这个数组轮询监听,随时将不活跃的Socket资源删除掉。后面的参数含义自行手册即可。
该函数有几点需要注意的地方:
- 每当新客户端请求连接时,将触发服务端Socket资源变成活跃。
- 每当客户端发送数据或者客户端关闭连接,都将触发该客户的Socket资源变成活跃。
- 若客户端发送数据没有被读取,那么该客户端的Socket资源将一直处于活跃状态。
- 当客户端已经断开连接后,$read数组中不能再包含该客户Socket资源,必须及时将其删除,否则该客户Socket资源将保持活跃状态,而且读取不了任何信息,将导致其一直活跃下去。
// 千言万语不如几行代码解释的透彻
// 不了解的看第一篇
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$res = socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
$res = socket_bind($server, '0.0.0.0', 12345);
$res = socket_listen($server);
// 保存服务端与客户端所有的Socket资源的连接池
$sockets = [$server];
// 还是那个死循环
while (true) {
// 要监控的Socket资源数组
$reads = $sockets;
$write = NULL;
$except = NULL;
// 监听所有的Socket资源,将其中不活跃的删除掉
socket_select($reads, $write, $except, NULL);
// 遍历所有活跃的Socket资源
foreach ($reads as $sock) {
// 若活跃的资源中有服务端Socket资源,则说明有新客户端请求连接
// 服务端Socket资源只在新客户端请求连接时触发活跃
// 赶紧找接待员接待
if ($sock == $server) {
// 接待员准备就绪
// $client则是新客户端
$client = socket_accept($server);
//将新连接进来的客户端Socket资源存进连接池
$sockets[] = $client;
// 为新客户升级协议
$buffer = socket_read($client, 1024);
$fer = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
$key = trim(substr($fer, 0, strpos($fer, "\r\n")));
$enKey = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
$header = "HTTP/1.1 101 Switching Protocols\r\n";
$header .= "Upgrade: websocket\r\n";
$header .= "Sec-WebSocket-Version: 13\r\n";
$header .= "Connection: Upgrade\r\n";
$header .= "Sec-WebSocket-Accept: " . $enKey . "\r\n\r\n";
socket_write($client, $header, strlen($header));
} else {
// 老客户要么发来了消息,要么断开了连接
// 原来的代码,不了解的看第二篇
$length = 0;
$buffer = '';
do {
$len = socket_recv($sock, $buf, 1024, 0);
$length += $len;
$buffer .= $buf;
} while ($l == 1024);
// 说明客户断开了连接
if ($len < 7) {
//断开相应连接
socket_close($sock);
// 将连接池中的客户端Socket删除掉
foreach ($sockets as $key => $user) {
if($user == $sock){
unset($sockets[$key]);
}
}
continue;
}
// 正常回应
$str = enCode('做出回应:'.deCode($buffer));
socket_write($sock, $str, strlen($str));
}
}
}
// 还是那个永远执行不到
socket_close($server);
// 编码
function enCode($msg)
{
$frame = [];
$frame[0] = '81';
$len = strlen($msg);
if ($len < 126) {
$frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
} else if ($len < 65025) {
$s = dechex($len);
$frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
} else {
$s = dechex($len);
$frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
}
$frame[2] = ordHex($msg);
$data = implode('', $frame);
return pack("H*", $data);
}
// 解码
function deCode($buffer)
{
$decoded = '';
$len = !empty($buffer[1]) ? (ord($buffer[1]) & 127) : 0;
if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
} else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
} else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}
for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
// 进制转换
function ordHex($data)
{
$msg = '';
$l = strlen($data);
for ($i = 0; $i < $l; $i++) {
$msg .= dechex(ord($data[$i]));
}
return $msg;
}
以上就是select模型的简单实现,请仔细阅读上面的几个需要注意的点,并结合代码注释一并了解,弄明白其中的关键再看代码就非常简单了。
作为世界上最好的语言,PHP早就有成熟的socket框架:
Workerman: 主要用到PHP的socket扩展、pcntl扩展、event(libevent)扩展、posix扩展等,详情请移步 >>>。
再者有Hyperf、Swoft、easySwoole、MixPHP、Swoolefy等:不过这些框架都是使用PHP的swoole扩展,这个扩展是国人开发的,商业化比较严重,嗯,是宣传很到位!了解框架前最好先去了解下该扩展,详情请移步 >>>。
(简单的说几句:swoole扩展是PHP众多扩展中的一个。PHP扩展千千万,每个扩展都有自己独特的领域。因为当今社会发展的比较流行的物联网、云计算等领域正好这个扩展比较擅长,所以PHPer在不改语言的情况下可以优先选择。但是不能混淆概念,扩展就是扩展,再好用也是某个领域,还能脱离主子吗?貌似现在养大了,真能……)