【问题标题】:PHP websocket using socket_recv() - will I ever receive a partial frame?使用 socket_recv() 的 PHP websocket - 我会收到部分帧吗?
【发布时间】:2018-03-25 00:08:40
【问题描述】:

我正在用 PHP 编写一个 websocket 服务器(使用 sockets 扩展名),我需要一些帮助来了解我需要在多大程度上处理碎片消息。

我对websocket信息如何传递的理解如下:

  1. 客户端应用程序将MESSAGE(任意长度)发送到客户端 API。
  2. 客户端 API 将 MESSAGE 拆分为一个或多个 FRAMES(也是任意长度)并将它们发送到网络层。
  3. 网络层将数据拆分成多个PACKETS,通过TCP通过网络发送。
  4. 服务器接收 TCP PACKETS(可能是乱序的,但如有必要,它会重新排序)并将它们传递给正在侦听相关端口的应用程序。
  5. 应用调用socket_recv()从socket读取接收到的数据。

我想了解的是我的应用程序在使用socket_recv() 读取 websocket 数据流时会看到什么数据?

具体来说,我需要在多大程度上担心碎片化?


为了帮助解释我的问题,以下是上述过程的图表形式:

1. Web app  (messages):   [Message_1][Message_2]
2. Browser  (frames)  :   [Messag][e_1][Messag][e_2]
3. TCP send (packets) :   [Mess][ag][e_1][Mess][ag][e_2]
4. TCP recv (packets) :   [ag][Mess][e_2][ag][Mess][e-1]
5. socket_recv        :   ???

如果我在循环中调用socket_recv(),直到它返回长度为零(每次都添加到我的内部缓冲区),我能保证得到一个完整的MESSAGE吗?

socketrecv: [Message_1]
socketrecv: [Message_2]

还是一个完整的FRAME

socketrecv: [Messag]
socketrecv: [e_1]
socketrecv: [Messag]
socketrecv: [e_2]

或者,它实际上会是一个任意系列的PACKETS,代表迄今为止收到的任何数据(因此可能是部分FRAME,甚至是多个FRAMES)?

socketrecv: [Messag
socketrecv: e_1][Mess
socketrecv:
socketrecv: ag
socketrecv: e_2]

还是别的什么?

我很高兴将各种FRAMES 数据拼接在一起,但如果我可以假设每次轮询中接收到的数据的第一个字节(使用socket_select() 发起)总是@ 987654341@ 标头,而不必将其作为原始字节流处理,在开始之前需要将其缝合回FRAMES

【问题讨论】:

  • 您的问题得到答复了吗?我对这样的解释很感兴趣,但找不到任何相关信息:/
  • 不——还没有。我的实现假设帧按发送顺序出现,但是从套接​​字读取的任何内容都可能包含此数据的任意片段。因此,我有两个缓冲区:流缓冲区,包含从套接字读取的原始字节,以及帧缓冲区,它包含完整的帧,一旦它们被完全接收。不知道这是必需的还是只是大刀阔斧,但在没有答案的情况下,这是最安全的方法,而且似乎效果很好。
  • 感谢您的回复,我会尝试类似的东西

标签: php sockets websocket


【解决方案1】:

哦,好吧,我曾经在我的 C++ 代码中使用 websocket,是的,由于 TCP 协议的工作方式,它可能会被分割。

websocket 有两种类型的数据流:Hixie(旧),Hybi(新),还有版本,例如 hybi-13 .. hybi-17。

但没关系,因为您的问题是 socket_recv() 仅从缓冲区检索数据,缓冲区取决于您的网络设置(MTU)以及操作系统和硬件......如此复杂......甚至可以读取 1 字节也高达 16MB。

因此,如果您想在 PHP 中实现 websocket,则必须读取并解析框架并获取其大小,如果大小可用,则剪切并处理然后继续,如果没有阅读更多内容。

很有可能收到不完整的帧或一帧和不完整的下一帧。 因此,您必须跨过并找到帧的开头,计算它的大小并向前迈进,如果您有剩余的数据必须保留在缓冲区(也称为变量)中,并且必须在之后附加下一个读数。

