5

I'm having a weird issue with PHP's sockets library: I do not seem to be able to detect/distinguish server EOF, and my code is helplessly going into an infinite loop as a result.

Further explanation below; first of all, some context (there's nothing particularly fancy going on here):

<?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";

}

The above code simply connects to a server and prints what it reads. It's a cut-down version of what I have in the small socket library I'm writing.

For testing, I'm currently using ncat -vvklp 8081 to bind a socket and be a server. With that running, I can fire up the code above and it connects and works - eg, I can type in the ncat window, and PHP receives it. (Sending data from PHP is working too, but I've excluded that code as it's not relevant.)

However, the moment I ^C ncat, the code above enters a hard infinite loop - and PHP says there's no error on the socket.

I am trying to figure out where the button is that whacks PHP upside the head and makes it realize that the peer has disconnected.

  • socket_get_status() is a great misnomer - it's an alias for stream_get_meta_data(), and it doesn't actually work on sockets!

  • feof() similarly spouts Warning: feof(): supplied resource is not a valid stream resource.

I can't find a socket_* function for detecting peer EOF.

One of the PHP manual notes for socket_read() initially dissuaded me from using that function so I used socket_recv() instead, but I eventually tried it just in case - but no dice; switching the receive call has no effect.

I have discovered that watching the socket for writing and then attempting to write to it will suddenly make PHP go "oh, wait, right" and start returning Broken pipe - but I'm not interested in writing to the server, I want to read from it!

Finally, regarding the commented part - I would far prefer to use PHP's builtin stream functionality, but the stream_* functions do not provide any means for handling asynchronous connect events (which I want to do, as I'm making multiple connections). I can do stream_socket_client(... STREAM_CLIENT_ASYNC_CONNECT ...) but then cannot find out when the connection has been established (6yo PHP bug #52811).

i336_
  • 1,813
  • 1
  • 20
  • 41
  • That is not how TCP/IP sockets work. There is no 'natural' way to detect if the other end of the link has 'gone away'. These are not file reads / writes. This is message passing by 'wish it to get there' or 'hoping to get something back' :) With that in mind, there are some common techniques for detecting that, the 'client as gone away'. The easiest way is design something into the protocol that you use to 'exchange data' with the client. I use 'a ping request'. No reply within a short time - it has gone. – Ryan Vincent Sep 09 '16 at 12:01
  • Some approaches... maybe interesting? These apply to PHP as well... http://stackoverflow.com/questions/22860969/how-do-i-detect-tcp-client-disconnect-with-gen-tcp. Also, http://stackoverflow.com/questions/283375/detecting-tcp-client-disconnect?rq=1, PHP: http://stackoverflow.com/questions/11451840/php-socket-client-detect-server-disconnect – Ryan Vincent Sep 09 '16 at 12:02
  • I see what you're getting at, and thanks heaps for those references. Note that, when my code fails and falls into an infinite loop, that infinite loop includes a read operation, and it's after the read operation that the socket functions report "Success" (and continue looping forever). For *this* reason I suspect I am looking at arguably buggy behavior in PHP. – i336_ Sep 09 '16 at 12:21
  • Oh. So I'll never get 0 bytes of data under normal circumstances? Is that actually how it works? – i336_ Sep 09 '16 at 12:54
  • [useful?](http://www.binarytides.com/php-socket-programming-tutorial/) Ok, `socket_select` is clever and will check that the client is still there. It will wait until it has useful information to return. 1) You must check for error returns from the `socket_select` statement. Currently you are treating it a file reads. Please don't. 2) If no errors then always check the data length returned - zero means the client has 'gone away'. You should get more useful results from the code by doing this. I would tend to use a long timeout of around 60 seconds. – Ryan Vincent Sep 09 '16 at 13:27
  • I appreciate this information. The PHP manual pages for [socket_read](http://php.net/socket_read) and [socket_recv](http://php.net/socket_recv) don't make it clear how to detect these types of situations! For example, `socket_recv()` apparently sets the return buffer to NULL "If an error occurs, if the connection is reset, or if no data is available" (that's helpful), and `socket_read()` says that it returns FALSE "if the remote host has closed the connection", while it ***also*** says that it returns an empty string "when there is no more data to read." – i336_ Sep 09 '16 at 13:36
  • "No more data to read" and "peer has closed connection" don't sound *exactly* like the same thing to me, at least not on the surface. So thanks heaps for clearing this up. – i336_ Sep 09 '16 at 13:37
  • yes, `socket_read` and 'socket_recv' are specific to one socket and not really related to what `socket_select` actually does ;-/ They run into the `no 'natural' way of detecting 'client has gone away' issue`. So, it will be confusing information in the manual. imo, The main issue is that 'tcp/ip sockets' has rather more 'possible data states and socket connection states' than does 'file reads'. That are bi-directional as well. This makes it interesting but confusing. – Ryan Vincent Sep 09 '16 at 13:43
  • Right. I do understand the basics of sockets, I've implemented a few clients and servers in the past, although I'm trying to add in comprehensive error handling for the small codebase I'm writing now. My previous interactions with sockets on PHP used `fsockopen()`, so I was able to use `feof()` to neatly handle connection-gone-away events. This time around I'm actually using `socket_import_stream()` because I need to bind to a specific local port, which the socket_* functions can't do. – i336_ Sep 09 '16 at 13:48
  • I guess PHP's documentation isn't the best; in all the years I've used the language on-and-off, I never really read anything that made it explicitly clear that `(strlen(recv()) == 0) === disconnect` (to paraphrase). Thanks a lot for clearing that up, that'll definitely help me when I use TCP in other environments! – i336_ Sep 09 '16 at 13:50
  • Yes, It is due to having to use 'socket_select' that causes a lot of bother. It is 'clumsy' but the only way a PHP process can do ''parallel processing' of many sockets at once. – Ryan Vincent Sep 09 '16 at 13:51
  • ...I have **got** to learn to **read the manual**, and especially *all* the notes in PHP's case :) The note regarding the behavior I've been battling is [at the top of the comments for `socket_recv()`](http://php.net/manual/en/function.socket-recv.php#47182). Feel kinda stupid now - but once again the clarification (0 == peer disconnect) is massively appreciated (I don't think I would have figured it out...) – i336_ Sep 10 '16 at 00:42

1 Answers1

1

Okay, I figure I might as well turn the comments above into an answer. All credit goes to Ryan Vincent for helping my thick head figure this out :)

socket_recv will return 0 specifically if the peer has disconnected, or FALSE if any other network error has occurred.

For reference, in C, recv()'s return value is the length of the new data you've just received (which can be 0), or -1 to indicate an error condition (the value of which can be found in errno).

Using 0 to indicate an error condition (and just one arbitrary type of error condition, at that) is not standard and unique to PHP in all the wrong ways. Other network libraries don't work this way.

You need to to handle it like this.

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

}
i336_
  • 1,813
  • 1
  • 20
  • 41