Search code examples
phpsocketstcparduinoiot

PHP socket_write failure only returns error on next call


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);

Solution

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