但首先,您必须首先读取至少 4 个字节。 (标题大小) 很高兴知道 hybi 协议使用“压缩”,因此使用有效负载的 crafeul 帧字节可能会根据其整数类型而有所不同。

参见下面的 C 代码。

        payload_length = frame[1] & 0x7f;
        if (payload_length < 126) 
        {
            hdr_length = 2;
            payload_length = payload_length; // FYI / DUMMY
        } 
        else if (payload_length == 126) 
        {
            payload_length = (frame[2] << 8) + frame[3];
            hdr_length = 4;
        } 
        else 
             ....

【讨论】:

  • 这个答案涵盖了我试图问的问题,即应用程序从原始套接字读取时会看到什么数据。它支持我的直觉,我们将得到的大致是我的问题中的第二个示例,即我们将按照发送顺序获取每个 websocket 消息,但不一定作为单独的消息 - 就像所有消息都连接起来一样有效成一个长的二进制字符串,然后任意切成不同大小的块。它验证了我的方法(我担心它被过度设计),这是为了考虑到这一点而编写的。赏金奖励!
【解决方案2】:

我非常擅长网络,并且在我这一天写了很多 Twisted 网络代码(Python 中的网络套接字库)

我家里有《Unix 网络编程第 3 版》这本书,我看了看它是怎么说的……我几年前从图书馆买了这本书,因为据说它是“权威”关于 TCP/IP 堆栈及其规范。

来自第 2 章“传输层”

两台主机之间路径中最小的MTU 称为路径MTU。今天,以太网 MTU 为 1,500 字节,通常是路径 MTU。 ... 当 IP 数据报要从接口发出时,如果数据报的大小超过链路 MTU,则由 IPV4/IPV6 堆栈执行分片。碎片通常不会重新组装,直到它们到达最终目的地。在 IPv4 上,主机和路由器都可以执行分段。在 IPv6 上,只有主机可以执行分片。
...
IPv4 和 IPv6 定义了最小重组缓冲区大小,我们保证任何实现都必须支持的最小数据报大小。对于 IPv4,这是 576 字节

应用程序

保证任何应用程序或 IPv4 主机堆栈始终接收link MTU 大小的数据报在应用程序级别,即socket_recv

您的应用程序可能会收到较少量的数据,因为可能会发送较少的数据,这就是为什么套接字服务器能够知道消息何时结束以及新消息何时开始的原因。

典型的套接字服务器

ssize_t numBytesRcvd = recv(clntSocket, buffer, BUFSIZE, 0)
if (numBytesRcvd < 0 ) // 0 indicates end of stream
    exit(1);

在上面的 sn-p 中,进程从操作系统接收最多 BUFSIZE 个字节。这并不意味着它不会收到更少,或者连接的另一端没有发送更少。

关于堆栈较低级别发生的事情的整个讨论实际上对您的目的毫无意义。

当你在 PHP 中调用 socket_recv 时,它会做同样的事情,这里是源代码:

    if ((retval = recv(php_sock->bsd_socket, ZSTR_VAL(recv_buf), len, flags)) < 1) {
        zend_string_efree(recv_buf);
        ZEND_TRY_ASSIGN_REF_NULL(buf);
    } else {
        ZSTR_LEN(recv_buf) = retval;
        ZSTR_VAL(recv_buf)[ZSTR_LEN(recv_buf)] = '\0';
        ZEND_TRY_ASSIGN_REF_NEW_STR(buf, recv_buf);
    }

您也可以看到它也尝试接收len 字节。
然后,它使用函数ZEND_TRY_ASSIGN_REF_NEW_STR 将这些字节添加到recv_buf,并在末尾添加一个空值以终止接收到的字符串。

真正的答案

任何套接字应用程序都需要一种方法来区分消息的长度和结构。
消息大小可能是任意的,消息本身也可能是任意的,具体取决于您的要求。
这就是protocols 存在的原因。协议只是字节大小和排列的规范。

在您的情况下,您想从客户端发送消息并在服务器上接收它并知道消息何时结束,然后可能无限期地重复该循环。

你真正要问的是:

我如何构造数据报结构的规范并知道数据报何时结束 - 你需要一个协议!

