我为什么会写这篇文章?

当初作为编程小白的我,刚刚从事后台工作,觉得 HTTP 是个很牛逼的东西,然而后面随着自己深入学习并实践之后,觉得原来和我所想的天壤之别,没大家想象的那么复杂,仅仅是个协议嘛!后面学习的东西多了,慢慢的就淡定了。今天这里之所以要讲 WebSocket,而不是其它的协议,从某种意义上来说(请允许我装个逼),更能说明问题,如果你把 WebSocket 都搞懂了,那么 HTTP 对于你来说,简直就是雕虫小技啊,关于 WebSocket 的代码,以前我使用 C 和 C++ 写的,但是为了 PHP 的 Coder(PHP 是世界上最好的语言)能明白,我用 PHP 重新写了一遍,但是个精简版,对于我们彻底搞懂 WebSocket,理解它的精华所在,已经足够了。代码我已经上传到了码云(php-websocket-base-implemention),请大家一定一定要下载下来,并亲自运行实践才是检验真理的唯一标准啊,代码是完全可以运行的,如果运行的时候有障碍,请联系我。该博文差不多修修改改了 3 天(幸亏公司里面事不多),尽可能的给大家讲清楚。突然感觉,写文章好累啊,这都不重要,希望大家能够看懂,不然我写的就没啥用了。更希望大家遇到不懂的,提出疑问。写完之后,我再次审查了当前博文的内容,修改了一些拼写错误,可能还会有一些漏网之鱼,希望大家多多指正。

准备工作

在阅读这篇博文之前,需要大家有一定的基础知识储备,下面我会给大家列出来,先装一下逼:

file

socket 基础

基本的 Socket 编程技能,如果你不知道,也不要慌,以防万一,我已经为大家准备好了,请参考 PHP 编写基本的 Socket 程序

位运算

因为在一般的 PHP 编程当中,很少遇到会有位操作的情况,所以遗忘和不熟悉就理所当然了,我们可以参考 PHP 官方文档,但是我还是要讲一点,异或(^)操作,请看下面,这个结论很重要,请大家一定要记住,切记切记,重要的事情讲三遍。

a ^ b = c  可以推导出 c ^ b = a

二进制数据和文本数据

是不是有的时候打开一个文件显示乱码,就像下面这样:

file

因为你打开的是二进制数据,二进制数据和文本数据的最根本的区别就是在数字的存储,举个例子,假设数字 int a=100,我们假设它会占用 4 个字节的空间,但是注意了,如果将它作为字符串存储,结果只需要三个字节(每一位占用一个字节),文本软件不管这些啊,都当做文本,显示的内容就成了乱码了。因此如果某个二进制文件不是你写入的,想要解析它的内容,不太现实。

大端序和小端序,网络字节序

之所以存在这种说法,是因为不同的 CPU 架构下,多字节数据在内容中的存储格式有所不同,这里我们以 int(假设为 4 字节)数据 m(数据采用 16 进制格式)为例,m=0x12345678,来进行说明,请仔细体会 a,b,c,d的内存地址依次增大。

file

  • 小端序,低字节存储在低位地址,高字节存储在高位地址,什么意思呢?此时 0x78 存储在 a,0x56 存储 b,0x34 存储 c,0x12 存储 d。
  • 大端序,高位字节存储在低位,低位字节存储在高位,此时 0x78 存储在 d,0x56 存储 c,0x34 存储 b,0x12 存储 a。
  • 网络字节序,网络字节序是大端字节序,这已经成为标准。

从上面的分析可以知道,当我们从网络数据中解析多字节数据时,是一定要考虑字节的顺序的,这就是我这里着重强调的原因。

协议的诞生

WebSocket 协议如今应用非常广泛,造成这一现象的很大原因,在于 HTTP 协议的短暂性,客户端和服务器之间每一次的请求应答都需要建立 TCP 三次握手,这对于流量很大的服务器来说是非常恐怖的(系统级资源),所以这个时候 WebSocket 诞生了,具体的诞生日期是哪一年已经不得而知了,但是真正的标准化时间是在 2011 年,由 IETF 正式完成,具体请参考 RFC6455

协议工作流程

下面有一张图,可以说明这一点,改图片来自 Google:

