php实现webSocket协议- - workerman源码理解

戈博易
2023-12-01

1. 从 连接socket的 接收缓冲区中copy数据,怎么copy?

$buffer = @\fread($socket, self::READ_BUFFER_SIZE);  // 65535

每次读取最多65535字节,放到用户程序的缓冲区(这里指的是php tcpConnection对象的_recvBuffer 属性)

每读一次就尝试解析 用户程序的缓冲区(_recvBuffer)  ,使用$parser::input 方法去解析,如果返回一个> 0 数字证明,_recvBuffer 有足够多的 够一个 完整frame 的数据了(怎么叫完整呢,对HTTP来说就是完整的页面/ 完整post请求,不管这个页面/ post请求多大,对websocket来说如果数据比较大那可能就是 由多个FIN =0 和最后一个FIN=1组成 ),接下来就可以 调用 decode去解析frame了,对于websocket 来说 解掩码处理就是其中一项工作。

2. 检查包的长度

public static function input($recv_buffer, ConnectionInterface $connection);

return 0 : 意味着需要更多数据

false : 出现错误了

> 0 : 意味着够一个包的数据了, 数值 的大小: 如果是 HTTP 就是 HTTP请求头+HTTP请求体的大小,如果是websocket, 并且这个数据包很大(由 几个 FIN = 0 及最后FIN=1) 组成,那么这个数值指的是最后一个FIN=1 frame的大小。

 如果是 非数据类的 frame ,则直接处理了 ,如(ping, pong close) frame  (这些属于控制类frame)。

返回 >0 ,代表当前是数据 frame 。

3  细节:webSocket Frame  头部解析

//头部解析 FIN, opcode, mask bit, masking-key
            $firstbyte    = \ord($buffer[0]);
            $secondbyte   = \ord($buffer[1]);
            $data_len     = $secondbyte & 127;
            $is_fin_frame = $firstbyte >> 7;
            $masked       = $secondbyte >> 7;
......
......

            $opcode       = $firstbyte & 0xf;


//计算frame的长度
$data_len     = $secondbyte & 127;
......
// Calculate packet length.
$head_len = 6;
if ($data_len === 126) {
    $head_len = 8;
    if ($head_len > $recv_len) {
        return 0;
    }
    $pack     = \unpack('nn/ntotal_len', $buffer);
    $data_len = $pack['total_len'];
} else {
    if ($data_len === 127) {
        $head_len = 14;
        if ($head_len > $recv_len) {
            return 0;
        }
        $arr      = \unpack('n/N2c', $buffer);
        $data_len = $arr['c1']*4294967296 + $arr['c2'];
    }
}

//4个字节的 mask-key 的解析
        $len = \ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = \substr($buffer, 4, 4);

        } else {
            if ($len === 127) {
                $masks = \substr($buffer, 10, 4);

            } else {
                $masks = \substr($buffer, 2, 4);

            }
        }

4. $parser::input 的流程梳理

它的中心思想:

返回 0 代表需要更多数据;   还不足够是 一个包

返回 false 代表发了错误

返回 代表 得到了包的长度 (对websocket来说 分两种情况, 只有一个FIN==1 自成一个包, FIN==0, FIN==0… FIN==1 组成),这里的长度都是 指的最后一个 FIN包的长度。

如果FIN == 1  是 ping ,pong 包 处理了,如果$recv_len > $current_frame_length, 则递归处理下一个frame(即调用input)

如果FIN == 1 && 是数据包, 则返回 包长度 $current_frame_length 

如果FIN == 0   &&  是数据包 , 则:

$connection->websocketCurrentFrameLength = $current_frame_length;

收到的数据 ($recv_len)正好是 一个 frame 的长度: 则 decode 解包处理了,把$connection->websocketCurrentFrameLength 为0 并且 return 0

收到的数据 ($recv_len)大于 一个frame的长度,       则 decode  解包处理了,把$connection->websocketCurrentFrameLength 为0,  递归地处理下一个 frame  (即调用 input )

收到的数据  ($recv_len)小于 一个 frame 的长度, return 0

5. 对上层的tcp来说,只有一个“完整数据frame" 到来了,才进行decode 解码处理, 才进行 onMessage的回调,从而进行上层应用程序/协议(如webosocket,HTTP等)的业务处理。

