用 Python 實現一個簡易版 HTTP 客戶端

此文爲《用 Python 擼一個 Web 服務器》系列教程的一個補充,這個系列教程介紹瞭如何使用 Python 內置的 socket 庫實現一個簡易版的 Web 服務器。html

之因此寫這篇文章,是由於我發現不少人並不清楚 HTTP 客戶端的概念,覺得只有瀏覽器才叫 HTTP 客戶端。事實上並不是如此,咱們在 Web 開發中常見的 Postman爬蟲程序curl 命令行工具 等,這些均可以稱爲 HTTP 客戶端。python

服務器程序示例

這裏以一個 Hello World 程序來做爲示例服務器,實現以下:json

# server.py

import socket
import threading


def process_connection(client):
    """處理客戶端鏈接"""
    # 接收客戶端發來的數據
    data = b''
    while True:
        chunk = client.recv(1024)
        data += chunk
        if len(chunk) < 1024:
            break

    # 打印從客戶端接收的數據
    print(f'data: {data}')
    # 給客戶端發送響應數據
    client.sendall(b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>')

    # 關閉客戶端鏈接對象
    client.close()


def main():
    # 建立 socket 對象
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 容許端口複用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 綁定 IP 和端口
    sock.bind(('127.0.0.1', 8000))
    # 開始監聽
    sock.listen(5)

    while True:
        # 等待客戶端請求
        client, addr = sock.accept()
        print(f'client type: {type(client)}\naddr: {addr}')

        # 建立新的線程來處理客戶端鏈接
        t = threading.Thread(target=process_connection, args=(client,))
        t.start()


if __name__ == '__main__':
    main()

服務器端程序不作過多解釋,若有不明白的地方能夠參考 用 Python 擼一個 Web 服務器-第2章:Hello-World 一節。瀏覽器

極簡客戶端

知道了如何用 socket 庫實現服務器端程序,那麼理解客戶端程序的實現就很是容易了。客戶端程序代碼實現以下:服務器

# client.py

import socket

# 建立 socket 對象
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 指定服務器 IP 和端口,進行鏈接
sock.connect(('127.0.0.1', 8000))
# 向 URL "/" 發送 GET 請求
sock.send(b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n\r\n')

# 接收服務端響應數據
data = b''
while True:
    chunk = sock.recv(1024)
    data += chunk
    if len(chunk) < 1024:
        break
# 打印響應數據
print(data)

# 關閉鏈接
sock.close()

相對來講客戶端程序要簡單一些,建立 socket 對象的代碼與服務器端程序並沒有差異,客戶端 socket 對象根據 IP 和端口來鏈接指定的服務器,創建好鏈接後就能夠發送數據了,這裏根據 HTTP 協議構造了一個針對 / URL 路徑的 GET 請求,爲了簡單起見,請求頭中僅攜帶了 HTTP 協議規定的必傳字段 Host,請求發送成功後即可以接收服務器端響應,最後別忘了關閉 socket鏈接。微信

僅用幾行代碼,咱們就實現了一個極簡的 HTTP 客戶端程序,接下來對其進行測試。curl

首先在終端中使用 Python 運行服務器端程序:python3 server.py。而後在另外一個終端中使用 Python 運行客戶端程序:python3 client.pysocket

能夠看到客戶端打印結果以下:函數

b'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Hello World</h1>'

以上,咱們實現了一個極簡的 HTTP 客戶端。工具

參考 requests 實現客戶端

用 Python 寫過爬蟲的同窗,必定據說或使用過 requests 庫,如下是使用 requests 訪問 Hello World 服務端程序的示例代碼:

# demo_requests.py

import requests

response = requests.request('GET', 'http://127.0.0.1:8000/')
print(response.status_code)  # 響應狀態碼
print('--------------------')
print(response.headers)  # 響應頭
print('--------------------')
print(response.text)  # 響應體

在終端中使用 python3 demo_requests.py 運行此程序,將打印以下結果:

200
--------------------
{'Content-Type': 'text/html'}
--------------------
<h1>Hello World</h1>

接下來修改咱們上面實現的極簡 HTTP 客戶端程序,使其可以支持 response.status_coderesponse.headersresponse.text功能。

# client.py

import socket
from urllib.parse import urlparse


class HTTPClient(object):
    """HTTP 客戶端"""

    def __init__(self):
        # 建立 socket 對象
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 初始化數據
        self.status_code = 200
        self.headers = {}
        self.text = ''

    def __del__(self):
        # 關閉鏈接
        self.sock.close()

    def connect(self, ip, port):
        """創建鏈接"""
        self.sock.connect((ip, port))

    def request(self, method, url):
        """請求"""
        # URL 解析
        parse_result = urlparse(url)
        ip = parse_result.hostname
        port = parse_result.port or 80
        host = parse_result.netloc
        path = parse_result.path
        # 創建鏈接
        self.connect(ip, port)
        # 構造請求數據
        send_data = f'{method} {path} HTTP/1.1\r\nHost: {host}\r\n\r\n'.encode('utf-8')
        # 發送請求
        self.sock.send(send_data)
        # 接收服務端響應的數據
        data = self.recv_data()
        # 解析響應數據
        self.parse_data(data)

    def recv_data(self):
        """接收數據"""
        data = b''
        while True:
            chunk = self.sock.recv(1024)
            data += chunk
            if len(chunk) < 1024:
                break
        return data.decode('utf-8')

    def parse_data(self, data):
        """解析數據"""
        header, self.text = data.split('\r\n\r\n', 1)
        status_line, header = header.split('\r\n', 1)
        for item in header.split('\r\n'):
            k, v = item.split(': ')
            self.headers[k] = v
        self.status_code = status_line.split(' ')[1]


if __name__ == '__main__':
    client = HTTPClient()
    client.request('GET', 'http://127.0.0.1:8000/')
    print(client.status_code)
    print('--------------------')
    print(client.headers)
    print('--------------------')
    print(client.text)

代碼實現比較簡單,我寫了較爲詳細的註釋,相信你可以看懂。其中使用了內置函數 urlparse ,此函數可以根據 URL 格式規則將 URL 拆分紅多個部分。

在終端中使用 python3 client.py 運行此程序,打印結果與使用 requests 的結果徹底相同。

200
--------------------
{'Content-Type': 'text/html'}
--------------------
<h1>Hello World</h1>

僅用幾十行代碼,咱們就實現了一個簡易版的 HTTP 客戶端程序,而且還實現了相似 requests 庫的功能。

接下來你能夠嘗試用它去訪問現實世界中真實的 URL,好比訪問 http://httpbin.org/get,看看打印結果如何。

P.S.

Web 開發本質是圍繞着 HTTP 協議進行的,HTTP 協議是 Web 開發的基石。因此對於何爲 HTTP 服務端、何爲 HTTP 客戶端的概念不夠清晰的話,實際上都是對 HTTP 協議不夠理解。

最後,給你們留一道做業題,實現 requests 庫的 response.json() 方法。

聯繫我:

相關文章
相關標籤/搜索