Python实现http客户端代码示例

文章目录

  • 一. 创建HTTP请求
      • 1. HTTPConnection类
      • 2. 编写请求行
      • 3. 添加请求头
      • 4. 发送请求正文
  • 二. 获取响应数据
      • 1. HTTPResponse类
      • 2. 解析状态行
      • 3. 解析响应头
      • 4. 接收响应正文
  • 三. 复用TCP连接
      • 1. 判断连接是否会断开
      • 2. 正确地关闭HTTPResponse对象
      • 3. HTTP请求的生命周期
  • 四. 总结
      • 1. 完整代码
      • 2. 需要注意的点
      • 3. 结果测试
      • 关于Python技术储备
        • 一、Python所有方向的学习路线
        • 二、Python基础学习视频
        • 三、精品Python学习书籍
        • 四、Python工具包+项目源码合集
        • ①Python工具包
        • ②Python实战案例
        • ③Python小游戏源码
        • 五、面试资料
        • 六、Python兼职渠道


本文用python在TCP的基础上实现一个HTTP客户端, 该客户端能够复用TCP连接, 使用HTTP1.1协议.

一. 创建HTTP请求


HTTP是基于TCP连接的, 它的请求报文格式如下:

Python实现http客户端代码示例_第1张图片

因此, 我们只需要创建一个到服务器的TCP连接, 然后按照上面的格式写好报文并发给服务器, 就实现了一个HTTP请求.

1. HTTPConnection类

基于以上的分析, 我们首先定义一个HTTPConnection类来管理连接和请求内容:

class HTTPConnection:
  default\_port = 80
  \_http\_vsn = 11
  \_http\_vsn\_str = 'HTTP/1.1'

  def \_\_init\_\_(self, host: str, port: int = None) -> None:
    self.sock = None
    self.\_buffer = \[\]
    self.host = host
    self.port = port if port is not None else self.default\_port
    self.\_state = \_CS\_IDLE
    self.\_response = None
    self.\_method = None
    self.block\_size = 8192

  def \_output(self, s: Union\[str, bytes\]) -> None:
    if hasattr(s, 'encode'):
      s = s.encode('latin-1')
    self.\_buffer.append(s)

  def connect(self) -> None:
    self.sock = socket.create\_connection((self.host, self.port))

对于这个HTTPConnection对象, 我们只需要创建TCP连接, 然后按照HTTP协议的格式把请求数据写入buffer中, 最后把buffer中的数据发送出去就行了.

2. 编写请求行

请求行的内容比较简单, 就是说明请求方法, 请求路径和HTTP协议. 使用下面的方法来编写一个请求行:

def put\_request(self, method: str, url: str) -> None:
  self.\_method = method

  url = url or '/'

  request = f'{method} {url} {self.\_http\_vsn\_str}'
  self.\_output(request)

3. 添加请求头

HTTP请求头和python的字典类似, 每行都是一个字段名与值的映射关系. HTTP协议并不要求设置所有合法的请求头的值, 我们只需要按照需要, 设置特定的请求头即可. 使用如下代码添加请求头:

def put\_header(self, header: Union\[bytes, str\], value: Union\[bytes, str, int\]) -> None:
  if hasattr(header, 'encode'):
    header = header.encode('ascii')

  if hasattr(value, 'encode'):
    value = value.encode('latin-1')
  elif isinstance(value, int):
    value = str(value).encode('ascii')

  header = header + b': ' + value
  self.\_output(header)

此外, 在HTTP请求中, Host请求头字段是必须的, 否则网站可能会拒绝响应. 因此, 如果用户没有设置这个字段, 这里就应该主动把它加上去:

def \_add\_host(self, url: str) -> None:
  # 所有HTTP / 1.1请求报文中必须包含一个Host头字段
  # 如果用户没给,就调用这个函数来生成
  netloc = ''
  if url.startswith('http'):
    nil, netloc, nil, nil, nil = urllib.parse.urlsplit(url)

  if netloc:
    try:
      netloc\_enc = netloc.encode('ascii')
    except UnicodeEncodeError:
      netloc\_enc = netloc.encode('idna')
    self.put\_header('Host', netloc\_enc)
  else:
    host = self.host
    port = self.port

    try:
      host\_enc = host.encode('ascii')
    except UnicodeEncodeError:
      host\_enc = host.encode('idna')

    # 对IPv6的地址进行额外处理
    if host.find(':') >= 0:
      host\_enc = b'\[' + host\_enc + b'\]'

    if port == self.default\_port:
      self.put\_header('Host', host\_enc)
    else:
      host\_enc = host\_enc.decode('ascii')
      self.put\_header('Host', f'{host\_enc}:{port}')

4. 发送请求正文

我们接受两种形式的body数据: 一个基于io.IOBase的可读文件对象, 或者是一个能通过迭代得到数据的对象. 在传输数据之前, 我们首先要确定数据是否采用分块传输:

def request(self, method: str, url: str, headers: dict = None, body: Union\[io.IOBase, Iterable\] = None,
      encode\_chunked: bool = False) -> None:
  ...
  if 'content-length' not in header\_names:
    if 'transfer-encoding' not in header\_names:
      encode\_chunked = False
      content\_length = self.\_get\_content\_length(body, method)
      if content\_length is None:
        if body is not None:
          # 在这种情况下, body一般是个生成器或者可读文件之类的东西,应该分块传输
          encode\_chunked = True
          self.put\_header('Transfer-Encoding', 'chunked')
      else:
        self.put\_header('Content-Length', str(content\_length))
    else:
      # 如果设置了transfer-encoding,则根据用户给的encode\_chunked参数决定是否分块
      pass
  else:
    # 只要给了content-length,那么一定不是分块传输
    encode\_chunked = False
  ...


@staticmethod
def \_get\_content\_length(body: Union\[str, bytes, bytearray, Iterable, io.IOBase\], method: str) -> Optional\[int\]:
  if body is None:
    # PUT,POST,PATCH三个方法默认是有body的
    if method.upper() in \_METHODS\_EXPECTING\_BODY:
      return 0
    else:
      return None

  if hasattr(body, 'read'):
    return None

  try:
    # 对于bytes或者bytearray格式的数据,通过memoryview获取它的长度
    return memoryview(body).nbytes
  except TypeError:
    pass

  if isinstance(body, str):
    return len(body)

  return None

在确定了是否分块之后, 就可以把正文发出去了. 如果body是一个可读文件的话, 就调用_read_readable方法把它封装为一个生成器:

def \_send\_body(self, message\_body: Union\[str, bytes, bytearray, Iterable, io.IOBase\], encode\_chunked: bool) -> None:
  if hasattr(message\_body, 'read'):
    chunks = self.\_read\_readable(message\_body)
  else:
    try:
      memoryview(message\_body)
    except TypeError:
      try:
        chunks = iter(message\_body)
      except TypeError:
        raise TypeError(
          f'message\_body should be a bytes-like object or an iterable, got {repr(type(message\_body))}')
    else:
      # 如果是字节类型的,通过一次迭代把它发出去
      chunks = (message\_body,)

  for chunk in chunks:
    if not chunk:
      continue

    if encode\_chunked:
      chunk = f'{len(chunk):X}\\r\\n'.encode('ascii') + chunk + b'\\r\\n'
    self.send(chunk)

  if encode\_chunked:
    self.send(b'0\\r\\n\\r\\n')


def \_read\_readable(self, readable: io.IOBase) -> Generator\[bytes, None, None\]:
  need\_encode = False
  if isinstance(readable, io.TextIOBase):
    need\_encode = True
  while True:
    data\_block = readable.read(self.block\_size)
    if not data\_block:
      break
    if need\_encode:
      data\_block = data\_block.encode('utf-8')
    yield data\_block

二. 获取响应数据