file

WebSocket 协议和 HTTP 协议都属于应用层协议(在 TCP/IP 之上),但是 WebSocket 协议相对于 HTTP 协议多了一个 握手(这个握手不是平时所说的 TCP 三次握手啊,注意了)的过程,从上面的图可以很清晰的看出来,HTTP 是是一个文本协议,但是 WebSocket 有所不同,它有自己严格的字节格式,稍后会讲到。

数据包格式

file

看到这张图,有没有想到 TCP、IP 的协议,不过这个图相对来说要简单一些,后面我会给大家详细的讲解每一部分的含义,慢慢来,不要慌,慌个啥子额。

file

协议流程概览

该协议由 2 部分组成,握手数据传输,握手部分并不复杂,并且 握手是建立在 HTTP 协议之上 的,下面我们先来看一下协议的握手过程。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务器响应如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

无论是请求或者是响应包,头部字段的顺序是没有要求的,这其中有些字段相信大家都非常熟悉了,就算不熟悉,百度一下,还是很容易搞清楚的,我们来仔细的讨论一下 WebSocket 所特有的一些字段:

Upgrade 字段

这个字段表示需要升级到的协议,这个字段是必须的,并且它的值必须是 websocket

Connection

这个字段表示需要升级协议,也是必须的,它的值必须是 Upgrade

Sec-WebSocket-Key 和 Sec-WebSocket-Accept

这个是用来客户端和服务器握手使用的,必须传递,因为服务器会使用这个值进行一定的转换然后回传给客户端,客户端再检查这个值,是否和自己计算的值一样,如果不一样,那么客户端会认为,服务端是有问题的,那么结果只能是连接失败了。在介绍具体的操作之前,我们还需要介绍一个常量 GUID,它的值为 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,这个值是固定的,任何的 WebSocket 服务器和客户端(包括浏览器)必须定义这个值。现在我们重点来看一下这个字段,假如客户端传递的值为 dGhlIHNhbXBsZSBub25jZQ==,那么用 PHP 代码来表示的话,就会是下面这样:

$GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
$sec_websocket_key = "dGhlIHNhbXBsZSBub25jZQ==";
$result = base64_encode(sha1($sec_websocket_key . $GUID));

这个计算出的 $result 值最终会被回传给客户端的 HTTP 响应头 Sec-WebSocket-Accept,客户端会验证这个值,这个就是客户端的事了。

Sec-WebSocket-Version

WebSocket 协议的版本号,根据 RFC6455 的文档,我们知道,这个值必须是 13,其它的任何值都不行,下面是它的描述:

The request MUST include a header field with the name |Sec-WebSocket-Version|. The value of this header field MUST be 13.
NOTE: Although draft versions of this document (-09, -10, -11,and -12) were posted (they were mostly comprised of editorial changes and clarifications and not changes to the wire protocol), values 9, 10, 11, and 12 were not used as valid values for Sec-WebSocket-Version. These values were reserved in the IANA registry but were not and will not be used.

Sec-WebSocket-Protocol

选择 WebSocket 所使用的子协议,这个字段不是必须的,取决于具体的实现,如果你使用的是 Google 浏览器的话,那么这个值是不会传递的。

握手阶段

在讲解完了 WebSocket 主要的 HTTP 头部字段之后,我们来看一下服务端的检查代码,这里我把实例程序中的代码贴出来,给大家分析一哈:

/**
 * @param $client_socket_handle
 * @throws Exception
 */
private function shakehand($client_socket_handle)
{
    if (socket_recv($client_socket_handle, $buffer, 1000, 0) < 0) {
        throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
    }
    while (1) {
        if (preg_match("/([^\r]+)\r\n/", $buffer, $match) > 0) {
            $content = $match[1];
            if (strncmp($content, "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key")) == 0) {
                $this->websocket_key = trim(substr($content, strlen("Sec-WebSocket-Key:")), " \r\n");
            }
            $buffer = substr($buffer, strlen($content) + 2);
        } else {
            break;
        }
    }
    //响应客户端
    $this->writeToSocket($client_socket_handle, "HTTP/1.1 101 Switching Protocol\r\n");
    $this->writeToSocket($client_socket_handle, "Upgrade: websocket\r\n");
    $this->writeToSocket($client_socket_handle, "Connection: upgrade\r\n");
    $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Accept:" . $this->calculateResponseKey() . "\r\n");
    $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Version: 13\r\n\r\n");
}

