ข้ามไปยังเนื้อหา
กลับไปหน้าบทความ
การพัฒนาซอฟต์แวร์

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 นาทีในการอ่าน

ในบทความนี้

คำตอบสั้น ๆ คือ ทั้งสามเวอร์ชันมี 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 นั้นครับ

response ของสามเวอร์ชัน เมื่อ packet หายหนึ่งตัว stream 1, 3 และ 5 ของ HTTP/2 ต้องรอทั้งหมด ส่วน HTTP/3 รอแค่ stream 4

ทั้งสามเวอร์ชันมีอะไรเหมือนกัน

ทั้งสามเวอร์ชันใช้ความหมาย (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 แค่อ้างถึงตาราง ไม่ได้แก้ตาราง

ข้อต่างอื่นที่ควรรู้มีดังนี้

สรุปเป็นตาราง

ประเด็น 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 ออกจากปัญหาของแอปได้เร็วขึ้น

ถ้าตอบสี่ข้อนี้ได้ การเลือกเวอร์ชันจะเป็นเรื่องของการตั้งค่า ไม่ใช่การเดาครับ

แหล่งอ้างอิง

อ่านต่อ