在baseRead中,tcp希望得到一个完整的解析好的数据,后才回调 tcp的 onMessage 回调。

“完整”含义:像 websocket  有可能是 FIN==0  ,FIN==0, FIN ==1  3个组成的,所有的数据frame都到了,并被decode了,才算完整。

6. 解包 decode

对于,webSocket来说就是把客户端发来的数据 解掩码处理了。

对于,http来说就是把客户端发送来的请求,解析为 request对象,一些数据解析道全局变量中。

    $dataLength = \strlen($data);

    $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);

    $decoded = $data ^ $masks;

/**
 * Websocket decode.
 *
 * @param string              $buffer
 * @param ConnectionInterface $connection
 * @return string
 */
public static function decode($buffer, ConnectionInterface $connection)
{
    $len = \ord($buffer[1]) & 127;
    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);
        }
    }
    $dataLength = \strlen($data);
    $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
    $decoded = $data ^ $masks;
    if ($connection->websocketCurrentFrameLength) {      // 当前是 FIN == 0 , 后边一定有FIN ==1 的frame
        $connection->websocketDataBuffer .= $decoded;
        return $connection->websocketDataBuffer;
    } else {
        if ($connection->websocketDataBuffer !== '') {   //  FIN ==1 这个frame 前面有 几个 FIN == 0
            $decoded                         = $connection->websocketDataBuffer . $decoded;
            $connection->websocketDataBuffer = '';
        }
        return $decoded;       // FIN ==1  这是一个完整的包
        //                     注意后边两种情况 返回的数据才有意义,才需要去接收,处理。
        //                     "return $connection->websocketDataBuffer"  这个地方的返回是没有意义的,实际上也没有被接收
    }
}

解掩码后的数据 ,放到 tcpConnection对象的 websocketDataBuffer  属性上。这是个动态属性 后加上的。

7. 封包 encode

8. 每种包的处理:

如果没有设置$masked , 就关闭连接

除了 0x0, 0x1, 0x2, 0x8, 0x9, 0xa;  其他都认为是错误的 opcode ,就关闭连接$connection->close();

 9. webSocket的握手包/frame的处理

注意websocket的握手请求包,和websocket的握手响应包 都是HTTP协议格式

    /**
     * Websocket handshake.
     *
     * @param string                              $buffer
     * @param TcpConnection $connection
     * @return int
     */
    public static function dealHandshake($buffer, TcpConnection $connection)
    {
        if (0 === \strpos($buffer, 'GET')) {
            // Find \r\n\r\n.
            $heder_end_pos = \strpos($buffer, "\r\n\r\n");
            if (!$heder_end_pos) {
                return 0;
            }
            $header_length = $heder_end_pos + 4;

            // Get Sec-WebSocket-Key.
            $Sec_WebSocket_Key = '';
            if (\preg_match("/Sec-WebSocket-Key: *(.*?)\r\n/i", $buffer, $match)) {
                $Sec_WebSocket_Key = $match[1];
            } else {
                $connection->send("HTTP/1.1 200 Websocket\r\nServer: workerman/".Worker::VERSION."\r\n\r\n<div style=\"text-align:center\"><h1>Websocket</h1><hr>powered by <a href=\"https://www.workerman.net\">workerman ".Worker::VERSION."</a></div>",
                    true);
                $connection->close();
                return 0;
            }
            // Calculation websocket key.
            $new_key = \base64_encode(\sha1($Sec_WebSocket_Key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
            // Handshake response data.
            $handshake_message = "HTTP/1.1 101 Switching Protocols\r\n"
                                ."Upgrade: websocket\r\n"
                                ."Sec-WebSocket-Version: 13\r\n"
                                ."Connection: Upgrade\r\n"
                                ."Sec-WebSocket-Accept: " . $new_key . "\r\n";

10.  发送数据

       把数据放到 tcp的 对应socket的 发送缓冲区中,

       至于怎么放,使用socket_write 还是 epoll  ?缓冲区是否满了怎么办?  成功写入一部分怎么办?  这些属于 tcp 的问题范畴,不在这里讨论。

20. 用到的php函数

ord

unpack

pcntl_signal_dispatch

 类似资料: