Потеря 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 секунд, и узнать, как это остановить. И, возможно, узнать новую идею по устранению неполадок. Думайте об этом как кровожадное интеллектуальное любопытство:-)
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 байт.
Советы по устранению неполадок:
- Добавьте в код:
echo "**".date("Y-m-d H:i:s");print_r(stream_get_meta_data($fp));ob_flush();flush();
В метаданных есть запись, указывающая, когда у последнего действия потока был тайм-аут. Метка даты имеет важное значение. - Перекрестная ссылка с
tcpdump -A -i lo port http
из командной строки. Сравнение временных меток с метками из строк отладки в PHP позволило мне обнаружить подозрительное поведение.
Честно говоря, я не совсем уверен, почему ваш код ведет себя таким образом, но способ, которым он считывает данные, неверен; Я бы переписал его примерно так:
$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
. Все еще не объясняет, почему это будет работать на защищенных сокетах;-)