HTTP parser dạng máy trạng thái

HTTP/1.1 message là chuỗi byte có cấu trúc dòng theo thứ tự cố định: request-line, các header, dòng trống, body. Khi gói tin đến qua TCP, kernel có thể chia làm nhiều chunk tới ứng dụng vào các thời điểm khác nhau. Parser phải nhận từng chunk, giữ trạng thái giữa các lần nhận, và phát ra sự kiện khi parse xong từng phần. Cấu trúc tự nhiên cho bài toán này là máy trạng thái (state machine).

Năm trạng thái cơ bản

Một parser HTTP/1.1 tối giản cần năm trạng thái tương ứng với năm phần của message:

Mỗi lần feed_data được gọi, parser bắt đầu từ trạng thái hiện tại, quét đến token kết thúc của trạng thái đó, emit callback rồi chuyển sang trạng thái kế tiếp. Nếu chunk thiếu dữ liệu (không tìm thấy token kết thúc), parser nối phần đã có vào current_token và return; lần feed_data sau sẽ tiếp tục từ đúng vị trí.

stateDiagram-v2
    [*] --> METHOD
    METHOD --> PATH : space
    PATH --> HTTP_VERSION : space
    HTTP_VERSION --> HEADER : newline
    HEADER --> HEADER : header line
    HEADER --> BODY : empty line
    BODY --> [*]

Tách parsing và xử lý qua callback

Parser không tự đóng gói kết quả — nó nhận một protocol object và gọi các callback on_method, on_path, on_http_version, on_header, on_headers_completed, on_body. Pattern này tách hoàn toàn phần parse (đọc byte → cấu trúc) và phần xử lý (tạo scope, schedule task, gửi vào application). Cùng một parser dùng được cho mọi server: callbacks là điểm tích hợp.

class Parser:
    def feed_data(self, data: bytes):
        idx = 0
        while idx < len(data):
            if self.current_state == STATE_METHOD:
                i = data.find(b" ", idx)
                if i == -1:
                    self.current_token += data[idx:]
                    break
                else:
                    self.protocol.on_method(self.current_token + data[idx:i])
                    self.current_state = STATE_PATH
                    idx = i + 1
            # ...

llhttp (parser của Node.js dùng trong httptools) cùng kiến trúc nhưng được sinh tự động từ định nghĩa LLParse, tận dụng tối đa branchless coding và inline.

Hiệu năng giữa các cài đặt

Benchmark cùng một message trên cùng máy:

Khoảng cách giữa CPython và PyPy minh hoạ chi phí của interpreter khi xử lý byte-level; khoảng cách giữa PyPy và C minh hoạ giới hạn của JIT khi không có inline assembly và branchless tricks như llhttp. Đây là lý do mọi server hiệu năng cao trên Python đều dùng parser viết bằng C qua binding thay vì thuần Python.

Trải nghiệm cá nhân

Wiki này được kết tinh từ một trong bốn thí nghiệm threading dump lên gist ngày 2023-07-12 tại Teko — phần của chuỗi học hai năm gist → xthread → uASGI về Python low-level. Cùng pattern parser sau đó được lặp lại bằng C trong repo http-parser để củng cố thêm. Chi tiết bối cảnh trong transcript phỏng vấn.

Cập nhật: 2026-05-29