如何检查套接字是否存活

I have a small server (written in PHP) listening on a socket created with socket_create and accepting connections using socket_accept. All connections are scanned using socket_select, and whenever that call returns, the server either accepts a new connection of reads from the socket. When a client disconnects, the socket associated with it is returned by socket_select, and then the server is able to detect the disconnect by checking that socket_read returns an empty string.

So far it all is "by the book" and works well, but the problem is that if a client drops the connection (if, for example, it crashes), then its socket gets into the list returned by socket_select (as expected), but when the server calls socket_read on it, it triggers an error:

socket_read(): unable to read from socket

Obviously, I have to add some check on the socket before calling socket_read, but I couldn't find what that would be. What would be the way to check that socket? Please see also comments in the code.

define( LISTEN_ADDR, 0 );
define( LISTEN_PORT, 31415 );

/* Set up the listening */
$main_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );

$ok = $main_socket
    && socket_set_option( $main_socket, SOL_SOCKET, SO_REUSEADDR, 1 )
    && socket_bind( $main_socket, LISTEN_ADDR, LISTEN_PORT )
    && socket_listen( $main_socket );

if( !$ok ) {
    fprintf( STDERR, "You screwed it up!
" );
    exit(1);
}


/* Here user connections are stored. */
$user_sockets = array();

/* Used to pass NULL by reference. */
$null = null;

while( true )
{
    $readable_sockets = $user_sockets;
    $readable_sockets[] = $main_socket;

    $r = socket_select( $readable_sockets, $null, $null, null );
    if( $r === false ) {
            fprintf( STDERR, "Socket error: %d (%s)", socket_last_error(),
                    socket_strerror(socket_last_error()) );
            continue;
    }
    /*
    var_dump( $r ) - always tells int, > 0
    var_dump( $readable_sockets ) - just shows an array of sockets
    var_dump( socket_last_error( $s ) ) - always int(0)
    */

    foreach( $readable_sockets as $s )
    {
        /* If this is the main socket, then we have a new connection. */
        if( $s == $main_socket ) {
            $user_sockets[] = socket_accept( $main_socket );
            continue;
        }

        /* If not, then a user says something. */

        $buf = socket_read( $s, 4096, PHP_BINARY_READ );
        /* And here occasionally goes:
            "socket_read(): unable to read from socket"
        + description from the OS telling that the remote host has
        dropped the connection. */

        if( $buf == '' ) {
            /* <remove $s from $user_sockets> */
        }
        else {
            /* <process what the user has sent> */
        }
    }
}

You are not checking the result of select, which must be done in order to know if there is any socket available to be read.

$r = socket_select($readable_sockets, $null, $null,  0);
if ($r === false) {
    /* socket_select  Error handling */
} else if ($r > 0) {
    /* At least at one of the sockets something interesting happened */
}

The select function will change the $readable_sockets array in case there is something to be read, or it may be undefined or untouched in case of timeout.

Note: Be aware that some socket implementations need to be handled very carefully.

A few basic rules:

  • You should always try to use socket_select() without timeout. Your
    program should have nothing to do if there is no data available. Code that depends on timeouts is not usually portable and difficult to
    debug.

  • No socket resource must be added to any set if you do not
    intend to check its result after the socket_select() call, and
    respond appropriately. After socket_select() returns, all socket
    resources in all arrays must be checked. Any socket resource that is available for writing must be written to, and any socket resource
    available for reading must be read from
    .

  • If you read/write to a socket returns in the arrays be aware that they do not necessarily read/write the full amount of data you have requested. Be prepared to even only be able to read/write a single byte.

  • It's common to most socket implementations that the only exception caught with the except array is out-of-bound data received on a socket.

There is no way to know when a socket crashed until you try to read from it, according to my experience select never return a crashed socket as candidate to read, except when a gracefully close or shutdown is done at the other end. One thing you can do is to have a timeout for each socket, if the socket has no activity during, let's say 30 minutes, then you may discard it.

I'd also separate the user's socket from the main socket. Do a select for user's socket and dispatch their data and then do a select for the main socket and accept new connections. But it's up to you, if you want to check for the main socket in every single iteration, it's ok.