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

作者: 乘风御上者 分类: JavaScript,PHP 发布时间: 2020-05-12 12:44

上篇简单的写了下多进程模型的实现方式,本篇就写写IO多路复用模型的实现方式。

常用的IO多路复用模型有三种:

  1. select模型:早期解决方案,通过轮询方式监控客户端资源,但是在单个进程能够监视的客户端资源的数量存在最大限制。
  2. poll模型:解决select模型的最大限制问题,可以维持任意数量的连接。但是通过轮询方式依然会浪费系统资源。
  3. 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资源删除掉。后面的参数含义自行手册即可。

该函数有几点需要注意的地方:

  1. 每当新客户端请求连接时,将触发服务端Socket资源变成活跃。
  2. 每当客户端发送数据或者客户端关闭连接,都将触发该客户的Socket资源变成活跃。
  3. 若客户端发送数据没有被读取,那么该客户端的Socket资源将一直处于活跃状态。
  4. 当客户端已经断开连接后,$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在不改语言的情况下可以优先选择。但是不能混淆概念,扩展就是扩展,再好用也是某个领域,还能脱离主子吗?貌似现在养大了,真能……)

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

发表回复