6

我一直在并排运行两个套接字客户端,收集 http 流数据(不是 Twitter,而是类似的东西)。数据以分块编码的形式出现。

其中一个客户端是 curl(在命令行上,而不是 php-curl),其中 http 和 https 都可以正常工作。另一个是我自己的 PHP 脚本,使用fsockopenfgets. 适用于 https,但我对 http 有一个特定的问题。具体到什么程度?仅当流安静 60 秒时才会发生这种情况。如果只有 50 秒的安静时间,它就可以正常工作。我一直在比较 curl 发送和接收到我的脚本的 http 标头,并删除了所有差异。我以为我知道所有关于 PHP 套接字的知识,尤其是分块编码,但现在是吃不起眼的馅饼的时候了,因为这个让我难过。

因此,使用“--trace - --trace-time”运行 curl,我看到在 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 接收到的完全相同。


版本详情

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 dict ldap ldaps http 文件 https ftps 功能:GSS-Negotiate IDN IPv6大文件 NTLM SSL 库

带有 Suhosin-Patch (cli) 的 PHP 5.3.2-1ubuntu4.18(构建时间:2012 年 9 月 12 日 19:12:47)

服务器:Apache/2.2.14 (Ubuntu)

(到目前为止,我只测试了 localhost 连接。)


循环的其余部分如下所示:

$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 秒后发生了什么变化,并学习如何阻止它发生。也许学习一个新的故障排除思路。把它想象成血腥的求知欲:-)

4

2 回答 2

1

老实说,我不确定为什么你的代码会这样,但它读取数据的方式是不正确的;我会把它改写成这样的:

$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_timeout60 秒,因此任何后续读取都应返回false. 仍然没有解释为什么它可以在安全套接字上工作;-)

于 2012-12-12T07:09:24.517 回答
1

这是错误修复:

$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 中的调试行进行比较,让我能够发现可疑行为。
于 2012-12-12T07:46:48.300 回答