HTTP/1.1, HTTP/2 และ HTTP/3 ต่างกันอย่างไร
ทั้งสามเวอร์ชันใช้ semantics เดียวกันตาม RFC 9110 แต่ส่งข้อมูลต่างกัน HTTP/1.1 เป็น text บน TCP, HTTP/2 เป็น binary frame บน TCP, HTTP/3 วิ่งบน QUIC ผ่าน UDP
ปานกลาง9 นาทีในการอ่าน
HTTP/1.1 TCP · whole responses, in request order ● client aa─bbbbbbbbbbbbbbbbbb─cc───aa─bbbbbbbbbbbbbbbb◂ server c cannot start until all of b has arrived HTTP/2 TCP · frames of streams 1 3 5 interleaved ● client 1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─◂ server one byte stream for every request HTTP/3 QUIC over UDP · streams 0 4 8 separate 0──0──0──0──0──0──0──0──0──0──0──0──0──0──0──0 ● client 4──4──4──4──4──4──4──4──4──4──4──4──4──4──4──4◂ server 8──8──8──8──8──8──8──8──8──8──8──8──8──8──8──8 each request on its own QUIC stream ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ x lost packet dim = waiting for retransmission same HTTP semantics (RFC 9110), three wire formats
ในบทความนี้
คำตอบสั้น ๆ คือ ทั้งสามเวอร์ชันมี method, status code และ header ชุดเดียวกัน สิ่งที่ต่างคือวิธีส่ง message บนสาย HTTP/1.1 ส่งเป็น text บน TCP และหนึ่ง connection ส่ง response ได้ทีละตัวตามลำดับ request HTTP/2 แบ่ง message เป็น binary frame แล้วส่งหลาย stream สลับกันบน TCP connection เดียว HTTP/3 ส่ง frame บน QUIC ซึ่งวิ่งบน UDP และแยก stream กันตั้งแต่ระดับ transport
ผลที่เห็นได้ชัดที่สุดคือตอน packet หาย บน HTTP/2 packet หายหนึ่งตัวทำให้ทุก request บน connection นั้นต้องรอ แต่บน HTTP/3 รอแค่ stream ที่ข้อมูลอยู่ใน packet นั้นครับ
HTTP/1.1 TCP · whole responses, in request order ● client aa─bbbbbbbbbbbbbbbbbb─cc───aa─bbbbbbbbbbbbbbbb◂ server c cannot start until all of b has arrived HTTP/2 TCP · frames of streams 1 3 5 interleaved ● client 1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─5─1─3─◂ server one byte stream for every request HTTP/3 QUIC over UDP · streams 0 4 8 separate 0──0──0──0──0──0──0──0──0──0──0──0──0──0──0──0 ● client 4──4──4──4──4──4──4──4──4──4──4──4──4──4──4──4◂ server 8──8──8──8──8──8──8──8──8──8──8──8──8──8──8──8 each request on its own QUIC stream ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ x lost packet dim = waiting for retransmission same HTTP semantics (RFC 9110), three wire formats
ทั้งสามเวอร์ชันมีอะไรเหมือนกัน
ทั้งสามเวอร์ชันใช้ความหมาย (semantics) ชุดเดียวกันที่กำหนดใน RFC 9110 ข้อนี้เขียนไว้ตรง ๆ ว่า ทั้งสาม major version ของ HTTP อิงกับ semantics ที่นิยามในเอกสารนี้ ตั้งแต่ปี พ.ศ. 2565 IETF แยกเอกสารออกเป็นส่วน semantics (RFC 9110) กับส่วนที่บอกว่าแต่ละเวอร์ชันส่งข้อมูลอย่างไร คือ RFC 9112 สำหรับ HTTP/1.1, RFC 9113 สำหรับ HTTP/2 และ RFC 9114 สำหรับ HTTP/3
ดังนั้น GET /orders ที่ได้ 200 OK กลับมามีความหมายเหมือนกันทุกเวอร์ชัน โค้ดฝั่งแอปพลิเคชันส่วนใหญ่ไม่ต้องรู้ว่าวิ่งบนเวอร์ชันไหน RFC 9110 ยังบอกด้วยว่าทั้งสามเวอร์ชันไม่ได้มาแทนที่กัน เพราะแต่ละตัวมีข้อดีและข้อจำกัดต่างกันตามบริบท ทุกวันนี้จึงยังเจอทั้งสามเวอร์ชันอยู่ในระบบเดียวกันได้
ส่วนที่ต่างกันจริงอยู่ข้างล่างนี้ทั้งหมด ได้แก่ รูปแบบ message, การส่งหลาย request พร้อมกัน, การบีบอัด header และ transport ที่อยู่ข้างใต้
HTTP/1.1 ส่ง request และ response อย่างไร
HTTP/1.1 ส่ง message เป็น text ที่คนอ่านได้ RFC 9112 กำหนดว่า message หนึ่งตัวมี start-line, header field หลายบรรทัด, บรรทัดว่างที่บอกว่า header จบแล้ว และ message body ที่จะมีหรือไม่มีก็ได้ ถ้าจับ packet ของ http:// จะเห็นแบบนี้ตรง ๆ
GET /orders HTTP/1.1
Host: api.example.com
Accept: application/json
HTTP/1.1 ใช้ persistent connection เป็นค่าเริ่มต้น คือใช้ TCP connection เดิมส่งหลาย request ต่อกันได้ไม่ต้องเปิดใหม่ทุกครั้ง แต่ใน connection เดียวยังต้องรอทีละตัว ถ้าใช้ pipelining คือส่งหลาย request ออกไปโดยไม่รอ response server ต้องส่ง response กลับตามลำดับที่ได้รับ request (หัวข้อ 9.3.2)
กฎข้อนี้ทำให้เกิดปัญหาที่เรียกว่า head-of-line blocking ระดับแอปพลิเคชัน RFC 9112 อธิบายว่า request ที่ server ต้องประมวลผลนานหรือส่งข้อมูลก้อนใหญ่ จะบล็อก request ถัดไปบน connection เดียวกัน (หัวข้อ 9.4) ถ้า request b ต้องส่งไฟล์ขนาดใหญ่ response ของ c ต้องรอจนไฟล์ของ b ส่งครบก่อน แม้ server จะเตรียม c เสร็จแล้วก็ตาม
ทางที่ client ใช้แก้คือเปิดหลาย connection พร้อมกัน RFC ฉบับเดียวกันบอกว่าการใช้หลาย connection มักใช้เพื่อเลี่ยง head-of-line blocking แต่แต่ละ connection ใช้ทรัพยากรของ server และอาจทำให้เครือข่ายที่แออัดอยู่แล้วแย่ลง ส่วน RFC 9114 ชี้อีกข้อว่า TCP ไม่ได้แบ่ง congestion control ร่วมกันระหว่างหลาย connection
HTTP/2 แก้ปัญหาอะไร และยังเหลือปัญหาอะไร
HTTP/2 แก้ head-of-line blocking ระดับ HTTP โดยให้ message หลายตัวสลับกันส่งบน connection เดียวได้ และเข้ารหัส header ให้เล็กลง แต่ยังวิ่งบน TCP ตัวเดียว จึงยังเหลือ head-of-line blocking ระดับ TCP
message ถูกแบ่งเป็น frame อย่างไร
ทุกอย่างใน HTTP/2 เป็น binary frame ทุก frame ขึ้นต้นด้วย header ขนาดคงที่ 9 octet ประกอบด้วย Length (24 bit), Type (8 bit), Flags (8 bit), bit สำรองหนึ่ง bit และ Stream Identifier (31 bit) ตามด้วย payload
request หนึ่งตัวกับ response ของมันอยู่ใน stream เดียวกัน stream ที่ client เปิดต้องใช้หมายเลขคี่ ส่วน stream 0 ใช้กับ message ควบคุมของทั้ง connection เมื่อ frame ของ stream 1, 3 และ 5 สลับกันวิ่งได้ response ของ c จึงไม่ต้องรอให้ b ส่งครบก่อนแล้ว
header ถูกบีบอัดด้วย HPACK ซึ่งเก็บ state ไว้ที่ทั้งสองฝั่ง header ที่ซ้ำกันใน request ถัดไปจึงส่งเป็นตัวอ้างอิงสั้น ๆ แทนข้อความเต็มได้ ชื่อ header ใน HTTP/2 ต้องเป็นตัวพิมพ์เล็ก และข้อมูลอย่าง method กับ path ส่งเป็น pseudo-header เช่น :method, :scheme, :authority แทน request line แบบ HTTP/1.1 header ที่ผูกกับ connection อย่าง Connection, Keep-Alive, Transfer-Encoding และ Upgrade ห้ามมีใน message ของ HTTP/2
client รู้ได้อย่างไรว่าจะใช้ HTTP/2
สำหรับ https:// client ตกลงกับ server ระหว่าง TLS handshake ผ่าน ALPN โดยใช้ตัวระบุ h2 และ HTTP/2 ต้องใช้ TLS 1.2 ขึ้นไป วิธี upgrade จาก HTTP/1.1 ด้วย header Upgrade: h2c ไม่เคยใช้กันแพร่หลาย และ RFC 9113 ประกาศเลิกใช้แล้ว ถ้าไม่ใช้ TLS client ที่รู้ล่วงหน้าว่า server รองรับ HTTP/2 จะส่ง connection preface ทันที ซึ่งขึ้นต้นด้วยข้อความ PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n ตามด้วย SETTINGS frame
ทำไม packet หายแล้วทุก stream ต้องรอ
เพราะ TCP ส่งข้อมูลเป็น byte stream เดียวที่ต้องมาถึงตามลำดับ TCP ไม่รู้ว่าข้างในมีหลาย stream ของ HTTP/2 RFC 9114 อธิบายว่าเพราะ multiplexing ของ HTTP/2 มองไม่เห็นจากกลไก loss recovery ของ TCP packet ที่หายหรือมาผิดลำดับหนึ่งตัวจึงทำให้ทุก transaction ที่ค้างอยู่หยุดรอ ไม่ว่า packet นั้นจะมีข้อมูลของ transaction นั้นหรือไม่ RFC 9113 เองก็เขียนไว้ว่า TCP head-of-line blocking ไม่ได้ถูกแก้ด้วย protocol นี้
HTTP/3 และ QUIC ต่างจากเดิมตรงไหน
HTTP/3 ย้ายการแยก stream ลงไปไว้ใน transport คือ QUIC ซึ่งส่ง packet ใน UDP datagram แต่ละคู่ request-response ใช้ QUIC stream หนึ่งตัว และ stream เป็นอิสระต่อกัน stream ที่ติดหรือเสีย packet ไม่ได้ทำให้ stream อื่นหยุด
RFC 9000 ระบุขอบเขตของข้อนี้ไว้ละเอียดกว่า เมื่อ packet หาย เฉพาะ stream ที่มีข้อมูลอยู่ใน packet นั้นที่ต้องรอ retransmission ถ้า packet หนึ่งตัวบรรจุข้อมูลของหลาย stream การเสีย packet นั้นก็ทำให้ทุก stream ในนั้นหยุดเหมือนกัน และ QUIC ไม่รับประกันลำดับของ byte ข้าม stream ลำดับยังคงอยู่แค่ภายใน stream เดียว
ผลข้างเคียงของการเลิกรับประกันลำดับข้าม stream คือ HPACK ใช้ต่อไม่ได้ เพราะ HPACK ต้องได้ header ตามลำดับที่เข้ารหัส HTTP/3 จึงใช้ QPACK แทน โดย QPACK ใช้ unidirectional stream แยกต่างหากสำหรับแก้ไขตาราง header ส่วน frame ที่มี header แค่อ้างถึงตาราง ไม่ได้แก้ตาราง
ข้อต่างอื่นที่ควรรู้มีดังนี้
- TLS อยู่ใน QUIC: QUIC ใช้ TLS 1.3 ทำ handshake (RFC 9001) และ encrypt ส่วนของ packet ให้มากที่สุดเท่าที่ทำได้ พร้อม authenticate ทั้ง packet
- connection ID: QUIC ระบุ connection ด้วย connection ID แทน IP และ port connection จึงไม่หลุดเมื่อ address ของฝั่งใดฝั่งหนึ่งเปลี่ยน (หัวข้อ 9) เช่นโทรศัพท์ย้ายจาก Wi-Fi ไปใช้เครือข่ายมือถือ
- การค้นหา server ที่รองรับ: client ใช้ token
h3ใน ALPN และ server ประกาศได้ว่ามี HTTP/3 ด้วย headerAlt-Svcเช่นAlt-Svc: h3=":50781" - เมื่อ UDP ถูกบล็อก: ถ้าเปิด QUIC connection ไม่ได้ เช่นเพราะ UDP ถูกบล็อก client ควรกลับไปใช้ HTTP เวอร์ชันที่วิ่งบน TCP
สรุปเป็นตาราง
| ประเด็น | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| spec | RFC 9112 | RFC 9113 | RFC 9114 |
| transport | TCP | TCP | QUIC บน UDP |
| รูปแบบ message | text | binary frame | binary frame |
| หลาย request พร้อมกัน | หลาย connection | หลาย stream ใน connection เดียว | หลาย QUIC stream |
| packet หายหนึ่งตัว | connection นั้นรอ | ทุก stream รอ | เฉพาะ stream ใน packet นั้นรอ |
| บีบอัด header | ไม่มี | HPACK | QPACK |
| TLS | ไม่บังคับ | h2 ต้องใช้ TLS 1.2 ขึ้นไป |
TLS 1.3 อยู่ใน QUIC |
firewall หรือ proxy เห็นอะไรในแต่ละเวอร์ชัน
ถ้าเป็น http:// แบบไม่เข้ารหัส ตัวกลางเห็นทุกอย่างของ HTTP/1.1 ทั้ง method, path, header และ body ถ้าเป็น https:// ไม่ว่าเวอร์ชันไหน ตัวกลางที่ไม่ได้ถอดรหัสจะเห็นแค่ข้อมูลใน TLS ClientHello และข้อมูลของ transport ผมแยกเป็นข้อตามที่เจอบ่อยในงาน network security ครับ
HTTP/1.1 และ HTTP/2 บน TLS
ใน TLS 1.3 handshake message ทุกตัวหลัง ServerHello ถูก encrypt สิ่งที่ยังอ่านได้คือ ClientHello ซึ่งมักมี SNI คือชื่อ server ที่ client ต้องการเชื่อมต่อ และ ALPN ซึ่งบอกชื่อ protocol ที่ client รองรับ ตัวกลางจึงรู้ได้ว่า client เสนอ h2 หรือ http/1.1 และกำลังจะคุยกับ hostname อะไร แต่ไม่เห็น path หรือ header
ตัวกลางที่ถอดรหัส TLS แล้วส่งต่อ เช่น proxy หรือ firewall ที่ทำ decryption ต้องเข้าใจ HTTP/2 framing ด้วย เพราะข้อความข้างในเป็น binary frame และ header ถูกบีบอัดด้วย HPACK ไม่ใช่ text แบบ HTTP/1.1
HTTP/3 บน QUIC
ข้อแรก traffic เป็น UDP ไม่ใช่ TCP โดยทั่วไป HTTP/3 ใช้ UDP port 443 แต่ RFC 9312 เตือนว่ามี application อื่น เช่น SMB over QUIC ใช้ UDP 443 ด้วย และ DNS over QUIC ก็มีใช้งานจริง จึงสรุปจาก port อย่างเดียวไม่ได้ว่า flow นั้นเป็น QUIC หรือเป็น application อะไร
ข้อที่สอง header ของ QUIC เปิดเผยน้อยกว่า TCP RFC 9312 บอกว่าการ encrypt control signal ของ transport เกือบทั้งหมดทำให้เครือข่ายเห็นข้อมูลน้อยกว่า TCP และ header ทั้งหมดมี integrity protection ตัวกลางจึงแก้ไขอะไรใน header ไม่ได้ Initial packet แรกของ client เปิดเผย Version และ Connection ID ทั้งสองฝั่งโดยไม่เข้ารหัส และ connection ID เปลี่ยนได้ระหว่างอายุของ connection การเห็น connection ID ใหม่จึงไม่ได้แปลว่าเป็น connection ใหม่เสมอ
ข้อที่สาม SNI ยังอ่านได้ แต่ต้องทำงานมากกว่า ClientHello ของ QUIC อยู่ใน Initial packet ที่ถูกป้องกันด้วย key ซึ่งสร้างจาก Destination Connection ID ของ client กับ salt ค่าคงที่ที่เขียนไว้ใน RFC 9001 (หัวข้อ 5.2) ใครที่เห็น packet ก็คำนวณ key ได้ RFC 9312 อธิบายขั้นตอนไว้ว่า ตัวกลางคำนวณ Initial secret เพื่อถอด payload แล้วอ่าน CRYPTO frame ที่มี ClientHello อุปกรณ์ที่ไม่ได้ทำขั้นนี้จะมองไม่เห็น SNI ของ QUIC ณ ตอนที่ RFC 9312 ออก (กันยายน พ.ศ. 2565) IETF กำลังทำ Encrypted ClientHello ซึ่งถ้าใช้จะทำให้อ่าน SNI จากสายไม่ได้ทั้งบน TCP และ QUIC
บล็อก UDP แล้วจะเกิดอะไร
ถ้า firewall บล็อก UDP 443 ทั้งหมด client ที่ทำตาม RFC 9114 ควรกลับไปใช้ HTTP บน TCP traffic จะกลับมาเป็น HTTP/2 หรือ HTTP/1.1 บน TLS ซึ่ง policy เดิมจัดการได้ ข้อควรระวังคือการ throttle UDP แบบสุ่มทิ้งบาง packet RFC 9312 อธิบายว่าการทิ้ง QUIC packet แบบสุ่มทำให้ handshake จำนวนมากยังสำเร็จ client จึงไม่ fallback ไป TCP แต่ connection เสีย packet หนัก และแนะนำให้บล็อกทั้ง flow แทนการทิ้งราย packet
ควรเปิดใช้เวอร์ชันไหน
ไม่มีเวอร์ชันที่ควรเปิดตัวเดียวแล้วปิดที่เหลือครับ เพราะ semantics เหมือนกันและ RFC 9110 ก็บอกว่าให้แต่ละ implementation เลือก transport ที่เหมาะกับบริบทของตัวเอง สิ่งที่ผมดูก่อนตัดสินใจมีไม่กี่ข้อ
- ปัญหาที่เจออยู่คืออะไร: ถ้าหน้าเว็บโหลด resource เล็ก ๆ จำนวนมากบน HTTP/1.1 การเปิด HTTP/2 ลดจำนวน connection ได้ ถ้าผู้ใช้อยู่บนเครือข่ายที่เสีย packet บ่อย ข้อที่ HTTP/3 แก้คือ head-of-line blocking ของ TCP
- ระหว่างทางมีอะไรคั่นอยู่บ้าง: load balancer, proxy และ firewall ทุกตัวบนเส้นทางต้องรองรับเวอร์ชันนั้น ถ้าตัวใดไม่รับ UDP 443 client จะ fallback ไป TCP เอง ดังนั้นการเปิด HTTP/3 ที่ server อย่างเดียวไม่ได้รับประกันว่าผู้ใช้จะได้ HTTP/3
- ฝั่ง security ยังมองเห็นเท่าเดิมหรือไม่: ถ้า policy หรือ log อิง SNI ต้องเช็กว่าอุปกรณ์อ่าน SNI จาก QUIC Initial packet ได้ ไม่อย่างนั้น traffic HTTP/3 จะผ่านไปโดยไม่ถูกจัดหมวด
- log บอกเวอร์ชันหรือไม่: เวลาไล่ปัญหา การรู้ว่า request นั้นมาทาง
h2หรือh3ช่วยแยกปัญหาของ transport ออกจากปัญหาของแอปได้เร็วขึ้น
ถ้าตอบสี่ข้อนี้ได้ การเลือกเวอร์ชันจะเป็นเรื่องของการตั้งค่า ไม่ใช่การเดาครับ
แหล่งอ้างอิง
- RFC 9110: HTTP Semantics
- RFC 9112: HTTP/1.1
- RFC 9113: HTTP/2
- RFC 9114: HTTP/3
- RFC 9204: QPACK: Field Compression for HTTP/3
- RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport
- RFC 9001: Using TLS to Secure QUIC
- RFC 9312: Manageability of the QUIC Transport Protocol
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
อ่านต่อ
บทความที่เกี่ยวข้อง
การพัฒนาซอฟต์แวร์
RPC กับ REST แตกต่างกันอย่างไร และควรเลือกใช้แบบไหน
RPC เรียก procedure บน server ด้วยชื่อและพารามิเตอร์ ส่วน REST ส่งคำขอไปที่ resource ผ่าน interface กลางของ HTTP เทียบทีละประเด็นพร้อมตัวอย่าง gRPC และ JSON-RPC อิงจาก Fielding, RFC 9110 และ spec ต้นทาง
เริ่มต้น10 นาที
- API
- RPC
- REST
- gRPC
- JSON-RPC
- HTTP
การพัฒนาซอฟต์แวร์
WebSocket SSE และ Polling ต่างกันอย่างไร ควรเลือกแบบไหน
Polling ส่ง request ใหม่ทุกรอบ SSE เปิด response ค้างให้ server ส่ง event ทางเดียว WebSocket upgrade แล้วส่ง frame ได้สองทาง พร้อมเรื่อง reconnect และ proxy
ปานกลาง9 นาที
- WebSocket
- Server-Sent Events
- SSE
- Long Polling
- HTTP
- Reverse Proxy
- nginx
การพัฒนาซอฟต์แวร์
แอปที่ใช้งานได้แม้ไม่มีเน็ตทำงานอย่างไร
แอปแบบ offline-first อ่านและเขียนข้อมูลในเครื่องก่อน แล้วค่อย sync ผ่านคิว อธิบายทีละขั้นตั้งแต่กดบันทึก การ retry จนถึงการเลือกข้อมูลเมื่อแก้ชนกัน
ปานกลาง9 นาที
- offline-first
- sync
- IndexedDB
- Service Worker
- WorkManager
- Flutter
- HTTP