Textalk / websocket-php

WebSocket client and server in PHP
Other
922 stars 253 forks source link

Empty read; connection dead? Stream state: {"timed_out":true,"blocked":true,"eof":false,"stream_type":"tcp_socket\/ssl","mode":"r+","unread_bytes":0,"seekable":false} #64

Closed ghost closed 4 years ago

ghost commented 4 years ago

$ws_url = config(ENVIRONMENT . '.page_ws_url'); $str = '{"req":"market.' . $symbol . '.kline.1min","id":' . microtime(true) . ',"from":' . time() . ',"to":' . time() . '}' . "\r\n"; try { $client = new \WebSocket\Client($ws_url); $client->send($str); $data = $client->receive(); $list = json_decode(gzdecode($data), true); if (isset($list['status']) && ($list['status'] === 'ok')) { $end = end($list['data']); $newprice = $end['close']; } } catch (RequestException $e) { if ($e->hasResponse()) { $mark = $e->getResponse()->getBody()->getContents(); } else { $mark = $e->getMessage(); } Log::info($ws_url); Log::info($mark); }

[0] ConnectionException in Base.php line 269 Empty read; connection dead? Stream state: {"timed_out":true,"blocked":true,"eof":false,"stream_type":"tcp_socket\/ssl","mode":"r+","unread_bytes":0,"seekable":false}

tianjin500w commented 4 years ago

Have you solved it?How?

Filipowicz251 commented 4 years ago

I have the same issue. Anybody knows the solution?

makashtekar commented 4 years ago

Same issue here

Severity: error --> Exception: Empty read; connection dead?  Stream state: {"timed_out":true,"blocked":true,"eof":false,"stream_type":"tcp_socket\/ssl","mode":"r+","unread_bytes":0,"seekable":false} application/vendor/textalk/websocket/lib/Base.php 269
mtrosin commented 4 years ago

Hello guys, in my case I had to remove the "->close()", because somehow the connection was already closing automatically, then I tried to close something already closed...

CatoMaior commented 4 years ago

Hello guys, in my case I had to remove the "->close()", because somehow the connection was already closing automatically, then I tried to close something already closed...

I am having the same problem, where did you remove the "->close()" from?

VladAndIt commented 4 years ago

The same problems guys. I have an empty read on my PHP script. But with another site, it's working (example: echo.websocket.org). Any suggestings?

Gitchenjiangbin commented 4 years ago

我的处理方法是注释掉 $client->receive(); 因为我这边不需要他返回的数据 如果需要的话,建议仔websocket server 的 onClose 里面做处理,不断掉当前的fd 希望的我处境,对你们有帮助。谢谢。

sirn-se commented 4 years ago

If someone can post a code snippet that re-produces this issue, I can have a look at it.

sirn-se commented 4 years ago

The error implies that the servers has closed connection because of timeout settings. As the code is written, it is expected to throw exception on failed send/receive operations. The current solution is to wrap operations in a try/catch clause, and have the client reconnect and repeat the failed operation.

babipanghang commented 4 years ago

For me, this piece of code (which subscribes to a couple of streams containing live crypto trading information and then prints incoming data) inevitably triggers this error, usually in less than a minute. I've tried with another websocket client which does not appear to have similar disconnects. Anyway, here goes:

    require('vendor/autoload.php');

    use WebSocket\Client;

    define('TRADES', 'live_trades_');
    define('ORDERS', 'live_orders_');

    $tps = [
        "btcusd", "btceur", "eurusd", "xrpusd", "xrpeur", 
        "xrpbtc", "ltcusd", "ltceur", "ltcbtc", "ethusd", 
        "etheur", "ethbtc", "bchusd", "bcheur", "bchbtc"
    ];

    function getSubscribeMessage(string $collectionType, string $tradingPair):string{
        $json_message = '{
            "event": "bts:subscribe",
            "data": {
                "channel": "%s%s"
            }
        }';
        return sprintf($json_message, $collectionType, $tradingPair);
    } 

    $ws = new Client('wss://ws.bitstamp.net/');
    foreach($tps as $tp){
        $ws->send(getSubscribeMessage(TRADES, $tp));
    }         

    while (true) {
        printf("%s\n\n", $ws->receive());
    }
sirn-se commented 4 years ago

@babipanghang

A stream can be closed at any given time, in this case by server time out settings. So it's all about how the client manages streams. I suppose that many clients silently reconnects when encountering a closed stream, but this code doesn't.

