Bạn gọi fetch(), requests.get(), hay http.Get() mỗi ngày, nhưng đã bao giờ thấy một request HTTP thô trông thế nào chưa? Đằng sau mọi thư viện HTTP hào nhoáng là một sự thật đơn giản đến mức nhiều người không tin: HTTP/1.1 chỉ là vài dòng text, kết thúc bằng ký tự CRLF, gửi qua một socket TCP. Không mã hoá nhị phân bí ẩn, không nghi thức bắt tay phức tạp ở tầng ứng dụng. Bài mở đầu sê-ri này bóc lớp vỏ đó ra: ta xem từng byte curl gửi đi, rồi tự gõ tay một request mà không dùng thư viện HTTP nào — chạy thật trong container với curl 7.88 và Python 3.11.

Cấu trúc một message HTTP

Cả request lẫn response đều chung một khung bốn phần:

Dòng đầu       GET /index.html HTTP/1.1     (request-line)
Các header     Host: ...   Content-Type: ...
Dòng TRỐNG     <CRLF>                       (báo hết header)
Body           <dữ liệu, có thể rỗng>

Ba điều đáng khắc cốt: (1) mỗi dòng kết thúc bằng CRLF (\r\n), không phải chỉ \n; (2) một dòng trống (\r\n đứng riêng) ngăn phần header với body — đây là ranh giới quan trọng nhất; (3) response chỉ khác request ở dòng đầu: thay request-line bằng status-line (HTTP/1.0 200 OK), phần còn lại y hệt. Nắm khung này là đọc được mọi trao đổi HTTP.

curl -s --trace-ascii - http://127.0.0.1:8099/index.html   # in từng byte gửi/nhận
curl -i http://...       # -i: in header response + body
curl -v http://...       # -v: > gửi, < nhận, * ghi chú

Ảnh chụp đoạn mã nền tối giải thích một request HTTP thực chất là text gửi qua TCP, cấu trúc một message HTTP request và response giống khung dòng đầu là request-line GET slash index.html HTTP/1.1, các header Host Content-Type, dòng trống CRLF báo hết header, body dữ liệu có thể rỗng, mỗi dòng kết thúc bằng CRLF backslash r backslash n response đổi dòng đầu thành status-line HTTP/1.0 200 OK phần còn lại y hệt, xem thô bằng curl trace-ascii in ra từng byte curl gửi Send header và nhận Recv header, curl -i in cả header response và body curl -v hiện gửi nhận ghi chú, chứng minh gõ tay request bằng socket thô python import socket tạo kết nối tới 127.0.0.1 8099 req là GET slash data.txt HTTP/1.1 Host Connection close dòng trống hết header sendall rồi recv 4096 in ra server trả text HTTP

Hình 1: Khung một message HTTP — dòng đầu, các header, một dòng trống, rồi body. Ta xem thô bằng curl --trace-ascii và chứng minh bằng một request gõ tay qua socket.

Đo thật: xem từng byte curl gửi và nhận

Dựng một server tĩnh đơn giản (python3 -m http.server 8099) phục vụ một index.html dài đúng 41 byte, rồi gọi curl với --trace-ascii để in ra chính xác những gì đi qua dây:

Ảnh chụp bảng kết quả đo thật nền tối chạy curl 7.88 và python 3.11, server python3 -m http.server 8099, phần một REQUEST curl gửi đi bằng trace-ascii GET slash index.html HTTP/1.1 Host 127.0.0.1 8099 User-Agent curl 7.88.1 Accept sao chéo sao dòng trống kết thúc header không body vì GET, phần hai RESPONSE server trả về bằng curl -i HTTP/1.0 200 OK Server SimpleHTTP 0.6 Python 3.11.2 Date Wed 30 Sep 2026 Content-type text/html Content-Length 41 Last-Modified dòng trống rồi body h1 Xin chao h1 p Trang thu nghiem p đúng 41 byte, phần ba gõ tay bằng socket thô HTTP chỉ là text trên TCP gửi GET slash data.txt HTTP/1.1 Host 127.0.0.1 Connection close CRLF CRLF nhận HTTP/1.0 200 OK Content-type text/plain Content-Length 15 body du lieu bi mat không thư viện HTTP nào chỉ socket cộng text đúng khuôn

