php连接符-老驱动带你用PHP实现Websocket协议

2023-08-29 0 6,919 百度已收录

我为什么要写这篇文章?

作为一个编程初学者,刚开始在后台工作,我觉得http是一个非常强大的东西。 然而,深入学习和实践后,我感觉与我想象的相差甚远,并没有你想象的那么复杂。 ,只是一份契约而已! 。 学了很多东西后,我渐渐平静下来。 今天之所以要在这里讲websocketphp连接符,而不是其他合约,从某种意义上来说(请允许我装逼),更能说明问题。 如果您了解 websocket,那么 http 就适合您。 这只是一个小技巧。 关于websocket代码,我之前是用C和C++写的,但是为了让PHP(PHP是世界上最好的语言)的编码者理解,我用PHP重新编写了它,但它是一个简化版本。 我们彻底了解websocket,了解其本质就足够了。 我已经将代码上传到码云(php-websocket-base-implemention),请自行下载运行。 实践是检验真理的唯一标准。 代码可以完整运行。 侯有障碍,请与我联系。 博文已经修改了快3天了(还好公司事情不多),所以我尽量给大家解释清楚。 突然觉得写文章好冷啊,没关系,希望你能理解,不然我写的东西就没啥用了。 也希望大家遇到不明白的地方提出疑问。 写完之后,我再次审阅了当前博文的内容,并纠正了一些拼写错误。 也许有一些漏网之鱼。 我希望你能原谅我。

文章链接

PHP实现Base64编码

准备

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

套接字基础知识

基本的socket编程技巧,如果你不会,不要惊慌,以防万一,我已经给你准备好了,请参考PHP编写基本的socket程序

位运算

因为在平时的php编程中,很少会遇到位操作,所以忘记和不熟悉是很自然的。 我们可以参考php官方文档,但是我还是想讲一件事,异或(^)操作,请看下面,这个推论很重要,请记住,记住,重要的事情说三遍。

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

二进制数据和文本数据

是否有时打开文件时显示乱码,如下

因为你打开的是二进制数据,所以二进制数据和文本数据最根本的区别就在于数字的存储。 举个计数器的例子,假设数字int a=100,我们假设它会占用4个字节的空间,但是要注意,如果它存储为字符串,结果只需要三个字节(每一位占用一个字节),并且文本软件无论如何都会将其视为文本,显示的内容就会变成乱码。 因此,如果一个二进制文件不是你写的,解析它的内容是不现实的。

大端和小端、网络字节顺序

之所以有这样的说法,是因为在不同的CPU架构下,内容中多字节数据的存储格式是不同的。 这里我们用int(假设为4字节)数据m(十六进制格式的数据)为例,m=0x12345678,来解释,请仔细感受a、b、c、d的内存地址依次递减。

php连接符-老驱动带你用PHP实现Websocket协议

从里面的分析我们可以知道,我们在从网络数据中解析多字节数据的时候,必须要考虑字节的顺序,这也是我这里强调的原因。

协议的诞生

Websocket合约现在被广泛使用,造成这种现象的主要原因是HTTP协议的短暂性。 客户端和服务器之间的每一次请求和响应都需要构建TCP三向握手。 非常可怕(系统级资源),所以websocket就在这个时候诞生了。 具体诞生日期不详,但真正的标准化时间是2011年,即将由IETF完成。 详细信息请参阅RFC6455。

协议工作流程

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

websocket合约和http合约都属于应用层协议(TCP/IP之上),但是websocket合约比http合约多了一次握手(这个握手不是通常的tcp三向握手,付费注意)过程,从里面的图片可以清楚的看到。 HTTP是文本契约,但websocket不同。 它有自己严格的字节格式,稍后会提到。

数据包格式

看到这张图,你有没有想到TCP和IP契约,不过这张图比较简单,后面我会详细解释各个部分的含义,慢慢来,不要慌,不要慌。

协议流程概述

合约由握手和数据传输两部分组成。 握手部分并不复杂,握手是建立在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 特有的一些数组:

升级领域

php连接符-老驱动带你用PHP实现Websocket协议

该数组代表需要升级到的合约。 该数组是必需的,其值必须是 websocket。

