基于《简易协程-2》提供的协程框架,实现一个异步的HTTP请求的函数。HTTP协议广泛使用,提供这样一个函数实现可以方便的各处使用。
整个函数基本分为两个部分,发送请求和解析响应。
HTTP请求的格式如下所示。
{method} {url} {HTTP version}\r\n
{head1}:{value1}\r\n
...
\r\n
{data}
发送请求的第一步就是将输入的参数拼接成如上格式的字节流。
接着便是监听socket的可写事件,一旦可写了则不断写入请求数据。由于请求可能很大,一次只能传输一段数据,所以这个过程会反复几次,直至所有数据发送完成。
然后便是接受服务端的响应。
HTTP的响应格式如下所示。
{HTTP version} {code} {reason}\r\n
{head1}:{value1}\r\n
...
Content-Length: xxx\r\n
...
\r\n
{data}
响应的格式和请求的格式很类似,仅仅是首行不太一样而已。接收响应有一点需要注意的就是响应数据长度。正常的话,这个是由头部Content-Length表示,有的服务端不给出这个字段,则默认data长度是0。另外一个特殊情况就是HEAD请求,这个情况下,data必定是空,可以忽略Content-Length值。
接收响应的方法主要是监听socket的可读事件,一旦可读则接收数据,最多接收32KB。这个数字是经过实际测试得到的一个比较好的结果,即使在10Gb的网卡上也能取得很好的性能,太小太大都降低性能。
在解析头部之前,每收取到数据都要尝试解析头部,标志是\r\n\r\n
,之前是头部,之后就是响应的data部分。头部解析完成,才能知道data的具体长度,然后一心一意的接收data数据。
流程大致如上。具体代码如下所示。
def async_urlopen(sock, url, method="GET", headers=(), data=""):
"""
async HTTP request
:param sock:
:param url:
:param method:
:param headers: (head, value) headers list
:param data:
:return response: (code, reason, headers, body)
"""
# 拼接请求数据
pieces = [method, ' ', url, ' HTTP/1.1\r\n', ]
for head, val in headers:
pieces.extend((head, ':', val, '\r\n'))
pieces.extend(('Content-Length:', str(len(data)), '\r\n'))
pieces.append('Connection: keep-alive\r\n\r\n')
pieces.append(data)
req_bin = ''.join(pieces)
# 发送请求
while req_bin:
# 发出监听可写事件的请求
yield SocketIO(sock.fileno(), read=False)
# 协程框架检查到可写事件,当前sock可发送数据了
sent = sock.send(req_bin)
req_bin = req_bin[sent:]
resp_bin = ""
resp_len = -1
# 接收响应
while resp_len != len(resp_bin):
yield SocketIO(sock.fileno(), read=True)
data = sock.recv(32 << 10)
# 头部已经解析,当前数据追加到 resp_bin尾部
if resp_len > 0:
resp_bin += data
else:
resp_bin += data
# 尝试解析头部
parts = resp_bin.split('\r\n\r\n', 1)
# '\r\n\r\n'分隔符未找到,头部还没有全部接收到
if len(parts) != 2:
continue
head_bin, resp_bin = parts
lines = head_bin.split('\r\n')
status_line = lines[0]
version, code, reason = status_line.split(' ', 2)
code = int(code)
headers = [line.split(':', 1) for line in lines[1:-1]]
# HEAD 请求不需要计算data长度
if method == 'HEAD':
break
# 查找content-length头部,计算data长度
resp_len = 0
for head, val in headers:
if head.lower() == 'content-length':
resp_len = int(val)
break
yield (code, reason, headers, resp_bin)
网友评论