PHP socket_write failure only returns error on next call

Viewed 456

I am trying to send data by PHP sockets, the client is an Arduino device, it receives the data OK when I send it multiple times, but if I reset the client (Arduino device), it reboots in a few seconds, it says it connected to the PHP socket, then when I want to send data again by socket_send() it fails silently, the PHP socket_send() is not returning an error on first actual error, only the second time I try (and fail), only then it returns error ("zero bytes sent"). When this error is received, I create another socket_accept() and successfully send the message.

What could cause this ? I want it to properly detect a lost connection so I can resend data if needed.

It feels like it sends data to an old connection and only realizes it on second try, is that possible ?

Can this be fixed by socket_select() ? I have trouble understanding what that does.

Clarification: If client restarts and connects again, then sending data to it returns int, then false, false, false (unless I unset the $accept and I do a socket_accept() again). If client remains offline, then the sending always returns int, int, int. int being the size of the string sent.

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("Could not create socket\n");

// reuse any existing open port to avoid error
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

$result = socket_bind($socket, $host, $port) or die("Could not bind to socket\n");

$result = socket_listen($socket) or die("Could not set up socket listener\n");

    
do{

    if(!isset($accept)){
        echo "\nwaiting for clients";
        $accept = @socket_accept($socket) or die("Could not accept incoming connection");
        echo "\nclient connected";
    }
    
    // memcached will return a message here like: "my message\r\n"
    $message_to_send = $memcached->get('my_socket_message');
    
    if($message_to_send!=''){

        echo "\nsending: ".$message_to_send;
    
        $total_data_sent = @socket_send($accept, $message_to_send, strlen($message_to_send), MSG_EOR);
    
        // if data was not send (sent to an old connection ?!)...
        // then clear $accept, so a new connection is accepted
        // and keep the my_socket_message variable, so message is sent again

        if($total_data_sent === false){
            echo "\nSEND FAILED, will retry message: ".$message_to_send;
            unset($accept);
        } else {
            $memcached->delete('my_socket_message');
        }
    
    }
    
} while (true);
2 Answers

I've experimented a bit and was able to reproduce the issue. Here is my solution for it (maybe not the best way, but works):

@socket_send($accept, $message_to_send, strlen($message_to_send), MSG_EOR);
$dataSent = @socket_write($accept, '', 0);

    if ($dataSent === false) {
        unset($accept);
        // ...

edit

I came up with a more neat solution. We just do what the socket_send function is supposed to do. Return false or int number of received bytes.

On the receiving end you just decode the message ($data) and send back the strlen of $data['payload']. The client can verify it has received all data like this too. To do so just compare strlen of $data['payload'] with $data['length']. If you really get paranoid you can implement some checksum aswell.

Code for Server use send instead of socket_send:

function send($client, $message) {
    $messageLength = strlen($message);

    $data = [
        'length' => $messageLength,
        'payload' => $message
    ];
    $jsonData = json_encode($data);

    try {
        @socket_send($client, $jsonData, strlen($jsonData), MSG_EOR);
        $response = @socket_read($client, 1024);

        if (intval($response) !== $messageLength) {
            return false;
        }
    } catch (Exception $e) {
        return false;
    }
    
    return $response;
}

Even though this works, I thought TCP does exactly that - ensure the data has been received. Maybe we are still missing something here.

This is what I ended up using and it seems to work nicely. It waits for a reply/confirmation from client, if no confirmation received, message_to_send is preserved, connection is cleared, on next loop it reconnects and retries to send the existent message_to_send.

$total_data_sent = @socket_send($accept, $message_to_send, strlen($message_to_send), MSG_EOR);

// set a timeout for waiting a reply, in this case 500 milliseconds
socket_set_option($accept, SOL_SOCKET, SO_RCVTIMEO, array("sec"=>0, "usec"=>500000));

// read a reply, which should be "ok" (text sent by client)
$reply = socket_read($accept, 1024);

// if reply was not "ok", then unset the connection and will reconnect on next loop
if($reply != "ok"){
    unset($accept);
}

// if reply was "ok", then clear this message as it was successfuly sent
if($reply == "ok"){
    unset($message_to_send);
}
Related