使用socket_recv()的PHP websocket - 我会收到部分帧吗?

时间:2017-10-12 22:49:52

标签: php websocket

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

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

  • 客户端应用程序向客户端API发送MESSAGE(任意长度)。
  • 客户端API将MESSAGE拆分为一个或多个FRAMES(也是任意长度)并将其发送到网络层。
  • 网络层将数据拆分为多个PACKETS,以通过TCP在网络上发送。
  • 服务器收到TCP PACKETS(可能是无序的,但如果需要,它会重新排序)并将它们发送到正在侦听相关端口的应用程序。

我想要了解的是我的应用程序在使用socket_recv()读取流时会看到的数据,我假设它返回了该端口上的原始数据。

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

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

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

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

3 个答案:

答案 0 :(得分:1)

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

我家里有一本书“ Unix Network Programming 3rd Edition”,我偷看了一下……几年前我从图书馆买了这本书,因为据说这是“权威”在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);

在上面的代码段中,该进程从操作系统接收的MOST 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字节。
然后,它使用函数recv_buf将这些字节添加到ZEND_TRY_ASSIGN_REF_NEW_STR,并在末尾添加一个空值以终止接收到的字符串。

真正的答案

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

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

您实际上要问的是:

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

这是最简单的协议如何工作的基础:

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

如何在现实生活中实现这一目标

以上内容是一个有趣的学术练习,可以帮助我们了解TCP套接字客户端和服务器的工作方式-但在其之上构建自己的应用程序并非最简单。

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

Wamp是专为网络套接字设计的协议,可轻松定义消息格式并确保可靠地发送/接收消息。

Wamp的PHP实现称为Ratchet

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

祝你好运!

答案 1 :(得分:1)

好吧,我以前用C ++代码编写websockets,是的,由于TCP协议的工作原理,它可以被分段。

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

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

因此,如果您想在PHP中实现websocket,则必须读取并解析框架并获取其大小,如果有可用的大小,则可以对其进行剪切和处理,如果没有更多内容,请继续操作。

很有可能接收到不完整的一帧或一帧以及不完整的下一帧。 因此,如果您必须将剩余数据保留在缓冲区(又称为变量)中,则必须跨步查找帧的开头并计算其大小,然后向前迈进,并且必须在其后追加下一个读数。

但是首先,您必须至少读取4个字节。 (标题大小) 众所周知,hybi协议使用“压缩”,因此随着有效载荷的增加,帧字节可能会因其整数类型而有所不同。

请参见下面的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 
             ....

答案 2 :(得分:0)

它在Internet上有完整记录... TCP是可靠且面向连接。

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

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

UDP是最重要的部分,您必须注意所有细节。您可能会以错误的顺序,多次,损坏/不完整或其他有缺陷的方式获取数据报,甚至永远不会!意思是:您可能最终会得到一个包含缺口的序列,并且必须忍受它。