সরাসরি পর্বে যান
পর্ব ৩৭ · ৬২-এর মধ্যে · মডিউল ৬

HTTP/2 আর HTTP/3 — multiplexing, header compression আর QUIC

HTTP/2 and HTTP/3 — Binary Framing, Streams & Multiplexing, Flow Control, HPACK, Server Push, QPACK and HTTP over QUIC

Taught in Bangla, this lesson explains how HTTP/2 and HTTP/3 fix HTTP/1.1's one-request-per-connection bottleneck: binary framing, streams and multiplexing over one TCP connection, stream flow control, HPACK header compression and why not gzip, the decline of server push, TCP head-of-line blocking, and HTTP/3 with QPACK running over QUIC.

এই পর্বে যা শিখবেন

  • HTTP/1.1 কেন আধুনিক ওয়েবপাতায় ধীর, আর ডেভেলপাররা কোন কোন কৌশলে তা সামলাতেন
  • SPDY থেকে HTTP/2-এর ইতিহাস, ALPN আর connection preface, আর ৯ বাইটের frame header হাতে পড়া
  • Stream, multiplexing, priority (পুরোনো tree আর নতুন Extensible Priorities) আর দুই স্তরের flow control
  • HPACK-এর static table, dynamic table, N-bit prefix integer আর Huffman — বাইট গুনে গুনে
  • Server push কেন ব্যর্থ হলো, HTTP/3 কীভাবে QUIC-এর ওপর বসে, QPACK কেন দরকার, আর বাস্তবে কীভাবে protocol যাচাই করবেন

১ · HTTP/1.1 কোথায় আটকায় — একটি পাতা, শত ফাইল

পর্ব ৩৬-এ (HTTP) আপনি HTTP/1.1-এর request আর response দেখেছেন: GET /index.html HTTP/1.1, তারপর header, তারপর উত্তর। ১৯৯০-এর দশকে একটি ওয়েবপাতা মানে ছিল একটি HTML ফাইল আর কয়েকটি ছবি। আজ একটি সাধারণ সংবাদ বা কেনাকাটার সাইট খুলতে ব্রাউজার কয়েক ডজন থেকে শতাধিক ফাইল আনে: HTML, কয়েকটি CSS, অনেকগুলো JavaScript, font, ছবি, আইকন। এখানেই HTTP/1.1-এর নকশা ভারী হয়ে ওঠে।

সমস্যা ১: একটি সংযোগে একসাথে একটিই অনুরোধ

HTTP/1.1-এ একটি persistent connection অনেক অনুরোধে ব্যবহার করা যায়, কিন্তু পালাক্রমে: একটি অনুরোধ পাঠান, পুরো উত্তর আসা পর্যন্ত অপেক্ষা করুন, তারপর পরেরটি। প্রতিটি ফাইলে অন্তত একটি RTT চলে যায় শুধু অনুরোধ যাওয়া আর উত্তর শুরু হওয়ায়। RTT ৮০ মিলিসেকেন্ড হলে ৩০টি ফাইল একটি সংযোগে আনতে শুধু অপেক্ষাতেই ২.৪ সেকেন্ড যায়।

ভাবুন একটি ডাকঘরের একটিমাত্র কাউন্টারে আপনার ৩০টি চিঠি, আর প্রতিটির জন্য আলাদা করে লাইনে দাঁড়াতে হচ্ছে। Pipelining মানে সব চিঠি একসাথে জমা দেওয়া যায়, কিন্তু রসিদ মিলবে জমার ক্রমে; প্রথমটি রেজিস্ট্রি ডাক হলে বাকি ২৯টির রসিদ তৈরি থাকলেও আটকে থাকে। ৬টি সংযোগ মানে ৬টি কাউন্টার, প্রতিটির আলাদা লাইন আর খরচ। HTTP/2 হলো একটি কাউন্টার, যেখানে সব চিঠি টোকেন নম্বরসহ একসাথে জমা হয়, আর যেটির কাজ আগে শেষ, সেটির রসিদ আগে।

সমস্যা ২: pipelining-এর head-of-line blocking

HTTP/1.1 মানে pipelining রেখেছিল: উত্তরের অপেক্ষা না করে কয়েকটি অনুরোধ পরপর পাঠানো যায়। কিন্তু উত্তর আসতে হবে অনুরোধের ক্রমেই, কারণ উত্তরে এমন কোনো চিহ্ন নেই যা বলে এটি কোন অনুরোধের উত্তর। প্রথম উত্তরটি ধীর হলে (ধরুন সার্ভার ডেটাবেস থেকে বড় একটি পাতা বানাচ্ছে) পেছনের সব উত্তর তৈরি থাকলেও আটকে থাকে। এটা application স্তরের head-of-line blocking. তার ওপর কিছু proxy আর পুরোনো সার্ভার pipelined অনুরোধ ভুলভাবে সামলাত। ফলে প্রধান ব্রাউজারগুলো হয় কখনো ডিফল্টে pipelining চালু করেনি, নয়তো পরে সরিয়ে দিয়েছে।

সমস্যা ৩: সংযোগ বাড়ানোর কৌশল, আর তাদের দাম

উপায় না পেয়ে ব্রাউজার একই সার্ভারে একাধিক TCP সংযোগ খুলতে লাগল। প্রচলিত সীমা প্রতি host-এ ৬টি। ডেভেলপাররা আরও এগোলেন:

কৌশলকী করা হতোদাম
Domain shardingছবি রাখা হতো img1.example.com, img2.example.com-এর মতো কয়েকটি নামে, যাতে প্রতি নামে আরও ৬টি সংযোগ খোলা যায়প্রতিটি নামে বাড়তি DNS lookup আর TCP + TLS handshake; congestion control একে অপরের সাথে লড়াই করে
Sprite sheetঅনেক ছোট আইকন একটি বড় ছবিতে জোড়া; CSS দিয়ে প্রয়োজনীয় অংশ দেখানোএকটি আইকন বদলালে পুরো ছবি নতুন করে ডাউনলোড; অব্যবহৃত আইকনও আসে
Concatenationসব JavaScript একটি বড় ফাইলে, সব CSS একটিতেএক লাইন বদলালে পুরো ফাইলের cache বাতিল; প্রথম পাতাতেই অপ্রয়োজনীয় কোড
Inliningছোট ছবি বা CSS সরাসরি HTML-এর ভেতরে (data URI)আলাদা করে cache হয় না; প্রতি পাতায় আবার আসে

সমস্যা ৪: প্রতিবার একই লম্বা header

HTTP/1.1-এর header সাধারণ লেখা (text). একটি পাতার ৮০টি অনুরোধের প্রতিটিতে প্রায় একই User-Agent, Accept, Accept-Language আর কয়েকশো বাইটের Cookie যায়। নতুন TCP সংযোগের congestion window শুরুতে ছোট (পর্ব ৩২), তাই ধীর upload-এর মোবাইল নেটওয়ার্কে এই পুনরাবৃত্ত header একাই প্রথম কয়েকটি RTT খেয়ে ফেলতে পারে।

মূল ধারণা

সমস্যাটা HTTP-র অর্থে নয়। GET, POST, 200, 404, Cache-Control সবই ঠিক আছে। সমস্যা হলো তারে পাঠানোর ধরনে: text বার্তা, একটির পর একটি, প্রতিবার সব header নতুন করে। HTTP/2 ঠিক এই জায়গাটাই বদলায়, অর্থ একই রেখে।

২ · SPDY থেকে HTTP/2 — একই ভাষা, নতুন খাম

২০০৯ সালে Google SPDY (উচ্চারণ “স্পিডি”) নামে একটি পরীক্ষামূলক protocol ঘোষণা করে। মূল ধারণা ছিল HTTP-র অর্থ একই রেখে একটি TLS সংযোগে অনেক অনুরোধ binary frame-এ পালাক্রমে চালানো, আর header সংকোচন করা। অন্য ব্রাউজার আর বড় সাইটও এটি গ্রহণ করে। ২০১২ সালে IETF-এর HTTP working group নতুন HTTP-র ভিত্তি হিসেবে SPDY বেছে নেয়। মে ২০১৫-এ প্রকাশিত হয় HTTP/2 (RFC 7540) আর তার header সংকোচন HPACK (RFC 7541). এরপর Chrome ২০১৬ সালের দিকে SPDY সমর্থন তুলে নেয়। ২০২২ সালের জুনে HTTP-র মানগুলো নতুন করে সাজানো হয়: অর্থ RFC 9110-এ, HTTP/1.1 RFC 9112-এ, আর HTTP/2-এর হালনাগাদ সংস্করণ RFC 9113, যা RFC 7540-কে প্রতিস্থাপন করে।

HTTP/2 বুঝতে প্রথম কথা: HTTP/2 নতুন কোনো HTTP নয়, পুরোনো HTTP-র নতুন খাম। একটি GET অনুরোধ এখনো GET, উত্তর এখনো 200 বা 404, header-এর নাম আর অর্থ একই। বদলেছে শুধু এগুলো:

  • Request line আর status line নেই, তাদের জায়গায় pseudo-header: :method, :scheme, :authority (আগের Host), :path, আর উত্তরে :status. নামের আগে কোলন থাকে, যাতে সাধারণ header-এর সাথে না মেশে।
  • Header-এর নাম ছোট হাতের অক্ষরে (content-type), আর Connection, Keep-Alive, Transfer-Encoding-এর মতো সংযোগ-নির্দিষ্ট header নিষিদ্ধ। সংযোগ চালানো এখন frame-এর কাজ।
  • সব কিছু binary frame-এ যায়। লেখা নয়, নির্দিষ্ট মাপের ঘরে সংখ্যা, তাই পার্স করা দ্রুত আর নির্ভুল।

কীভাবে বোঝা যায় দুই পক্ষ HTTP/2 বলবে — ALPN

একই port 443-এ HTTP/1.1 আর HTTP/2 দুটোই চলে। তাহলে সংযোগের শুরুতে কে কোনটি বলবে, সেটা ঠিক হয় কীভাবে? উত্তর ALPN (Application-Layer Protocol Negotiation, RFC 7301). TLS handshake-এর প্রথম বার্তা ClientHello-তে ব্রাউজার একটি তালিকা পাঠায়, যেমন ["h2", "http/1.1"]. সার্ভার ServerHello-র সাথে একটি বেছে জানায়। এতে বাড়তি কোনো RTT লাগে না, কারণ handshake তো এমনিতেই হচ্ছে (TLS-এর বিস্তারিত পর্ব ৪৮-এ)। এনক্রিপশন ছাড়া HTTP/2-কে বলে h2c. কোনো প্রধান ব্রাউজার এটি চালায় না, তাই h2c দেখা যায় মূলত ডেটা সেন্টারের ভেতরের সংযোগে।

Connection preface — প্রথম ২৪ বাইট

ALPN-এ “h2” ঠিক হওয়ার পর ক্লায়েন্ট প্রথমে একটি নির্দিষ্ট ২৪ বাইটের লেখা পাঠায়, যার নাম connection preface: PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n. এরপরই একটি SETTINGS frame. সার্ভারও তার প্রথম বার্তা হিসেবে SETTINGS পাঠায়। অদ্ভুত এই লেখার উদ্দেশ্য: ভুল করে কোনো HTTP/1.1 সার্ভার এটি পেলে PRI নামের অজানা method দেখে ত্রুটি দেবে, বৈধ অনুরোধ ভেবে কাজ শুরু করবে না।

নিচের C প্রোগ্রাম একই কম্পিউটারের ভেতরে একটি socketpair (দুই প্রান্ত জোড়া দেওয়া একটি byte-stream চ্যানেল) বানায়। এক প্রান্ত ক্লায়েন্টের মতো preface, একটি SETTINGS আর একটি PING frame লেখে। অন্য প্রান্ত সার্ভারের মতো পড়ে preface যাচাই করে, তারপর প্রতিটি frame-এর ৯ বাইটের header পড়ে। এখানে TLS বা আসল নেটওয়ার্ক নেই, শুধু বাইটের বিন্যাস দেখানো উদ্দেশ্য। socket নিয়ে বিস্তারিত পর্ব ৪১-এ।

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>

static const char PREFACE[] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";   /* 24 bytes, RFC 9113 sec 3.4 */

int main(void) {
    int sv[2];
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); return 1; }
    /* client side: preface + SETTINGS frame (MAX_CONCURRENT_STREAMS=100) + PING frame */
    unsigned char out[64];
    size_t n = strlen(PREFACE);
    memcpy(out, PREFACE, n);
    unsigned char frames[] = { 0,0,6, 0x04, 0x00, 0,0,0,0,  0x00,0x03, 0,0,0,100,
                               0,0,8, 0x06, 0x00, 0,0,0,0,  'a','b','c','d','e','f','g','h' };
    memcpy(out + n, frames, sizeof frames);
    n += sizeof frames;
    if (write(sv[0], out, n) != (ssize_t)n) { perror("write"); return 1; }
    printf("client wrote %zu bytes\n", n);

    /* server side: check the preface, then read frames one 9-byte header at a time */
    unsigned char in[64];
    ssize_t got = read(sv[1], in, sizeof in);
    printf("server read %zd bytes, preface %s\n", got, memcmp(in, PREFACE, 24) == 0 ? "OK" : "BAD");
    for (ssize_t i = 24; i + 9 <= got; ) {
        unsigned len = (unsigned)in[i] << 16 | (unsigned)in[i + 1] << 8 | in[i + 2];
        unsigned sid = ((unsigned)in[i + 5] << 24 | (unsigned)in[i + 6] << 16 | (unsigned)in[i + 7] << 8 | in[i + 8]) & 0x7fffffffu;
        printf("frame type=0x%02x flags=0x%02x stream=%u length=%u\n", in[i + 3], in[i + 4], sid, len);
        i += 9 + (ssize_t)len;
    }
    close(sv[0]); close(sv[1]);
    return 0;
}

