Потеря 6 байт, но только если сокет замолчит на 60 секунд?


Я запускал два клиента сокетов бок о бок, собирая потоковые данные http (не Twitter, но что-то подобное). Данные поступают в фрагментированном кодировании.

Одним из клиентов является curl (в командной строке, а не php-curl), где отлично работают как http, так и https. Другой - мой собственный PHP-скрипт, использующий fsockopen и fgets. Отлично работает для https, но у меня есть определенная проблема с http. Насколько конкретно? Это происходит только в том случае, если поток затихает на 60 секунд. Если есть только 50 секунд тишины - все работает нормально. Я сравнивал отправленные и полученные http-заголовки curl с моим сценарием и удалил все различия. Я думал, что знаю все, что нужно знать о сокетах PHP, и особенно о фрагментированном кодировании, но пришло время съесть скромный пирог, так как это поставило меня в тупик.

Итак, запустив curl с "--trace - --trace-time", я вижу, что это происходит для первого пакета после 60-секундного периода тишины:

05:56:57.025023 <= Recv data, 136 bytes (0x88)
0000: 38 32 0d 0a 7b 22 64 61 74 61 66 65 65 64 22 3a 82..{"datafeed":
0010: 22 64 65 6d 6f 2e 31 64 36 2e 31 6d 2e 72 61 6e "demo.1d6.1m.ran
...
0080: 34 22 7d 5d 7d 0a 0d 0a                         4"}]}...

82 является шестнадцатеричным для размера куска. То \r\n отмечает конец строки размера фрагмента. Фрагмент начинается с "{".

На стороне PHP мой цикл начинается так:

while(true){
  if(feof($fp)){fclose($fp);return "Remote server has closed\n";}
  $chunk_info=trim(fgets($fp)); //First line is hex digits giving us the length
  $len=hexdec($chunk_info);   //$len includes the \r\n at the end of the chunk (despite what wikipedia says)

С https или с интервалом менее 60 секунд это работает нормально, $len равен 100 или независимо от размера блока. Но после этого 60-секундного промежутка я получаю в $chunk_info следующее:

datafeed":"demo.1d6.1m.ran...

Итак, я, кажется, потерял первые шесть байтов: 38 32 0d 0a 7b 22

Все последующие фрагменты в порядке и точно такие же, как и curl получение.


Сведения о версии

Завиток 7.19.7 (x86_64-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15 Протоколы: tftp ftp telnet дикт ldap ldaps http файл https ftps Особенности: GSS-Согласование IDN IPv6 большого файла NTLM SSL libz

PHP 5.3.2-1ubuntu4.18 с Suhosin-патчем (cli) (построен: сентябрь 12 2012 19:12:47)

Сервер: Apache/2.2.14 (Ubuntu)

(До сих пор я тестировал только с подключениями к локальному хосту.)


В остальная часть цикла выглядит так:

$s='';
$len+=2;    //For the \r\n at the end of the chunk
while(!feof($fp)){
    $s.=fread($fp,$len-strlen($s));
    if(strlen($s)>=$len)break;  //TODO: Can never be >$len, only ==$len??
    }
$s=substr($s,0,-2);
if(!$s)continue;
$d=json_decode($s);
//Do something with $d here
}

(В стороне: в том виде, в котором я тестировал до сих пор, код прошел через этот цикл ровно один раз, до 60-секундного периода тишины.)


ПРИМЕЧАНИЕ: У меня есть множество обходных путей, чтобы заставить все работать: например, принудительно использовать https или использовать curl-php. Этот вопрос задан потому, что я хочу знать, что происходит, знать, что меняется через 60 секунд, и узнать, как это остановить. И, возможно, узнать новую идею по устранению неполадок. Думайте об этом как кровожадное интеллектуальное любопытство:-)

Author: Darren Cook, 2012-12-12

2 answers

Вот исправление ошибки:

$chunk_info=trim(fgets($fp)); //First line is hex digits giving us the length
if($chunk_info=='')continue; //Usually means the default 60 second time-out on fgets() was reached.
...

Если fgets($fp) возвращает что-то, то вам нужно прочитать фрагмент. Если это что-то равно нулю, то у вас есть пустой кусок для обработки. Но когда он возвращает ничего, это означает, что время ожидания fgets истекло. Похоже, что время ожидания по умолчанию для tcp:// составляет 60 секунд; в то время как время ожидания по умолчанию для ssl:// больше (извините, я еще не выяснил, что это такое - возможно, оно блокируется навсегда).

Пытаясь обработать фрагмент, когда читать было нечего, все вышло из-под контроля. Отсюда и украденные 6 байт.

Советы по устранению неполадок:

  1. Добавьте в код: echo "**".date("Y-m-d H:i:s");print_r(stream_get_meta_data($fp));ob_flush();flush(); В метаданных есть запись, указывающая, когда у последнего действия потока был тайм-аут. Метка даты имеет важное значение.
  2. Перекрестная ссылка с tcpdump -A -i lo port http из командной строки. Сравнение временных меток с метками из строк отладки в PHP позволило мне обнаружить подозрительное поведение.
 1
Author: Darren Cook, 2012-12-12 07:46:48

Честно говоря, я не совсем уверен, почему ваш код ведет себя таким образом, но способ, которым он считывает данные, неверен; Я бы переписал его примерно так:

$chunkmeta = fgets($f);
// chunked transfer can have extensions (indicated by semi-colon)
// see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
$chunklen = hexdec(current(explode(';', $chunkmeta, 2)));

// chunk length is non-zero
if ($chunklen) {
  $chunk = '';
  // only read up to chunk length
  while (!feof($f) && $chunklen) {
      if ($buf = fread($f, $chunklen)) {
          $chunklen -= strlen($buf);
          $chunk .= $buf;
      } else {
          // uh oh, something bad happened
          die("Could not read chunk");
      }
  }
  if ($chunklen == 0) {
      // read the trailing CRLF
      fread($f, 2); // read CRLF
      // data is ready
  }
}

Обновление

Должен был пойти на это с моим внутренним чутьем (хотя приведенный выше код должен работать просто отлично в любом случае); default_socket_timeout по умолчанию значение равно 60 секундам, поэтому любые последующие чтения должны возвращать false. Все еще не объясняет, почему это будет работать на защищенных сокетах;-)

 1
Author: Ja͢ck, 2012-12-12 08:09:07