HTTP响应报文的格式与请求报文大同小异, 它大致是这样的:

Python实现http客户端代码示例_第2张图片

因此, 我们只要用HTTPConnection的socket对象读取服务器发送的数据, 然后按照上面的格式对数据进行解析就行了.

1. HTTPResponse类

我们首先定义一个简单的HTTPResponse类. 它的属性大致上就是socket的文件对象以及一些请求的信息等等, 调用它的begin方法来解析响应行和响应头的数据, 然后调用read方法读取响应正文:

class HTTPResponse:

  def \_\_init\_\_(self, sock: socket.socket, method: str = None) -> None:
    self.fp = sock.makefile('rb')
    self.\_method = method
    self.headers = None
    self.version = \_UNKNOWN
    self.status = \_UNKNOWN
    self.reason = \_UNKNOWN
    self.chunked = \_UNKNOWN
    self.chunk\_left = \_UNKNOWN
    self.length = \_UNKNOWN
    self.will\_close = \_UNKNOWN

  def begin(self) -> None:
    ...

  def read(self, amount: int = None) -> bytes:
    ...

2. 解析状态行

状态行的解析比较简单, 我们只需要读取响应的第一行数据, 然后把它解析为HTTP协议版本,状态码和原因短语三部分就行了:

def \_read\_status(self) -> Tuple\[str, int, str\]:
  line = str(self.\_read\_line(), 'latin-1')
  if not line:
    raise RemoteDisconnected('Remote end closed connection without response')
  try:
    version, status, reason = line.split(None, 2)
  except ValueError:
    # reason只是给人看的, 一般和status对应, 所以它有可能不存在
    try:
      version, status = line.split(None, 1)
      reason = ''
    except ValueError:
      version, status, reason = '', '', ''
  if not version.startswith('HTTP/'):
    self.\_close\_conn()
    raise BadStatusLine(line)

  try:
    status = int(status)
    if status < 100 or status > 999:
      raise BadStatusLine(line)
  except ValueError:
    raise BadStatusLine(line)
  return version, status, reason.strip()

如果状态码为100, 则客户端需要解析多个响应状态行. 它的原理是这样的: 在请求数据过大的时候, 有的客户端会先不发送请求数据, 而是先在header中添加一个Expect: 100-continue, 如果服务器愿意接收数据, 会返回100的状态码, 这时候客户端再把数据发过去. 因此, 如果读取到100的状态码, 那么后面往往还会收到一个正式的响应数据, 应该继续读取响应头. 这部分的代码如下:

def begin(self) -> None:
  while True:
    version, status, reason = self.\_read\_status()
    if status != HTTPStatus.CONTINUE:
      break
    # 跳过100状态码部分的响应头
    while True:
      skip = self.\_read\_line().strip()
      if not skip:
        breakself.status = status
  self.reason = reason
  if version in ('HTTP/1.0', 'HTTP/0.9'):
    self.version = 10
  elif version.startswith('HTTP/1.'):
    self.version = 11
  else:
    # HTTP2还没研究, 这里就不写了
    raise UnknownProtocol(version)

  ...

3. 解析响应头

解析响应头比响应行还要简单. 因为每个header字段占一行, 我们只需要一直调用read_line方法读取字段, 直到读完header为止就行了.

def \_parse\_header(self) -> None:
  headers = {}
  while True:
    line = self.\_read\_line()
    if len(headers) > \_MAX\_HEADERS:
      raise HTTPException('got more than %d headers' % \_MAX\_HEADERS)
    if line in \_EMPTY\_LINE:
      break
    line = line.decode('latin-1')
    i = line.find(':')
    if i == -1:
      raise BadHeaderLine(line)
    # 这里默认没有重名的情况
    key, value = line\[:i\].lower(), line\[i + 1:\].strip()
    headers\[key\] = value
  self.headers = headers

4. 接

你可能感兴趣的:(python,http,开发语言,计算机网络,网络,学习,经验分享)