异步HTTP请求

2019-03-02  本文已影响0人  大海无垠_af22

基于《简易协程-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)
上一篇下一篇

猜你喜欢

热点阅读