这是最简单协议的基本原理:

  1. 定义一个固定大小的header 这个标头将通过一些字节告诉您消息的长度。将任何元信息放在标题中。
    重要的部分是标题的lengthfixed。我们将此标题长度称为HEADER_LEN
  2. 接收消息时,构造一个缓冲区,继续写入该缓冲区,直到至少收到HEADER_LEN
  3. 将字符串拆分为headerextra,其中header 是您收到的标头字节,extra 是您在使用标头字节时收到的额外字节。
  4. 使用 PHP 的 unpack 函数解析 header。它能够解析 BINARY/C 整数。
    假设您已将 HEADER_LEN 定义为 5 个字节 = [4 BYTE INT + NULL]
    将标题 4 byte int 解析为 body_length 变量 - 这将是一个整数,告诉我们您的身体有多长。 此设计假设 CLIENT 根据我们的规范构建了一个至少 5 个字节的正确组合的标头。
    ...
    如果没有,我们还有一大堆问题要处理。即,丢弃格式错误的消息并找到下一个格式正确的消息。
    不幸的是,这篇文章的纠错对话会很长。
  5. 从套接字读取额外的body_length 字节。这包括我们已经收到的extra 字节。
  6. 您现在已收到整个数据报
  7. 等待下一个数据报,重复。

如何在现实生活中做到这一点

以上是一个有趣的学术练习,可帮助我们了解 TCP 套接字客户端和服务器的工作原理 - 但在此基础上构建我们自己的应用程序并不是最简单的。

幸运的是其他人为我们完成了这项工作。

Wamp 是为 websocket 设计的协议,可以轻松定义消息格式并确保可靠地发送/接收它们。

Wamp 的 PHP 实现称为Ratchet

与滚动您自己的协议相比,这些工具更可取,因为它们可以自行处理格式错误的消息和错误恢复。

祝你好运!

【讨论】:

  • 这是一个非常详细和彻底的关于 TCP 堆栈的内容,它广泛地描述了我在问题中的图表已经涵盖的内容。但是,它没有回答关于“下一步做什么”的基本问题,即当应用程序查看从 TCP 层填充的缓冲区时,它会获取单个消息还是(正确排序的)数据的任意块。一个有趣的答案,但不完整。
【解决方案3】:

互联网上有完整的文档记录... TCP 可靠并且面向连接。

您收到完整且顺序正确的消息 - 或者永远不会。消息的每个片段都必须由接收者确认,如果未完成,则再次发送该片段(几次......)。消息的重新组装是由 TCP 堆栈完成的,因此您不必担心应用程序中的数据包顺序或丢失数据包......无论您收到完整的消息还是错误。

不要误解缓冲区...当您调用 socket_recv() 时,您将提供一个缓冲区,但这与底层 TCP 堆栈正在使用的缓冲区不同。

UDP 是对应部分,您必须注意所有细节。您可能会以错误的顺序、多次、损坏/不完整或有其他缺陷......甚至永远不会获得数据报!意思是:你可能最终得到一个包含间隙的序列,你将不得不忍受它。

【讨论】:

  • 您能否澄清一下您在第二段中所说的“信息”是什么意思。您的意思是每个socket_recv() 调用(假设提供了足够的缓冲区空间)都会给我一个或零个websocket 消息,还是您使用的message 的含义与问题中给出的含义不同?你的回答暗示我的第一个例子是正确的,但如果这是明确的,那就太好了。
  • TCP 是可靠的,但 TCP 也是面向流的,而不是面向数据报的。 TCP 保证构成流的消息顺序正确,因此您可以将其作为流读取,但应用程序级框架(例如 OP 的 websocket 问题)是另一回事。
  • 我认为这个回答回答了错误的问题——这不是关于 TCP 层如何工作,而是关于socket_recv() 的结果,即我们在堆栈上有多远。我还认为您对问题使用了不同含义的“消息”,这有点令人困惑。
猜你喜欢
  • 1970-01-01
  • 2023-03-30
  • 2015-10-11
  • 2013-07-26
  • 1970-01-01
  • 1970-01-01
  • 2015-04-10
  • 2022-11-14
  • 1970-01-01
相关资源
最近更新 更多