Possibly we can add a method or option for continuously reading the stream, handling stream closing, but for now you can achieve the same effect by putting the receive operation within a try/catch clause;

    while (true) {
        try {
            printf("%s\n\n", $ws->receive());
        } catch (\WebSocket\ConnectionException $e) {
            ;
        }
    }
babipanghang commented 4 years ago

@sirn-se Your solution doesn't appear to solve anything. After the first execption gets caught, it just causes the client to keep throwing the same exception over and over, while apparently not receiving any more data. So in my mind, there's one of two scenarios going on here.

  1. One is the client somehow assumes the server closed the connection, and because it thinks that happened, it just keeps throwing the same exception without actually trying to fetch new data.
  2. The server actually did close the connection.

In the second scenario, i have a theory. According to specifications, a ping system may be used to keep the connection alive. Does the client automatically send a proper response to pings received from the server? Does it send pings every now and then to the server itself? If not, can we manually intercept/construct/send a ping message?

babipanghang commented 4 years ago

After some debugging I found there were 2 issues actually going on. First, a short default timeout period (5 seconds). This was easily fixed by doing $ws = new Client('wss://ws.bitstamp.net/', ['timeout' => 60]);

Second, as guessed in my previous message, it appears the client does indeed not send a proper response to pings. Adding the following to receive_fragment() in Base.php (somewhere below the point where $opcode_int is generated) appears to fix the issue:

    if (9 == $opcode_int){
      $this->send('pong', 'pong');
    }
sirn-se commented 4 years ago

@babipanghang

The lacking ping-pong is fixed and scheduled for v1.3. It's already merged to master, if you want to try it out.

sirn-se commented 4 years ago

Ping/pong fixed in v.1.3. Also added documentation on how to continuously listen to a connection.

Will consider a listen() convenience method for future release.

Logioniz commented 4 years ago

I have the same problem.

In my case reconnect is not solution because after reconnect the context is lost and for server it is new connection (reading in the loop is interrupted by timeout and I get this error, because for new connections the server does not respond to the request from the previous connection). Need to repeat some operation to repair context.

So, it’s important for me to be able to understand when a close event or disconnection occurred. It seems that close event is final state when reading (from this commit bbf589059f9c1). It seems that 1.3.0 is just released, good. Thanks.

But what about disconnection whithout explicitly closing the connection?

Logioniz commented 4 years ago

It seems that in such keys we get ConnectionException with ["timed_out":false,"blocked":true,"eof":true ... ]

sirn-se commented 4 years ago

The opcode: close frame concerns closing connection in an orderly fashion, enabling parts to adjust to changed state. The underlying connection should not be closed until corresponding part has acknowledged the close frame.

The underlying socket-stream might also close on a lower level. Server shutdown, of course. But sockets also have a time out, which allows servers to "garbage collect" inactive sockets.

Both of these cases will result in a re-connect if any send/receive function is called after connection has closed. The second case will throw exception, as the code expects an open connection, but there isn't. v1.3 decreases the risk, as it always checks underlying connection before sending/receiving, but it's never a 100% guarantee.

Not sure what you mean by "final state". The $final in the code signifies it is the final frame in a fragmented message.

For continuous connections, there are two strategies. Both of them require some code on top of the functionality provided by this library;

babipanghang commented 4 years ago

There is still a bug with ping/pong in v1.3. The code for sending ping in response to pong is now

        if ($opcode === 'ping') {
            $this->send($payload, 'pong', true);
        }