首先我们从客户端 Socket 中读取 1000 字节的内容,这 1000 的字节足以读出所有的头部了(但是在企业级代码中,我们不能这么写,我们永远不能假设整个 HTTP 头部有多大,在这片博文中,我们为了突出问题的重点,简化了很多代码,但是你放心,对我们来说,丝毫没有影响,socket_recv 请参考我上面所说的),接下来的 while 循环遍历我们读取到的内容,要看懂循环里面的代码,我们有必要提下 HTTP 协议的格式了,看下图:

file

我觉得上面的图片,已经足以描述 HTTP 协议的格式了,如果你还不懂,没关系,给大家推荐一篇来自简书的博文(HTTP 协议格式详解),现在对于我们来说,最关心的是当前请求的 Sec-WebSocket-Key 头部,因为这个值需要返回给客户端,获取到这个值之后,我们把它存储在当前对象中。紧接着我们需要回应客户端吧,如果你不知道它的格式,我稍微讲一下:

file

对于 WebSocket 握手来说,如果服务端同意客户端的连接的话,那么返回的状态码必须是101,至于后面的文本,不一定得是 Switching Protocol,只是别人都这么传,那就这么传了。其次,Upgrade: websocket,Connection: upgrade 还有 Sec-WebSocket-Version: 13,必须传递给客户端,这个是固定的,应该没有啥难度吧,另外的,Sec-WebSocket-Accept 我们前面已经说了,它的计算代码,我上面已经贴出来了,这个计算方式也是固定的,千万不要忘记每一行后面得有 \r\n 啊,最后一行后面得有两个 \r\n

分析数据协议

看了上面握手的代码之后,是不是觉得自己要上天了,感觉真是太简单了?骚年,醒醒,醒醒。哈哈,真是太年轻了,年轻就是好。

file

看到我上面贴出来的 WebSocket 数据包格式了么,是时候解开它面纱的时候了,这部分可能有点儿难度,不要怕,有我在。下面我来个 原子级别 的分析。

FIN

FIN 位,也是整个片段的第一个字节的最高位,他只能是 0 或者是 1,这个位的作用只有一个,如果它为 1,表示这个片段是整个消息的最后一个片段,如果是 0,表示这个片段之后,还有其它的片段。是不是听着直接懵逼了,啥是 片段?啥是 消息?非常好,看来我装逼的时候已经来临了,废话不多说。为了搞清楚这几个概念,代码为敬:

(new WebSocket()).send("我是奥巴马");

这是一段 JavaScript 代码,send 函数的参数就是一条消息,非常短,但是注意了,我们不能假设任何时间,任何地点,都这么短,当它变得很长的时候,客户端就有可能对它进行切割,比如,我有一个字符串,大小为 4M,我把它分为 4 个 1M 的字符串,那么每一个 1M 的字符串,就只能成为一个片段,每个片段独立发送,四个片段组合在一起形成了一条消息,每一个片段的格式都是固定的,格式和上面的贴图是一样的,按照刚才说的,前面的三个片段,FIN 都是 0,第四个才是 1,清楚了么?So easy!

RSV1,RSV2,RSV3

这三位是保留给扩展使用的,基本不会用到,反正我没用到,所以我们可以把它们当做空气就行,永远设置为 0,就是这么果断。

opcode

opcode 顾名思义就是操作码,占用第一个字节的低四位,所以 opcode 可以代表 16 种不同的值。你是不是想问,opcode 是用来干嘛的?

