美文网首页
使用socket编写HTTP网络请求方法

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

作者: kingron | 来源:发表于2019-10-10 14:33 被阅读0次

在做爬虫的时候,经常使用的是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 文档

相关文章

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

    在做爬虫的时候,经常使用的是requests等高级模块进行操作,虽然很方便,但是仍然不免要想这样的方式是如何实现的...

  • socket简单理解

    socket:底层网络通信对象,又称"套接字",向网络发出请求或向网络做出回应. Http协议是基于socket,...

  • 【重读iOS】网络请求1:基础知识

    基础知识 HTTP基础知识(状态码,请求方法,请求头,cookies) socket/webSocket 系统请求...

  • 【重读iOS】网络请求2:应用

    基础知识 HTTP基础知识(状态码,请求方法,请求头,cookies) socket/webSocket 系统请求...

  • 使用socket发送http请求

    #python 3.X # requests -->urllib -->socket #导入socket 模块 i...

  • iOS在App和Server间创建socket教程

    原文地址 iOS在App和Server间创建socket教程 在大多数场景下,使用HTTP能满足我们的网络请求,但...

  • dubbo

    zk查找方法所在服务器的ipdubbo默认使用socket长连接,即首次访问建立连接以后,后续网络请求使用相同的网...

  • Socket 网络编程(一)

    Socket Socket,又称“套接字”,应用程序通常通过“套接字”向网络发出请求或应答网络请求。 Socket...

  • 从Socket到Django

    socket web服务器本质上可以认为是一段代码,可以不断的处理http协议的网络请求,而http协议可以使用s...

  • Android客户端与Server用socket的交互(一)

    【Socket与HTTP连接的区别】 1. HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,...

网友评论

      本文标题:使用socket编写HTTP网络请求方法

      本文链接:https://www.haomeiwen.com/subject/vltapctx.html