在sae,fetchurl是所有http請求的出口(正向代理),fetchurl基於nginx開發,以下稱做nginx。這次碰到的問題是這樣的,某個uri,假設為xuri,使用nginx抓取(使用curl請求nginx抓取xuri),返回了部分數據,然後超時。使用curl直接抓取,總是正常。curl行,nginx不行,則可能是nginx的問題。把該xuri對應的資源放到測試的apache上,記做yuri,則nginx可以正常抓取。這樣,又可能不是nginx的問題,而是xuri所在服務器的問題,但是卻解釋不了為什麼curl直接抓取行。為了還nginx一個清白,我拿perl模擬nginx(主要是模擬請求的http header)抓取xuri,xuri抓取正常。為了還xuri服務器一個清白,我拿perl模擬xuri服務器(主要是模擬響應的http header),nginx抓取正常。既然都清白,為什麼nginx抓取xuri不行,一定是這中間的模擬邏輯有問題,但是我找不出原因。我認為http協議的所有語義都在請求header和響應header中,而這兩者我都模擬了。
苦悶中,想起nginx的一個參數proxy_buffering,它的含義是:nginx不是立即轉發收到的請求,而是等buffer滿之後再轉發。默認是on。把它改成off,使用curl請求nginx抓取xuri就行了,數據是完整的,沒有超時。我找到問題的所在了,我把它重新設為off,並且修改perl模擬xuri服務器的邏輯,發送完xuri的內容後,不是立即關閉連接,而是sleep。這下模擬出來了,nginx抓取失敗。
由此推測,整個事情是這樣的。nginx抓取時,已經讀取了所有的xuri的內容,因為proxy_buffering on的緣故,它沒有立即把抓取的內容發送給客戶端(這裡是curl),它在等待xuri服務器關閉連接,注意,此時它已經讀到了完整的數據,但是很遺憾,xuri服務器沒有關閉連接,它一直等到了超時,xuri服務器都沒有關閉,此時nginx關閉了和客戶端的連接,丟棄了buffer中的數據。修改proxy_buffering為off為什麼就可以了呢?這要從curl的實現說起,curl通過http響應頭Content-Length,知道http響應體的字節數,讀夠了這些字節數後,curl主動斷開了連接。在這裡因為nginx不緩存了,所以curl讀取到了完整的數據,它斷開了和nginx的連接,所以看起來既沒有超時,又讀取了完整的數據。curl能直接正常抓取xuri也是因為如此。另外,nginx可以正常抓取位於apache的yuri,是因為apache在發送完yuri對應的資源後,關閉了和nginx的連接。
原因找到了,解決有兩個辦法,一是關閉nginx的proxy_buffering,二是xuri服務器發送完數據,立即關閉連接。nginx和xuri服務器通信用的是http/1.0,不知道協議是怎麼規定的,是xuri服務器發送完數據立即關閉連接,還是客戶端(這裡是curl)根據Content-Length判斷數據發送結束,主動關閉連接,還是客戶端(這裡是nginx)不管Content-Length,根據服務器端關閉了連接,判斷傳輸結束,然後關閉連接。不管標準如何,事實如此,也只能相機行事。
附perl的模擬代碼,模擬失敗的原因是,準確模擬了傳輸的字節流,卻沒有準確模擬客戶端/服務端對收到/發送字節流後的處理。
客戶端,準確的模擬了nginx發送http頭,卻沒有準確模擬nginx處理http響應,這裡模擬的是curl如何處理響應。準確模擬應該是阻塞在recv,直到服務器端斷開連接,而不是根據Content-Length主動斷開連接。
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
my $socket = IO::Socket::INET->new(PeerAddr => "t0.qlogo.cn",
PeerPort => 80,
Proto => "tcp")
or die "can't connect to host";
my $request = "GET /mbloghead/87aa5f74cd34e94596a0/100 HTTP/1.0\r\nHost: t0.qlogo.cn\r\nUser-Agent: SAE/fetchurl-omy123xojx\r\nConnection: close\r\nAccept: */*\r\n\r\n";
$socket->send($request, 0);
my $response;
$socket->recv($response, 9000, 0);
my ($header, $body) = split /\r\n\r\n/, $response, 2;
if ($header =~ /Content-Length:\s*(\d+)/) {
my $len = int($1);
$len -= length($body);
while ($len > 0) {
$socket->recv($response, 9000, 0);
$len -= length($response);
$body .= $response;
}
print $body;
}
模擬xuri服務端,它準確的模擬了yuri所在的apache,卻沒有準確模擬xuri所在的服務器(是hphttp)。準確模擬應該是在$client->send($c, 0),之後調用sleep。
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket::INET;
open(IN, "<x.jpg");
my ($c, $off) = ("", 0);
while (my $n = read(IN, $c, 1024, $off)) {
$off += $n;
}
close(IN);
my $size = int(-s "x.jpg");
my $server = IO::Socket::INET->new(Listen => 10,
LocalPort => 8080,
Proto => "tcp",
ReuseAddr => 1)
or die "can't listen at 8080";
while (my $client = $server->accept()) {
my $request;
$client->recv($request, 1024, 0);
my $response = "HTTP/1.0 200 OK\r\nContent-Length: $size\r\nConnection:Close\r\n\r\n";
$client->send($response, 0);
$client->send($c, 0);
$client->close();
}