আউটপুট:

client wrote 56 bytes
server read 56 bytes, preface OK
frame type=0x04 flags=0x00 stream=0 length=6
frame type=0x06 flags=0x00 stream=0 length=8

মোট ৫৬ বাইট = preface ২৪ + SETTINGS (৯ বাইটের header + ৬ বাইটের একটি setting) ১৫ + PING (৯ + ৮) ১৭. সার্ভার প্রথম ২৪ বাইট মিলিয়ে “OK” পেল। এরপর সে আর লেখা পড়ে না, শুধু ৯ বাইটের header থেকে দৈর্ঘ্য জেনে পরের frame-এ লাফ দেয়। এটাই binary framing-এর সুবিধা: HTTP/1.1-এর মতো লাইনের শেষ খোঁজা বা Content-Length পার্স করার দরকার নেই। Type 0x04 মানে SETTINGS, 0x06 মানে PING. দুটোই stream 0-তে, কারণ এরা পুরো সংযোগের বার্তা, কোনো একটি অনুরোধের নয়। বাস্তবে সার্ভার প্রতিটি SETTINGS-এর উত্তরে ACK flag দেওয়া একটি খালি SETTINGS, আর PING-এর উত্তরে একই ৮ বাইটের ACK-সহ PING ফেরত পাঠায়। PING দিয়ে RTT মাপা যায়, সংযোগ বেঁচে আছে কি না তা-ও দেখা যায়।

৩ · Binary frame — ৯ বাইটের header হাতে পড়া

HTTP/2-এর প্রতিটি frame একই ৯ বাইটের header দিয়ে শুরু হয় (RFC 9113 §4.1). সব সংখ্যা network byte order-এ থাকে, মানে বড় অংশ আগে:

HTTP/2 frame header (৯ বাইট)
┌───────────────────────────────────────────┐
│ Length (24 bit) — payload কত বাইট           │  বাইট 0–2
├─────────────────────┬─────────────────────┤
│ Type (8 bit)        │ Flags (8 bit)       │  বাইট 3, 4
├─┬───────────────────┴─────────────────────┤
│R│ Stream Identifier (31 bit)              │  বাইট 5–8, R = সংরক্ষিত bit
├─┴─────────────────────────────────────────┤
│ Payload (Length বাইট) …                    │
└───────────────────────────────────────────┘

Length ২৪ bit হলেও ডিফল্টে কোনো পক্ষ ১৬,৩৮৪ বাইটের (২১৪) বড় payload পাঠাতে পারে না। প্রাপক SETTINGS-এ MAX_FRAME_SIZE বাড়িয়ে দিলে সর্বোচ্চ ২২৪ − ১ পর্যন্ত যাওয়া যায়। ছোট frame-এর সুবিধা হলো, একটি বড় ফাইল সংযোগ দখল করে রাখতে পারে না। তার frame-এর ফাঁকে ফাঁকে অন্য stream-এর frame ঢুকে যেতে পারে। Frame-এর ধরনগুলো:

TypeনামকাজStream
0x0DATAবার্তার body; END_STREAM flag মানে এই দিকের শেষ১ বা বেশি
0x1HEADERSHPACK-এ সংকুচিত header; নতুন stream খোলে১ বা বেশি
0x2PRIORITYপুরোনো priority tree-র তথ্য; এখন অপ্রচলিত১ বা বেশি
0x3RST_STREAMএকটি stream বাতিল (যেমন CANCEL); সংযোগ অক্ষত১ বা বেশি
0x4SETTINGSসংযোগের নিয়ম জানানো; ACK flag দিয়ে স্বীকার০
0x5PUSH_PROMISEসার্ভার জানায় সে নিজে থেকে একটি উত্তর পাঠাবে (server push)১ বা বেশি
0x6PING৮ বাইট পাঠানো, হুবহু ফেরত; RTT মাপা আর সংযোগ সচল রাখা০
0x7GOAWAYসংযোগ বন্ধের ঘোষণা: শেষ কোন stream পর্যন্ত কাজ হয়েছে, আর ত্রুটির কোড০
0x8WINDOW_UPDATEflow control-এর জানালা বাড়ানো (অংশ ৫)০ বা stream
0x9CONTINUATIONএকটি HEADERS-এ না আঁটলে বাকি header১ বা বেশি

Flag-এর অর্থ frame-এর ধরনের ওপর নির্ভর করে। 0x1 মানে DATA আর HEADERS-এ END_STREAM, কিন্তু SETTINGS আর PING-এ ACK. 0x4 মানে END_HEADERS (header এখানেই শেষ, CONTINUATION আসবে না), 0x8 মানে PADDED, 0x20 মানে PRIORITY. SETTINGS, PING আর GOAWAY সবসময় stream 0-তে।

বাইট: 00 00 11 │ 01 │ 05 │ 00 00 00 01 │ payload …
কোন বাইট
bit-এ
অর্থ
মোট পড়া
  1. Length: প্রথম তিন বাইট। 00 00 11 একসাথে একটি ২৪ bit-এর সংখ্যা, 0x11 = ১৬ + ১ = ১৭. অর্থাৎ ৯ বাইটের header-এর পরে ঠিক ১৭ বাইট payload আসবে। প্রাপক এটুকু জেনেই বোঝে পরের frame কোথা থেকে শুরু।
  2. Type: চতুর্থ বাইট 0x01. টেবিল অনুযায়ী 0x1 মানে HEADERS. এটি একটি নতুন অনুরোধ, বা একটি উত্তরের header.
  3. Flags: পঞ্চম বাইট 0x05. বাইনারিতে 0000 0101, তাই 0x01 আর 0x04 দুটো bit চালু। HEADERS-এ 0x01 মানে END_STREAM: এই অনুরোধে আর কোনো body আসবে না, যেমন একটি সাধারণ GET. 0x04 মানে END_HEADERS: header এই frame-এই শেষ, পেছনে CONTINUATION নেই।
  4. Stream ID: শেষ চার বাইট। সবচেয়ে ওপরের bit (R) সংরক্ষিত, পড়ার সময় মুছে ফেলা হয়। বাকি ৩১ bit-এ মান 1. বিজোড় নম্বর মানে ক্লায়েন্ট stream-টি খুলেছে। নতুন সংযোগে এটাই ক্লায়েন্টের প্রথম অনুরোধ।
  5. Payload: HPACK-এ সংকুচিত header. 0x82, 0x87, 0x84-এর প্রথম bit 1, মানে static table-এর সারি: 2 = :method GET, 7 = :scheme https, 4 = :path /. 0x41 মানে “:authority নামের সাথে নতুন মান”. 0x8c-তে H bit = 1 (Huffman) আর দৈর্ঘ্য ১২, তারপর ১২ বাইটে www.example.com. পুরো অনুরোধ ২৬ বাইট (বিস্তারিত অংশ ৬-এ)।

এবার একই কাজ কোডে। নিচের parser কয়েকটি hex frame পড়ে ধরন, flag, stream আর দৈর্ঘ্য ছাপে। SETTINGS, WINDOW_UPDATE, RST_STREAM আর GOAWAY-এর payload-ও খুলে দেখায়। শেষ লাইনে উল্টো কাজ: সংখ্যা থেকে ৯ বাইটের header বানানো।

// HTTP/2 frame header (RFC 9113 sec 4.1): Length 24 bits | Type 8 | Flags 8 | R 1 + Stream ID 31
const TYPES = ["DATA","HEADERS","PRIORITY","RST_STREAM","SETTINGS","PUSH_PROMISE","PING","GOAWAY","WINDOW_UPDATE","CONTINUATION"];
const SET = ["", "HEADER_TABLE_SIZE", "ENABLE_PUSH", "MAX_CONCURRENT_STREAMS", "INITIAL_WINDOW_SIZE", "MAX_FRAME_SIZE"];
const ERR = { 0: "NO_ERROR", 1: "PROTOCOL_ERROR", 8: "CANCEL" };
const u32 = (b, i) => ((b[i] << 24) >>> 0) + (b[i+1] << 16) + (b[i+2] << 8) + b[i+3];

function parse(hex) {
  const b = hex.replace(/ /g, "").match(/../g).map(h => parseInt(h, 16));
  const len = (b[0] << 16) | (b[1] << 8) | b[2], type = b[3], flags = b[4];
  const sid = u32(b, 5) & 0x7fffffff;                 // drop the reserved top bit
  const p = b.slice(9, 9 + len);                      // payload
  const names = { 1: (type === 4 || type === 6) ? "ACK" : "END_STREAM", 4: "END_HEADERS", 8: "PADDED", 32: "PRIORITY" };
  const fl = [1, 4, 8, 32].filter(f => flags & f).map(f => names[f]).join("|") || "-";
  let info = "";
  if (type === 4) for (let i = 0; i < len; i += 6) info += ` ${SET[(p[i] << 8) | p[i+1]]}=${u32(p, i+2)}`;
  if (type === 8) info = ` increment=${u32(p, 0) & 0x7fffffff}`;
  if (type === 7) info = ` last_stream=${u32(p, 0) & 0x7fffffff} ${ERR[u32(p, 4)]}`;
  if (type === 3) info = ` ${ERR[u32(p, 0)]}`;
  if (type === 0) info = ` "${String.fromCharCode(...p)}"`;
  console.log(`${TYPES[type].padEnd(13)} len=${String(len).padEnd(3)} flags=0x${flags.toString(16).padStart(2, "0")} ${fl.padEnd(22)} stream=${sid}${info}`);
}
function build(len, type, flags, sid) {               // the reverse: 9 header bytes
  const b = [len >> 16 & 255, len >> 8 & 255, len & 255, type, flags, sid >>> 24 & 127, sid >> 16 & 255, sid >> 8 & 255, sid & 255];
  return b.map(x => x.toString(16).padStart(2, "0")).join(" ");
}
parse("00000c 04 00 00000000 0003 00000064 0004 0000ffff");   // SETTINGS from the client
parse("000000 04 01 00000000");                              // SETTINGS ACK
parse("000011 01 05 00000001 828784418cf1e3c2e5f23a6ba0ab90f4ff"); // HEADERS (HPACK block)
parse("000005 00 01 00000001 68656c6c6f");                   // DATA "hello", last frame of stream 1
parse("000004 08 00 00000000 00ef0001");                     // WINDOW_UPDATE for the connection
parse("000004 03 00 00000003 00000008");                     // RST_STREAM on stream 3
parse("000008 07 00 00000000 00000005 00000000");            // GOAWAY
console.log("build(16384, DATA, END_STREAM, stream 7) ->", build(16384, 0, 1, 7));

আউটপুট:

SETTINGS      len=12  flags=0x00 -                      stream=0 MAX_CONCURRENT_STREAMS=100 INITIAL_WINDOW_SIZE=65535
SETTINGS      len=0   flags=0x01 ACK                    stream=0
HEADERS       len=17  flags=0x05 END_STREAM|END_HEADERS stream=1
DATA          len=5   flags=0x01 END_STREAM             stream=1 "hello"
WINDOW_UPDATE len=4   flags=0x00 -                      stream=0 increment=15663105
RST_STREAM    len=4   flags=0x00 -                      stream=3 CANCEL
GOAWAY        len=8   flags=0x00 -                      stream=0 last_stream=5 NO_ERROR
build(16384, DATA, END_STREAM, stream 7) -> 00 40 00 00 01 00 00 00 07

লাইন ধরে: (১) SETTINGS-এ দুটি setting, প্রতিটি ৬ বাইট (২ বাইট id + ৪ বাইট মান), তাই len=12: সর্বোচ্চ ১০০টি stream, আর প্রতিটি stream-এর শুরুর জানালা ৬৫,৫৩৫ বাইট। (২) len=0 আর ACK মানে “SETTINGS পেয়েছি, মেনে নিয়েছি”. (৩) stepper-এর সেই HEADERS frame. (৪) সার্ভারের উত্তর “hello”, END_STREAM-সহ, তাই stream 1 এখন বন্ধ। (৫) WINDOW_UPDATE stream 0-তে, মানে পুরো সংযোগের জানালা ১,৫৬,৬৩,১০৫ বাইট বাড়ল (কিছু ব্রাউজার শুরুতেই এমন বড় মান দেয়)। (৬) stream 3 বাতিল, কোড 8 = CANCEL. বাকি সংযোগ চলতে থাকে; HTTP/1.1-এ একটি অনুরোধ মাঝপথে থামাতে পুরো সংযোগ কাটতে হতো। (৭) GOAWAY: “stream 5 পর্যন্ত কাজ হয়েছে, এরপর নতুন stream নয়”. ক্লায়েন্ট জানে ৫-এর পরের অনুরোধ নিরাপদে নতুন সংযোগে আবার পাঠানো যায়। শেষ লাইন: দৈর্ঘ্য ১৬,৩৮৪ = 0x004000, তাই প্রথম তিন বাইট 00 40 00.