联系

这个数组表示合约需要升级,而且也是必须的,其值必须是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-版本

websocket合约的版本号,根据RFC6455文档,我们知道这个值必须是13,任何其他值都不起作用,以下是它的说明:

请求必须包含名称为 |Sec-WebSocket-Version| 的标头字段。 此标头字段的值必须为 13。 注意:尽管已发布本文档的草稿版本(-09、-10、-11 和 -12)(它们主要由编辑更改和澄清组成,而不是对线路的更改)协议),值 9、10、11 和 12 未用作 Sec-WebSocket-Version 的有效值。 这些值已在 IANA 注册表中保留,但并未也不会被使用。

Sec-WebSocket-协议

选择websocket使用的分包。 这个数组不是必需的,取决于具体的实现。 如果您使用的是 Google 浏览器,则不会传递该值。

握手阶段

解释完Websocket的主要http back数组,我们来看一下服务端的检测代码。 这里我将示例程序中的代码贴出来给大家分析一下。

/**
     * @param $client_socket_handle
     * @throws Exception
     */
    private function shakehand($client_socket_handle)
    {
        if (socket_recv($client_socket_handle, $buffer, 1000, 0) socket_handle)));
        }
        while (1) {
            if (preg_match("/([^r]+)rn/", $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:")), " rn");
                }
                $buffer = substr($buffer, strlen($content) + 2);
            } else {
                break;
            }
        }
        //响应客户端
        $this->writeToSocket($client_socket_handle, "HTTP/1.1 101 Switching Protocolrn");
        $this->writeToSocket($client_socket_handle, "Upgrade: websocketrn");
        $this->writeToSocket($client_socket_handle, "Connection: upgradern");
        $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Accept:" . $this->calculateResponseKey() . "rn");
        $this->writeToSocket($client_socket_handle, "Sec-WebSocket-Version: 13rnrn");
    }

首先我们从客户端socket读取1000个字节,这足够读完所有的肚皮了(但是在企业级代码中,我们不能这样写,我们永远不能假设整个http回来有多大,在这篇博客中帖子里,我们为了突出问题的关键点,简化了很多代码,但是你放心,它对我们完全没有影响,socket_recv请参考我里面说的),接下来的while循环遍历,我们阅读为了理解循环上面的代码,我们需要提到http合约的格式,见右图

我想上一张图就足以描述http协议的格式了。 如果你不明白,没关系。 推荐简书的一篇博文(HTTP合约格式解读)。 对于我们来说,最关心的就是当前请求后面的Sec-WebSocket-Key,因为这个值需要返回给客户端,获得这个值后,我们将其存储在当前对象中。 然后我们需要回复客户。 如果你不知道它的格式,让我解释一下:

php连接符-老驱动带你用PHP实现Websocket协议

对于websocket握手来说,如果服务器同意客户端的连接,那么返回的状态码一定是101。至于前面的文字,不一定非得是Switching Protocol,只是别人传递过来的,所以它应该被传承下去。 其次,Upgrade:websocket、Connection:upgrade 和 Sec-WebSocket-Version:13 必须传递给客户端。 这是固定的,应该不难。 另外,上面我们已经提到了Sec-WebSocket-Accept。 它的估算代码,我已经贴出来了,这个估算方法也是固定的。 不要忘记每行前面必须有rn,最后一行前面必须有两个rn。

分析数据协议

看完上面的握手代码,你是不是觉得自己上天堂了呢? 感觉这么简单? ? 骚年,醒醒,醒醒,哈哈,你好年轻,年轻就好

你看到我发布的websocket数据包的格式了吗? 是时候揭开它的面纱了。 这部分可能有点困难。 别害怕,我在这儿。 让我来进行原子级别的分析。

FIN 位也是整个片段第一个字节的最低位。 它只能是 0 或 1。该位只有一个功能。 如果为1,则表示该分片是整个消息的最后一个分片。 如果为0,则表示该片段之后还有其他片段。 听到这里你是不是一头雾水,什么是碎片? 什么是新闻? 很好,看来我装酷的时候到了,就不废话了。为了理清这些概念,尊重代码

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

