HTTP/1.1, HTTP/2 — request/response-এর আসল খরচ
HTTP/1.1, HTTP/2 and HTTP/3 Evolution
Method-status-header-র semantics তিন দশক অক্ষত; বদলেছে transport-এর খরচ — HTTP/1.1-এর HOL blocking থেকে HTTP/2-এর multiplexing, আর সেই খরচ মাপার হাতিয়ার raw socket client।
আগে এটা বুঝি
আজকের web-এ ঘটছে একটা অদ্ভুত বিষয়। একদিকে protocol-টার ভাষা —
GET, 404, Content-Type — ১৯৯১ থেকে প্রায় অপরিবর্তিত। অন্যদিকে
সেই ভাষা কীভাবে পরিবহণ হয়, তা তিনবার পুরোপুরি rewrite হয়েছে:
HTTP/1.1 (1997) → HTTP/2 (2015) → HTTP/3 (2022)।
Paradox-টাই আজকের পাঠ: semantics আর transport দুইটা আলাদা engineering সমস্যা। Browser-এ link-click করলে ঠিক কী ঘটল — কয় packet, কয় connection, কোথায় অপেক্ষা, কোথায় একই তথ্য বারবার? খরচ মাপতে পারলে বুঝবেন কেন h2-এর জন্ম, কেন server-push মরল, আর TCP-র ভেতরের HOL কেন QUIC-র জন্ম দিল।
আজকের প্রশ্ন: এক page-load-এর আসল invoice কী, কোন line-item অপ্রয়োজনীয়?
মূল ধারণা
Semantics — যে স্তর কখনো বদলায়নি
RFC 9110 সাবধানে নাম রেখেছে Semantics — নির্দিষ্ট version নয়:
| Methods | চরিত্র | উদাহরণ |
|---|---|---|
GET, HEAD | safe — server-state বদলায় না | page fetch |
PUT, DELETE | idempotent — কতবার পাঠালেও ফল এক | replace/remove |
POST | কিছুই guarantee নেই | form submit, payment |
PATCH | partial modify | field update |
Status-class গল্প বলে: 1xx informational, 2xx success (200, 204),
3xx redirect (301 permanent, 304 not-modified — আজকের hero!),
4xx client-দোষ (404, 429), 5xx server-দোষ (500, 503)।
Headers metadata-র ভাষা — আর এখানেই প্রথম খরচ:
GET /index.html HTTP/1.1 ← request line
Host: example.com ← 1.1-এ mandatory
User-Agent: Mozilla/5.0 ...
Accept: text/html,...
Accept-Encoding: gzip, deflate, br
← খালি লাইন = header শেষপ্রতি request-এ ~500 byte প্রায়-অপরিবর্তিত plaintext। সংখ্যাটা মনে রাখুন — h2-এর HPACK ঠিক এটারই শিকারি।
HTTP/1.1 — keep-alive আর pipelining-এর মৃত্যু
1.0-এ প্রতি object-এ নতুন TCP connection (handshake+TLS+slow-start প্রতিবার)। 1.1-এর উত্তর: keep-alive — connection খোলা, পরের request একই pipe:
HTTP/1.0: conn ─GET▶ ◀resp─ close conn ─GET▶ ...
HTTP/1.1: conn ─GET▶ ◀resp─ ─GET▶ ◀resp─ ─GET▶ ◀resp─ ...কিন্তু ৩০ object = ৩০ serial round-trip। প্রস্তাব ছিল pipelining — উত্তর অপেক্ষা না করে সব request একসাথে পাঠানো। তাত্ত্বিকভাবে ১ RTT, বাস্তবে মৃত্যুদণ্ড — এক বাধ্যবাধকতার কারণে: response অবশ্যই request- order-এ আসবে।
Browser-দের workaround: parallel connections — per-host ৬টা। ৩০ object → ৬ lane-এ ৫টা করে। দাম: ৬× handshake, ৬× slow-start, server-ভিড়। এই compromise-ই 2015 পর্যন্ত web-এর বাস্তবতা।
HTTP/2 — framing-এর বিপ্লব
SPDY-বংশধর h2 (RFC 9113): এক connection-এ streams — প্রতিটা independent request/response-এর ID-বাহী লজিক্যাল পথ; wire-এ সব কিছু ছোট binary frame:
┌────────────────── one TCP connection ──────────────────┐
│ [HEADERS s=1] [HEADERS s=3] [DATA s=1] [DATA s=3] │
│ [HEADERS s=5] [DATA s=1] [DATA s=5] [DATA s=3] ... │
└────────────────────────────────────────────────────────┘
frame ≈ 9-byte header + payload; type = DATA/HEADERS/
SETTINGS/WINDOW_UPDATE/PING/RST_STREAM/...Interleaved frames — stream-3 আটকে গেলেও 1, 5 এগোয়। Application-level HOL dead। Bonus দুটো: HPACK header-compression — static-table (৬১ common header) + per-connection dynamic-table + Huffman; আগের 500-byte header 20-30 byte-এ, repeated User-Agent একবারই যায়। আর server push — server অনুমানে resource ধাক্কা দেবে… যা ব্যর্থ: server জানত না cache-এ কী আছে, push-গুলো বেশিরভাগ wasted bytes। Chrome 106 (2022)-এ removed — protocol-feature-র dignified মৃত্যু; পাঠ: receiver-context- dependent speculation sender-এ নয়।
Caching — সবচেয়ে সস্তা request যেটা যায় না
প্রথম visit:
GET /logo.png ▶
◀ 200 OK, Cache-Control: max-age=31536000, ETag: "abc123"
এক বছরের মধ্যে আবার: ← request-ই যায় না! disk-cache
max-age শেষে:
GET /logo.png, If-None-Match: "abc123" ▶
◀ 304 Not Modified ← body নেই! ~200 byte বনাম ~50KBETag = representation-fingerprint। no-cache = cache করো কিন্তু প্রতিবার
revalidate; no-store = কোনো স্মৃতি নয় (banking)।
Cookies — stateless-এর উপর পাতলা state
HTTP amnesiac — প্রতিটা request স্বাধীন। Login-state-এর দরকার মেটায়
cookie: server Set-Cookie: session=xyz; HttpOnly; Secure; SameSite=Lax,
client পরের প্রতিটা request-এ Cookie: session=xyz ফেরত। পুরো ‘login
state’ = client-বাহিত string, server-session-store-এর চাবি। HttpOnly
= JS পড়বে না (XSS-defense); Secure = HTTPS-only; SameSite = cross-site
request নয় (CSRF-defense)।
ভেতরে কী ঘটছে
Frame-by-frame — h2-এ একটা GET-এর দেহ
→ SETTINGS (empty) ← connection preamble
→ HEADERS (stream 1)
:method GET ← pseudo-headers ':' দিয়ে —
:scheme https request-line+Host-এর
:authority example.com উত্তরাধিকার
:path /
user-agent curl/8.4.0 ← HPACK-indexed!
accept */* ← static-table index
→ SETTINGS ACK
← HEADERS (stream 1) :status 200, content-type..., etag...
← DATA (stream 1) [html bytes, END_STREAM]লক্ষণীয়: (১) request-line+Host → চার pseudo-header — ambiguity-free parsing (h1-এর whitespace/newline parsing-bug-ইতিহাস: request smuggling!)। (২) প্রতিটা frame stream-ID বহন — interleaving-এর mechanism। (৩) flow-control frame-level (WINDOW_UPDATE credit) — TCP-র global-window-এর চেয়ে সূক্ষ্ম।
খরচ-এর anatomy — এক page, তিন যুগ
৩০-object page, 50ms RTT:
HTTP/1.1, 6 conns: 6×handshake(parallel)≈1RTT + 30÷6×1RTT≈5RTT ≈ 6 RTT = 300ms
(+ 6× congestion-window warm-up)
HTTP/2, 1 conn: handshake 1RTT + 30 streams interleaved 1-2RTT ≈ 2-3 RTT
= 100-150ms (+ single slow-start)
HTTP/2 + loss: যে-কোনো stream-এর packet-loss → সব-৩০ freeze +1 RTTShape সঠিক: h2-এর জয় connection-amortization-এ; vulnerability shared- bytestream-এ।
উদাহরণ
পূর্ণ exchange — conditional-request-এর dance
# ১. প্রথম fetch
→ GET /api/status HTTP/1.1
← HTTP/1.1 200 OK
← Cache-Control: max-age=60
← ETag: "v17-f3a9c2"
← (body: 4812 bytes)
# ২. ৯০ সেকেন্ড পর — max-age expired
→ GET /api/status HTTP/1.1
→ If-None-Match: "v17-f3a9c2" ← আগের fingerprint ফেরত
← HTTP/1.1 304 Not Modified ← body ZERO
← Cache-Control: max-age=60 ← নতুন freshness-টিকিট
# ৩. cache রাখল পুরনো body, max-age রিসেট — ৬০ সেকেন্ড আরো শান্তি
# ৪. content সত্যিই বদলালে
→ If-None-Match: "v17-f3a9c2"
← HTTP/1.1 200 OK, ETag: "v18-91bd04", (নতুন body)Economics: step-2-এ গেল ~150 byte, এড়াল 4812 — 97% saving প্রতি
repeat-visit-এ; CDN-scale-এ এই dance-ই origin-bandwidth-bill ভাগ করে।
304 নিজেই design-lesson: ‘no change’ একটা first-class উত্তর।
ছবিতে
তিন version, এক ছবি — object-গুলো কীভাবে যায়
HTTP/1.1 keep-alive (serial): time ▶
conn: GET1▶ ◀R1 GET2▶ ◀R2 GET3▶ ◀R3 GET4▶ ◀R4 GET5▶ ◀R5 GET6▶ ◀R6
└────── প্রতি object = 1 RTT ──────────────────────────────┘
6 RTT total (1 conn)
HTTP/1.1 pipelining (dead dream):
conn: GET1 GET2 GET3 GET4 GET5 GET6 ▶ ◀R1..........◀R6
↑
R1 slow → পেছনের ৫ ready-ও আটকে
= application-level HOL → browsers বর্জন
HTTP/2 multiplexing (1 conn):
conn: H1 H3 H5 H2 H6 H4 D1 D3 D5 D2 D4 D6 ...
stream-3 slow? 1,5,2,6 এগোচ্ছে ≈2 RTT
TCP underneath (সব version):
loss on stream-3's data → retransmit →
★ h1: শুধু ওই conn থামে ★ h2: সব stream একসাথে থামে ★
(transport-HOL → QUIC)নিজে চালিয়ে দেখুন
curl দিয়ে version-war আর header-forensics
ধাপ ১ — একই URL, দুই version:
curl -sv --http1.1 https://www.wikipedia.org -o /dev/null 2>&1 | grep '< HTTP'
curl -sv --http2 https://www.wikipedia.org -o /dev/null 2>&1 | grep 'ALPN\|< HTTP'
# প্রথমটা: < HTTP/1.1 200 OK
# দ্বিতীয়টা: * ALPN: offers h2 ... < HTTP/2 200TLS-lesson-এর ALPN মনে করুন — version negotiate হয়েছিল handshake-এর ভেতরেই।
ধাপ ২ — header-size comparison (HPACK-জাদু):
for v in http1.1 http2; do
curl -sv --$v https://www.wikipedia.org -o /dev/null 2>&1 |
awk -v V=$v '/> /{req+=length($0)} END{print V": request bytes ≈ "req}'
done
# h1-এ প্রতি request full headers; h2-তে compressedধাপ ৩ — response-header forensics (quiz-style):
curl -sI https://github.com | grep -i 'server\|cache\|strict\|etag'প্রতিটা line-এর গল্প বের করুন: Server: field CDN-নাকি origin-software?
Cache-Control: private, no-cache logged-in-site-এ কেন বাধ্যতামূলক?
Strict-Transport-Security (HSTS) কীভাবে future-downgrade attack আগেই
বন্ধ করে? x-served-by/x-cache থাকলে CDN/reverse-proxy signature —
পরের-পরের লেসনের preview!
ধাপ ৪ — 304 dance live:
etag=$(curl -sI https://httpbin.org/etag/test | awk -F'"' 'NR==1{print $2}')
curl -sv -H "If-None-Match: \"$etag\"" https://httpbin.org/etag/test -o /dev/null 2>&1 | grep '< HTTP'
# < HTTP/2 304 ← নিজে হাতে conditional-request!একই URL দুই version-এ negotiation, header-size আর timing-এ পার্থক্য হাতে-নাগাল; popular-site-এর response-headers পড়ে production caching/security posture পড়া যায়।
নিজে বানান
rawhttp.py — library-ছাড়া HTTP client: GET, parse, ETag-flow
- socket.create_connection দিয়ে TCP connect
- Request bytes হাতে: request line + Host + Connection: close + blank line
- recv loop — Content-Length পর্যন্ত জমাও
- Status line + headers parse — CRLF split, case-insensitive
- ETag ধরে If-None-Match revalidation — 304 যাচাই
HTTP আসলে কত ছোট — এই client প্রমাণ (~৮০ লাইন, stdlib):
#!/usr/bin/env python3
"""rawhttp.py — stdlib-socket-এ হাতে-লেখা HTTP/1.1 client"""
import socket
def request(host, path='/', extra_headers=None, port=80):
sock = socket.create_connection((host, port), timeout=10)
try:
lines = [f'GET {path} HTTP/1.1', f'Host: {host}',
'Connection: close', '']
for k, v in (extra_headers or {}).items():
lines.insert(2, f'{k}: {v}')
raw = ('\r\n'.join(lines) + '\r\n\r\n').encode()
print(f'--- পাঠালাম ({len(raw)} bytes) ---')
print(raw.decode(), end='')
chunks = b''
while True:
part = sock.recv(65536)
if not part:
break
chunks += part
finally:
sock.close()
head, _, body = chunks.partition(b'\r\n\r\n')
status_line, *header_lines = head.decode('latin1').split('\r\n')
version, code, reason = status_line.split(' ', 2)
headers = {}
for h in header_lines:
name, _, value = h.partition(':')
headers[name.strip().lower()] = value.strip()
clen = int(headers.get('content-length', len(body)))
return {'code': int(code), 'headers': headers,
'body': body[:clen], 'raw_size': len(chunks)}
if __name__ == '__main__':
host, port = '127.0.0.1', 8000
# fixture server চালু রাখুন: python3 fixture_server.py
r1 = request(host, '/', port=port)
print('--- উত্তর ---')
print(f'status : {r1["code"]}')
etag = r1['headers'].get('etag', '(etag নেই)')
print(f'etag : {etag} size: {r1["raw_size"]} bytes')
r2 = request(host, '/', port=port,
extra_headers={'If-None-Match': etag})
print('revalidate with If-None-Match:')
print(f'status : {r2["code"]} size: {r2["raw_size"]} bytes')
saved = r1['raw_size'] - r2['raw_size']
pct = 100 * saved / max(r1['raw_size'], 1)
print(f'304-dance savings: {saved} bytes ({pct:.0f}% less)')Fixture server (ETag-সহ, ১৫ লাইন):
# fixture_server.py
import hashlib, http.server
BODY = b'hello from the fixture server - ' + bytes(range(256)) * 4
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
etag = hashlib.md5(BODY).hexdigest()
if self.headers.get('If-None-Match') == etag:
self.send_response(304); self.send_header('ETag', etag)
self.end_headers(); return
self.send_response(200)
self.send_header('ETag', etag)
self.send_header('Content-Length', str(len(BODY)))
self.end_headers(); self.wfile.write(BODY)
def log_message(self, *a): pass
http.server.HTTPServer(('127.0.0.1', 8000), Handler).serve_forever()Output:
--- উত্তর ---
status : 200
etag : 9a0364b9e99bb480dd25e1f0284c8555 size: 1489 bytes
revalidate with If-None-Match:
status : 304 size: 122 bytes
304-dance savings: 1367 bytes (92% less)HTTPS-এ একই করতে socket-কে ssl.SSLContext().wrap_socket() — TLS-lesson-
এর stack মনে করুন। নিজে বাড়ান: POST+Content-Length; chunked-decoding;
duplicate-header এলে আপনার parser-এর কী হয় — নিজেই আবিষ্কার করুন।
বাস্তব সিস্টেমে
HTTP-র খরচ-হিসাব বাস্তবে
Domain-sharding-এর মৃত্যু। img1..img6.example.com ছিল h1-এর
6-connection cap ফাঁদ-পারের trick — h2-তে anti-pattern: এক multiplexed
connection-ই optimal, sharding-এ HPACK dynamic-table ভাঙে। Migration
checklist-গুলোতে এই line-ই সবচেয়ে ভুল-হওয়া।
Header-bloat সংখ্যায়। Modern-app request-এ cookie+tracking+security headers 2-8 KB — mobile-uplink-এ দামি। HPACK + cookie-trimming + Brotli — page-weight-war-এর প্রথম সারি; measurement: median request-header h1-এ ~450 byte, h2 HPACK-এ ~120।
Server-push-এর কবর। Chrome telemetry: pushed-resource ~41% কখনো
ব্যবহৃত হয়নি। যে জিতল: 103 Early Hints — server hint দেয় (‘CSS
এগুলো লাগবে’), content ধাক্কা দেয় না; client cache-aware ভাবে নিজে
চেয়ে নেয়। Speculation→suggestion — textbook API-pivot।
Caching = billion-dollar। Wikipedia text-page hit-ratio 90%+ —
origin-database কাজ করে কারণ edge ৯০% প্রশ্নের উত্তর দেয়। Cache-Control/ETag
দুই header-ই সেই economics-এর control-panel — CDN-lesson-এ geo-scale।
যে ভুলগুলো সবাই করে
“HTTP/2 সবসময় HTTP/1.1-এর চেয়ে দ্রুত।”
Measure না করে দাবি বিপজ্জনক। Single-large-object, low-loss — পার্থক্য নগণ্য (multiplexing-লাভ শূন্য)। High-loss-এ h2 কখনো h1-এর চেয়ে খারাপও: h1-এর ৬ connection-এ loss এক connection থামায়, h2-এর এক connection-এ loss সবাইকে (transport-HOL)। ৬× parallel slow-start-ও উল্টো খেলতে পারে। Version-choice = performance-decision, dogma নয়।
“HTTP stateless মানে server কোনো state রাখতে পারে না।”
মানে সীমাবদ্ধ-precise: protocol-level-এ এক request অন্যটার উপর নির্ভর করে না — প্রতিটা self-contained। Server ভেতরে যা খুশি state (session-store, db) — শর্ত: সেই state-এর reference client-ই পাঠায় (cookie/token)। Fielding-এর design-goal: reliability+scalability — proxy/cache যে-কোনো replica-তে পাঠাতে পারে। Statelessness = visible-contract, memory-less-server নয়।
“404 মানে server ভেঙে পড়েছে।”
404 একটা সফল, সঠিক উত্তর: ‘চাওয়া resource এখানে নেই’ — processing নিখুঁত সম্পন্ন। Class-পড়া: 4xx = request-এর সমস্যা (server healthy), 5xx = server fail। Monitoring-এ এই ভাগ জীবন-মরণ: 404-rate বাড়া = broken-links/scanner; 500-rate বাড়া = rollback এখনই। Status-class-reading ops-literacy-র প্রথম অক্ষর।
“Cookie-ই একমাত্র state-mechanism — token আলাদা জিনিস।”
JWT/session-token যে-path-এই যাক — cookie বা Authorization: Bearer
header — মূলনীতি এক: client-carried credential প্রতিটা self-contained
request-এ। পার্থক্য policy-র: cookie automatic (SameSite নিয়ন্ত্রণে),
CSRF-classic-surface; header explicit — XSS-ঝুঁকি বেশি, CSRF প্রায়
শূন্য। Transport-choice; state-model দুটোতেই এক।
বুঝেছেন কি না দেখুন
1Pipelining-এর ordering-requirement না থাকলে কী হতো? নিয়মটা ঠিক কাদের
বাঁচায়?
যুক্তি
Response-গুলো request-এর সাথে match করার উপায় হারাত — h1-message-এ কোনো ID নেই, response identify হয় position-এই। h2-এর stream-ID-ই সেই সমাধান — interleaving-এর প্রথম শর্ত। নিয়মটা বাঁচায় simple-parser-দের: identity-by-position (simple, serial) বনাম explicit-identity (parallel, demux-complex) — design-tension-এর পাঠ।
2max-age=3600 CSS deploy-এর পর বদলাল। User-cache-এ পুরনোটা — কতক্ষণে
নতুন পাবে? Production-solution কী?
প্রয়োগ
max-age=3600 CSS deploy-এর পর বদলাল। User-cache-এ পুরনোটা — কতক্ষণে
নতুন পাবে? Production-solution কী?সর্বোচ্চ ১ ঘণ্টা — max-age চলাকালীন request-ই যায় না। Solution:
cache-busting via URL — app.a1b2c3.css (content-hash filename) +
max-age=31536000, immutable; HTML-এ new-hash reference, HTML নিজে
no-cache। Immutable-asset long-TTL + volatile-document no-cache —
golden pattern; content-change instant (HTML বদলাতেই)।
3HPACK dynamic-table কেন connection-scoped? Domain-sharding এর কী ক্ষতি?
যুক্তি
Table-এর মূল্য স্মৃতিতে — আগের request-এর header-value মনে রেখে index-reference। Connection-lifetime জুড়ে share হলে hit-rate বেশি; per-request reset-এ compression প্রায় শূন্য। Sharding = প্রতি hostname আলাদা connection = আলাদা empty-table = full-headers আবার — h1-era optimization h2-era-তে anti-pattern। Layer-assumption বদলালে optimization পুনঃমূল্যায়ন বাধ্যতামূলক।
4Real-time dashboard — ৫ সেকেন্ডে update। Polling, long-polling, SSE,
WebSocket-এর খরচ বিশ্লেষণ করুন; WS কবে জেতে?
ডিজাইন
Polling: প্রতি ৫-এ full-request; idle-ও খরচ; latency ≤5s; simplest। Long-polling: server ধরে রাখে — latency কমে, কিন্তু per-update reconnect+headers, hanging-request state। SSE: এক persistent server-push-stream, auto-reconnect built-in — one-way feed-এ perfect (dashboard!)। WS: bidirectional, ~6-byte frame — জেতে client-ও frequent পাঠালে (chat, game, cursors)। Dashboard read-heavy → SSE যথেষ্ট। প্রশ্নটা নিজেকে করুন: upstream-এ কি সত্যিই frequent traffic?
5h2-এ এক stream-এর lifecycle-এ কোন frame-type, connection-setup-এ কোনগুলো?
স্মরণ
Connection-level: SETTINGS (+ACK)। Stream-level: HEADERS (pseudo-headers),
DATA, ঐচ্ছিক WINDOW_UPDATE, cancel-এ RST_STREAM; keepalive PING,
shutdown GOAWAY। সব binary frame, প্রতিটা stream-ID-বাহী (connection-mgmt
frames বাদে)।
এরপর কী
এরপর কী
Application-level HOL মরেছে, কিন্তু TCP-র ordered-bytestream এক packet-loss- কেও সব-stream-stall বানায়। Kernel-TCP বদলানো প্রায় অসম্ভব — কিন্তু UDP- র উপর userspace-এ নতুন transport লেখা সম্ভব। পরের লেসনে QUIC — কেন TCP+TLS নতুন-করে-লেখাই একমাত্র পথ, connection-ID-র roam-magic, আর Google/Meta/Cloudflare কেন kernel bypass করল।
আরও পড়ুন
- RFC 9110 — HTTP Semantics — R. Fielding, M. Nottingham, J. Reschke (eds.) · Methods, status codes, caching, cookies-র আনুষ্ঠানিক সংজ্ঞা — version-neutral document
- RFC 9113 — HTTP/2 — M. Thomson, C. Benfield (eds.) · Binary framing layer, stream multiplexing, HPACK — wire format-এর specification
- Architectural Styles and the Design of Network-based Software Architectures (Fielding dissertation, 2000) — Roy T. Fielding · REST-এর জন্ম — statelessness আর uniform interface-এর design rationale