৪ · Stream আর multiplexing — এক সংযোগে অনেক অনুরোধ

HTTP/2-এর stream হলো একটি সংযোগের ভেতরের একটি অনুরোধ-উত্তরের জোড়া। প্রতিটি frame-এর গায়ে stream ID লেখা থাকে, তাই বিভিন্ন stream-এর frame যেকোনো ক্রমে মিশে যেতে পারে। প্রাপক ID দেখে আলাদা করে নেয়। এটাই HTTP স্তরের multiplexing. নিয়মগুলো:

  • ক্লায়েন্টের stream বিজোড় (1, 3, 5, …), সার্ভারের নিজে খোলা stream জোড় (2, 4, …, শুধু server push-এ)। দুই পক্ষ একই নম্বর বেছে নেওয়ার সংঘাত এভাবেই এড়ানো হয়। Stream 0 সংযোগের নিজের জন্য।
  • নম্বর শুধু বাড়ে, আবার ব্যবহার হয় না। নতুন stream-এর ID আগের সব ID-র চেয়ে বড় হতে হয়। ৩১ bit শেষ হয়ে গেলে নতুন সংযোগ খুলতে হয়।
  • একসাথে কতটি stream চলবে, তা প্রাপক SETTINGS_MAX_CONCURRENT_STREAMS দিয়ে ঠিক করে দেয়। প্রায়ই ১০০ বা তার কাছাকাছি একটি মান দেখা যায়।
  • একটি stream বাতিল করতে RST_STREAM, পুরো সংযোগ গুছিয়ে বন্ধ করতে GOAWAY.
  1. ব্রাউজারstream 1, 3, 5তিনটি অনুরোধ
  2. একটি TCP সংযোগframe-এর সারিএকটি byte stream
  3. সার্ভারযেটি তৈরি, সেটি আগেউত্তর পালাক্রমে
তারে কী যাচ্ছে, আর প্রতিটি stream-এর অবস্থা
তারে
stream 1 · index.html
stream 3 · style.css
stream 5 · logo.png
HTTP/1.1 হলে
  1. তিনটি অনুরোধ একসাথে। ধরুন ব্রাউজার আগেই জানে তিনটি ফাইল লাগবে। সে পরপর তিনটি HEADERS পাঠায় stream 1, 3, 5-এ, কোনো উত্তরের অপেক্ষা না করে; তিনটিতেই END_STREAM, কারণ GET-এর body নেই।
  2. যেটি আগে তৈরি, সেটি আগে। style.css একটি স্থির ফাইল, সার্ভারের কাছে তৈরিই ছিল। তাই stream 3-এর HEADERS আর পুরো DATA আগে চলে গেল, যদিও অনুরোধ এসেছিল দ্বিতীয়তে। HTTP/1.1 pipelining-এ এটা অসম্ভব ছিল, কারণ সেখানে উত্তরের ক্রম অনুরোধের ক্রমে বাঁধা।
  3. index.html তৈরি হলো। সার্ভার stream 1-এর header আর প্রথম ১৬ KB-এর DATA frame পাঠাল। ১৬,৩৮৪ বাইটই ডিফল্ট সর্বোচ্চ frame-এর মাপ।
  4. দুটি stream পালাক্রমে। এখন stream 5 (ছবি) আর stream 1 (পাতার বাকিটা) দুটোরই ডেটা আছে। সার্ভার frame-গুলো মিশিয়ে পাঠাচ্ছে। কোনটিকে আগে কতটা দেবে, তা ঠিক হয় priority দিয়ে (নিচে)। ক্লায়েন্ট stream ID দেখে প্রতিটি frame ঠিক ফাইলে জোড়া দেয়।
  5. সব শেষ, একটি সংযোগে। শেষ DATA frame-গুলোতে END_STREAM. একটি handshake, আর তিনটি ফাইলের জন্য এক RTT-র অনুরোধ-ধাপ; HTTP/1.1-এ লাগত তিনটি RTT, নয়তো তিনটি সংযোগ।

কত সময় বাঁচে — একটি সরল মডেল

তিনটি পদ্ধতির পাতা-লোডের সময় তুলনা করা যাক: একটি সংযোগে HTTP/1.1, ৬টি সংযোগে HTTP/1.1, আর একটি সংযোগে HTTP/2. মডেলটি ইচ্ছা করেই সরল রাখা হয়েছে। slow start, সার্ভারের সময়, DNS আর header-এর বাইট বাদ। প্রতিটি সংযোগের শুরুতে TCP + TLS 1.3-এর ২ RTT ধরা হয়েছে। একটি packet হারালে যে TCP সংযোগে হারাল, সেটি প্রায় ১ RTT থেমে থাকে (fast retransmit, পর্ব ৩০)। সেই সংযোগের সারিতে থাকা সব ডেটাও অপেক্ষা করে। এটা আসল ব্রাউজারের মাপ নয়, শুধু প্রবণতা বোঝার জন্য।

// A deliberately SIMPLE page-load model (not a real browser): N objects of equal size on one site.
// Ignores slow start, server time, DNS, header bytes. Setup = TCP + TLS 1.3 handshakes = 2 RTT.
// A loss stalls the TCP connection it hits for about 1 RTT (fast retransmit), and
// everything queued on that connection waits (in-order byte stream).
const N = 30, SIZE = 25000, RTT = 80, MBPS = 10;
const tx = SIZE * 8 / (MBPS * 1000);                  // ms to send one object at full link speed = 20
const setup = 2 * RTT;

function h1single(loss) {                             // 1 connection, one request at a time
  return setup + N * (RTT + tx) + loss * RTT;
}
function h1six(loss) {                                // 6 connections share the link, rounds of 6
  let t = setup, left = N;
  while (left > 0) { const k = Math.min(6, left); t += RTT + k * tx; left -= k; }
  return t + Math.ceil(loss / 6) * RTT;               // losses spread over different connections
}
function h2(loss) {                                   // 1 connection, all N requests sent at once
  return setup + RTT + N * tx + loss * RTT;           // every loss stalls ALL streams
}
console.log(`N=${N} objects x ${SIZE / 1000} KB, RTT=${RTT} ms, link ${MBPS} Mbit/s (one object = ${tx} ms)`);
console.log("losses   HTTP/1.1 x1   HTTP/1.1 x6   HTTP/2 x1   (page complete, ms)");
for (const loss of [0, 1, 3, 6])
  console.log(String(loss).padEnd(9) + [h1single(loss), h1six(loss), h2(loss)].map(v => String(v).padStart(10)).join("    "));
const ideal = setup + RTT + N * tx;
console.log(`lower bound (setup + 1 RTT + all bytes): ${ideal} ms; HTTP/2 with 0 loss reaches it`);

আউটপুট:

N=30 objects x 25 KB, RTT=80 ms, link 10 Mbit/s (one object = 20 ms)
losses   HTTP/1.1 x1   HTTP/1.1 x6   HTTP/2 x1   (page complete, ms)
0              3160          1160           840
1              3240          1240           920
3              3400          1240          1080
6              3640          1240          1320
lower bound (setup + 1 RTT + all bytes): 840 ms; HTTP/2 with 0 loss reaches it

ক্ষতি ছাড়া (প্রথম সারি) একটি সংযোগের HTTP/1.1-এ লাগে ৩১৬০ ms: ৩০টি ফাইলের প্রতিটিতে ৮০ ms অপেক্ষা আর ২০ ms পাঠানো। ৬টি সংযোগে ৫টি দফা লাগে, প্রতিটিতে ৮০ + ৬ × ২০ = ২০০ ms, মোট ১১৬০. HTTP/2 সব অনুরোধ একসাথে পাঠায়, তাই একটি RTT-র পরে link শুধু বাইট পাঠাতে ব্যস্ত থাকে: ১৬০ + ৮০ + ৩০ × ২০ = ৮৪০ ms, এই মডেলের সর্বনিম্ন সম্ভাব্য সময়। এবার loss-এর কলাম দেখুন। HTTP/2-এ প্রতিটি ক্ষতি সব stream থামায়, তাই প্রতিটিতে পুরো ৮০ ms যোগ হয়। ৬টি সংযোগে ক্ষতিগুলো ভিন্ন সংযোগে ছড়িয়ে পড়ে, বাকি সংযোগ চলতে থাকে। ৬টি ক্ষতিতে HTTP/2 (১৩২০) ৬-সংযোগের HTTP/1.1-এর (১২৪০) চেয়েও ধীর হয়ে গেল। মডেলটি কাঁচা, তবু বাস্তবের পর্যবেক্ষণের সাথে মেলে: খুব lossy নেটওয়ার্কে HTTP/2-এর সুবিধা কমে, এমনকি উল্টেও যেতে পারে। আসল TCP-তে loss-এ congestion window-ও অর্ধেক হয়, আর একটি সংযোগে তার প্রভাব সব stream-এর ওপর পড়ে। এই সমস্যার সমাধানই HTTP/3 (অংশ ৭)।

Priority — কোনটি আগে পাবে

সব অনুরোধ একসাথে গেলে সার্ভারকে ঠিক করতে হয় কোনটির frame আগে পাঠাবে। পাতা দেখাতে CSS আর মূল HTML সবচেয়ে জরুরি, পাতার নিচের ছবি কম জরুরি। RFC 7540 একটি জটিল ব্যবস্থা দিয়েছিল: stream-গুলো একটি dependency tree-তে সাজানো, প্রতিটির একটি weight (১–২৫৬)। বাস্তবে ব্রাউজারগুলো tree ভিন্নভাবে বানাত, আর অনেক সার্ভার তা ঠিকমতো মানত না। তাই RFC 9113 এই পদ্ধতিকে অপ্রচলিত (deprecated) ঘোষণা করেছে। তার জায়গায় এসেছে RFC 9218-এর Extensible Priorities, যা অনেক সহজ। ক্লায়েন্ট একটি priority header (বা PRIORITY_UPDATE frame) পাঠায়, যাতে দুটি মান থাকে: u = urgency, ০ (সবচেয়ে জরুরি) থেকে ৭, ডিফল্ট ৩; আর i = incremental, অর্থাৎ ফাইলটি টুকরো টুকরো পেলেও কাজে লাগে কি না (যেমন progressive ছবি)। উদাহরণ: priority: u=0 CSS-এর জন্য, priority: u=5, i নিচের ছবির জন্য। এই ব্যবস্থা HTTP/3-তেও একই; তবে কতটা মানা হয় তা বাস্তবায়নভেদে ভিন্ন।

৫ · Flow control, server push আর 103 Early Hints

TCP-র জানালা থাকতেও আবার flow control কেন?

পর্ব ৩১-এর TCP receive window পুরো সংযোগকে একটি পাইপ হিসেবে দেখে। ধরুন একটি stream-এ বড় ভিডিও আসছে, আর ব্যবহারকারী ভিডিও থামিয়ে রেখেছেন। TCP-র জানালা দিয়ে থামালে সব stream থেমে যায়। তাই HTTP/2 নিজের flow control যোগ করেছে, দুই স্তরে:

  • প্রতিটি stream-এর জানালা, শুরুতে ৬৫,৫৩৫ বাইট। প্রাপক SETTINGS_INITIAL_WINDOW_SIZE দিয়ে এটি বদলাতে পারে।
  • পুরো সংযোগের জানালা, শুরুতে ৬৫,৫৩৫ বাইট। এটি SETTINGS দিয়ে বদলানো যায় না, শুধু stream 0-তে WINDOW_UPDATE দিয়ে বাড়ানো যায়।
  • শুধু DATA frame জানালা খরচ করে। HEADERS, SETTINGS, PING খরচ করে না, তাই নিয়ন্ত্রণের বার্তা কখনো আটকে যায় না।
  • প্রেরক একটি DATA পাঠাতে পারে শুধু তখনই, যখন সেই stream-এর জানালা আর সংযোগের জানালা দুটোতেই জায়গা আছে। প্রাপক ডেটা পড়ে ফেললে WINDOW_UPDATE পাঠিয়ে জানালা আবার খোলে।

একটি গার্মেন্টস কারখানার গুদামে কয়েকজন ক্রেতার মাল একই ট্রাকে আসে। গুদামের মোট জায়গা সীমিত (সংযোগের জানালা), আর প্রতিটি ক্রেতার আলাদা তাক (stream-এর জানালা)। একজনের তাক ভরলে শুধু তার মাল থামে; তাক খালি করে তিনি ফোনে জানান “আরও এত কার্টন পাঠান” — এটাই WINDOW_UPDATE.

Server push — ভালো ধারণা, খারাপ ফল