opcode 是用 来解析当前片段的载荷(携带的数据)的,具体的后面会再次说明。

  • 0x00,表示当前片段是连续片段,这是啥意思呢?还记得上面讨论 FIN 的时候,一条消息被分割成多条片段?如果当前片段不是第一个,那么 opcode 必须设置为 0。
  • 0x01,表示当前片段所携带的数据是文本数据(记得最开始说的文本数据和二进制数据的区别??),如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
  • 0x02,表示当前片段所携带的数据是二进制数据,如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
  • 0x03-0x07,保留给将来使用,也就是说暂时还没用到。
  • 0x08,表示关闭 websocket 连接,这个后面我会再一次讲到,先放着
  • 0x09,发送 Ping 片段,说白了,它主要是用来检测远程端点是否还存活,我想检查我的对象是不是已经死了,但是这个片段可以携带数据,如果端点的一方发送了 Ping,那么接受方,必须返回 Pong 片段,用中国人的话来说,就是礼尚往来嘛。
  • 0xA,发送 Pong,用以回复 Ping,是不是很简单?
  • 0xB-F,保留给将来使用,也就是说暂时还没用到。

MASK

表示当前片段所携带的数据是否经过加密,位置为第二个字节的最高位,总共 1 位,它的值不是你想设置就设置的啊,RFC6455 明确规定,所有从客户端发送给服务器的数据必须加密,所以 mask 的值必须是 1。还有,所有从服务器发往客户端的数据,一定不能加密,所以呢,mask 必须为 0,就是这么简单粗暴。

Payload Length

这部分是用来定义负载数据的长度的,总共 7 位,所以最大值为 127,就这么简单?哼哼,不会的。

  • payload_length<=125,此时数据的长度就是 payload_length 的大小。
  • payload_length=126,那么紧接着 payload_length 的 2 个字节,就用来表示数据的大小,所以当数据大小大于 125,小于 65535 的时候,payload_length 设置为 126,后面分析代码的时候,我会再次讲到。
  • payload_length=127,也就是 payload_length 取最大值,那么紧接着 payload_length 的 8 个字节,就用来表示数据的大小,此可以表示的数据可就相当大了,后面分析代码的时候,我会再次讲到。

Mask key

它的位置紧接着数据长度的后面,大小为 0 或者是 4 个字节。前面分析了 mask 的作用,如果 mask 为 1 的话,数据需要加密,此时 mask key 占用 4 个字节,否则长度为 0,至于 mask key 如何用来解密数据的,后面会再次讲到。

payload data

这里就是我们从客户端接收到的数据,不过它是经过加密的,“我是奥巴马”,之前 payload_length 的长度,就是经过加密之后的数据的长度,而不是原始数据的长度。

讲解完上面的内容之后,我们可以开始分析如何用 PHP 来解析 WebSocket 消息片段了。

file

解析数据包

这篇博文的开头我就说过了,当前的 WebSocket 实现会专注于 WebSocket 最为精华,最困难的部分,所以会忽略掉一些内容,如果你理解了下面讲的内容,其余的一些细枝末节都不是问题。

计算数据的长度

//等待客户端新传输的数据
if (!socket_recv($client_socket_handle, $buffer, 1000, 0)) {
    throw new Exception(socket_strerror(socket_last_error($client_socket_handle)));
}
//解析消息的长度
$payload_length = ord($buffer[1]) & 0x7f;//第二个字符的低7位
if ($payload_length >= 0 && $payload_length < 125) {
    $this->current_message_length = $payload_length;
    $payload_type = 1;
    echo $payload_length . "\n";
} else if ($payload_length = 126) {
    $payload_type = 2;
    $this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff);
    echo $this->current_message_length;
} else {
    $payload_type = 3;
    $this->current_message_length =
        (ord($buffer[2]) << 56)
        | (ord($buffer[3]) << 48)
        | (ord($buffer[4]) << 40)
        | (ord($buffer[5]) << 32)
        | (ord($buffer[6]) << 24)
        | (ord($buffer[7]) << 16)
        | (ord($buffer[8]) << 8)
        | (ord($buffer[7]) << 0);
}

对于上面的代码,下面进行逐行解析

$payload_length = ord($buffer[1]) & 0x7f;//第二个字符的低7位

读取第二个字节的低 7 位,也就是之前讨论的 payload_length,0x7f 转换为二进制就是 01111111,ord($buffer[1]) 就是把第二个字符转换为对应的 ASCII 数值,两个进行与运算,就可以得到第二个字节的低 7 位对应的数值(与运算不熟悉的朋友,请先查看我在这篇博文前面给大家指定的链接),

if ($payload_length >= 0 && $payload_length < 125) {
    $this->current_message_length = $payload_length;
    $payload_type = 1;
    echo $payload_length . "\n";
}