Hình 2: REQUEST curl gửi (dòng GET, ba header, một dòng trống); RESPONSE server trả (status-line, các header, dòng trống, rồi body dài đúng Content-Length: 41); và một request gõ tay bằng socket vẫn nhận lời đáp HTTP hợp lệ.

Kết quả xác nhận đúng khung lý thuyết. Request curl gửi vỏn vẹn bốn dòng: GET /index.html HTTP/1.1, Host:, User-Agent:, Accept:, rồi một dòng trống — không có body vì GET không mang body. Response mở đầu bằng HTTP/1.0 200 OK, một loạt header, một dòng trống, rồi 41 byte HTML đúng bằng Content-Length đã khai. Header Host là bắt buộc trong HTTP/1.1 (nhiều site chung một IP, server dựa vào nó để biết bạn hỏi site nào — sẽ nói kỹ ở bài header).

Tự gõ tay một request — không thư viện HTTP nào

Để chứng minh HTTP thật sự chỉ là text trên TCP, ta bỏ hết thư viện HTTP và chỉ dùng một socket thô:

import socket
s = socket.create_connection(("127.0.0.1", 8099))
req = ("GET /data.txt HTTP/1.1\r\n"
       "Host: 127.0.0.1\r\n"
       "Connection: close\r\n\r\n")   # \r\n\r\n = dòng trống, hết header
s.sendall(req.encode())
print(s.recv(4096).decode())          # server trả về text HTTP

Server trả về HTTP/1.0 200 OK, Content-type: text/plain, Content-Length: 15, rồi nội dung file. Không có phép màu nào: chỉ cần một socket TCP và một chuỗi text đúng khuôn (chú ý \r\n\r\n ở cuối — thiếu dòng trống này server sẽ chờ mãi vì tưởng header chưa hết). Đây chính xác là những gì mọi thư viện HTTP làm bên dưới, chỉ khác chúng lo giúp bạn phần dựng chuỗi, phân tích response, quản lý kết nối và lỗi.

Đánh đổi và lưu ý

Text dễ đọc nhưng tốn byte — đó là lý do HTTP/2 ra đời. Header dạng text lặp lại ở mọi request (cùng User-Agent, cùng cookie...) rất phí băng thông. HTTP/2 nén header nhị phân (HPACK) để khắc phục — chủ đề một bài sau. Nhưng chính tính "text thuần" của HTTP/1.1 khiến nó dễ debug bằng mắt, và đó là giá trị lớn khi đi tìm lỗi.

CRLF không phải LF. Chuẩn HTTP yêu cầu \r\n. Đa số server dễ tính chấp nhận cả \n, nhưng đừng dựa vào đó — khi tự sinh request (proxy, test tool), dùng đúng \r\n để khỏi gặp lỗi khó hiểu.

http.server của Python là HTTP/1.0, chỉ để học/thử. Nó trả HTTP/1.0, không keep-alive mặc định, không chịu tải — tuyệt đối không dùng cho production. Ở đây nó hoàn hảo để soi giao thức vì đơn giản và trong suốt.

Ba ý mang về

  1. Một message HTTP là text đúng khuôn: dòng đầu (request-line hoặc status-line) → các header → một dòng trống (\r\n) → body; mỗi dòng kết thúc bằng CRLF. Request và response chung khung, chỉ khác dòng đầu.
  2. Xem thô bằng curl: --trace-ascii in từng byte gửi/nhận, -i in header response, -v hiện cả hai chiều — đo thật, request GET chỉ bốn dòng, response có body dài đúng Content-Length.
  3. HTTP chỉ là text trên TCP: một request gõ tay bằng socket Python (không thư viện HTTP) vẫn được server trả lời — mọi thư viện HTTP chỉ là lớp tiện lợi bọc quanh việc này; nhớ \r\n\r\n báo hết header nếu không server chờ mãi.

Nguồn

Phần sau ta soi kỹ các header quan trọng nhất — Host, Content-Type, và cặp Content-Length vs Transfer-Encoding: chunked — vì sao chọn sai một trong hai khiến response bị treo hoặc cắt cụt.