这是一段 JAVASCRIPT 代码。 send函数的参数是一条消息,消息很短,但是注意我们不能假设它在任何时间任何地点都这么短。 当它看起来很长时,客户端可能会忽略它。 切割,比如我有一个大小为4M的字符串,我把它分成了4个1M的字符串,那么每个1M的字符串只能成为一个分片,并且每个分片都是独立发送的,四个分片组合起来生成一条消息。 每个片段的格式是固定的,格式与里面的纹理相同。 按照我刚才说的,前三个分片的FIN是0,第四个是1,清楚了吗? 太简单!!

RSV1、RSV2、RSV3

这三位是保留扩展用的,基本不会用到。 反正我也不用它们,所以我们可以把它们当作空气,永远设置为0。 就是这么决定性的。

操作码

顾名思义,opcode就是操作码,占据第一个字节的低四位,所以opcode可以表示16个不同的值。 想问一下,opcode是用来做什么的? opcode用于分析当前片段的负载(携带的数据),具体细节会再次说明。

面具

指示当前分片携带的数据是否加密。 该位置为第二个字节的最低位,共1位。 它的值没有按照你想要的设置。 RFC6455明确规定所有从客户端发送到服务器的数据都必须加密,因此mask的值必须为1。同时,所有从服务器发送到客户端的数据都必须不加密,因此mask必须为0 ,就是这么简单粗暴。

有效负载长度

这部分用来定义加载数据的宽度,一共7位,所以最大值是127,这么简单? 哼php连接符,没有。

面膜键

它的位置紧邻数据宽度,大小为0或4字节。 前面分析过mask的作用。 如果掩码为1,则需要对数据进行加密。 此时掩码密钥占用4个字节,否则宽度为0。至于如何使用掩码密钥来泄露秘密数据,稍后会再次提到。

有效负载数据

这里是我们从客户端收到的数据,但是是加密的,“我是奥巴马”,前面的payload_length的宽度是加密数据的宽度,而不是原始数据的宽度。

解释完前面的内容,我们就可以开始分析如何使用php来解析Websocket消息片段了。

解析数据包

正如我在这篇博文开头所说,目前的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 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) <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 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) <current_message_length;
  }

当payload_length的宽度等于126时,就会出现一些麻烦。 此时,第三个和第四个字节组合成一个无符号的16位整数。 还记得我们之前说过的网络字节顺序吗? 高位字节在前,低位字节在前,所以我们读取的时候,第三个字节是高8位,第四个字节是低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位位保持不变。

解析掩码键

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

要找到掩码键,您必须首先找到其在当前段中的偏斜。 如果payloadlength126,那么skew就是(2+8)=10,而mask key的大小是4个字节,这样就找到了skew和width,mask key就可以得到了。

解密数据

//获取加密的内容
$real_message = substr($buffer, $mask_key_offset + 4);
$i = 0;
$parsed_ret = '';
//解析加密的数据
while ($i mask_key[$i % 4]))));
    $i++;
}

解密数据的第一步是找到当前段中加密数据的倾斜。 这很简单。 这个值等于maskkey的skew(上面已经得到)+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 > 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,操作码设置为1,然后检测数据的宽度。 这部分内容正好和解析数据宽度的步骤相反,所以不再分析。 如果你把前面的我理解的话,这里应该没有问题,但是要非常小心,我们之前已经提到过,服务器返回给客户端的数据是无法加密的,所以掩码必须设置为0,并且掩码键的宽度为0。

运行实例

正如本博文开头提到的,我写了一个简单的websocket实现,请务必自行下载运行,光看是没有用的:php-websocket-base-implemention

如何运行 websocket 服务器

为了让您看到实际的运行结果,请打开websocket.html文件,如果页面出现此内容,则说明运行成功。

运行前请检查8080端口是否被占用。 当然,你可以将websocket.html更改为其他内容,只要确保它没有被占用即可。 如果您无法运行,请与我联系。 如果您想查看其他内容,请同时更改websocket.html文件,然后重新启动服务器。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 php php连接符-老驱动带你用PHP实现Websocket协议 https://www.wkzy.net/game/178168.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务