socket_recv 未收到完整数据

socket_recv not receiving full data

我正在通过 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 免费程序一起使用。

这是完整的source code

1394 大约是 MTU 的常见大小,尤其是当您通过 VPN 建立隧道时(是吗?)。

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

您通过指定 MSG_DONTWAIT 将我们的套接字设置为非阻塞,因此它将在读取第一个数据块后 return EAGAIN,而不是等待更多数据。删除 MSG_DONTWAIT 标志并使用 MSG_WAITALL 代替,以便它等待接收所有数据。

有几种方法可以知道您是否收到了您期望的所有数据:

  1. 发送数据的长度。如果您想发送多个可变长度内容块,这将很有用。例如,如果我想发送三个字符串,我可能会先发送一个“3”来告诉接收方期望有多少个字符串,然后对于每个字符串,我将发送字符串的长度,然后是字符串数据。
  2. 使用固定长度的消息。如果您需要多条消息,但每条消息的大小都相同,那么您可以只从套接字读取,直到至少有那么多字节,然后处理消息。请注意,您可能会在一次 recv() 调用中收到多条消息(包括部分消息)。
  3. 关闭连接。如果您只发送一条消息,则可以半关闭连接。这是有效的,因为 TCP 连接保持发送和接收的独立状态,因此服务器关闭发送连接,但让接收连接保持打开状态以供客户端回复。在这种情况下,服务器将其所有数据发送到客户端,然后调用 socket_shutdown(1)

1 和 2 如果您想在接收数据的同时处理数据,则很有用 - 例如,如果您正在编写游戏、聊天应用程序或套接字保持打开状态并来回传递多条消息的其他应用程序。 #3 是最简单的一个,当你只想一次接收所有数据时很有用,例如文件下载。

我想知道您的 websockets 连接是否有问题。你在上面引用的 while 循环在我看来驻留在客户端握手失败的代码的一部分,它在 if($client->getHandshake()) { ... } else { ... }else 中。

据我所知,$client 是一个单独的 class,所以我看不到 class 的外观或 Client::getHandshake() 的功能,但是我猜它是一个布尔值的 getter ,它保存了 websocket 升级握手的成功或失败。

如果我是正确的,握手失败并且连接被客户端关闭。从代码中我可以看到您使用的服务器代码需要规范的版本 13。您没有提及您使用的是哪个客户端库,但其他服务器将接受除此服务器以外的其他版本。

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

如果我的建议是错误的,那么在服务器获得传入连接且传输失败时发布服务器的详细输出将会有所帮助。

这只是我的 2 美分。 socket_recv 可以 return 错误。它也可以在非阻塞 IO 中接收零 (0) 字节。

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

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

虽然我也会检查套接字是否有错误并添加一些 usleep 调用以防止 "CPU burn".

$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"
    }
}

但是,您粘贴的代码部分不是包含在 else 块中吗?在我看来握手没有通过的其他块?

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

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

$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";
    }
}

如果程序真的转到您提供的代码部分,那么有必要研究一下握手失败的原因。

服务器 Class 有第三个参数 verboseMode,当设置为 true 时,将为您提供详细的调试日志,说明到底发生了什么。

我们只是在没有调试日志的情况下进行推测,但如果提供了调试日志,我们可以提出更好的建议。