HTTP/2-এর একটি আলোচিত বৈশিষ্ট্য ছিল server push. সার্ভার জানে index.html-এর সাথে style.css লাগবে। তাহলে ব্রাউজারের চাওয়ার অপেক্ষা কেন? সার্ভার একটি PUSH_PROMISE frame-এ জানায় “আমি /style.css-এর উত্তর stream 2-তে পাঠাচ্ছি”, তারপর পাঠিয়ে দেয়। এতে এক RTT বাঁচার কথা। বাস্তবে সমস্যা দেখা দিল:

  • ব্রাউজারের cache-এ ফাইলটি আগে থেকেই থাকলে সার্ভার তা জানে না। push করা বাইট নষ্ট হয়, আর সেই সময়ে জরুরি ডেটার জায়গা দখল হয়।
  • কোন ফাইল push করতে হবে, তা ঠিকমতো বাছাই করা কঠিন। বিভিন্ন পরিমাপে অনেক সাইটে লাভ খুব কম বা নেতিবাচক পাওয়া গেছে।
  • বাস্তবায়ন জটিল, অথচ খুব কম সাইট বা CDN এটি ব্যবহার করত।

ফলে Chrome ২০২২ সালের দিকে (সংস্করণ ১০৬ নাগাদ) HTTP/2 server push ডিফল্টে বন্ধ করে, আর পরে Firefox-ও সমর্থন তুলে নিয়েছে বলে জানা যায়। HTTP/3-এর মানে push আছে, কিন্তু ব্রাউজারে প্রায় ব্যবহার হয় না। ক্লায়েন্ট চাইলে SETTINGS_ENABLE_PUSH = 0 দিয়ে push পুরোপুরি নিষেধ করতে পারে।

Push-এর জায়গায় এখন জনপ্রিয় 103 Early Hints (RFC 8297). মূল উত্তর তৈরি হতে সময় লাগলে সার্ভার আগেই একটি অন্তর্বর্তী 103 উত্তর পাঠায়, যাতে শুধু ইঙ্গিত থাকে:

HTTP/1.1 103 Early Hints        (HTTP/2-তে একই অর্থ, :status 103)
Link: </style.css>; rel=preload; as=style
Link: <https://cdn.example.com>; rel=preconnect

HTTP/1.1 200 OK
Content-Type: text/html
…

সিদ্ধান্ত এবার ব্রাউজারের। তার cache-এ style.css থাকলে সে কিছুই আনে না। না থাকলে মূল উত্তরের অপেক্ষার সময়েই সেটি চেয়ে নেয়, আর CDN-এর সাথে আগেভাগে সংযোগ খুলে রাখে। লাভ প্রায় push-এর মতোই, কিন্তু নষ্ট বাইটের ঝুঁকি নেই। Chrome ২০২২ সাল নাগাদ এটি সমর্থন শুরু করে; অন্য ব্রাউজারের বর্তমান সমর্থন ব্যবহারের আগে যাচাই করে নিন। Cache আর preload-এর বিস্তারিত পর্ব ৩৮-এ (Web cache আর CDN).

৬ · HPACK — header-এর সংকোচন, বাইট গুনে গুনে

কেন gzip নয়? CRIME আর BREACH-এর শিক্ষা

SPDY শুরুতে header সংকোচনে DEFLATE (gzip-এর পদ্ধতি) ব্যবহার করত। ২০১২ সালে গবেষকরা CRIME আক্রমণ দেখান: গোপন cookie আর আক্রমণকারীর পাঠানো লেখা একই সংকোচনে গেলে অনুমান মিললে বার্তা ছোট হয়, তাই এনক্রিপ্ট করা বার্তার দৈর্ঘ্য দেখেই গোপন মান আন্দাজ করা যায়, এনক্রিপশন না ভেঙেই। ২০১৩ সালের BREACH একই ধারণা উত্তরের body-তে দেখায়। প্রতিরক্ষা: TLS-স্তরের সংকোচন বন্ধ (TLS 1.3-এ নেই-ই), আর সংবেদনশীল মান প্রতিবার আলাদা রূপে পাঠানো (যেমন CSRF token mask করা)। HPACK এই শিক্ষা থেকেই বানানো: এটি শুধু পুরো header মিললে পুনঃব্যবহার করে, আংশিক মিল নয়। আর সংবেদনশীল header-এর জন্য “never indexed” চিহ্ন রাখে।

তিনটি উপকরণ

Static table

RFC-তে স্থির করা ৬১টি সারি, সবচেয়ে প্রচলিত header নিয়ে: 2 = :method GET, 4 = :path /, 7 = :scheme https, 8 = :status 200, 32 = cookie (শুধু নাম), 58 = user-agent (শুধু নাম)… দুই পক্ষ আগে থেকেই জানে।

১–৬১

Dynamic table

সংযোগজুড়ে দুই পক্ষ একই তালিকা রাখে। নতুন সারি সামনে (৬২ নম্বরে) ঢোকে, পুরোনোরা এক ঘর করে পিছিয়ে যায়। জায়গা না থাকলে সবচেয়ে পুরোনো সারি বাদ পড়ে। প্রতিটি সারির মাপ = নামের দৈর্ঘ্য + মানের দৈর্ঘ্য + ৩২ বাইট। ডিফল্ট সীমা ৪,০৯৬ বাইট।

৬২ থেকে

Huffman code

নতুন লেখা পাঠাতে হলে একটি স্থির Huffman code ব্যবহার করা যায়, যা বাস্তব HTTP header-এর বড় নমুনা থেকে বানানো। প্রচলিত অক্ষর ৫–৬ bit-এ, বিরল অক্ষর অনেক বেশি bit-এ। www.example.com-এর ১৫ বাইট নেমে আসে ১২ বাইটে।

Huffman

এখানে Huffman code স্থির, অর্থাৎ প্রতিটি বার্তার জন্য আলাদা করে বানানো হয় না। তাই code table পাঠানোর খরচ নেই। Huffman code কীভাবে বানানো হয়, তা DSA কোর্সের Huffman coding পর্বে আছে।

একটি header লেখার পাঁচ উপায়

প্রথম বাইটের শুরুনামপরে কী আসেtable-এ যোগ?
1xxxxxxxIndexed৭ bit-এ সারির নম্বর; নাম আর মান দুটোই table থেকেনা (আগেই আছে)
01xxxxxxLiteral, incremental indexing৬ bit-এ নামের সারি (০ হলে নামও লেখা), তারপর মানহ্যাঁ
0000xxxxLiteral, without indexing৪ bit-এ নামের সারি, তারপর মাননা (যেমন প্রতিবার বদলানো :path)
0001xxxxLiteral, never indexedএকই, কিন্তু মাঝের proxy-ও যেন কখনো table-এ না রাখেকখনো না (যেমন authorization, ছোট গোপন cookie)
001xxxxxDynamic table size update৫ bit-এ নতুন সীমাtable ছোট/বড় করা

লেখা (string) পাঠানোর সময় প্রথমে এক বাইট আসে: সবচেয়ে ওপরের bit H (১ হলে Huffman) আর বাকি ৭ bit-এ দৈর্ঘ্য। তারপর আসে লেখার বাইটগুলো।

N-bit prefix integer — একটি বাইটের অবশিষ্ট bit-এ সংখ্যা

লক্ষ করুন, সারির নম্বর বসে প্রথম বাইটের বাকি bit-এ: indexed-এ ৭ bit, literal-এ ৬ বা ৪ bit. সংখ্যা বড় হলে কী হবে? HPACK-এর prefix integer নিয়ম (RFC 7541 §5.1) এর সমাধান দেয়। N bit-এ সর্বোচ্চ মান 2N − 1. সংখ্যা তার চেয়ে ছোট হলে সরাসরি বসে। না হলে prefix-এর সব bit 1 করা হয়, আর বাকি অংশ (সংখ্যা − (2N − 1)) পরের বাইটগুলোতে ৭ bit করে, নিচের অংশ আগে যায়। প্রতি বাইটের ওপরের bit 1 মানে “আরও বাইট আছে”. RFC-র নিজের উদাহরণ C.1 হাতে মেলানো যাক:

  • ১০, ৫-bit prefix: ১০ < ৩১, তাই সরাসরি 01010, প্রথম বাইট 0x0a.
  • ১৩৩৭, ৫-bit prefix: ১৩৩৭ ≥ ৩১. প্রথম বাইটে 11111 = 0x1f. বাকি ১৩৩৭ − ৩১ = ১৩০৬. ১৩০৬ mod ১২৮ = ২৬, “আরও আছে” bit-সহ ২৬ + ১২৮ = ১৫৪ = 0x9a. ১৩০৬ ÷ ১২৮ = ১০ (পূর্ণসংখ্যা), যা ১২৮-এর ছোট, তাই শেষ বাইট 0x0a. ফল 1f 9a 0a.
  • ৪২, ৮-bit prefix (বাইটের শুরু থেকে): সরাসরি 0x2a.

এবার একটি ছোট HPACK encoder. এটি Huffman ব্যবহার করে না (শুধু H = 0), আর static table-এর শুধু দরকারি সারিগুলো রাখে, তবে সেগুলোর আসল নম্বরসহ। পুরো table-এ ৬১টি সারি। Eviction দেখাতে dynamic table-এর সীমা ইচ্ছা করে ২৫৬ বাইট রাখা হয়েছে (আসলে ডিফল্ট ৪,০৯৬)। একই সাইটে পরপর দুটি অনুরোধ encode হয়, পার্থক্য শুধু :path-এ।

// HPACK sketch (RFC 7541), no Huffman. Only the static-table rows we need (real indexes; full table = 61).
const STATIC = { ":authority": 1, ":method GET": 2, ":path": 4, ":path /": 4, ":path /index.html": 5, ":scheme https": 7, "accept": 19, "cookie": 32, "user-agent": 58 };
function int(v, n, first = 0) {                     // N-bit prefix integer (RFC 7541 sec 5.1)
  const max = (1 << n) - 1; if (v < max) return [first | v];
  const out = [first | max]; v -= max;
  while (v >= 128) { out.push((v % 128) + 128); v = Math.floor(v / 128); }
  return out.concat(v);
}
const hex = a => a.map(x => x.toString(16).padStart(2, "0")).join(" ");
const bytes = s => int(s.length, 7).concat([...s].map(c => c.charCodeAt(0)));   // H=0: raw string
console.log("RFC C.1 checks:", hex(int(10, 5)), "|", hex(int(1337, 5)), "|", hex(int(42, 8)));

const MAX = 256, dyn = [];                           // dynamic table, newest first (small MAX to show eviction)
const size = e => e[0].length + e[1].length + 32, used = () => dyn.reduce((s, e) => s + size(e), 0);
function encode(headers) {
  let out = [];
  for (const [name, value] of headers) {
    const d = dyn.findIndex(e => e[0] === name && e[1] === value);
    const full = STATIC[name + " " + value] || (d >= 0 ? 62 + d : 0);
    if (full) { out = out.concat(int(full, 7, 0x80)); continue; }        // indexed field: 1 byte
    out = out.concat(int(STATIC[name] || 0, 6, 0x40), STATIC[name] ? [] : bytes(name), bytes(value));
    dyn.unshift([name, value]);                                          // literal + incremental indexing
    while (used() > MAX) console.log("   evict:", dyn.pop().join(": "));
  }
  return out;
}
const req1 = [[":method", "GET"], [":scheme", "https"], [":authority", "shop.example.com"], [":path", "/index.html"],
  ["user-agent", "Mozilla/5.0 (demo)"], ["accept", "*/*"], ["cookie", "sid=7f3a9c21e4b8"]];
const req2 = req1.map(h => h[0] === ":path" ? [":path", "/style.css"] : h);
for (const [label, req] of [["request 1", req1], ["request 2", req2]]) {
  const text = req.reduce((s, [n, v]) => s + n.length + v.length + 4, 0);   // "name: value\r\n"
  const enc = encode(req);
  console.log(`${label}: ${enc.length} bytes (as plain text: ${text})  ${enc.length > 24 ? hex(enc.slice(0, 8)) + " ..." : hex(enc)}`);
  console.log("   table:", dyn.map((e, i) => `[${62 + i}] ${e[0]}: ${e[1]}`).join("  "), "| size", used());
}

আউটপুট:

RFC C.1 checks: 0a | 1f 9a 0a | 2a
request 1: 64 bytes (as plain text: 151)  82 87 41 10 73 68 6f 70 ...
   table: [62] cookie: sid=7f3a9c21e4b8  [63] accept: */*  [64] user-agent: Mozilla/5.0 (demo)  [65] :authority: shop.example.com | size 213
   evict: :authority: shop.example.com
request 2: 18 bytes (as plain text: 150)  82 87 c1 44 0a 2f 73 74 79 6c 65 2e 63 73 73 c1 c0 bf
   table: [62] :path: /style.css  [63] cookie: sid=7f3a9c21e4b8  [64] accept: */*  [65] user-agent: Mozilla/5.0 (demo) | size 202

