tinyredisclient icon indicating copy to clipboard operation
tinyredisclient copied to clipboard

fread fails to return full string (but fgets works)

Open ghost opened this issue 8 years ago • 2 comments

Data is php serialized string. EG 'a:6:{s:7:"success";b:1;s:5:"total";s:1:"8";s:4:"data";a:8:{i:0;a:17..." etc

Data is 7169 bytes long. Returned as "RESP Bulk String"."

$line = fread($this->getSocket(), $result + 2); // FAILS TO RETURN 7171 bytes

Ask to read 7171 but get back ony 2888 bytes. So php unserialize fails.

Verify no timeout or blocking:

017-12-17 17:10:37
'META'
array (
  'stream_type' => 'tcp_socket/ssl',
  'mode' => 'r+',
  'unread_bytes' => 0,
  'seekable' => false,
  'timed_out' => false,             OK
  'blocked' => true,                OK
  'eof' => false,
)

// This works. I get the full string (7169 bytes) $line = fgets($this->getSocket());

So now I think this is ok provided I only use it for serialized string (never contain CRLF).

But I dunno why this is necessary.

ghost avatar Dec 17 '17 17:12 ghost

If value is JSON-encoded string, fails too (((

aleksraiden avatar Mar 17 '18 09:03 aleksraiden

In case you need to deal with CRLF in the returned data, here's an alternative:

Change line 56 and 57 from

                $line = fread($this->getSocket(), $result + 2);
                $result = substr($line, 0, strlen($line) - 2);

To

                $line="";
                for($i=0;$i<1000 && $result > 0;$i++) {
                    $chunk = fread($this->getSocket(), $result+2);
                    $result -= strlen($chunk);
                    $line .= $chunk;
                }
                $result = substr($line, 0, strlen($line) - 2);

From the php documentation for read:

if the stream is read buffered and it does not represent a plain file, at most one read of up to a number of bytes equal to the chunk size (usually 8192) is made; depending on the previously buffered data, the size of the returned data may be larger than the chunk size.

To see how the patch is working:


class TinyRedisClient
{
    /** @var string */
    private $server;
    /** @var resource */
    private $socket;

    public function __construct($server = 'localhost:6379')
    {
        $this->server = $server;
    }

    public function __call($method, array $args)
    {
        array_unshift($args, $method);
        $cmd = '*' . count($args) . "\r\n";
        foreach ($args as $item) {
            $cmd .= '$' . strlen($item) . "\r\n" . $item . "\r\n";
        }
        fwrite($this->getSocket(), $cmd);

        return $this->parseResponse();
    }

    private function getSocket()
    {
        return $this->socket
            ? $this->socket
            : ($this->socket = stream_socket_client($this->server));
    }

    private function parseResponse()
    {
        $line = fgets($this->getSocket());
        list($type, $result) = array($line[0], substr($line, 1, strlen($line) - 3));
        if ($type == '-') { // error message
            throw new Exception($result);
        } elseif ($type == '$') { // bulk reply
            if ($result == -1) {
                $result = null;
            } else {
                $line="";
                for($i=0;$i<1000 && $result > 0;$i++) {
                    $chunk = fread($this->getSocket(), $result+2);
                    $result -= strlen($chunk);
                    $line .= $chunk;
                    echo "Chunk $i is: $chunk \n";
                    echo "Have $result bytes left to fetch \n";
                    ob_flush();
                }

                $result = substr($line, 0, strlen($line) - 2);
            }
        } elseif ($type == '*') { // multi-bulk reply
            $count = ( int ) $result;
            for ($i = 0, $result = array(); $i < $count; $i++) {
                $result[] = $this->parseResponse();
            }
        }

        return $result;
    }
}

aaronkuchma avatar Jan 11 '24 00:01 aaronkuchma