关于套接字:使用socket_recv()的PHP websocket-我会收到部分框架吗?

PHP websocket using socket_recv() - will I ever receive a partial frame?

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

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

  • 客户端应用程序将MESSAGE(任意长度)发送到客户端API。
  • 客户端API将MESSAGE拆分为一个或多个FRAMES(也具有任意长度),并将其发送到网络层。
  • 网络层将数据拆分为多个PACKETS,以通过TCP通过网络发送。
  • 服务器接收到TCP PACKETS(可能是无序的,但是如果需要,它将对其重新排序),然后将它们传递给在相关端口上侦听的应用程序。
  • 应用程序调用socket_recv()从套接字读取接收到的数据。
  • 我想了解的是使用socket_recv()读取Websocket数据流时,应用程序将看到哪些数据?

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

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

    1
    2
    3
    4
    5
    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

    1
    2
    socketrecv: [Message_1]
    socketrecv: [Message_2]

    还是一个完整的FRAME

    1
    2
    3
    4
    socketrecv: [Messag]
    socketrecv: [e_1]
    socketrecv: [Messag]
    socketrecv: [e_2]

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

    1
    2
    3
    4
    5
    socketrecv: [Messag
    socketrecv: e_1][Mess
    socketrecv:
    socketrecv: ag
    socketrecv: e_2]

    或者是其他东西?

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


    哦,好吧,我以前在C ++代码中使用过websockets,是的,由于TCP协议的工作原理,它可能会碎片化。

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

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

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

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

    但是首先,您必须至少读取4个字节。 (标题大小)
    众所周知,hybi协议使用"压缩",因此对于有效负载而言,帧字节可以根据其整数类型而有所不同。

    请参见下面的C代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
            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
                 ....


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

    我在家中有一本书" Unix网络编程第三版",我偷看了一下……几年前我从图书馆购买了这本书,因为据说这是TCP上的"权威" / IP堆栈及其规范。

    摘自第2章"传输层"

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

    应用程序

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

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

    典型的套接字服务器

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

    在上面的代码段中,该进程从操作系统接收了MOST BUFSIZE个字节。这并不意味着它不会收到更少的消息,或者连接的另一端也没有收到更少的消息。

    实际上,在堆栈的较低级别发生什么的整个讨论对您的目的实际上是没有意义的。

    当您在PHP中调用socket_recv时,它的作用相同,这是源代码:

    1
    2
    3
    4
    5
    6
    7
    8
        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存在的原因。协议只是大小和字节排列的规范。

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

    您实际上要问的是:

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

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

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

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

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

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

    Wamp的PHP实现称为Ratchet

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

    祝好运!

    好。


    它在Internet上有完整的文档说明。TCP是可靠的且面向连接。

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

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

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