Without being actually sure, it seems possible that $payload actually remains empty (why would a ping frame have a payload?). This causes the pong frame not to be sent, because (in Base.php, send() method):

        $fragment_cursor = 0;
        // while we have data to send
        while ($payload_length > $fragment_cursor) {

Since $payload_length and $fragment_cursor are actually both 0, the condition evaluates to false and no data is sent.

Logioniz commented 4 years ago

Yes, according to rfc application data may be omitted. Thanks. I create #82 that fix this problem.

Logioniz commented 4 years ago

But i see another problems in project.

The first problem is reconnect if connection lost

    public function send($payload, $opcode = 'text', $masked = true)
    {
        if (!$this->isConnected()) {
            $this->connect();
        }
    public function receive()
    {
        if (!$this->isConnected()) {
            $this->connect();
        }

I think when connection is lost then do not need to do reconnect because of lack of previous context. So, its totally wrong. I think the client code should decide when to reconnect and what additional steps should be performed in this case. So, need to add additional connect method to explicitly perform connect.

The second problem is stopping writing to socket when written less than the length of the sending message.

    protected function write($data)
    {
        $written = fwrite($this->socket, $data);

        if ($written < strlen($data)) {
            throw new ConnectionException(
                "Could only write $written out of " . strlen($data) . " bytes."
            );
        }
    }

I sometime have this error (may be when conneciton is lost, but I'm not sure what actually case). I found this note in php doc of fwrite function and it seems that need to try write whole message instead of part. It seems that may be written 0 bytes when perform fwrite (i know that this links from another php func).

mxr576 commented 4 years ago
fwrite(): send of 106 bytes failed with errno=32 Broken pipe

/mnt/files/local_mount/modules/FOO/build/vendor/textalk/websocket/lib/Base.php:274
/mnt/files/local_mount/modules/FOO/build/vendor/textalk/websocket/lib/Base.php:138
/mnt/files/local_mount/modules/FOO/build/vendor/textalk/websocket/lib/Base.php:80
/mnt/files/local_mount/modules/FOO/build/vendor/dmore/chrome-mink-driver/src/DevToolsConnection.php:71

Indeed, there is still some issue with the fwrite() part, but the patch in #82 seems to solved other issues.

mxr576 commented 4 years ago

Experimented with this fwrite_stream() trick mentioned in the note of fwrite() but it does not seem to solve the issue, continuing with socket_write().

mxr576 commented 4 years ago

I think when connection is lost then do not need to do reconnect because of lack of previous context. So, its totally wrong.

In addition, the connect() method is only defined in Client and not in Base class.

Logioniz commented 4 years ago

Yes, i experemented too. It seems that fwrite value is always not false even when the connection is closed. The only way to determine if the connection is alive is to make fread or send very big message.

So, if fwrite is always return false then we can not use code from this note.

But socket_write works as expected, it return false when connection is dead. So, it seems that need to use socket_create/socket_read/socket_write instead of stream_socket_create/fread/fwrite.

fwrite(): send of 106 bytes failed with errno=32 Broken pipe

This error occured when connection is broken/closed (you send big message when connection is closed).

sirn-se commented 4 years ago

@mxr576 The missing Server.connect() is fixed in v1.3.

As of reconnecting, the auto reconnect do include headers, auth and stream-context specified in the constructor. Any session context or equivalent need to be handled by implementing code.

That said, I do believe the library should separate low level I/O and convenience methods. It's a bit mixed as of now. That would be a breaking change though, which means 2.0 candidate.

Possibly, we could make the connect() method public and add reconnect as an option, set to true as default. Doing so would enable implementing code to handle connect() itself, without breaking implementations that rely on auto-reconnect.

Logioniz commented 4 years ago

I thought a lot about what to do, and i had some vision for this situation.

First I focus on the next moment: when the connection is broken, the only way to find out that the connection is interrupted is to read from socket (sometimes sending to a socket, but not always).

When the connection is broken (there was no websocket close request), such code

  if (!$this->isConnected()) {
      $this->connect();
  }

does not essentially lead to a reconnection and i consider it correct.

So, when applicaiton get exception {"timed_out":true,"eof":false ... } it means that server doesn't reply for some amount of time for some reasons and application code need correctly handle this error.

When application get exception {"timed_out":false,"eof":true ...} it means that connection closed without websocket close message. When application get exception Could only write ... out of .. it means that connection closed without websocket close message.

So, i think that this topic need to close. It seems all works correctly and need correctly handle errors in application code. The only way when reconnection happens is when socket receive a close packet so its correct too.

Some improvements I can offer:

  1. Correctly send message Wrote above about it. Need to add this logic. But it dodn't work as expected because in my tests fwrite always return NOT false. So need to replace stream_socket_create/fread/fwrite by socket_create/socket_read/socket_write.

  2. It is not convenient to get the connection status at the time of the error. https://github.com/Textalk/websocket-php/blob/master/lib/Base.php#L309 It would be nice to get it in some method.

What about this pull request https://github.com/Textalk/websocket-php/pull/82? Do you doubt accept it or not? We can discuss this moment here.

mxr576 commented 4 years ago

It seems all works correctly and need correctly handle errors in application code.

Can we discuss what is the expected behavior for certain scenarios in an application level? I am trying to fix an issue in this library: https://gitlab.com/DMore/chrome-mink-driver/-/merge_requests/85/diffs?diff_id=93733985

mxr576 commented 4 years ago

Hm, maybe I finally managed to solve it based on the detailed summary provided by @Logioniz, kudos! https://gitlab.com/DMore/chrome-mink-driver/-/merge_requests/85/diffs?diff_id=94001321

Logioniz commented 4 years ago

Yes, sure. I will use pseudo code close to php. Need to use minimum 1.3 version. I will give an example code that takes into account timeout, broken connections and other websocket frames

func connect($url)
{
  ...
  $this->clinet = new Client($url, ['timeout' => 5]);
}

func send($command)
{
  ...
  $this->client->send($payload);
  $data = $this->waitFor(function ($data) use (&$payload) {
    return $payload['id'] == $data[id];
  }, 20);
}

func waitFor($is_ready, $operation_timeout)
{
  $endTime = microtime(true) + $operation_timeout;

  while (microtime(true) < $endTime) {
    try {
      $respose = $this->client->receive();
    } catch (ConnectionException $e) {

      $pos = strpos($e->getMessage(), '{');
      if ($pos !== false) {
        $state = json_decode(substr($e->message, $pos), true);
        if ($state['eof']) {
          // connection is closing unexpectedly and you not get answer for your send in new connection becuase lack of context in new connection
          // options what to do:
          // 1. repeat last send command
          // 2. repeat whole procedure
          // 3. throw exception
          // I don't know what vaiant is best
          // but keep in mind that your context is lost and your state is lost
        } else if ($state['time_out']) {
          // if timeout then need to check on operation_timeout and wait a little longer
          continue;
        }
      }
    }

    $opcode = $this->client->getLastOpcode();
    if ($opcode === 'close') {
        // connection is closing and you not get answer for your send in new connection becuase lack of context
        // options what to do:
        // 1. repeat last send command
        // 2. repeat whole procedure
        // 3. throw exception
        // I don't know what vaiant is best
        // but keep in mind that your context is lost and your state is lost
    }

    // exists ping/pong requests, need to skip
    if (!in_array($opcode, ['text', 'binary']))
      continue;

    ...
    // all code below to process response and check is_ready
  }

}
sirn-se commented 4 years ago

@Logioniz The PR looks fine to me. I'll merge and schedule for v1.3.1 release.

The socket_create/socket_read/socket_write functions require the sockets compile time extensions, and is not compatible with some other functions used. So I think we should avoid them if possible.

Also, I do see the need to check stream state in application code, but extracting from embedded json seem … well. Maybe we should make it more accessible? Either by;

  1. Set ConnectionException.code to a constant that specifies cause
  2. Set the entire stream state as own properties on ConnectionException
  3. Wrap stream_get_meta_data in a public method in Base.php
Logioniz commented 4 years ago

@sirn-se it would be great. I don't know which variant is more convenient.

mxr576 commented 4 years ago

Tried to apply suggestion from https://github.com/Textalk/websocket-php/issues/64#issuecomment-638755354 but the code is still failing with the fwrite() error when $this->client->send($payload); is called, so waitFor() is not even called... I think some further adjustments are still needed in this lib because the auto-reconnect is not working. (Or I still misunderstand something.)

sirn-se commented 4 years ago

@mxr576 Your code need to catch any error in the send() operations, just as with the receive() operation in your linked PR. It will re-connect when you make another send/receive, but the error will still be there and need to be handled.

sirn-se commented 4 years ago

https://github.com/Textalk/websocket-php/pull/83 adds error code to exceptions in read() and write(). Constants in ConnectionException class;

    const TIMED_OUT = 1024;
    const EOF = 1025;
    const BAD_OPCODE = 1026;

Example code above can then be simplified;

    try {
      $respose = $this->client->receive();
    } catch (ConnectionException $e) {
      $code = $e->getCode();
      switch ($code) {
        case ConnectionException::TIMED_OUT:
          // Do things
          break;
        case ConnectionException::EOF:
          // Do things
          break;
        default:
          throw $e;
      }
    }
sirn-se commented 4 years ago

Version 1.3.1 with fixes referred to in this issue is released.

As this issue refers to a number of problems, including things fixed in 1.3 versions, I'm closing it. If there are still problems with 1.3, they should be reported in a new issue.