当 payload_length 的长度小于 125 的话,数据长度就等于片段长度。

if ($payload_length = 126) {
    $payload_type = 2;
    $this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff);
    echo $this->current_message_length;
}

当 payload_length 的长度等于 126 的时候,就有些麻烦了,此时第 3 和第 4 个字节组合为一个无符号 16 位整数,还记得我们之前说的,网络字节序吗?高位字节在前,低位字节在后面,所以当我们读的时候,第 3 个字节就是高 8 位,第 4 个字节就是低 8 位,所以我们首先将高 8 位左移 8 位再和低 8 位做或运算。

$payload_type = 3;
$this->current_message_length =
    (ord($buffer[2]) << 56)
    | (ord($buffer[3]) << 48)
    | (ord($buffer[4]) << 40)
    | (ord($buffer[5]) << 32)
    | (ord($buffer[6]) << 24)
    | (ord($buffer[7]) << 16)
    | (ord($buffer[8]) << 8)
    | (ord($buffer[9]) << 0);

当 payload_length 的长度等于 127 的时候,此时的第 3 到第 10 位组合为一个无符号 64 位整数,所以最高的 8 位需要左移 56 位,后面的依次类推,低 8 位保持不动。

解析mask key

//解析掩码,这个必须有的,掩码总共4个字节
$mask_key_offset = ($payload_type == 1 ? 0 : ($payload_type == 2 ? 2 : 8)) + 2;
$this->mask_key = substr($buffer, $mask_key_offset, 4);

要找到 maskey,首先必须找到它在当前片段的偏移,如果 payload_length<=125,那么偏移就是 2,如果 payload_length==126,那么偏移就是 (2+2)=4,如果 payload_length>126,那么偏移就是(2+8)=10,同时 mask key 的大小为 4 个字节,所以找到了偏移和长度,mask key 就可以获取到了。

解密数据

//获取加密的内容
$real_message = substr($buffer, $mask_key_offset + 4);
$i = 0;
$parsed_ret = '';
//解析加密的数据
while ($i < strlen($real_message)) {
    $parsed_ret .= chr((ord($real_message[$i]) ^ ord(($this->mask_key[$i % 4]))));
    $i++;
}

解密数据的第一步就是要找到加密数据在当前片段中的偏移,很简单,这个值等于 maskkey 的偏移(上面已经求过了) + maskkey 本身的长度 4,那么怎么来解密数据呢?看上面的代码,就可以看出来,解密的过程其实就是遍历加密数据的每一个字符的 ASCII 值和数据(当前遍历的位置对 4 取模,得出的数据必定是 0,1,2,3,将得出的数据找到 maskkey 对应位置的 ASCII 值)进行异或运算求得,这个算法是 RFC6455 规定的,全世界都是这样。

返回数据给客户端

从客户端发送到服务器和服务器传递给客户端的数据格式都遵循着同样的数据包格式,所以在我的实现中,代码如下:

function echoContentToClient($client_socket, $content)
{
    $len = strlen($content);
    //第一个字节
    $char_seq = chr(0x80 | 1);

    $b_2 = 0;
    //fill length
    if ($len > 0 && $len <= 125) {
        $char_seq .= chr(($b_2 | $len));
    } else if ($len <= 65535) {
        $char_seq .= chr(($b_2 | 126));
        $char_seq .= (chr($len >> 8) . chr($len & 0xff));
    } else {
        $char_seq .= chr(($b_2 | 127));
        $char_seq .=
            (chr($len >> 56)
                . chr($len >> 48)
                . chr($len >> 40)
                . chr($len >> 32)
                . chr($len >> 24)
                . chr($len >> 16)
                . chr($len >> 8)
                . chr($len >> 0));
    }
    $char_seq .= $content;
    $this->writeToSocket($client_socket, $char_seq);
}

为了简便起见,第一个字节中 FIN=1,opcode 设置为 1,接下来检查数据的长度,这部分内容和解析数据长度的步骤刚好相反,就不再分析了,如果你把之前的都看懂了,这里也应该没有问题,但是特别注意了,之前我们就已经提到过,服务器返回给客户端的数据,不能加密,所以 mask 必须设置为 0,mask key 的长度为 0。

