使用WebSocket与PHP通信的思路梳理(二)
上篇Socket服务器只是跑通,要想跑起来(忽略效率)使用还有很长的路要走。但是其中的函数使用方法还是要熟练掌握。
所有实践代码都是完全过程写法,虽然不够优雅,但是对于理解代码思路却比面向对象的方式要好的多。学会了原理,其他都不叫事!
在上篇的技术基础上,要使服务跑起来,只要在里面再增加一个循环即可:
// 上面代码还是创建Socket服务的代码(请查阅上篇文章)
// 还是那个无限循环
while (true) {
// 阻塞循环等待请求
$client = socket_accept($server);
// 读取协议信息
$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";
$res = socket_write($client, $header, strlen($header));
// 在此处增加一个循环
while ($client) {
$length = 0;
$buffer = '';
do {
// 这里我们将socket_read()换成了socket_recv()
// $buf是接收的信息,1024是接收数据的长度,$len是接收到的数据长度
$len = socket_recv($client, $buf, 1024, 0);
// 累加之后$length就是所有接收到的数据总长度
$length += $len;
// 全部数据
$buffer .= $buf;
// 每次接收的数据长度之后两种情况,要么小于1024要么等于,绝不会大于
// 小于1024则说明缓冲区没有数据了,循环同时停止
// 等于1024则说明缓冲区有可能还有数据,继续循环
// 此处的1024与recv函数的1024是任意相同的数,你应该懂得
} while ($l == 1024);
// 数据长度小于7则说明客户端关闭了连接(对端关闭)
if ($length < 7) {
socket_close($client);
$client = false;
continue;
}
// 还是那两个编码解码的函数
$str = enCode('原封不动返回去:'.deCode($buffer));
socket_write($client, $str, strlen($str));
}
}
OK,经过修改之后,在Linux中启动。打开一个客户端,此时你就可以任意发消息(不要发送回车换行哦),而服务端也会做出正确的回应。
再打开一个客户端,此时你会发现,无法连接到服务端。因为内部的循环读取(socket_recv)阻塞了进程,始终只有一个客户端是正常的。
这肯定不是我们所希望的服务功能,该如何改进呢?实践中有两种常用方案:
1、多进程模型
当客户端请求时,主进程产生(pcntl_fork)一个子进程,让子进程负责与客户端的后续通信。当客户端关闭时,同样将该子进程关闭。
优点:连接请求少时,子进程少,效率比较高。
缺点:连接数过多时,子进程过多,系统资源很快被耗光。
2、IO多路复用模型
多路是指多个客户端连接,复用就是指复用少数几个进程。单进程(非多线程)通过调用socket_select函数来处理多个连接请求。
优点:单进程可支持同时处理多个客户端连接。
缺点:最大并发1024个(32位机器),当并发数过大时,其处理性能很低。(在epoll中已经解决)
本篇说下第一种多进程模型实现多客户端并发请求处理,先上代码,改动非常小:
// 依旧是它,死循环
while (true) {
// 阻塞循环等待请求
$client = socket_accept($server);
// 创建一个子进程,父进程和子进程都从pcntl_fork()执行成功那一刻开始向下继续执行。
// 不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0。
// 返回int值:-1则开启失败,0则是子进程,大于0则是父进程
$pid = pcntl_fork();
// 使用刚创建的子进程执行新客户请求
if ($pid == 0) {
$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";
$res = socket_write($client, $header, strlen($header));
while (true) {
$length = 0;
$buffer = '';
do {
$l = socket_recv($client, $buf, 1024, 0);
$length += $l;
$buffer .= $buf;
} while ($l == 1024);
if ($length < 7) {
socket_close($client);
// 该客户关闭时,不要忘记将对应的子进程一并关闭
// 更完善的防止僵尸进程问题,自行百度哦
exit();
}
$str = enCode('原封不动返回去:' . deCode($buffer));
socket_write($client, $str, strlen($str));
}
}
}
改进的服务端就可以正常的跑起来啦!不过不要开启太多的客户端,这里没有对进程数做限制,真实项目是要控制fork的进程数量。
这种直接fork出多进程服务多客户请求的方式,最大的问题在于需要根据实际业务预估进程数量。如此使用大量进程来解决高并发问题,可能会出现CPU浪费在进程间切换上,还有可能会出现惊群现象(简单理解就是1000个进程在等带客户端连接,来了一个客户端但是所有进程都被唤醒了,但最终只有一个进程为这个客户端服务,其余999个白折腾)。那么,有没有一种解决方案可以使得少量进程服务于多个客户端呢?
答案就是IO多路复用,下一篇再写写该模型的实现方法!