প্রথম লাইন RFC-র তিনটি উদাহরণের সাথে হুবহু মেলে। প্রথম অনুরোধ ৬৪ বাইট। :method, :scheme আর :path /index.html static table-এ পুরোপুরি আছে, তাই প্রতিটি ১ বাইট (0x82, 0x87, 0x85). :authority-র শুধু নাম আছে, তাই 0x41, দৈর্ঘ্য 0x10, তারপর ১৬ বাইট লেখা, আর সারিটি dynamic table-এ যোগ হলো; user-agent, accept আর cookie-ও তাই। table-এর মাপ ৫৮ + ৬০ + ৪১ + ৫৪ = ২১৩ বাইট। লক্ষ করুন, নতুন সারি সবসময় [62]-এ ঢোকে, তাই সবচেয়ে শেষে যোগ হওয়া cookie এখন [62]. দ্বিতীয় অনুরোধ মাত্র ১৮ বাইট, যেখানে সাধারণ লেখায় ১৫০ বাইট লাগত। শুধু নতুন :path (/style.css) পুরো লেখা হয়েছে (0x44 + দৈর্ঘ্য + ১০ বাইট = ১২ বাইট)। বাকি ৬টি header ১ বাইট করে। কিন্তু /style.css যোগ হতেই table ২১৩ + ৪৭ = ২৬০ বাইট হলো, যা সীমা ২৫৬ ছাড়ায়। তাই সবচেয়ে পুরোনো সারি (:authority) বাদ পড়ল। পরের অনুরোধে :authority আবার পুরো লিখতে হবে, তাই বাস্তবে table ৪ KB বা বড় রাখা হয়। দ্বিতীয় অনুরোধে c1 দুবার এসেছে, দুবার ভিন্ন অর্থে — নিচের stepper দেখুন।

Dynamic table (নতুন সারি সবসময় [62]-এ), সীমা ২৫৬ বাইট
header
কোথায় পাওয়া গেল
বাইট
মোট
table
  1. :method GET → 0x82. Static table-এর সারি ২-এ নাম আর মান দুটোই আছে। Indexed representation: ওপরের bit 1, বাকি ৭ bit-এ ২, তাই 1000 0010 = 0x82. মাত্র ১ বাইট।
  2. :scheme https → 0x87. Static সারি ৭, তাই 0x80 + 7 = 0x87. মোট ২ বাইট।
  3. :authority shop.example.com → 0xc1. প্রথম অনুরোধে এটি dynamic table-এ যোগ হয়েছিল, এখন [65]-এ। 0x80 + 65 = 0xc1. প্রথম অনুরোধে এই header-এ লেগেছিল ১৮ বাইট, এখন ১.
  4. :path /style.css → ১২ বাইট, আর eviction. এই মান কোথাও নেই, কিন্তু নাম “:path” static সারি ৪-এ আছে। তাই 01 + ৬ bit-এ ৪ = 0x44, দৈর্ঘ্য 0x0a (১০), তারপর ১০টি ASCII বাইট। সারিটি [62]-এ যোগ হলো (৫ + ১০ + ৩২ = ৪৭ বাইট)। মোট ২৬০ বাইট সীমা ছাড়িয়ে গেল, তাই সবচেয়ে পুরোনো :authority (৫৮ বাইট) বাদ পড়ল। Decoder-ও ঠিক একই হিসাব করে একই সারি বাদ দেয়। দুই পক্ষের table এভাবে সবসময় মিলে থাকে।
  5. user-agent → আবার 0xc1! নতুন সারি সামনে ঢোকায় পুরোনো সব সারি এক ঘর পিছিয়েছে। user-agent আগে [64]-এ ছিল, এখন [65]-এ। তাই একই বাইট 0xc1 এখন অন্য header বোঝায়। এই কারণেই decoder-কে header ঠিক পাঠানোর ক্রমে পড়তে হয়। একটি বাইটের অর্থ নির্ভর করে তার আগের সব যোগ-বিয়োগের ওপর। (অংশ ৭-এ দেখবেন HTTP/3-এ এটাই সমস্যা।)
  6. accept → 0xc0, cookie → 0xbf. [64] = 0x80 + 64 = 0xc0, [63] = 0x80 + 63 = 0xbf. মোট ১৮ বাইট, যেখানে প্রথম অনুরোধে ৬৪ আর সাধারণ লেখায় ১৫০. একটি পাতার ৮০টি অনুরোধে এই সাশ্রয় বড় হয়, বিশেষ করে মোবাইলের ধীর upload-এ।

৭ · HTTP/3 — একই HTTP, QUIC-এর ওপর

যে সমস্যা HTTP/2 সারাতে পারেনি

HTTP/2 application স্তরের head-of-line blocking দূর করেছে, কিন্তু নিচের TCP এখনো একটি একক, ক্রমবদ্ধ byte stream. একটি segment হারালে তার পরের সব বাইট kernel-এর buffer-এ আটকে থাকে, সেগুলো যে stream-এরই হোক। এটাই transport-level head-of-line blocking. অংশ ৪-এর মডেলে দেখেছেন, loss বাড়লে HTTP/2-এর সুবিধা কমে যায়। সংখ্যাসহ ধাপে ধাপে simulator দেখতে চাইলে পর্ব ৩৩-এর QUIC অংশ ২ খুলে দেখুন। সেখানে একই সংযোগে তিনটি stream-এর একটি packet হারালে TCP আর QUIC-এ কী ঘটে, তা পাশাপাশি দেখানো আছে। এখানে আর পুনরাবৃত্তি করছি না।

সমাধান এসেছে নিচের স্তর বদলে। QUIC (পর্ব ৩৩) UDP-র ওপর নিজেই নির্ভরযোগ্যতা দেয়, আর তার stream-গুলো transport স্তরেই স্বাধীন। একটি stream-এর packet হারালে শুধু সেই stream অপেক্ষা করে। HTTP/3 (RFC 9114, জুন ২০২২) হলো এই QUIC-এর ওপর HTTP-র ম্যাপিং। অর্থ আবারও একই (RFC 9110). QUIC-এর handshake, connection ID, migration আর varint পর্ব ৩৩-এ; এখানে শুধু HTTP-র দিক।

HTTP/2 থেকে HTTP/3 — কাজ কোথায় সরল

বিষয়HTTP/2 (TCP + TLS)HTTP/3 (QUIC)
Stream আর তার IDHTTP/2 নিজে; প্রতিটি frame-এ stream IDQUIC stream; HTTP/3 frame-এ stream ID নেই
অনুরোধ যায় কোথায়ক্লায়েন্টের বিজোড় stream: 1, 3, 5…ক্লায়েন্টের দুই-দিকের QUIC stream: 0, 4, 8…
Frame header৯ বাইট স্থির: length, type, flags, streamশুধু type আর length, দুটোই varint; flag নেই
Flow controlWINDOW_UPDATE frameQUIC-এর MAX_DATA / MAX_STREAM_DATA
Stream বাতিলRST_STREAMQUIC-এর RESET_STREAM / STOP_SENDING
PING, নির্ভরযোগ্যতা, এনক্রিপশনPING frame; TCP; TLSসব QUIC-এর ভেতরে (QUIC-এর PING frame, TLS 1.3)
SETTINGS, GOAWAYstream 0-তেআলাদা এক-দিকের control stream-এ
Header compressionHPACK (RFC 7541)QPACK (RFC 9204)
ALPNh2h3

HTTP/3 অনেক হালকা, কারণ তার বহু কাজ QUIC করে দেয়। প্রতিটি পক্ষ শুরুতে কয়েকটি এক-দিকের stream খোলে, যার প্রথম বাইট (varint) বলে দেয় stream-টি কী কাজের: 0x00 = control stream (প্রথম frame অবশ্যই SETTINGS), 0x01 = push stream, 0x02 = QPACK encoder stream, 0x03 = QPACK decoder stream. প্রতিটি অনুরোধ-উত্তর একটি দুই-দিকের stream-এ চলে: HEADERS frame, তারপর DATA frame, তারপর stream-এর FIN.

QPACK — HPACK কেন আর চলে না

অংশ ৬-এর stepper মনে করুন। HPACK-এর decoder প্রতিটি header block ঠিক পাঠানোর ক্রমে পড়ে, কারণ প্রতিটি block dynamic table বদলায়। একটি বাইট (0xc1) কোন header বোঝায়, তা নির্ভর করে আগের সব যোগ-বিয়োগের ওপর। TCP-তে এই ক্রম নিশ্চিত, কারণ সব stream একটি byte stream-এ যায়। HTTP/3-এ stream-গুলো স্বাধীন। ধরুন অনুরোধ A একটি নতুন সারি যোগ করল, আর অনুরোধ B সেই সারি ব্যবহার করল। A-র packet হারিয়ে গেল, B আগে পৌঁছাল। decoder হয় ভুল সারি পড়বে, নয়তো A-র অপেক্ষায় B-কে আটকাবে — আবার সেই head-of-line blocking.

QPACK সমাধান করেছে table-এর হালনাগাদ আলাদা করে:

  • Encoder stream (একটি এক-দিকের, ক্রমবদ্ধ stream): dynamic table-এ সারি যোগের সব নির্দেশ শুধু এখানে যায়।
  • Header block (অনুরোধের stream-এ) শুরুতে জানায় “আমাকে পড়তে table-এ অন্তত এতগুলো সারি যোগ হয়ে থাকতে হবে” (Required Insert Count). decoder ততদূর না পৌঁছালে শুধু এই stream অপেক্ষা করে, অন্যরা নয়।
  • Decoder stream: decoder জানায় কোন block পড়া শেষ আর কতগুলো সারি সে পেয়েছে। encoder চাইলে শুধু স্বীকৃত সারি ব্যবহার করে কোনো stream-কে কখনো আটকাতে না দিতে পারে, সংকোচন একটু কম হলেও। কতগুলো stream আটকে থাকার অনুমতি আছে, তা decoder SETTINGS_QPACK_BLOCKED_STREAMS দিয়ে ঠিক করে।
  • QPACK-এর static table-এ ৯৯টি সারি, নম্বর শুরু ০ থেকে। Dynamic table-এর ধারণক্ষমতা ডিফল্টে ০, decoder SETTINGS-এ জানালে তবেই ব্যবহার করা যায়।
মূল ধারণা

HPACK একটি “ক্রম নিশ্চিত” ধারণার ওপর দাঁড়িয়ে, আর QUIC সেই নিশ্চয়তা ইচ্ছা করে সরিয়ে দিয়েছে। নিচের স্তর বদলালে ওপরের স্তরের লুকানো অনুমানও বদলাতে হয়। HTTP/3 শুধু “HTTP/2 over QUIC” নয়, এই কারণেই।

ব্রাউজার কীভাবে জানে HTTP/3 আছে

HTTP/3 চলে UDP-তে, তাই https:// দেখে ব্রাউজার জানে না সার্ভার QUIC বোঝে কি না। দুটি পথ আছে, দুটিই পর্ব ৩৩-এর অংশ ৮-এ সংক্ষেপে এসেছে: Alt-Svc header, আর DNS-এর HTTPS record (DNS-এর বিস্তারিত পর্ব ৩৫, DNS). পুরো পথটা ধাপে ধাপে দেখা যাক।

  1. DNSA / AAAAঠিকানা
  2. TCP + TLSALPN: h2প্রথম সংযোগ
  3. উত্তরেalt-svc: h3মনে রাখা
  4. QUICUDP 443, ALPN h3পরের সংযোগ
ব্রাউজার কী করছে আর কী মনে রাখছে
ঘটনা
ব্রাউজারের মনে
ব্যবহারকারী টের পান
  1. DNS. ব্রাউজার নামের ঠিকানা চায়। alpn="h3,h2"-যুক্ত HTTPS record থাকলে প্রথম সংযোগই QUIC হতে পারত; এই উদাহরণে তা নেই।
  2. প্রথম সংযোগ TCP-তে। TLS handshake-এ ALPN দিয়ে ঠিক হলো h2.
  3. সার্ভারের ইঙ্গিত। alt-svc: h3=":443"; ma=86400 মানে “একই সেবা UDP port 443-এ HTTP/3-তে পাবেন, এক দিন মনে রাখুন”. ব্রাউজার চলতি সংযোগ বদলায় না, শুধু তথ্যটা রাখে।
  4. পরের সংযোগে QUIC. ব্রাউজার UDP 443-এ QUIC Initial পাঠায়, ALPN-এ h3; অনেক ব্রাউজার একই সময়ে TCP-ও চেষ্টা করে, যেটি আগে সফল সেটি নেয়।
  5. HTTP/3 সাজানো। দুই পক্ষ control stream খুলে SETTINGS পাঠায়, প্রয়োজনে QPACK-এর encoder আর decoder stream-ও। প্রথম অনুরোধ যায় ক্লায়েন্টের দুই-দিকের stream 0-তে। এখন একটি packet হারালে শুধু সেই stream অপেক্ষা করে।
  6. UDP আটকানো থাকলে। কিছু অফিস বা পাবলিক Wi-Fi-এর firewall UDP 443 আটকায়, তাই Initial-এর উত্তর আসে না। ব্রাউজার TCP-তে HTTP/2 চালায় আর h3-কে কিছু সময়ের জন্য “ভাঙা” ধরে নেয়। এজন্যই সার্ভারে TCP সবসময় চালু রাখতে হয়।

কতটা দ্রুত? সৎ উত্তর: নেটওয়ার্কের ওপর নির্ভর করে