运行实例

就和本篇博文开篇所提到的,我写了一个简单的 WebSocket 实现,请一定要下载自己运行起来,光看是没有用的:php-websocket-base-implemention

file

file

为了你可以看到实际运行的结果,请打开 websocket.html 文件,页面上出现这个就表示运行成功了。

file

运行之前,请检查端口 8080 是否被占用,当然你可以修改 websocket.html,改为其他的都可以,确保不被占用就可以了,如果你仍然无法运行,请联系我,如果你想看到其他的内容,也请修改 websocket.html 文件,然后重启服务器。

提示

本篇博文的目的仅仅是为了向大家简要的介绍 WebSocket 最为核心的内容,还有一些内容没有讲到(剩下的不难,感兴趣的自己可以去实现),出于让大家更为直观的看清楚 WebSocket 的目的,代码中去掉了错误检查等内容,因此并不严谨,祝你学习愉快。

源码

php-websocket-base-implemention / WebsocketServer.php

<?php

class  WebsocketServer
{
    private $port = 8080;
    private $addr = "127.0.0.1";
    private $socket_handle;
    private $back_log = 10;
    private $websocket_key;
    private $current_message_length;

    private $is_shakehanded = false;
    private $mask_key;

    public function __construct($port = 8080, $addr = "127.0.0.1", $back_log = 10)
    {
        $this->port = $port;
        $this->addr = $addr;
        $this->back_log = $back_log;
    }

    /**
     * @throws Exception
     */
    private function createSocket()
    {
        //创建socket套接字
        $this->socket_handle = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if (!$this->socket_handle) {
            //创建失败抛出异常,socket_last_error获取最后一次socket操作错误码,socket_strerror打印出对应错误码所对应的可读性描述
            throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
        } else {
            echo "create socket successful\n";
        }
    }

    /**
     * @throws Exception
     */
    private function bindAddr()
    {
        if (!socket_bind($this->socket_handle, $this->addr, $this->port)) {
            throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
        } else {
            echo "bind addr successful\n";
        }
    }

    private function listen()
    {
        if (!socket_listen($this->socket_handle, $this->back_log)) {
            throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
        } else {
            echo "socket  listen successful\n";
        }
    }

    /**
     * @throws Exception
     */
    private function accept()
    {
        while (true) {
            $client_socket_handle = socket_accept($this->socket_handle);
            if (!$client_socket_handle) {
                throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
            } else {
                //与客户端握手
                if (!$this->is_shakehanded) {
                    $this->shakehand($client_socket_handle);
                    $this->is_shakehanded = true;
                }
                //等待客户端新传输的数据
                if (!socket_recv($client_socket_handle, $buffer, 1000, 0)) {
                    throw new Exception(socket_strerror(socket_last_error($client_socket_handle)));
                }
                //解析消息的长度
                $payload_length = ord($buffer[1]) & 0x7f;//第二个字符的低7位
                if ($payload_length >= 0 && $payload_length < 125) {
                    $this->current_message_length = $payload_length;
                    $payload_type = 1;
                    echo $payload_length . "\n";
                } else if ($payload_length == 126) {
                    $payload_type = 2;
                    $this->current_message_length = ((ord($buffer[2]) & 0xff) << 8) | (ord($buffer[3]) & 0xff);
                    echo $this->current_message_length;
                } else {
                    $payload_type = 3;
                    $this->current_message_length =
                        (ord($buffer[2]) << 56)
                        | (ord($buffer[3]) << 48)
                        | (ord($buffer[4]) << 40)
                        | (ord($buffer[5]) << 32)
                        | (ord($buffer[6]) << 24)
                        | (ord($buffer[7]) << 16)
                        | (ord($buffer[8]) << 8)
                        | (ord($buffer[9]) << 0);
                }
                //解析掩码,这个必须有的,掩码总共4个字节
                $mask_key_offset = ($payload_type == 1 ? 0 : ($payload_type == 2 ? 2 : 8)) + 2;
                $this->mask_key = substr($buffer, $mask_key_offset, 4);
                //获取加密的内容
                $real_message = substr($buffer, $mask_key_offset + 4);
                $i = 0;
                $parsed_ret = '';
                //解析加密的数据
                while ($i < strlen($real_message)) {
                    $parsed_ret .= chr((ord($real_message[$i]) ^ ord(($this->mask_key[$i % 4]))));
                    $i++;
                }
                echo $parsed_ret . "\n";
                //把解析出来的数据直接返回给客户端
                $this->echoContentToClient($client_socket_handle, $parsed_ret);
            }
        }
    }

