ghedipunk / PHP-Websockets

A Websockets server written in PHP.
BSD 3-Clause "New" or "Revised" License
917 stars 375 forks source link

How to send PING? #86

Open RoxKilly opened 8 years ago

RoxKilly commented 8 years ago

A proxy between the client and my server disconnects the websocket connection if no data crosses the wire in any 100 second span.

To fix this I'm currently sending dummy data on a timed schedule, but that's a very clumsy solution.

I notice that if the browser sends a PING, the PHP-Websockets server will correctly reply with a PONG. How can I send a PING from the server? I'm willing to modify the server's PHP file to do so.

Xaraknid commented 7 years ago

instead of using dummy data use :

$this->send($user,$message,'ping');

The client will respond with a pong.

Or if you don't need or want an answer from client you can send directly a 'pong'.

inxomnyaa commented 4 years ago

@Xaraknid it's not about the message but the "pong frame"

Having the same issue here, after a while i am getting a disconnect (executed from browser) because the server appears inactive to the JS websocket. The solution would be to send a PING from PHP to the WS, as @RoxKilly already mentioned.

So far i have found no solutions online how to do that though.

Xaraknid commented 4 years ago

Ok it's been a while I worked on that and by that time I fork it and change alot of thing including the command send.

If you want the server to send a ping you can do that. You'll can do it by adding a new function inside websocket.php Clone this function.

protected` function send($user, $message) {
    if ($user->handshake) {
      $message = $this->frame($message,$user);
      $result = @socket_write($user->socket, $message, strlen($message));
    }
    else {
      // User has not yet performed their handshake.  Store for sending later.
      $holdingMessage = array('user' => $user, 'message' => $message);
      $this->heldMessages[] = $holdingMessage;
    }
  }

and modified it to that

protected` function send_ping($user, $message) {
    if ($user->handshake) {
      $message = $this->frame($message,$user,"ping");
      $result = @socket_write($user->socket, $message, strlen($message));
    }
    else {
      // DO NOTHING because User has not yet performed their handshake. 
    }
  }

After that from from your server loop up to you to use your own method to call that function at certain interval to keep connection alive.

inxomnyaa commented 4 years ago

I will give that a try, but it is weird that the server has to send the ping instead of the client

I saw a ping request happen from client->server once, and had some checks to validate if the server responds with pong - it did send it, but the client disconnected regardless.

Xaraknid commented 4 years ago

Maybe the connection close because of network issue. You are right it's not required that the server send the ping or the client the purpose of ping-pong other than to know the latency can be use to keep the connection alive at application level by sending data on it if there is a long time between transmission.

As I said above you can also directly send a pong instead of a ping and the client will receive it and not respond to that reducing data flow.

After that you need to know if it's server or client side that close the connection.

If it's server side you can add the option keep alive on the TCP that will make another layer of keeping connection alive as it'll do on TCP level ( 2 hours time frame check ). You can do that by adding SO_KEEPALIVE on the line 22 of websocket.php

socket_set_option($this->master, SO_KEEPALIVE, SOL_SOCKET, SO_REUSEADDR, 1) or die("Failed: socket_option()");

inxomnyaa commented 4 years ago

I have SO_KEEPALIVE set and i know that the client closes the connection since i got this bit of code:

            case SOCKET_ETIMEDOUT:
                return "Connection timed out";
    private function readPacket($client, ?string &$message)
    {
        $numBytes = @socket_recv($client, $message, 2048, 0);
        if ($this->stop) {
            return \false;
        } else if ($numBytes === false) {
            $sockErrNo = \socket_last_error($client);
            switch ($sockErrNo) {
                case SOCKET_ENETRESET: //Network dropped connection because of reset
                case SOCKET_ECONNABORTED: //Software caused connection abort
                case SOCKET_ECONNRESET: //Connection reset by peer
                case SOCKET_ESHUTDOWN: //Cannot send after transport endpoint shutdown -- probably more of an error on our part, if we're trying to write after the socket is closed.  Probably not a critical error, though.
                case SOCKET_ETIMEDOUT: //Connection timed out
                case SOCKET_ECONNREFUSED: //Connection refused -- We shouldn't see this one, since we're listening... Still not a critical error.
                case SOCKET_EHOSTDOWN: //Host is down -- Again, we shouldn't see this, and again, not critical because it's just one connection and we still want to listen to/for others.
                case SOCKET_EHOSTUNREACH: //No route to host
                case SOCKET_EREMOTEIO: //Remote I/O error -- Their hard drive just blew up.
                case 125: //ECANCELED - Operation canceled
                    $emsg = "Unusual disconnect $sockErrNo on socket $client (" . socket_strerror($sockErrNo) . " - " . $this->getSocketEString($sockErrNo) . ")";
                    $this->logger->error($emsg);
                    $this->disconnectClient($client, $emsg); // disconnect before clearing error, in case someone with their own implementation wants to check for error conditions on the socket.
                    break;
                default:
                    $this->logger->error('Socket error: ' . socket_strerror($sockErrNo));
            }

So the message logged was

Unusual disconnect (SOCKET_ETIMEDOUT number) ...

Xaraknid commented 4 years ago

Indeed it happens at TCP level and each browser have their own default settings for TIMEOUT preventing keeping ghost connection.

So the only solution is to keep sending stuff. You can do it on client side with a ping and server respong pong or the server sending ping or pong directly with pong the client didn't have to respond to it.