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.
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.