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

Ảnh chụp đoạn mã nền tối minh hoạ Range request tải một phần tài nguyên không tải lại từ đầu, server báo hỗ trợ Accept-Ranges trong response thường curl -I phim.bin Content-Length 1000 Accept-Ranges bytes server chấp nhận yêu cầu theo byte, client xin một khoảng byte bằng header Range curl -r a-b đặt header Range bytes a-b curl -r 0-19 phim.bin 20 byte đầu curl -r 20- phim.bin từ byte 20 tới hết tải tiếp curl -r -10 phim.bin 10 byte cuối suffix curl -r 0-9 990-999 phim.bin nhiều đoạn một lần, server đáp lại 206 chứ không phải 200 HTTP 206 Partial Content Content-Range bytes 0-19 trên 1000 đoạn này trên tổng kích thước Range vượt kích thước trả 416 Range Not Satisfiable nhiều đoạn Content-Type multipart byteranges

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:

Ảnh chụp bảng kết quả chạy thật nginx cộng curl output thật file phim.bin 1000 byte mỗi dòng DONGnnn X đúng 10 byte, 20 byte đầu bytes 0-19 trả 206 curl -r 0-19 phim.bin HTTP 206 Partial Content Content-Length 20 Content-Range bytes 0-19 trên 1000 nhận DONG000 X DONG001 X, tải tiếp từ byte 20 bytes 20- resume curl -r 20- phim.bin HTTP 206 Content-Range bytes 20-999 trên 1000 980 byte đầu phần tiếp DONG002 X, ghép hai phần lại bằng file gốc md5 khớp md5 gốc 0442bdfcaa md5 ghép 0442bdfcaa khớp, suffix và lỗi curl -r -10 phim.bin Content-Range bytes 990-999 trên 1000 curl -r 5000-6000 phim.bin 416 Content-Range bytes sao trên 1000

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-19 với phần 20- 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ề

  1. Range request lấy một khoảng byte, trả 206 Partial Content: đo thật bytes=0-19 → 206 + Content-Range: bytes 0-19/1000. Server phải báo trước khả năng bằng Accept-Ranges: bytes; đây là nền của tua video và tải một phần.
  2. 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.
  3. Range sai → 416 + Content-Range: bytes */1000; nhiều đoạn → multipart/byteranges; và resume an toàn cần If-Range với ETag để tránh ghép nhầm hai phiên bản.

Nguồn

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.