使用PHP套接字模块检测对等端断开连接(EOF)

5
我在使用PHP的sockets库时遇到了一个奇怪的问题:似乎无法检测/区分服务器的EOF,因此我的代码陷入了无限循环。

以下是更详细的说明;首先,一些背景信息(这里没有什么特别复杂的东西):

<?php

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '127.0.0.1', 8081);

for (;;) {

    $read = [$socket];
    $except = NULL;
    $write = [];

    print "Select <";
    $n = socket_select($read, $write, $except, NULL);
    print ">\n";

    if (count($read)) {

        print "New data: ";

        #socket_recv($socket, $data, 1024, NULL);
        $data = socket_read($socket, 1024);

        print $data."\n";

    }

    print "Socket status: ".socket_strerror(socket_last_error())."\n";

}

上面的代码只是连接到服务器并打印读取的内容。这是我正在编写的小套接字库中的简化版本。
为了测试,我目前使用“ncat -vvklp 8081”来绑定套接字并充当服务器。运行它后,我可以启动上面的代码,它会连接并工作 - 例如,我可以在“ncat”窗口中输入,PHP会接收它。(从PHP发送数据也可以工作,但我已经排除了那部分代码,因为它不相关。)
然而,一旦我^C ncat,上面的代码就进入了一个硬无限循环 - 而PHP说套接字没有错误。
我正在尝试找出让PHP意识到对等方已断开连接的按钮在哪里。
socket_get_status()是一个很大的误称 - 它是stream_get_meta_data()的别名,并且它实际上不适用于套接字!feof()同样会输出“Warning: feof(): supplied resource is not a valid stream resource”。
我找不到用于检测对等方EOF的socket_*函数。 PHP手册中socket_read()的注释之一最初让我不想使用该函数,因此我改用了socket_recv(),但最终还是试了一下 - 但没有任何效果;切换接收调用没有影响。
我已经发现,观察套接字的写入情况,然后尝试向其写入将突然使PHP“哦,等等,对了”并开始返回Broken pipe - 但我不想向服务器写入,我想从它读取!

最后,关于被注释的部分 - 我更希望使用PHP内置的流功能,但是stream_*函数不提供处理异步连接事件的任何方法(我想这样做,因为我正在建立多个连接)。我可以使用stream_socket_client(...STREAM_CLIENT_ASYNC_CONNECT...)但是无法找出何时建立连接(6yo PHP bug #52811)。


这不是TCP/IP套接字的工作方式。没有“自然”的方法来检测链接的另一端是否“断开”。这不是文件读取/写入。这是通过“希望它到达那里”或“希望得到一些回报”的消息传递 :) 有了这个想法,有一些常见的技术可以检测到“客户端已经离开”。最简单的方法是在与客户端“交换数据”的协议中设计一些东西。我使用“ping请求”。在短时间内没有回复-它已经离开了。 - Ryan Vincent
有用吗? Ok,socket_select很聪明,会检查客户端是否还在那里。它会等待直到有有用的信息返回。1)你必须检查socket_select语句的错误返回。目前你把它当作文件读取处理,请不要这样做。2)如果没有错误,总是检查返回的数据长度-零意味着客户端“离开了”。通过这样做,您应该能够从代码中获得更有用的结果。我倾向于使用长达60秒的超时时间。 - Ryan Vincent
感谢提供这些信息。PHP 的 socket_readsocket_recv 函数的手册并没有清晰说明如何检测这些类型的情况!例如,socket_recv() 函数显然会将返回缓冲区设置为 NULL "如果出现错误、连接被重置或者没有可用数据"(这很有帮助),而 socket_read() 函数则说当“远程主机关闭连接”时返回 FALSE,同时也表示在“没有更多可读数据”时返回一个空字符串。 - i336_
好的,我确实了解套接字的基础知识,之前我已经实现了一些客户端和服务器,但是现在我正在编写一个小代码库,并尝试添加全面的错误处理。我的以前使用PHP处理套接字时使用的是"fsockopen()",因此我能够使用"feof()"来处理连接中断事件。这一次我实际上是使用"socket_import_stream()",因为我需要绑定到特定的本地端口,而套接字函数无法做到这点。 - i336_
是的,这是因为必须使用'socket_select'导致了许多麻烦。它有些“笨拙”,但这是PHP进程能够实现同时处理多个套接字的唯一方式。 - Ryan Vincent
显示剩余7条评论
1个回答

1

好的,我想我不妨把上面的评论转化为答案。所有的功劳都归功于Ryan Vincent帮助我这个愚钝的脑袋弄清楚了这个问题 :)

socket_recv将会返回0,如果对等方已经断开连接,或者FALSE,如果发生了任何其他网络错误

参考一下,在C语言中,recv()的返回值是你刚刚接收到的新数据的长度(可以是0),或者-1表示错误条件(其值可以在errno中找到)。

使用0来表示错误条件(而且仅仅是一种任意类型的错误条件)在PHP中并不是标准的,也是独特的,所有的方式都是错误的。其他的网络库并不是这样工作的。

你需要像这样处理它。

$r = socket_recv($socket, $buf, $len);

if ($r === FALSE) {

   // Find out what just happened with socket_last_error()
   // (there's a great list of error codes in the comments at
   // http://php.net/socket_last_error - considering/researching
   // the ramifications of each condition is recommended)

} elseif ($r === 0) {

   // The peer closed the connection. You need to handle this
   // condition and clean up.

} else {

   // You DO have data at this point.
   // While unlikely, it's possible the remote peer has
   // sent you data of 0 length; remember to use strlen($buf).

}

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