Bạn kéo thanh tua của một video dài 2 tiếng tới phút 90 — và nó phát gần như tức thì, không cần tải 89 phút trước đó. Bạn tải một file cài đặt 3GB, mất mạng ở 80%, nối lại và nó tải tiếp chứ không bắt đầu lại. Hai điều tưởng như phép màu này đều dựa trên cùng một cơ chế HTTP: Range request — client xin server đúng một khoảng byte thay vì cả tài nguyên. Bài này (phần 16 loạt "HTTP và web đằng sau") dựng một nginx thật phục vụ một file, rồi đo bằng curl để thấy từng mảnh của cơ chế.
Server phải báo trước là nó hỗ trợ
Range request là tùy chọn — không phải server nào cũng làm được (một endpoint sinh nội dung động thường không). Server báo khả năng này qua header Accept-Ranges: bytes trong response thường. nginx phục vụ file tĩnh bật sẵn:
$ curl -I http://127.0.0.1:8093/phim.bin
Content-Length: 1000
Accept-Ranges: bytes # server chấp nhận yêu cầu theo byte
Thấy Accept-Ranges: bytes, client (trình duyệt, trình tải, player video) biết mình có thể xin từng phần. Nếu không có header này, client phải tải cả file.
Client xin một khoảng, server trả 206
Client gửi header Range: bytes=<đầu>-<cuối>. Trong curl, cờ -r làm việc đó. Điểm mấu chốt: đáp lại không phải 200 OK mà là 206 Partial Content, kèm header Content-Range cho biết chính xác đoạn nào trên tổng kích thước bao nhiêu.
$ curl -r 0-19 http://127.0.0.1:8093/phim.bin
HTTP/1.1 206 Partial Content
Content-Length: 20
Content-Range: bytes 0-19/1000

Hình 1: Server báo hỗ trợ bằng Accept-Ranges: bytes; client xin khoảng bằng header Range: bytes=a-b; server đáp 206 Partial Content + Content-Range. Range sai → 416, nhiều đoạn → multipart/byteranges.
Đo thật: các dạng Range và cách ghép lại
Mình dựng file phim.bin đúng 1000 byte, mỗi dòng DONGnnn X dài đúng 10 byte để offset byte dễ đối chiếu. Dưới đây là output thật:

Hình 2: Chạy thật — bytes=0-19 → 206 + Content-Range: bytes 0-19/1000; bytes=20- → tải tiếp 20-999/1000; ghép hai phần lại cho md5 khớp file gốc; suffix -10 → 990-999/1000; range vượt → 416 + Content-Range: bytes */1000.
Bốn dạng đáng nhớ, tất cả đo thật:
bytes=0-19→ 20 byte đầu,206,Content-Range: bytes 0-19/1000. Đây là cách player video tải đoạn đầu để đọc metadata rồi mới tua.bytes=20-(bỏ trống phần cuối) → "từ byte 20 tới hết" — chính là resume: trình tải biết mình đã có 20 byte, xin phần còn lại. Kết quảContent-Range: bytes 20-999/1000, 980 byte.bytes=-10(số âm) → 10 byte cuối (suffix range), trả990-999/1000. Hữu ích khi cần đọc phần đuôi file (ví dụ central directory của file ZIP).- Ghép lại đúng file gốc: nối phần
0-19với phần20-rồi so md5 — khớp hoàn toàn với file tải nguyên. Đây là bằng chứng resume/tải song song cho ra đúng dữ liệu, không sai lệch.
Khi Range sai: 416, và nhiều đoạn một lần
Xin một khoảng vượt quá kích thước file (bytes=5000-6000 trên file 1000 byte) → server trả 416 Range Not Satisfiable kèm Content-Range: bytes */1000 (dấu * = "khoảng bạn xin không hợp lệ, đây là tổng kích thước thật"). Client dựa vào đó để sửa lại yêu cầu.
Có thể xin nhiều đoạn trong một request (bytes=0-9,990-999). Khi đó server trả 206 với Content-Type: multipart/byteranges — thân response là nhiều phần ngăn bởi boundary, mỗi phần có Content-Range riêng:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000000000000000001
--00000000000000000001
Content-Range: bytes 0-9/1000
DONG000 X
--00000000000000000001
Content-Range: bytes 990-999/1000
DONG099 X
--00000000000000000001--
Đánh đổi cần cân nhắc
Resume an toàn cần validator — dùng If-Range. Nếu file trên server đổi giữa chừng (bạn tải được nửa file phiên bản cũ, phần còn lại là phiên bản mới), ghép lại sẽ hỏng. Giải pháp chuẩn: gửi If-Range: <ETag hoặc Last-Modified> cùng Range — server chỉ trả 206 nếu tài nguyên chưa đổi; nếu đã đổi, nó trả nguyên 200 cả file để client tải lại từ đầu. Đây là chỗ Range nối với ETag (bài về cache).
Tải song song nhiều đoạn nhanh hơn, nhưng không miễn phí. Trình tải hiện đại chia file thành N đoạn và tải đồng thời để lấp băng thông. Lợi ích thật trên mạng có độ trễ cao, nhưng mỗi đoạn là một kết nối — quá nhiều đoạn gây tải cho server và đôi khi bị giới hạn. Với HTTP/2 (một kết nối, nhiều luồng) lợi thế "nhiều kết nối" giảm đi.
Không phải nội dung nào cũng nên hỗ trợ Range. File tĩnh lớn (video, cài đặt, ảnh) thì có; còn nội dung sinh động hoặc nén động theo request thì server thường không đặt Accept-Ranges vì không thể trả một khoảng byte ổn định. Đừng giả định mọi URL đều tua/resume được — kiểm Accept-Ranges trước.
Ba ý mang về
- Range request lấy một khoảng byte, trả
206 Partial Content: đo thậtbytes=0-19→206+Content-Range: bytes 0-19/1000. Server phải báo trước khả năng bằngAccept-Ranges: bytes; đây là nền của tua video và tải một phần. bytes=20-là cơ chế resume, và ghép lại đúng file gốc: đo thật tải tiếp từ byte 20 (20-999/1000) rồi nối với phần đầu cho md5 khớp file nguyên — bằng chứng resume/tải song song không làm sai dữ liệu.- Range sai →
416+Content-Range: bytes */1000; nhiều đoạn →multipart/byteranges; và resume an toàn cầnIf-Rangevới ETag để tránh ghép nhầm hai phiên bản.
Nguồn
- MDN Web Docs — HTTP range requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
- RFC 9110 §14 — Range Requests (HTTP Semantics): https://datatracker.ietf.org/doc/html/rfc9110#name-range-requests
- MDN Web Docs — 206 Partial Content: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206
Phần sau ta rời khỏi mô hình request–response một chiều: WebSocket nâng cấp từ HTTP lên kênh hai chiều thường trực — cách bắt tay Upgrade, và khi nào nên dùng thay vì polling.