বড় প্রতিষ্ঠানগুলোর প্রকাশিত পরিমাপে একটি প্রবণতা বারবার দেখা যায়। HTTP/3-এর লাভ সবচেয়ে বেশি বেশি RTT আর বেশি loss-এর নেটওয়ার্কে, যেমন ভিড়ের মোবাইল নেটওয়ার্ক, দূরের সার্ভার, চলন্ত অবস্থায় Wi-Fi থেকে মোবাইল ডেটায় যাওয়া। ভালো, কাছের নেটওয়ার্কে পার্থক্য প্রায়ই সামান্য, আর QUIC-এর CPU খরচ (পর্ব ৩৩) সার্ভারে বাড়তি বোঝা। কোনো একটি শতাংশকে সার্বজনীন ধরবেন না; নিজের ব্যবহারকারীদের আসল ডেটা দিয়ে মাপুন।

৮ · নিজের চোখে দেখা আর ওয়েব-পারফরম্যান্সের নতুন অভ্যাস

ব্রাউজারের DevTools-এ

Chrome বা Edge-এ F12 চেপে DevTools খুলুন। Network ট্যাবে কলামের নামের ওপর right-click করে Protocol চালু করুন, তারপর পাতাটি আবার লোড করুন। প্রতিটি অনুরোধের পাশে দেখবেন http/1.1, h2 বা h3. Firefox-এ একই কলাম HTTP/2, HTTP/3 লেখে। একই সাইট প্রথমবার h2 আর পরে h3 দেখালে অবাক হবেন না — এটাই Alt-Svc-এর আচরণ।

curl দিয়ে

নিচের কমান্ডগুলো উদাহরণ। এই পাতার runner-এ ইন্টারনেট নেই, তাই এগুলো এখানে চালানো যায় না। নিজের কম্পিউটারে, নিজের বা যে সাইট পরীক্ষার অনুমতি আছে এমন সাইটে চালান।

$ curl -sI --http2 https://www.example.com/ | head -1
HTTP/2 200

$ curl -s -o /dev/null -w '%{http_version}\n' https://www.example.com/
2

$ curl -V | grep -o 'HTTP3'          # build-এ HTTP/3 সমর্থন আছে কি?
HTTP3

$ curl -sI --http3 https://www.example.com/ | head -1
HTTP/3 200

$ curl -sv --http2 https://www.example.com/ -o /dev/null 2>&1 | grep -i alpn
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2

--http2 ALPN-এ h2 প্রস্তাব করে; সার্ভার রাজি না হলে curl HTTP/1.1-এ চলে। --http3 শুধু তখনই কাজ করে, যখন curl HTTP/3 সমর্থনসহ build করা থাকে (curl -V-এর Features-এ HTTP3). Verbose আউটপুটের লেখা সংস্করণভেদে একটু আলাদা হয়। Windows 10/11-এর curl.exe-তেও একই flag চলে, যদি build সমর্থন করে।

পুরোনো কৌশল ছাড়ুন, মূল নীতি রাখুন

HTTP/1.1 যুগের অভ্যাসHTTP/2 / HTTP/3-এ
Domain shardingছেড়ে দিন। বাড়তি নাম মানে বাড়তি DNS আর handshake, আর priority কাজ করে না। একই certificate আর ঠিকানার নামগুলো ব্রাউজার একটি সংযোগে ভাগ করতে পারে (connection coalescing).
সব JS/CSS একটি বিশাল ফাইলেমাঝামাঝি পথ: কয়েকটি যুক্তিসংগত ভাগ, যাতে একটি বদলালে বাকিদের cache টেকে; তবে শত শত খুব ছোট ফাইলও ভালো নয়।
Sprite sheet, inliningসাধারণত দরকার নেই। আলাদা ফাইল আলাদাভাবে cache হয়। খুব ছোট, পাতা দেখাতে জরুরি CSS inline করা এখনো কাজে লাগতে পারে।
Server pushবাদ দিন। 103 Early Hints আর <link rel="preload"> ব্যবহার করুন।
বাইট কমানো, cache, সংকোচন, CDNআগের মতোই জরুরি। multiplexing অপেক্ষা কমায়, কিন্তু ৫ MB-এর ছবি পাঠানোর সময় কমায় না।

ওয়েবপাতার দিক থেকে ছবি আর লোডিং-এর অভ্যাস নিয়ে HTML & CSS কোর্সের Performance পর্বটি দেখতে পারেন।

আরও গভীরে: ২০২৩ সালের অক্টোবরে প্রকাশিত “HTTP/2 Rapid Reset” (CVE-2023-44487) দুর্বলতায় দেখা যায়, stream খুলেই সঙ্গে সঙ্গে RST_STREAM দিয়ে বাতিল করলে MAX_CONCURRENT_STREAMS-এর সীমা কাজে আসে না, কারণ বাতিল stream আর গোনা হয় না, অথচ সার্ভার প্রতিটির কাজ শুরু করে ফেলে। এর অপব্যবহারে বড় পরিসরের DDoS হয়েছিল। প্রতিরক্ষা হিসেবে সার্ভারগুলো এখন প্রতি সংযোগে বাতিলের হার মাপে আর সীমা ছাড়ালে GOAWAY দিয়ে সংযোগ বন্ধ করে। শিক্ষা: protocol-এর সীমা শুধু “সৎ ক্লায়েন্ট” ধরে বানালে যথেষ্ট নয় (বিস্তারিত পর্ব ৫০-এ)।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • HTTP/1.1 আর HTTP/2-এর মূল পার্থক্য? — অর্থ একই; HTTP/2 binary framing, একটি সংযোগে multiplexed stream, HPACK header compression আর stream-ভিত্তিক flow control আনে।
  • HTTP/2 কি head-of-line blocking পুরো দূর করে? — না; HTTP স্তরেরটা দূর করে, কিন্তু TCP-র packet loss-এ সব stream আটকায়। HTTP/3 (QUIC) সেটা দূর করে।
  • HTTP/2 frame header-এ কী থাকে? — ৯ বাইট: ২৪ bit length, ৮ bit type, ৮ bit flags, ১ সংরক্ষিত bit + ৩১ bit stream ID.
  • HPACK কেন gzip নয়? — CRIME-ধরনের আক্রমণ এড়াতে; static/dynamic table, স্থির Huffman, শুধু পুরো header মিললে পুনঃব্যবহার, আর never-indexed চিহ্ন।
  • HTTP/3-এ QPACK কেন? — QUIC stream স্বাধীন, ক্রম নিশ্চিত নয়; HPACK-এর table হালনাগাদ ক্রম চায়। QPACK table হালনাগাদ আলাদা encoder stream-এ পাঠায়।

৯ · AI-এর যুগে HTTP/2 আর HTTP/3 — শেখা আর যাচাই

HTTP/2-এর মান দুবার লেখা হয়েছে (7540 → 9113), priority বদলেছে, server push কার্যত উঠে গেছে — অথচ পুরোনো টিউটোরিয়াল প্রচুর, আর AI সেগুলো বর্তমান বলে চালায়। চেনা ভুলগুলো:

AI-এর ভুলআসল কথাকীভাবে যাচাই করবেন
“পারফরম্যান্সের জন্য server push চালু করুন”প্রধান ব্রাউজার push বন্ধ করেছে; 103 Early Hints বা preload ব্যবহার করুনব্রাউজারের বর্তমান release note আর DevTools-এ দেখুন push-এর কিছু আসে কি না
“HTTP/2-তে stream dependency আর weight ঠিক করুন”RFC 9113-এ এই পদ্ধতি অপ্রচলিত; RFC 9218-এর urgency/incrementalrfc-editor.org-এ RFC 9113 আর 9218 মিলিয়ে দেখুন
ভুল frame type বা ভুল HPACK বাইট (যেমন 0x82-কে :method POST বলা)0x82 = static ২ = GET; POST হলো ৩ (0x83)RFC 7541 Appendix A-র table আর এই পর্বের parser দিয়ে নিজে decode করুন
“HTTP/3 = HTTP/2 over UDP”, বা “HTTP/3-ও HPACK ব্যবহার করে”HTTP/3 QUIC-এর ওপর, QPACK ব্যবহার করে; frame-এর গঠন আলাদাRFC 9114 আর 9204 পড়ুন
“h3 কাজ করছে না? firewall-এ সব UDP খুলে দিন”শুধু দরকারি UDP 443, নিজের নীতি মেনে; TCP fallback তো আছেইনিরাপত্তা দলের সাথে নীতি মেলান; পর্ব ৪৯
বানানো সংখ্যা: “HTTP/3 সব সাইটকে ৫০% দ্রুত করে”লাভ নেটওয়ার্কভেদে খুব ভিন্ন; কোনো একক সংখ্যা সার্বজনীন নয়উৎস চেয়ে নিন, নিজের সাইটে আসল ব্যবহারকারীর ডেটা দিয়ে মাপুন

AI-কে কাজে লাগান হিসাব যাচাই আর ব্যাখ্যার সঙ্গী হিসেবে, তবে সংস্করণ আর সূত্র নির্দিষ্ট করে দিন:

তুমি একজন অভিজ্ঞ ওয়েব-প্রোটোকল প্রকৌশলীর মতো উত্তর দাও। HTTP/2 মানে RFC 9113 আর HPACK মানে RFC 7541; HTTP/3 মানে RFC 9114 আর QPACK মানে RFC 9204. ১. এই hex HTTP/2 frame ধাপে ধাপে decode করো: 00 00 04 08 00 00 00 00 03 00 01 00 00 — প্রতিটি ঘরের bit দেখাও। ২. HPACK-এ 1337 সংখ্যাটি 5-bit prefix-এ encode করো, আর RFC 7541 Appendix C.1-এর সাথে মেলাও। ৩. আমার নিজের সাইটে h2 থেকে h3-এ যাওয়ার আগে কী কী মাপা উচিত, তার একটি তালিকা দাও; কোনো শতাংশ বা সংখ্যা দিলে তার উৎস বলো। ৪. প্রতিটি দাবির পাশে RFC নম্বর আর section দাও; কোনো তথ্য পুরোনো হতে পারলে (যেমন server push, priority) স্পষ্ট করে বলো।

AI-এর HTTP/2–HTTP/3 উত্তর বিশ্বাস করার আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১HTTP/1.1-এর pipelining মানে থাকা সত্ত্বেও ব্রাউজারগুলো কেন তা ব্যবহার করল না? এর থেকে protocol নকশা নিয়ে কী শেখা যায়?

কাগজে pipelining ভালো ধারণা ছিল। ক্লায়েন্ট উত্তরের অপেক্ষা না করে কয়েকটি অনুরোধ পাঠায়, তাই প্রতি ফাইলে এক RTT-র অপেক্ষা কমে। HTTP/1.1-এর মান (১৯৯৭-এর RFC 2068, পরে RFC 2616) এটি অনুমোদন করেছিল। সমস্যা ছিল দুই ধরনের। প্রথমটি নকশাগত: উত্তরে কোনো পরিচয়-চিহ্ন নেই, তাই উত্তর অবশ্যই অনুরোধের ক্রমে আসতে হবে। একটি ধীর উত্তর পেছনের সব উত্তর আটকে দেয়। দ্বিতীয়টি বাস্তবায়নগত, আর আসলে এটাই বড়: পথের proxy, transparent cache, পুরোনো সার্ভার সফটওয়্যার অনেক সময় pipelined অনুরোধ ঠিকমতো সামলাত না। কেউ দ্বিতীয় অনুরোধ ফেলে দিত, কেউ উত্তর মিশিয়ে ফেলত, কেউ সংযোগ বন্ধ করত। ব্রাউজারের পক্ষে আগে থেকে জানার উপায় ছিল না পথে কে আছে।

ব্রাউজার নির্মাতাদের কাছে কিছু ব্যবহারকারীর পাতা ভাঙার ঝুঁকি গতির লাভের চেয়ে বড় মনে হয়েছিল। ফলে প্রধান ব্রাউজারে pipelining হয় কখনো ডিফল্টে চালু হয়নি, নয়তো পরে সরিয়ে দেওয়া হয়েছে। ব্যবহারকারীর দিকে জায়গা নিল একাধিক সংযোগ আর domain sharding-এর মতো কৌশল।

এখান থেকে তিনটি শিক্ষা আসে, যা HTTP/2-এর নকশায় সরাসরি দেখা যায়। এক, প্রতিটি বার্তায় পরিচয় (stream ID) থাকলে ক্রমের বাঁধন খুলে যায়। দুই, পুরোনো যন্ত্রের সাথে নতুন আচরণ মেশালে তারা বিভ্রান্ত হয়। তাই HTTP/2 এনক্রিপশনের ভেতরে চলে, ALPN দিয়ে দুই প্রান্ত স্পষ্ট সম্মতি দেয়, আর connection preface এমনভাবে বানানো যে ভুল সার্ভার সঙ্গে সঙ্গে ত্রুটি দেয়। মাঝের proxy আর ভেতরে দেখতে পারে না, তাই ভাঙতেও পারে না। তিন, ঐচ্ছিক বৈশিষ্ট্য যা “হয়তো কাজ করবে”, বাস্তবে প্রায়ই কাজ করে না। HTTP/2-এ multiplexing বাধ্যতামূলক, ঐচ্ছিক নয়। পরে QUIC একই শিক্ষা আরও এগিয়ে নিয়ে header-ও এনক্রিপ্ট করেছে।