    /**
     * @param $client_socket
     * @param $content
     * @throws Exception
     */
    private function echoContentToClient($client_socket, $content)
    {
        $len = strlen($content);
        //第一个字节
        $char_seq = chr(0x80 | 1);

        $b_2 = 0;
        //fill length
        if ($len > 0 && $len <= 125) {
            $char_seq .= chr(($b_2 | $len));
        } else if ($len <= 65535) {
            $char_seq .= chr(($b_2 | 126));
            $char_seq .= (chr($len >> 8) . chr($len & 0xff));
        } else {
            $char_seq .= chr(($b_2 | 127));
            $char_seq .=
                (chr($len >> 56)
                    . chr($len >> 48)
                    . chr($len >> 40)
                    . chr($len >> 32)
                    . chr($len >> 24)
                    . chr($len >> 16)
                    . chr($len >> 8)
                    . chr($len >> 0));
        }
        $char_seq .= $content;
        $this->writeToSocket($client_socket, $char_seq);
    }

    private function writeToSocket($client_socket, $content)
    {
        $ret = socket_write($client_socket, $content, strlen($content));
        if (!$ret) {
            throw new Exception(socket_last_error($client_socket));
        }
    }

    /**
     * @param $client_socket_handle
     * @throws Exception
     */
    private function shakehand($client_socket_handle)
    {
        if (socket_recv($client_socket_handle, $buffer, 1000, 0) < 0) {
            throw new Exception(socket_strerror(socket_last_error($this->socket_handle)));
        }
        while (1) {
            if (preg_match("/([^\r]+)\r\n/", $buffer, $match) > 0) {
                $content = $match[1];
                if (strncmp($content, "Sec-WebSocket-Key", strlen("Sec-WebSocket-Key")) == 0) {
                    $this->websocket_key = trim(substr($content, strlen("Sec-WebSocket-Key:")), " \r\n");
                }
                $buffer = substr($buffer, strlen($content) + 2);
            } else {
                break;
            }
        }
        //响应客户端
        $this->writeToSocket($client_socket_handle, "HTTP/1.1 101 Switching Protocol\r\n");
        $this->writeToSocket($client_socket_handle, "Upgrade: websocket\r\n");
        $this->writeToSocket($client_socket_handle, "Connection: upgrade\r\n");
        $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Accept:" . $this->calculateResponseKey() . "\r\n");
        $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Version: 13\r\n\r\n");
    }

    private function calculateResponseKey()
    {
        $GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

        $result = base64_encode(sha1($this->websocket_key . $GUID, true));
        return $result;
    }

    public function startServer()
    {
        try {
            $this->createSocket();
            $this->bindAddr();
            $this->listen();
            $this->accept();
        } catch (Exception $exception) {
            echo $exception->getMessage() . "\n";
        }
    }
}

setlocale(LC_ALL, "US");
$server = new WebsocketServer();
$server->startServer()

php-websocket-base-implemention / form.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>$Title$</title>
</head>
<body>
$END$
</body>
</html>

php-websocket-base-implemention / websocket.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test websocket</title>
</head>
<body>
<script charset="UTF-8">
    var ws = new WebSocket('ws://127.0.0.1:8080');
    ws.onopen = function () {
        setTimeout(function () {
            //ws.send(JSON.stringify({type: 1, authId: 100}));
            ws.send("martin123奥巴马");
        }, 1000)
    };
    ws.onmessage = function (e) {
        var h = document.createElement("h1");
        h.innerHTML = e.data;
        document.body.appendChild(h);
    };
    ws.onclose = function () {
        // alert("closed");
    };
    ws.onerror = function () {
        //alert("error happened");
    };

</script>
</body>
</html>

本文为转载,原文地址:

老司机带你用 PHP 实现 Websocket 协议