为什么Python socket在HTTP socket的recv循环中不接收最终的空值?
我用Python 3写了一个小型的代理模块,目的是让它在我的浏览器和网络之间转发数据。我的目标只是简单地代理来回的数据流。这个程序的一个功能是把我收到的网站响应保存在本地的一个文件夹里。
一切都按我预期的方式运行,唯一的问题是,在循环中使用socket.recv()
似乎从来不会得到文档中提到的空bytes
对象。几乎所有的在线示例都提到,当服务器关闭连接时,套接字会传来一个空字符串。
我猜测可能是因为使用了keep-alive头,远程服务器在自己的超时阈值到达之前不会关闭套接字。这种理解对吗?如果是这样,我该如何检测数据发送完毕呢?由于TCP的工作方式,仅仅观察接收到的数据小于我声明的块大小并不奏效。
为了演示,以下代码在谷歌的网络服务器上打开一个图像文件的套接字。我从浏览器的请求中复制了实际的请求字符串。运行这段代码(记得是Python 3!)显示接收到的二进制图像数据是完整的,但代码从来无法执行到break
语句。只有当服务器关闭套接字(大约在3分钟的空闲时间后)时,这段代码才会执行到文件末尾的print
命令。
那我该怎么解决这个问题呢?我的目标是不修改浏览器请求的行为——我不想设置keep-alive
头为false
或者其他复杂的东西。难道解决办法就是使用一些奇怪的超时设置(通过socket.settimeout()
)?这听起来有点可笑,但我不知道还有什么其他办法。
提前谢谢你们。
import socket
remote_host = 'www.google.com'
remote_port = 80
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((remote_host, remote_port))
remote_socket.sendall(b'GET http://www.google.com/images/logos/ps_logo2a_cp.png HTTP/1.1\r\nHost: www.google.com\r\nCache-Control: max-age=0\r\nPragma: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\n\r\n')
content = b''
while True:
msg = remote_socket.recv(1024)
if not msg:
break
print(msg)
content += msg
print("DONE: %d" % len(content))
4 个回答
当一个TCP连接关闭时,它会发送一个空的最终消息,表示这个连接已经关闭。收到这个消息后,你也应该在自己这边关闭这个连接。
有一种很简单的方法可以让服务器关闭连接,那就是在你的HTTP请求中加上这个头信息:
Connection: close
默认情况下,HTTP/1.1的服务器可以保持连接打开,这样你就可以发第二个请求。不过,你还是应该设置一个超时时间,以防服务器忽略这个头信息,导致你没有可用的连接。
如果你有一个保持连接的方式(叫做keep-alive连接),那么在响应的头部会有一些信息来告诉你消息的长度。你可以查看一下HTTP消息的相关内容。你需要用recv
这个方法来接收数据,直到你拿到完整的头部(头部以一个空行结束),然后确定消息主体的长度,最后准确地读取那么多数据。
下面是一个简单的类,用来缓冲TCP读取,直到读取到消息结束符或者特定数量的字节。我把它加到了你的例子里:
import socket
import re
class MessageError(Exception): pass
class MessageReader(object):
def __init__(self,sock):
self.sock = sock
self.buffer = b''
def get_until(self,what):
while what not in self.buffer:
if not self._fill():
return b''
offset = self.buffer.find(what) + len(what)
data,self.buffer = self.buffer[:offset],self.buffer[offset:]
return data
def get_bytes(self,size):
while len(self.buffer) < size:
if not self._fill():
return b''
data,self.buffer = self.buffer[:size],self.buffer[size:]
return data
def _fill(self):
data = self.sock.recv(1024)
if not data:
if self.buffer:
raise MessageError('socket closed with incomplete message')
return False
self.buffer += data
return True
remote_host = 'www.google.com'
remote_port = 80
remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote_socket.connect((remote_host, remote_port))
remote_socket.sendall(b'GET http://www.google.com/images/logos/ps_logo2a_cp.png HTTP/1.1\r\nHost: www.google.com\r\nCache-Control: max-age=0\r\nPragma: no-cache\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.794.0 Safari/535.1\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Encoding: gzip,deflate,sdch\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3\r\n\r\n')
mr = MessageReader(remote_socket)
header = mr.get_until(b'\r\n\r\n')
print(header.decode('ascii'))
m = re.search(b'Content-Length: (\d+)',header)
if m:
length = int(m.group(1))
data = mr.get_bytes(length)
print(data)
remote_socket.close()
输出
HTTP/1.1 200 OK
Content-Type: image/png
Last-Modified: Thu, 12 Aug 2010 00:42:08 GMT
Date: Tue, 21 Jun 2011 05:03:35 GMT
Expires: Tue, 21 Jun 2011 05:03:35 GMT
Cache-Control: private, max-age=31536000
X-Content-Type-Options: nosniff
Server: sffe
Content-Length: 6148
X-XSS-Protection: 1; mode=block
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01l\x00\x00\x00~\x08\x03\x00\ (rest omitted)