使用PHP进行套接字写入然后读取的问题

3
我正在使用PHP通过套接字编写命令,然后从服务器读取。我们有20台服务器都运行一个Node JS脚本,可以接收这些命令并执行它们。Node JS脚本将返回"ok",PHP会读回以确认命令已经传递。
Node JS脚本监听9000端口,并设置为允许半开连接
大多数情况下,这很好用,但当发送大量命令时,偶尔会收到错误信息,如下所示:
Contents: Message received back from socket was 'Unexpected token {'
Transport endpoint is not connected

运输端点消息提示我连接不成功。
我不是socket方面的专家,所以不知道我使用的实现是否“正确”。它大多数情况下都可以工作,但我知道有一些函数,比如socket_bindsocket_listen可能会更好,尽管我不确定它们做什么。
以下是我们正在使用的PHP代码。任何建议将不胜感激。
public function sendDaemonCommand($address, $template_id, $params = array()) {

    $hostname = $this->getHostnameFromPrivateIP($address);
    $port = 9000;
    $command = array('template_id' => $template_id, 'params' => $params);
    $command = json_encode($command);

    // Create a TCP Stream socket
    if (($sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {
        $this->mailError("Command Failed - " . $hostname, "Failed to create socket on " . $address . "\n\n" . socket_strerror(socket_last_error()) . "\n\nCommand:\n\n" . $command . "\n" . $this->functionTraceback());
        return false;
    }

    // Connect to socket
    if (socket_connect($sock, $address, $port) === false) {
        $this->mailError("Command Failed - " . $hostname, "Failed to connect to socket on " . $address . "\n\n" . socket_strerror(socket_last_error($sock)) . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
        socket_close($sock);
        return false;
    }

    // Write command to socket
    $_command = $command;
    $length = strlen($_command);

    while (true) {
        $sent = socket_write($sock, $_command, $length);

        if ($sent === false) {
            $this->mailError("Command Failed - " . $hostname, "Failed to write command to socket on " . $address . "\n\n" . socket_strerror(socket_last_error($sock)) . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
            socket_shutdown($sock, 2);
            socket_close($sock);
            return false;
        }

        if ($sent < $length) {
            $_command = substr($_command, $sent);
            $length -= $sent;
        }
        else {
            break;
        }
    }

    socket_shutdown($sock, 1);

    // Read back from socket
    if (($out = socket_read($sock, 1024)) !== false) {
        @socket_shutdown($sock, 0);
        $out = trim($out);
        if ($out !== "ok") {
            $this->mailError("Command Failed - " . $hostname, "Message received back from socket was '" . $out . "' on " . $address . "\n\n" . socket_strerror(socket_last_error($sock)) . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
            socket_close($sock);
            return false;
        }
    }
    else {
        $this->mailError("Command Failed - " . $hostname, "Failed to read from socket on " . $address . "\n\n" . socket_strerror(socket_last_error($sock)) . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
        socket_shutdown($sock, 0);
        socket_close($sock);
        return false;
    }

    socket_close($sock);
    return $out;
}

你不应该使用PHP sockets来连接Node.js,当处理大量打开的连接时,这样做非常低效。 - alessioalex
你有查看 PHP 服务器端的 netstat 吗?也许你的本地端口用尽了? - Eugen Rieck
@alessioalex 在 PHP 端还能用什么? - fire
@EugenRieck Node.js 不是应该是非阻塞的,可以处理同时的连接吗? - fire
我在谈论PHP端,而不是JS端:连接使用本地端口,(localIP,localPort,remoteIP,remotePort)元组必须是唯一的,因此虽然NodeJS处理了remoteIP,remotePort部分,但你可能会用尽本地端口 - 套接字选项可以帮助你解决这个问题,但请通过查看netstat来帮助诊断! - Eugen Rieck
使用Redis、RabbitMQ或者ZeroMQ将消息发送到Node.js中,这种方式对我来说更加高效。 - alessioalex
1个回答

1
对于这样一个简单的套接字客户端,我更喜欢使用fsockopen() - 它极大地减少了客户端代码的复杂性,并且不需要套接字扩展,这在某些情况下是不可用的。主要缺点是您会失去字符串错误消息,但这些消息很少有用 - 您仍然可以从创建套接字时获得错误字符串,如果读/写操作失败,则是因为套接字已断开连接。
我也不确定在这种情况下“允许半开放”有多有用 - 我认为它可能会引起更多问题而不是解决问题。调用socket_shutdown()在这里没有任何用处(并且可能是您问题的根源)- 只有在您操作的套接字不会关闭并且可能由其他代码在以后的某个时间点进行操作时才会使用此选项。由于您在同一例程中创建并销毁新套接字,因此不适用于此情况。

这是您的代码重写以使用fsockopen() - 如您所见,它相对较短且简单。我还稍微修改了一下,以使其始终返回一个布尔值-您的代码返回布尔值FALSE或字符串ok,由于只有这两个选项,因此更好地为函数始终返回一个布尔值。

public function sendDaemonCommand($address, $template_id, $params = array()) {

    // Prepare data
    $hostname = $this->getHostnameFromPrivateIP($address);
    $port = 9000;
    $command = array('template_id' => $template_id, 'params' => $params);
    $_command = $command = json_encode($command);
    $length = strlen($_command);

    // Connect to socket
    if (!$sock = fsockopen($address, $port, $errNo, $errStr)) {
        $this->mailError("Command Failed - " . $hostname, "Failed to connect to socket on " . $address . "\n\n" . $errStr . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
        return false;
    }

    // Write command to socket
    while (true) {

        // Try and write data to socket
        $sent = fwrite($sock, $_command, $length);

        // If it failed, error out
        if ($sent === false) {
            $this->mailError("Command Failed - " . $hostname, "Failed to write command to socket on " . $address . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
            fclose($sock);
            return false;
        }

        // If there is data left to send, try again
        if ($sent < $length) {
            $_command = substr($_command, $sent);
            $length -= $sent;
            continue;
        }

        // If we get here the write operation was successful
        break;

    }

    // Read back from socket and close it
    $out = fread($sock, 1024);
    fclose($sock);

    // Test the response from the server
    if ($out === false) {
        $this->mailError("Command Failed - " . $hostname, "Failed to read from socket on " . $address . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
        return false;
    } else if (($out = trim($out)) !== "ok") {
        $this->mailError("Command Failed - " . $hostname, "Message received back from socket was '" . $out . "' on " . $address . "\n\nCommand:\n\n" . $command. "\n" . $this->functionTraceback());
        return false;
    } else {
        return true;
    }

}

进一步的测试表明,如果有帮助的话,它被阻塞在fread的点上。 - fire
好的,你的意思是我应该这样做 $command = json_encode($command) . "\n",然后在Node接收数据时,应该检查最后一个字符是否为\n,如果是,则调用socket end函数? - fire
实际上,有一种方法可以在使用fsockopen()创建的内容上使用socket_shutdown():http://php.net/socket_import_stream。 - PypeBros
@PypeBros 还有stream_socket_shutdown(),显然当我写下这个答案时,也就是五年前,我不知道它的存在。还要注意的是,我今天不建议使用fsockopen(),而是使用stream_socket_client() - DaveRandom
好的。我猜fsockopen是一个遗留下来的函数,它出现在stream_socket_* API还没有出现的时代。 - PypeBros
显示剩余3条评论

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