使用socket编写HTTP网络请求方法

在做爬虫的时候,经常使用的是requests等高级模块进行操作,虽然很方便,但是仍然不免要想这样的方式是如何实现的呢?当然,不用想也知道一定会用到socket模块。在此不妨使用socket来实现一下简单的网络请求。大概思路如下:

    1. 写一个解析 URL 的类  用来解析并保存请求协议、地址、端口等信息
    2. 写一个请求类和响应类 分别用来包装请求地址、请求头、请求体和响应头、响应体等信息
    3. 写一个客户端类用于创建连接、发送请求、返回数据等

解析URL

地址解析可以直接调用urlparse方法,不过仍需要保存协议以及根据协议判定请求端口(如果未指定的话)等。UrlParser只是用来保存url解析结果,用于请求类的使用。

from urllib.parse import urlparse


class UrlParser(object):

    def __init__(self, url):
        result = urlparse(url)

        if result.scheme == 'https':
            self.port = 443
            self.ssl = True
        elif result.scheme == 'http':
            self.port = 80
            self.ssl = False

        if result.port:
            self.port = result.port

        self.path = result.path or '/'
        if result.query:
            self.path += '?' + result.query

        self._r = result

    def __getattr__(self, item):
        return getattr(self._r, item)

封装Request和Response

在发起请求前,我们需要两个对象用来存储请求信息和响应信息:

class Request(object):
    """请求对象"""
    def __init__(self, url, method='GET', headers=None, body=None):
        self.url = url
        self.method = method
        self.body = body
        self.headers = headers or {}


class Response(object):
    """响应对象"""
    def __init__(self, request, headers=None, body=None, status=None):
        self.request = request
        self.headers = headers
        self.body = body
        self.status = status

网络请求

当然,网络请求才是重点。如何将发送的数据组装为正确的格式,可以参考HTTP权威指南。在与服务器建立连接时,首先应看地址是否指定了端口号,如果没有则需要根据是否为HTTPS连接来判定,一般HTTP连接默认的端口是80,而HTTPS的则为443。如果是HTTPS,我们还需要对socket连接包装一下才行(通过ssl模块提供的wrap_socket方法)。
建立连接后,就需要发送请求头和请求体(如果有)信息了。请求头每行以\r\n结束,且与请求体相隔一个\r\n。如果有请求体,还需要传入Content-lengthContent-Type来指定请求体长度和类型。具体的请求体类型可以参考这篇文章:四种常见的 POST 提交数据方式 | JerryQu 的小站,这里为了方便,就没有具体指定类型和长度了。请求体最后以\r\n\r\n结束,注意请求头和请求体的数据都是bytes类型。
具体代码如下:

import socket
import ssl as _ssl

from collections import defaultdict
from http.client import HTTPResponse


class ClientSession(object):

    def __init__(self):
        self._sk = None

    def _make_sk(self, ssl=True):
        """创建socket对象"""
        sk = socket.socket()
        if ssl:
            sk = _ssl.wrap_socket(sk)
        self._sk = sk

    def _make_buffer(self, request, url):
        """组装请求头和请求体"""
        buffer = []
        buffer.append(f'{request.method} {url.path} HTTP/1.1')

        if not 'host' in request.headers:
            buffer.append(f'host: {url.hostname}')

        for k, v in request.headers.items():
            buffer.append(f'{k}: {v}')

        body = request.body
        if body is not None:
            buffer.append('\r\n')
            if isinstance(body, dict):
                buffer.append('&'.join(f'{k}={v}' for k, v in body.items()))
            elif isinstance(body, list):
                buffer.append('&'.join(f'{k}={v}' for k, v in body))
            elif isinstance(body, str):
                buffer.append(body)
            else:
                pass
        buffer.append('\r\n')
        return bytes('\r\n'.join(buffer), 'utf8')

    def _make_response(self, request):
        """封装为Response对象并返回"""
        r = HTTPResponse(self._sk, method=request.method)
        r.begin()

        headers = defaultdict(str)
        for k, v in r.headers.items():
            headers[k] += v

        return Response(request, r.headers, body=r.read(), status=r.status)

    def fetch(self, request):
        """发起请求"""
        url = UrlParser(request.url)
        if self._sk is None:
            self._make_sk(url.ssl)
        self._sk.connect((url.hostname, url.port))
        self._sk.send(self._make_buffer(request, url))
        return self._make_response(request)

    def close(self):
        if self._sk is not None:
            self._sk.close()

最后在封装Response对象时,偷懒就没有自己做解析了,而是引用了标准库的http.client模块中的HTTPResponse对象。该对象初始化时接收一个socket实例,调用begin时它就会自动解析服务器返回的响应头数据。而read方法则会读取并返回响应体数据。如果要手动接收这些数据,调用socketrecv方法即可,注意该方法是阻塞的,它会在以下三种情况返回:

    1. 接收到了服务器的数据;
    2. 服务器关闭了连接;
    3. 网络发生了错误。

如果使用死循环来不断接收数据,当没有数据时就退出循环,比如下面的例子。

while True:
    data = self._sk.recv(1024)
    if not data:
        break

这样的操作可能不会按照预期进行。原因是HTTP1.1中的Connection默认为Keep-Alive,即使服务端已经发送完数据,仍然会等到5秒后再断开连接,而客户端不断调用recv则会阻塞到服务器关闭连接或网络发生错误。解决的办法有:

  1. 在请求头中加入Connection: close方法告诉服务端发送完数据后主动关闭连接;
  2. 根据服务端返回的请求头中的Content-Length来判断数据接收长度,如果达到指定长度则主动退出;
  3. 使用非阻塞socket
  4. 使用HTTP 1.0 协议请求。

测试

最后测试一下写的代码:

>>> headers = {
...    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
...    'Accept-Language': 'en',
...    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36'
...}
>>> request = Request('http://www.jszyfw.com/techInfoCtr/static/toapptech.html?tdsourcetag=s_pcqq_aiomsg', headers=headers)
>>> s = ClientSession()
>>> r = s.fetch(request)
>>> r.status
200
>>> r.headers
Server: nginx/1.13.7
Date: Thu, 10 Oct 2019 06:26:27 GMT
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Content-Language: en
>>> s.close()

参考

  1. http.client --- HTTP 协议客户端 — Python 3.7.5rc1 文档
  2. socket --- 底层网络接口 — Python 3.7.5rc1 文档
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容