socket_recv未接收到完整数据。

12
我正在通过Websocket从浏览器发送大约5000字节的图像数据,但是这一行只接收到了1394个字节:
while ($bytes = socket_recv($socket, $r_data, 4000, MSG_DONTWAIT)) {
    $data .= $r_data;
}

握手完成后,正确接收到数据。JSON数据在1394个字节后被截断。 可能是什么原因?

在浏览器界面中,它将图像发送为JSON格式:

websocket.send(JSON.stringify(request));

通过我测试的其他PHP websocket免费程序也可以正常工作,因此浏览器界面很好。

这里是完整的源代码


1
一个找出问题的好方法是停止忽略socket_recv可能给出的任何错误。那些错误信息可能真的很有用。通过使用错误抑制运算符,你正在丢弃所有这些潜在有用的错误信息。在尝试随意猜测可能出了什么问题之前,我会从这个方面开始。 - Sherif
你确定没有进行gzip编码吗? - hek2mgl
@user5858 你是什么意思? - hek2mgl
看起来你在混合使用原始TCP/IP和Websockets。你向浏览器发送了任何数据吗?可能是你发送的数据没有使用Websocket协议进行封装 - 这会导致连接关闭。实际上,如果你有一些cookie数据,那么这1394个字节可能是Websocket连接请求...(你完成了握手吗?) - Myst
请阅读我的评论和/或查看此答案:https://dev59.com/QWjWa4cB1Zd3GeqPsJus#12492723 - alk
显示剩余4条评论
6个回答

7

1394大约是MTU(最大传输单元)的常见大小,特别是如果你通过VPN进行隧道连接的话(你是吗?)。

你不能期望一次调用读取所有字节,数据包可能根据网络MTU进行分段。


不,我没有使用任何VPN或代理。 - user5858
1
@user5858:正确的事实是,答案描述了使用TCP套接字进行任何类型的传输。在PHP中,这至少适用于socket_recv(),它包装了系统调用recv():http://man7.org/linux/man-pages/man2/recv.2.html 数据也可能被分成任意大小的片段,最多可达1个字节。 - alk

7
您已经通过指定MSG_DONTWAIT将我们的套接字设置为非阻塞,因此它在读取第一块数据后将返回EAGAIN,而不是等待更多数据。删除MSG_DONTWAIT标志,改用MSG_WAITALL,以便等待接收所有数据。
有几种方法可以知道是否已接收到您所期望的所有数据:
  1. 发送数据的长度。这在想要发送多个长度不同的内容块时非常有用。例如,如果我想发送三个字符串,我可能会先发送一个“3”告诉接收方期望接收多少个字符串,然后对于每个字符串,我都会发送字符串的长度,然后是字符串数据。
  2. 使用固定长度的消息。如果您期望接收多个消息,但每个消息的大小相同,则可以从套接字中读取,直到至少有足够多的字节以处理消息。请注意,您可能会在单个 recv() 调用中接收到多个消息(包括部分消息)。
  3. 关闭连接。如果您只发送一条消息,则可以半关闭连接。这是有效的,因为 TCP 连接维护发送和接收的不同状态,因此服务器可以关闭发送连接,但保留接收连接以等待客户端的回复。在这种情况下,服务器将所有数据发送给客户端,然后调用 socket_shutdown(1)
1和2在接收数据时非常有用,尤其是在编写游戏、聊天应用或其他需要保持套接字打开并传递多个消息的情况下。#3是最简单的方法,适用于只想一次性接收所有数据的情况,例如文件下载。

如果没有收到任何数据,它仅会设置“EAGAIN”,否则调用将返回接收到的字节数,即使在非阻塞套接字上也可能比它被告知的少。 - alk
很抱歉,如果我一直保持循环……在许多次EAGAIN之后,它确实会在一段时间后读取更多数据(经过几个循环迭代)。你是正确的。但服务器如何知道是否已接收到完整的有效负载数据,没有任何字节剩余? - user5858
嗨,请查看我的编辑,了解如何知道何时收到数据。根据您的需求,有几个选项可供选择。 - Zebra North

6

以下是关于此事的我的看法。socket_recv在出现错误时可能会返回false。在非阻塞IO中,它也可以接收零(0)字节。

你在循环中的检查应该是:

while(($bytes = socket_recv($resource, $r_data, 4000, MSG_DONTWAIT)) !== false) {}

尽管我也会检查套接字是否存在错误,并添加一些usleep调用以防止“CPU烧毁”。
$data = '';
$done = false;
while(!$done) {
    socket_clear_error($resource);
    $bytes = @socket_recv($resource, $r_data, 4000, MSG_DONTWAIT);

    $lastError = socket_last_error($resource);

    if ($lastError != 11 && $lastError > 0) {
        // something went wrong! do something
        $done = true;
    }
    else if ($bytes === false) {
        // something went wrong also! do something else
        $done = true;
    }
    else if (intval($bytes) > 0) {
        $data .= $r_data;
    }
    else {
        usleep(2000); // prevent "CPU burn"
    }
}

0

我想知道你的Websockets连接是否有问题。在你引用的while循环中,我看到它位于代码的某个部分,其中客户端握手失败了,它在if($client->getHandshake()) { ... } else { ... }else中。

据我所知,$client是一个单独的类,所以我看不到这个类的样子或者Client::getHandshake()做了什么,但我猜测它是一个布尔值的getter,表示websocket升级握手的成功或失败。

如果我没错的话,握手失败并且连接被客户端关闭。从代码中我可以看出,你正在使用的服务器代码要求规范的第13版。你没有提及你使用的客户端库是哪个,但其他服务器将接受与该服务器不同的版本。

请确保你的客户端库支持最新的版本。

如果我所建议的不正确,那么将服务器收到传入连接并且传输失败时的详细输出发布出来将会有帮助。


0

但是,你粘贴的代码部分不是包含在else块中吗?对我来说,这个else块看起来像是握手没有成功?

你能将接收到的字节打印成字符串吗?


0

我认为你的问题不正确。根据源代码,如果握手成功,则执行此代码段:

$data = '';

while (true) {
    $ret = socket_recv($socket, $r_data, 4000, MSG_DONTWAIT);
    if ($ret === false) {
        $this->console("$myidentity socket_recv error");
        exit(0);
    }
    $data .= $r_data;
    if (strlen($data) > 4000) {
        print "breaking as data len is more than 4000\n";
        break;
    } else {
        print "curr datalen=" . strlen($data) . "\n";
    }
}

如果程序确实进入了您提供的代码部分,那么值得研究为什么握手失败。
Server类有第三个参数“verboseMode”,当设置为true时,将为您提供详细的调试日志,以便了解发生了什么。
如果没有调试日志,我们只能进行推测,但是如果提供了调试日志,我们可以提出更好的建议。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接