মূল উপলব্ধি: একটি protocol বৈশিষ্ট্যের সাফল্য নির্ভর করে শুধু তার নকশার ওপর নয়, পথের সব পুরোনো যন্ত্রের সাথে তার সহাবস্থানের ওপরও। নতুন আচরণকে পুরোনো যন্ত্রের চোখের আড়ালে রাখা ছিল HTTP/2-এর সাফল্যের একটি গোপন কারণ।

প্র ০২HTTP/2 একটি সংযোগে সব কিছু চালায়, আর HTTP/1.1 চালাত ৬টিতে। “একটি সংযোগই ভালো” — এই দাবি কখন সত্য, কখন নয়?

একটি সংযোগের পক্ষে যুক্তি শক্ত। প্রতিটি TCP + TLS সংযোগে handshake-এর RTT আর CPU খরচ আছে। প্রতিটি নিজে slow start থেকে শুরু করে, তাই ৬টি নতুন সংযোগ মানে ৬টি ছোট congestion window, যারা একে অপরের অবস্থা জানে না। ৬টি সংযোগ একসাথে একটি বোতলের গলা-link ভাগ করলে তারা প্রতিযোগী হয়, আর অন্য অ্যাপের তুলনায় ৬ গুণ ভাগ নেয়। এটা নেটওয়ার্কের ন্যায্যতার দিক থেকেও প্রশ্নসাপেক্ষ (পর্ব ৩২-এর AIMD fairness মনে করুন)। একটি সংযোগে congestion control পুরো পাতার অবস্থা একসাথে দেখে, আর priority দিয়ে সার্ভার ঠিক করতে পারে কোন বাইট আগে যাবে। HPACK-এর dynamic table-ও সংযোগভিত্তিক, তাই একটি সংযোগে সংকোচন ভালো হয়।

কিন্তু একটি সংযোগ মানে সব ডিম এক ঝুড়িতে। TCP-র একটি packet হারালে সব stream থামে, আর congestion window অর্ধেক হলে পুরো পাতার গতি অর্ধেক হয়। ৬টি সংযোগে একটির ক্ষতি বাকি ৫টিকে প্রায় ছুঁয়ে যায় না। অংশ ৪-এর মডেলে এটাই দেখেছেন: loss বাড়লে ৬-সংযোগের HTTP/1.1 এক জায়গায় HTTP/2-কে ছাড়িয়ে গেল। দুর্বল মোবাইল নেটওয়ার্ক, ভিড়ের পাবলিক Wi-Fi বা দূরের সার্ভারে এই প্রভাব বাস্তব।

উত্তর তাই প্রেক্ষাপটনির্ভর, আর ইতিহাস এই টানাপোড়েনের পথেই এগিয়েছে। HTTP/2 একটি সংযোগে গেছে কারণ বেশির ভাগ নেটওয়ার্কে লাভ বেশি। HTTP/3 একটি সংযোগ রেখেও তার মূল দুর্বলতা সারিয়েছে: QUIC-এর stream স্বাধীন, তাই একটি ক্ষতিতে সব থামে না। Congestion control অবশ্য এখনো সংযোগভিত্তিক, তাই loss-এ পুরো সংযোগের গতি কমে। এটা ইচ্ছাকৃত, কারণ নেটওয়ার্কে ভিড়ের সংকেত সবার মানা উচিত।

মূল উপলব্ধি: সংযোগের সংখ্যা একটি আপস: কম সংযোগে সমন্বয় আর দক্ষতা, বেশি সংযোগে ক্ষতির বিচ্ছিন্নতা। HTTP/3 দুটোর ভালো দিক একসাথে পেতে transport স্তরই বদলে দিয়েছে।

প্র ০৩HPACK-এর dynamic table দুই প্রান্তে হুবহু একই থাকতে হয়। এই “ভাগ করা অবস্থা” কী কী সুবিধা আর ঝুঁকি আনে?

সুবিধা স্পষ্ট: একবার পাঠানো header পরের বার এক বাইটে যায়। HTTP-র অনুরোধগুলো খুব পুনরাবৃত্তিমূলক। একই user-agent, একই cookie, একই accept প্রতিটি অনুরোধে আসে। এমন ক্ষেত্রে অবস্থা মনে রাখা সংকোচনই সবচেয়ে কার্যকর। অংশ ৬-এর encoder-এ ৬৪ বাইট থেকে ১৮ বাইট হয়েছে, Huffman ছাড়াই। কিন্তু অবস্থা ভাগ করার তিনটি দাম আছে।

প্রথম দাম, ক্রমের ওপর নির্ভরতা। প্রতিটি header block table বদলায়, তাই decoder-কে block ঠিক পাঠানোর ক্রমে পড়তে হয়। TCP-তে এটা বিনা খরচে পাওয়া যায়, কিন্তু QUIC-এ নয়। তাই HTTP/3-কে QPACK-এর মতো জটিল ব্যবস্থা বানাতে হয়েছে: আলাদা encoder আর decoder stream, Required Insert Count, blocked stream-এর সীমা। দ্বিতীয় দাম, ত্রুটির ভঙ্গুরতা। একটি header block decode করতে ভুল হলে দুই প্রান্তের table আর মেলে না, পরের সব block ভুল পড়া হবে। তাই HPACK-এ decode ত্রুটি হলে পুরো সংযোগ বন্ধ করতে হয় (COMPRESSION_ERROR). শুধু একটি stream বাতিল করা যথেষ্ট নয়। তৃতীয় দাম, memory: বিদ্বেষী পক্ষ বড় header পাঠিয়ে decoder-এর memory খরচ করাতে পারে, তাই SETTINGS_HEADER_TABLE_SIZE আর MAX_HEADER_LIST_SIZE-এর মতো সীমা আছে।

গোপনীয়তার দিকও আছে। একই সংযোগে ভিন্ন উৎসের অনুরোধ মিশলে (যেমন একটি proxy অনেক ব্যবহারকারীর অনুরোধ একটি সংযোগে পাঠালে) table-এর আচরণ থেকে তথ্য ফাঁসের তাত্ত্বিক সুযোগ থাকে। একজনের cookie table-এ থাকলে আরেকজনের অনুমান করা মান ছোট হয়ে যেতে পারে। RFC 7541 এই ঝুঁকি আলোচনা করে, আর প্রতিরক্ষা হিসেবে সংবেদনশীল header-এর জন্য never-indexed representation দেয়, যাতে মাঝের কোনো যন্ত্রও সেগুলো table-এ না রাখে। প্রতিটি সংযোগের table আলাদা, তাই সাধারণ ব্রাউজার-সার্ভার সংযোগে ঝুঁকি সীমিত।

মূল উপলব্ধি: অবস্থা মনে রাখা সংকোচন বড় লাভ দেয়, কিন্তু তা দুই প্রান্তকে ক্রম, সঠিকতা আর নিরাপত্তায় একে অপরের সাথে বেঁধে ফেলে। নকশাকারীর কাজ হলো সেই বাঁধনের সীমা স্পষ্ট করে দেওয়া।

প্র ০৪Server push-এর মতো একটি “ভালো ধারণা” কেন ব্যর্থ হলো, আর 103 Early Hints কেন তুলনামূলকভাবে সফল? দুটির নকশাগত পার্থক্য কী?

Server push-এর যুক্তি ছিল সরল। সার্ভার জানে HTML-এর সাথে কোন CSS আর JS লাগবে। ব্রাউজার HTML পার্স করে সেগুলো চাইতে চাইতে এক RTT চলে যায়, তাই সার্ভার আগেভাগেই পাঠাক। কিন্তু এই নকশায় সিদ্ধান্ত নেয় সার্ভার, অথচ সবচেয়ে জরুরি তথ্যটি থাকে ব্রাউজারের কাছে: তার cache-এ কী আছে। পরের পরিদর্শনে style.css cache-এই আছে, তবু সার্ভার পাঠাতে শুরু করে। ব্রাউজার RST_STREAM দিয়ে বাতিল করার আগেই হয়তো অনেক বাইট চলে গেছে, যা জরুরি HTML-এর জায়গা দখল করেছে।

কোন ফাইল push করা উচিত, তা ঠিক রাখতে বাড়তি কনফিগারেশন লাগত, যা কোড বদলের সাথে পুরোনো হয়ে যেত। বিভিন্ন পরিমাপে অনেক সাইটে লাভ খুব কম বা নেতিবাচক পাওয়া যায়। আর ব্যবহার এত কম ছিল যে ব্রাউজার নির্মাতারা জটিল কোড রাখার যুক্তি পাননি। Chrome ২০২২ সাল নাগাদ এটি বন্ধ করে, পরে অন্যরাও একই পথে যায়।

103 Early Hints একই সমস্যার সমাধান করে, কিন্তু ভিন্ন দর্শনে। সার্ভার শুধু ইঙ্গিত দেয়, “এগুলো লাগবে”, আর সিদ্ধান্ত নেয় ব্রাউজার, যে নিজের cache জানে। ইঙ্গিত কয়েক ডজন বাইটের, তাই ভুল হলেও ক্ষতি সামান্য। এটি HTTP-র সাধারণ অন্তর্বর্তী উত্তর (1xx), তাই HTTP/1.1, HTTP/2, HTTP/3 সবখানে একই। সবচেয়ে বড় সুবিধা হলো সময়ের সদ্ব্যবহার: মূল উত্তর তৈরিতে সার্ভারের ডেটাবেস যে কয়েকশো মিলিসেকেন্ড নেয়, সেই অলস সময়ে ব্রাউজার CSS আনে আর CDN-এর সাথে সংযোগ খোলে।

মূল উপলব্ধি: যে পক্ষের কাছে সিদ্ধান্তের তথ্য আছে, সিদ্ধান্তও তার হাতে থাকা উচিত। Push সার্ভারকে অন্ধ সিদ্ধান্ত নিতে বলেছিল। Early Hints তথ্য দেয় সার্ভার থেকে, আর সিদ্ধান্ত রাখে ব্রাউজারে।

প্র ০৫বাংলাদেশের মতো দেশে, যেখানে অনেক ব্যবহারকারী মোবাইল ডেটায় আর আন্তর্জাতিক সার্ভার দূরে, একটি সংবাদ বা ই-কমার্স সাইট HTTP/2 আর HTTP/3 নিয়ে কী কৌশল নিতে পারে?

প্রথমে বাস্তবতা মাপা দরকার, অনুমান নয়। দেশে ইন্টারনেট ব্যবহারকারীর বড় অংশ মোবাইল নেটওয়ার্কে বলে ধরা হয়, যেখানে RTT আর loss সময় ও এলাকাভেদে অনেক ওঠানামা করে। আন্তর্জাতিক কনটেন্ট সাবমেরিন কেবল ঘুরে আসে, তাই দূরের origin-এর RTT বড়; আবার দেশের ভেতরের CDN cache আর BDIX-এর মতো আদান-প্রদান কেন্দ্র অনেক কনটেন্ট কাছে আনে। তাই শুরু করা উচিত নিজের ব্যবহারকারীদের আসল পরিমাপ (real user monitoring) দিয়ে।

দ্বিতীয় ধাপ: কাছে আনা। পাতার স্থির অংশ (ছবি, CSS, JS) এমন CDN-এ রাখা, যার দেশে বা কাছাকাছি উপস্থিতি আছে। এতে protocol যা-ই হোক, প্রতিটি RTT-নির্ভর খরচ কমে (পর্ব ৩৮)। তৃতীয় ধাপ: HTTP/2 নিশ্চিত করা (এখন প্রায় সব সার্ভার আর CDN-এ সাধারণ ব্যাপার), আর পুরোনো কৌশল ছাড়া। Domain sharding থাকলে সরানো, কারণ প্রতিটি বাড়তি নাম দূরের DNS আর handshake-এর খরচ আনে।

চতুর্থ ধাপ: HTTP/3 চালু করা, কিন্তু মেপে। এখানেই সম্ভাব্য লাভ সবচেয়ে বেশি। loss-এ কম আটকানো, দ্রুত handshake, আর বাসে বা রাস্তায় Wi-Fi থেকে মোবাইল ডেটায় যাওয়ার সময় connection migration ঠিক এই ব্যবহারকারীদের সমস্যা। তবে কিছু নেটওয়ার্ক UDP আটকায় বা সীমিত করে, তাই TCP-র পথ সবসময় চালু রাখতে হবে। A/B ভিত্তিতে দেখতে হবে HTTP/3-এ আসলে পাতা দেখানোর সময় (যেমন Largest Contentful Paint) কতটা বদলায়। পঞ্চম ধাপ: বাইট কমানো, যা কোনো protocol বদলাতে পারে না। ছবি ছোট ও আধুনিক ফরম্যাটে রাখা, অপ্রয়োজনীয় JavaScript বাদ দেওয়া, cache-এর নিয়ম ঠিক করা, আর 103 Early Hints বা preload দিয়ে জরুরি ফাইল আগে আনা।

