使用WebSocket与PHP通信的思路梳理(二)

作者: 乘风御上者 分类: JavaScript,PHP 发布时间: 2020-05-11 18:50

上篇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多路复用,下一篇再写写该模型的实现方法!

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复