Websocket-php学习笔记

创建一个套接字

socket_create创建并返回一个套接字,也称作一个通讯节点。一个典型的网络连接由 2 个套接字构成,一个运行在客户端,另一个运行在服务器端。

resource socket_create ( int $domain , int $type , int $protocol )
  • @parameter domain参数指定哪个协议用在当前套接字上
Domain 描述
AF_INET IPv4 网络协议。TCP 和 UDP 都可使用此协议。
AF_INET6 IPv6 网络协议。TCP 和 UDP 都可使用此协议。
AF_UNIX 本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。
  • @parameter type参数用于选择套接字使用的类型
类型 描述
SOCK_STREAM 提供一个顺序化的、可靠的、全双工的、基于连接的字节流。支持数据传送流量控制机制。TCP 协议即基于这种流式套接字。
SOCK_DGRAM 提供数据报文的支持。(无连接,不可靠、固定最大长度).UDP协议即基于这种数据报文套接字。
SOCK_SEQPACKET 提供一个顺序化的、可靠的、全双工的、面向连接的、固定最大长度的数据通信;数据端通过接收每一个数据段来读取整个数据包。
SOCK_RAW 提供读取原始的网络协议。这种特殊的套接字可用于手工构建任意类型的协议。一般使用这个套接字来实现 ICMP 请求(例如 ping)。
SOCK_RDM 提供一个可靠的数据层,但不保证到达顺序。一般的操作系统都未实现此功能。
  • @parameter protocol是设置指定 domain 套接字下的具体协议。这个值可以使用 getprotobyname() 函数进行读取。如果所需的协议是 TCP 或 UDP,可以直接使用常量 SOL_TCPSOL_UDP
名称 描述
icmp Internet Control Message Protocol 主要用于网关和主机报告错误的数据通信。例如“ping”命令(在目前大部分的操作系统中)就是使用 ICMP 协议实现的。
udp User Datagram Protocol 是一个无连接的、不可靠的、具有固定最大长度的报文协议。由于这些特性,UDP 协议拥有最小的协议开销。
tcp Transmission Control Protocol 是一个可靠的、基于连接的、面向数据流的全双工协议。TCP 能够保障所有的数据包是按照其发送顺序而接收的。如果任意数据包在通讯时丢失,TCP 将自动重发数据包直到目标主机应答已接收。因为可靠性和性能的原因,TCP 在数据传输层使用 8bit 字节边界。因此,TCP 应用程序必须允许传送部分报文的可能。
  • 返回值socket_create() 正确时返回一个套接字,失败时返回 FALSE。要读取错误代码,可以调用 socket_last_error()。这个错误代码可以通过 socket_strerror() 读取文字的错误说明。

socket_bind给套接字绑定名字,注意:绑定 addresssocket。 该操作必须是在使用 socket_connect() 或者 socket_listen() 建立一个连接之前。

 socket_bind ( resource $socket , string $address [, int $port = 0 ] ) : bool
  • @parameter socketsocket_create() 创建的一个有效的套接字资源。

  • @parameter address

    • 如果套接字是 AF_INET 族,那么 address 必须是一个四点分法的 IP 地址(例如 127.0.0.1 )。
    • 如果套接字是 AF_UNIX 族,那么 address 是 Unix 套接字一部分(例如 /tmp/my.sock )
  • @parameter port参数 port 仅仅用于 AF_INET 套接字连接的时候,并且指定连接中需要监听的端口号。


socket_listen监听socket连接

 socket_listen ( resource $socket [, int $backlog = 0 ] ) : bool
  • @parameter socketsocket_create() 创建的一个有效的套接字资源。
  • @parameter backlog端口号

创建套接字实例

public function service(){
    $tcp = getprotobyname("tcp");    //获取tcp协议号码。
    $sock = socket_create(AF_INET, SOCK_STREAM, $tcp);//创建一个套接字
    socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);
    if($sock < 0)
    {
        throw new Exception("failed to create socket: ".socket_strerror($sock)."\n");//获取错误
    }
    socket_bind($sock, $this->address, $this->port);//绑定IP和端口
    socket_listen($sock, $this->port);//监听端口
    echo "listen on $this->address $this->port ... \n";
    $this->_sockets = $sock;
}

握手处理

  • 我们直接从代码示例看,这个类方法传入了两个参数,一个是socket、一个是请求包,在接收到请求包后会遍历这个请求包,获取中的Sec-WebSocket-Key',Sec-WebSocket-Key 加上一个特殊字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算 SHA-1 ,之后进行 BASE-64 编码,将结果做为 Sec-WebSocket-Accept 头的值并且插入在响应包中 返回给客户端。
  • socket_write 将字符串写入socket中
socket_write( resource `$socket`   , string `$buffer`   [, int `$length` = 0  ] ) : int
/**
 * 握手处理
 * @param $newClient socket
 * @return int  接收到的信息
 */
public function handshaking($newClient, $line){

    $headers = array();
    $lines = preg_split("/\r\n/", $line);
    foreach($lines as $line)
    {
        $line = chop($line);//chop()若为空则移除\n\r空格之类的。
        if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))//正则获取header头
        {
            $headers[$matches[1]] = $matches[2];//将header存入数组
        }
    }
    $secKey = $headers['Sec-WebSocket-Key'];
    $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
    $upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
        "Upgrade: websocket\r\n" .
        "Connection: Upgrade\r\n" .
        "WebSocket-Origin: $this->address\r\n" .
        "WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n".
        "Sec-WebSocket-Accept:$secAccept\r\n\r\n";  //构造响应包
    return socket_write($newClient, $upgrade, strlen($upgrade));
}

Websocket的数据处理

  • 数据帧格式

    ​ WebSocket客户端,服务端通信的最小单位是帧(frame),由一个或多个帧组成一条完整的消息(message)

img

  • 数据帧格式详解

FIN

​ 如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是消息(message)的最后一个分片(fragment)

Opcode:

​ 4个比特。

操作码

​ Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作码是不认识的,那么接收端应该断开连接(fail the connection)

Mask: 1个比特。

​ 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

Payload length: 数据载荷的长度。 7位 或 7 + 16 位 或 7 + 64位。

​ 假设 Payload length == x

x 为 0-126: 数据长度为x字节.
x 为 126 : 后续2个字节代表一个16位无符号整数
x 为 127 : 后续8个字节代表一个64位无符号整数

掩码算法

首先,假设:

original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。

算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
  • 数据帧的解析如下:
/**
     * 解析接收数据
     * @param $buffer
     * @return null|string
     */
    public function message($buffer){
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;//取出第8-15位
        if ($len === 126)  { // 126<=数据长度<= 65535   Extended payload length 有 占用2个字节
            $masks = substr($buffer, 4, 4);//帧格式 Masking-key 占用4字节
            $data = substr($buffer, 8);//第九个字节(索引 8 )开始 是数据
        } else if ($len === 127)  { // 65535 <= 数据长度  ;Extended payload length 有 占用8个字节
            $masks = substr($buffer, 10, 4);// 说明 Extended payload length 占用 8个字节 2 + 8 所以从 10开始
            $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;  
    }
本文总阅读量
× 文章目录
  1. 1. 创建一个套接字
  2. 2. 创建套接字实例
  3. 3. 握手处理
  4. 4. Websocket的数据处理