মূল উপলব্ধি: HTTP/3 দুর্বল নেটওয়ার্কে সত্যিকারের সুবিধা দিতে পারে, কিন্তু এটি কম বাইট, কাছের কনটেন্ট আর ভালো cache-এর বিকল্প নয়। Protocol বদল হলো শেষ ধাপের উন্নতি, আর সেটিও মেপে নিশ্চিত হতে হয়।

অনুশীলন

খাতা-কলম নিন — বাইট, bit আর জানালা ধরে ধরে হিসাব করুন।

  1. হিসাব (frame header আর flow control): (ক) 00 40 00 00 01 00 00 00 07 — এই frame header-এর length, type, flag আর stream বের করুন। (খ) একটি নতুন HTTP/2 সংযোগে কোনো SETTINGS বা WINDOW_UPDATE বদল হয়নি (সব জানালা ডিফল্ট ৬৫,৫৩৫)। সার্ভার stream 1-এ ১৬,৩৮৪ বাইটের ৩টি DATA frame আর stream 3-এ ১০,০০০ বাইটের ১টি DATA frame পাঠিয়েছে। এখন সংযোগের জানালা আর দুটি stream-এর জানালা কত? সার্ভার কি এখনই stream 3-এ আরও ১৬,৩৮৪ বাইট পাঠাতে পারবে?

    (ক) Length = 0x004000 = ১৬,৩৮৪ (ডিফল্ট সর্বোচ্চ), type 0x00 = DATA, flag 0x01 = END_STREAM, stream = 7 (বিজোড়, তাই ক্লায়েন্টের খোলা stream; DATA-টি তাই সার্ভারের উত্তরের শেষ অংশ, বা ক্লায়েন্টের পাঠানো body-র শেষ অংশ)। অংশ ৩-এর build()-এর শেষ লাইন ঠিক এটাই ছাপে।

    (খ) Stream 1: ৬৫,৫৩৫ − ৩ × ১৬,৩৮৪ = ৬৫,৫৩৫ − ৪৯,১৫২ = ১৬,৩৮৩. Stream 3: ৬৫,৫৩৫ − ১০,০০০ = ৫৫,৫৩৫. সংযোগ: ৬৫,৫৩৫ − ৫৯,১৫২ = ৬,৩৮৩. Stream 3-এর নিজের জানালায় জায়গা আছে, কিন্তু সংযোগের জানালায় মাত্র ৬,৩৮৩. তাই সার্ভার এখন সর্বোচ্চ ৬,৩৮৩ বাইট পাঠাতে পারবে, পুরো ১৬,৩৮৪ নয়। বাকিটার জন্য ক্লায়েন্টের stream 0-তে WINDOW_UPDATE লাগবে। এজন্যই ব্রাউজার শুরুতেই সংযোগের জানালা অনেক বড় করে দেয় (অংশ ৩-এর উদাহরণ); নইলে throughput RTT প্রতি ৬৪ KB-তে আটকে থাকত।

  2. হাতে trace (HPACK): (ক) ৩০০ সংখ্যাটি ৬-bit prefix-এ encode করুন, আর literal-with-incremental-indexing-এর প্রথম দুই bit 01 বসিয়ে পুরো বাইটগুলো লিখুন। (খ) indexed representation-এ dynamic table-এর [62] আর [127] সারি কোন বাইট(গুলো)তে লেখা হবে? (গ) একটি dynamic table-এর সারি x-request-id: abc123-এর মাপ কত বাইট ধরা হয়?

    (ক) ৬ bit-এ সর্বোচ্চ ৬৩, আর ৩০০ ≥ ৬৩. তাই prefix-এর সব bit 1: 01 + 111111 = 0x7f. বাকি ৩০০ − ৬৩ = ২৩৭. ২৩৭ mod ১২৮ = ১০৯, “আরও আছে” bit-সহ ১০৯ + ১২৮ = ২৩৭ = 0xed. ২৩৭ ÷ ১২৮ = ১, তাই শেষ বাইট 0x01. ফল 7f ed 01.

    (খ) ৭ bit-এ সর্বোচ্চ ১২৭. [62]: ৬২ < ১২৭, তাই 0x80 + 62 = 0xbe, এক বাইট। [127]: ১২৭ = 27 − 1, তাই এটি সরাসরি বসে না। নিয়ম “সংখ্যা < 2N − 1 হলে সরাসরি”. prefix-এর সব bit 1 হয়ে 0xff, বাকি ১২৭ − ১২৭ = ০, তাই পরের বাইট 0x00. ফল ff 00, দুই বাইট। এই সীমার কেসটি encoder-এর সাধারণ বাগ।

    (গ) নাম ১২ অক্ষর + মান ৬ অক্ষর + ৩২ = ৫০ বাইট. ৩২ বাইট হলো প্রতিটি সারির আনুমানিক বাড়তি খরচ (pointer, দৈর্ঘ্য ইত্যাদি), যা RFC স্থির করে দিয়েছে, যাতে দুই প্রান্ত একই হিসাবে eviction করে।

  3. আউটপুট অনুমান করুন: অংশ ৪-এর page-load মডেলে শুধু RTT = 80 বদলে RTT = 200 করা হলো (দূরের সার্ভার বা ভিড়ের মোবাইল নেটওয়ার্ক)। losses-এর 0 আর 6 সারি দুটিতে তিনটি কলামে কী ছাপবে? কোন পদ্ধতি কোথায় এগিয়ে?
    0              7000          2000          1200
    6              8200          2200          2400

    setup = ২ × ২০০ = ৪০০. একটি সংযোগ: ৪০০ + ৩০ × (২০০ + ২০) = ৭০০০; ৬টি ক্ষতিতে + ৬ × ২০০ = ৮২০০. ছয় সংযোগ: ৪০০ + ৫ × (২০০ + ১২০) = ২০০০; ক্ষতি ৬টি সংযোগে ছড়ানো, তাই + ১ × ২০০ = ২২০০. HTTP/2: ৪০০ + ২০০ + ৬০০ = ১২০০; ৬টি ক্ষতিতে + ১২০০ = ২৪০০. RTT বড় হলে ক্ষতিহীন অবস্থায় HTTP/2-এর সুবিধা আরও বড় (৭০০০ বনাম ১২০০), কিন্তু প্রতিটি ক্ষতিও বেশি দামি (২০০ ms), আর HTTP/2-এ সব stream সেই দাম দেয় — তাই lossy অবস্থায় ৬-সংযোগ এগিয়ে। এই পরিস্থিতির জন্যই HTTP/3.

  4. কোড লিখুন: অংশ ৩-এর frame parser-এ PING frame (type 6)-এর ৮ বাইটের opaque ডেটা hex-এ ছাপার জন্য একটি লাইন যোগ করুন। তারপর parse("000008 06 01 00000000 6162636465666768") কী ছাপবে লিখুন। এটি কোন পক্ষ পাঠিয়েছে বলে মনে হয়?
      if (type === 6) info = " opaque=" + p.map(x => x.toString(16).padStart(2, "0")).join("");

    এই লাইনটি if (type === 0)-এর আগে বা পরে বসালেই চলবে। আউটপুট:

    PING          len=8   flags=0x01 ACK                    stream=0 opaque=6162636465666768

    flag 0x01 PING-এ মানে ACK, তাই এটি একটি উত্তর। যে পক্ষ আগে PING পাঠিয়েছিল (ধরুন ক্লায়েন্ট, opaque “abcdefgh”), তার অপর পক্ষ ঠিক একই ৮ বাইট ফিরিয়ে দিয়েছে। পাঠানো আর এই ACK পাওয়ার মাঝের সময়ই সংযোগের RTT-র একটি মাপ। Stream 0, কারণ PING পুরো সংযোগের।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমএকটি ব্রাউজার নতুন HTTPS সার্ভারের সাথে HTTP/2-এ কথা বলছে — ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. TLS ClientHello-তে ALPN তালিকা [h2, http/1.1] পাঠানো হয়
  2. সার্ভার ALPN-এ h2 বেছে নেয়, TLS handshake শেষ হয়
  3. ক্লায়েন্ট ২৪ বাইটের connection preface আর SETTINGS পাঠায়
  4. ক্লায়েন্ট stream 1-এ HEADERS frame-এ GET অনুরোধ পাঠায়
  5. সার্ভার stream 1-এ HEADERS আর END_STREAM-যুক্ত DATA পাঠায়
  6. সংযোগ বন্ধের আগে GOAWAY frame পাঠানো হয়

একটি HTTP/2 frame header-এর ৯ বাইট: 00 00 08 07 00 00 00 00 00. এটি কোন frame?

  • stream 0-এর ৮ বাইটের DATA frame
  • ৮ বাইট payload-এর GOAWAY frame, stream 0-তে
  • ৮ বাইটের PING frame
  • stream 8-এর WINDOW_UPDATE frame

একটি HTTP/2 সংযোগে ক্লায়েন্ট এ পর্যন্ত stream 1, 3, 5 আর 7 ব্যবহার করেছে। পরের নতুন অনুরোধের জন্য ক্লায়েন্ট কোন stream ID ব্যবহার করবে?

  • 1, কারণ stream 1 শেষ হয়ে গেছে
  • 8
  • 9
  • 0

HPACK-এর dynamic table-এ cookie: id=42 সারিটি কত বাইট জায়গা নেয় বলে গোনা হয়?

  • ৪৩ বাইট
  • ১১ বাইট
  • ৩২ বাইট
  • ৬৪ বাইট

HPACK-এর N-bit prefix integer নিয়মে ১৩৩৭ সংখ্যাটি ৫-bit prefix-এ লিখলে বাইটগুলো কী হবে?

  • 05 39
  • 1f 05 1b
  • 1f 0a 9a
  • 1f 9a 0a

HTTP/3 কেন HPACK-এর বদলে QPACK ব্যবহার করে?

  • QPACK HPACK-এর চেয়ে সবসময় বেশি সংকোচন করে
  • QUIC-এর stream-গুলো ক্রমের বাইরে পৌঁছাতে পারে, আর HPACK-এর dynamic table ক্রমবদ্ধ decode চায়
  • HTTP/3-এ header-এর নাম বড় হাতের অক্ষরে লেখা হয়
  • QPACK Huffman coding বাদ দিয়েছে বলে দ্রুত

একটি সাইট চায় মূল HTML তৈরি হওয়ার সময়েই ব্রাউজার জরুরি CSS আনা শুরু করুক, কিন্তু ব্রাউজারের cache-এ ফাইলটি আগে থেকে থাকলে বাইট নষ্ট না হোক। কোন পদ্ধতি সবচেয়ে উপযুক্ত?

  • HTTP/2 server push
  • CSS-কে আলাদা domain-এ রেখে domain sharding
  • 103 Early Hints-এ Link: rel=preload পাঠানো
  • সব CSS আর JS একটি বড় ফাইলে জোড়া দেওয়া

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. HTTP/1.1-এর বাধা: সংযোগপ্রতি একটি চলমান অনুরোধ, ক্রমে বাঁধা pipelining, host-প্রতি ~৬ সংযোগ আর sharding-এর মতো কৌশল, বারবার একই header.
  2. HTTP/2 = একই অর্থ, নতুন খাম: SPDY থেকে RFC 7540 (২০১৫), এখন RFC 9113. ALPN “h2”, ২৪ বাইটের preface + SETTINGS, ৯ বাইটের frame header (length ২৪, type ৮, flags ৮, R + stream ID ৩১)।
  3. Stream আর flow control: ক্লায়েন্টের stream বিজোড়, stream 0 সংযোগের; একটি সংযোগে frame মিশিয়ে multiplexing; RST_STREAM একটি stream থামায়, GOAWAY সংযোগ; stream আর সংযোগ দুই স্তরে ৬৫,৫৩৫ বাইটের জানালা, WINDOW_UPDATE দিয়ে বাড়ে; priority এখন RFC 9218-এর urgency/incremental.
  4. HPACK: ৬১ সারির static table, সারির মাপ = নাম + মান + ৩২-এর dynamic table (FIFO eviction), স্থির Huffman, N-bit prefix integer (১৩৩৭ → 1f 9a 0a); gzip নয়, কারণ CRIME. Server push প্রায় বাতিল, তার জায়গায় 103 Early Hints.
  5. HTTP/3 (RFC 9114): QUIC stream-এ অনুরোধ (0, 4, 8…), control stream-এ SETTINGS, QPACK (RFC 9204) আলাদা encoder/decoder stream দিয়ে ক্রমহীনতা সামলায়। Alt-Svc বা HTTPS record দিয়ে আবিষ্কার, UDP আটকালে HTTP/2-তে fallback. লাভ সবচেয়ে বেশি lossy আর দূরের নেটওয়ার্কে।

পরের পর্বে: HTTP যত দ্রুতই হোক, সবচেয়ে দ্রুত অনুরোধ হলো যেটি দূরে যেতেই হয় না। Browser cache, proxy আর CDN কীভাবে কনটেন্ট কাছে রাখে, Cache-Control আর ETag কী বলে, আর একটি CDN কীভাবে আপনাকে সবচেয়ে কাছের সার্ভারে পাঠায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান