- TCP (Transmission Control Protocol) คือโปรโตคอลหลักของอินเทอร์เน็ตที่ทำให้ การส่งข้อมูลที่เชื่อถือได้และรับประกันลำดับ เป็นไปได้ แม้อยู่ในสภาพแวดล้อมเครือข่ายที่ไม่เสถียร
- ขณะที่ IP รับผิดชอบเพียงการส่งข้อมูลระหว่างโฮสต์ TCP จะทำหน้าที่ การสื่อสารระหว่างโปรเซสบนพื้นฐานของพอร์ต รวมถึง การกู้คืนข้อผิดพลาด การส่งซ้ำ และการควบคุมลำดับ
- ผ่าน การควบคุมการไหล (flow control) และ การควบคุมความแออัด (congestion control) เพื่อปรับไม่ให้เกินขีดจำกัดของบัฟเฟอร์ฝั่งรับหรือแบนด์วิดท์เครือข่าย
- อธิบายขั้นตอนการสร้างซ็อกเก็ต การ bind การ listen การรับการเชื่อมต่อ และการรับส่งข้อมูลอย่างเป็นรูปธรรม ผ่าน ตัวอย่าง TCP server แบบง่ายและ HTTP server ที่เขียนด้วยภาษา C
- โครงสร้างภายในของ TCP เช่น หมายเลข sequence และ ACK, window, checksum, flags (SYN/ACK/FIN/RST) เป็นรากฐานสำคัญที่ทำให้อินเทอร์เน็ตทำงานได้อย่างเสถียรในปัจจุบัน
ความจำเป็นและบทบาทของ TCP
- IP รับผิดชอบเพียงการส่งแพ็กเก็ตระหว่างโฮสต์เท่านั้น และสำหรับ การสื่อสารระหว่างโปรเซส จำเป็นต้องมี ชั้นขนส่ง อย่าง TCP/UDP
- เปรียบ IP address เป็น ‘อาคาร’ และพอร์ตเป็น ‘อพาร์ตเมนต์’ โดยแต่ละแอปพลิเคชันจะ bind กับพอร์ตเพื่อสื่อสาร
- TCP ซ่อนความไม่เสถียรของเครือข่าย เช่น แพ็กเก็ตสูญหาย ซ้ำซ้อน หรือสลับลำดับ และรับประกันความเชื่อถือได้ผ่าน การส่งซ้ำและ checksum
- เราเตอร์จึงคงความเรียบง่ายไว้ได้ และ ความเชื่อถือได้จะถูกจัดการที่ปลายทั้งสองด้านของการสื่อสาร เพื่อลดความซับซ้อนของโครงสร้างพื้นฐานเครือข่าย
- ด้วยโครงสร้างนี้ บริการอินเทอร์เน็ตสำคัญอย่าง HTTP, SMTP, SSH จึงทำงานได้อย่างเสถียร
การควบคุมการไหลและการควบคุมความแออัด
- ฝั่งรับจะเก็บข้อมูลชั่วคราวผ่าน receive buffer ของเคอร์เนล โดยขนาดบัฟเฟอร์ตั้งค่าได้ด้วย
net.ipv4.tcp_rmem
- ฝั่งส่งจะปรับปริมาณการส่งตามข้อมูลปริมาณข้อมูลที่ฝั่งรับยอมรับได้ ซึ่งถูกส่งมาผ่านฟิลด์ window
- เพื่อป้องกัน ความแออัด (congestion) ที่เกิดจากความแตกต่างของแบนด์วิดท์ทั้งเครือข่าย TCP จึงนำ อัลกอริทึมควบคุมความแออัด มาใช้
- เหตุการณ์ congestion collapse ในปี 1986 เป็นจุดเปลี่ยนที่ทำให้มีการเพิ่มกลไก back-off
ตัวอย่าง TCP server และ HTTP server
- TCP echo server พื้นฐาน ที่เขียนด้วยภาษา C จะรับอินพุตจากไคลเอนต์ แล้วส่งกลับโดยเติม “you sent:” นำหน้า
- ใช้ Berkeley sockets API เช่น
socket(), bind(), listen(), accept(), send(), recv()
- ระหว่างที่เซิร์ฟเวอร์อยู่ใน
sleep() ข้อมูลจากไคลเอนต์จะ รออยู่ใน receive buffer และถูกประมวลผลตามลำดับภายหลัง
- ในตัวอย่าง HTTP/1.1 server แบบง่าย จะรับคำขอผ่านการเชื่อมต่อ TCP แล้วส่ง header
HTTP/1.1 200 OK และเนื้อหากลับไป
- นับจำนวนคำขอด้วย
i และเมื่อเรียก curl localhost:8080 จะได้การตอบกลับในรูปแบบ “[1] Yo, I am a legit web server”
โครงสร้างของ TCP segment และฟิลด์สำคัญ
- TCP segment ประกอบด้วย พอร์ตต้นทาง/ปลายทาง, หมายเลข sequence, หมายเลข ACK, ขนาด window, checksum, flags เป็นต้น
- พอร์ตถูกจัดสรรอย่างละ 16 บิต จึงใช้งานได้ สูงสุด 64K พอร์ต
- การเชื่อมต่อถูกระบุด้วย 5-tuple ของ
(protocol, source IP, source port, destination IP, destination port)
- หมายเลข sequence แสดงช่วงไบต์ที่ถูกส่ง และ หมายเลข ACK แสดงไบต์ที่รับครบแล้ว
- หากมีข้อมูลที่ขาดหาย ACK จะหยุดอยู่ที่จุดนั้น และจะอัปเดตเป็น cumulative ACK หลังการส่งซ้ำ
- flag bits ใช้ควบคุมสถานะการเชื่อมต่อ
SYN/ACK ใช้สร้างการเชื่อมต่อผ่าน 3-way handshake
FIN ใช้ปิดการเชื่อมต่อผ่าน 4-way handshake
RST ใช้ยุติการเชื่อมต่อทันทีเมื่อเกิดการปิดแบบผิดปกติหรือข้อผิดพลาด
- ฟิลด์ window ใช้ระบุปริมาณข้อมูลที่ยังรับได้ และสามารถตรวจสอบสถานะบัฟเฟอร์ (
rb131072, tb16384) ได้ด้วยคำสั่ง ss
- checksum ใช้ตรวจจับข้อผิดพลาดด้วยการรวมค่าใน segment ทีละ 16 บิต
บทสรุป
- TCP รับประกัน ความเชื่อถือได้ ลำดับ และความสมบูรณ์ของข้อมูล ทำให้แอปพลิเคชันทำงานได้ตามปกติแม้อยู่บนอินเทอร์เน็ตที่ไม่เสถียร
- หลายสิบปีก่อน การส่งข้อมูลเพียงไม่กี่ KB ยังเป็นเรื่องยาก แต่ปัจจุบันพัฒนาไปไกลจน การสตรีม 4K กลายเป็นเรื่องปกติ
- ความประณีตในการออกแบบและการนำไปใช้จริงของ TCP ที่ทำให้การสื่อสารเสถียรเช่นนี้ได้ คือรากฐานของการเติบโตอย่างต่อเนื่องของอินเทอร์เน็ต
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ถ้าพยายามสร้างสตรีมข้อมูลที่เชื่อถือได้บน ชั้น datagram ที่ไม่เชื่อถือได้ ผลลัพธ์สุดท้ายก็จะออกมาคล้าย TCP แทบทั้งหมด
ข้อจำกัดช่วงแรกของ TCP คือ ขนาดหน้าต่าง ที่เล็ก การจัดการแพ็กเก็ตสูญหายที่ยังไม่ดีพอ และการรองรับเพียงสตรีมเดียว
เพื่อแก้ปัญหาเหล่านี้จึงมี SCTP และ QUIC เกิดขึ้น
อัลกอริทึมควบคุมความคับคั่งไม่ใช่ส่วนหนึ่งของโปรโตคอล แต่เป็นโค้ดที่ทำงานอยู่ทั้งสองฝั่งของแต่ละการเชื่อมต่อ
อัลกอริทึมยุคแรก ๆ (เช่น Reno, Vegas) เรียบง่ายแต่มีประสิทธิภาพเพียงพอ และหลังจากนั้นก็มีงานวิจัยต่อเนื่องเกี่ยวกับบัฟเฟอร์ขนาดใหญ่, RTT ยาว, ความเป็นธรรม ฯลฯ
เมื่อก่อนเคยทำไลบรารี JavaScript ที่ควบคุม การจัดลำดับความสำคัญและการยกเลิก ของหลายดาวน์โหลดผ่านสตรีมเดียวได้
เคยใช้สคริปต์ GreaseMonkey ให้เว็บหาคู่โหลดภาพ thumbnail ล่วงหน้าในพื้นหลัง และพรีโหลดตามตำแหน่งการเลื่อนหน้าจอ
ผลคือช่วยลดภาระเซิร์ฟเวอร์พร้อมกับทำให้ประสบการณ์ใช้งานดีขึ้น
ที่ตลกคือเคยแชร์สคริปต์นั้นให้กับแมตช์คนหนึ่ง และทุกวันนี้ก็ยังคบกันอยู่ — เรียกได้ว่าเป็น Tinder ก่อนยุค Tinder เลย
TCP เป็นโครงสร้างที่ให้วงจรเสมือนบนเครือข่ายสลับแพ็กเก็ต และแนวคิดเรื่องความเชื่อถือได้ที่ทำผ่าน การส่งซ้ำ นั้นมีต้นกำเนิดจากเครือข่าย Cylades ของฝรั่งเศส
ผู้โจมตีสามารถ inject ข้อมูลจากที่ใดก็ได้ในเครือข่าย หรือปิดการเชื่อมต่อด้วย แพ็กเก็ต RST
การบล็อก RST ด้วยไฟร์วอลล์ช่วยเพิ่มเสถียรภาพได้ แต่ การโจมตีแบบ desynchronization จากหมายเลขลำดับปลอมก็ยังเป็นไปได้อยู่
ดังนั้นทุกแอปพลิเคชันจึงต้องทำ ฟังก์ชัน resume เองบนการเชื่อมต่อแยกต่างหาก และยังต้องแบกรับปัญหา slow start ของ TCP ไปด้วย
อีกทั้งยังมองว่าแนวคิดที่แยก address ออกจาก port เองก็ไม่มีประสิทธิภาพ
ตัวอย่างเช่นใน DNS over TLS(DoT) สามารถส่งหลาย query พร้อมกันผ่าน TCP connection เดียวและรับคำตอบ แบบไม่ต้องเรียงลำดับ ได้
วิธีนี้มีประสิทธิภาพกว่าและสุภาพกว่าการเปิดหลายการเชื่อมต่อ
QUIC จะเร็วกว่าไหมยังไม่แน่ใจ แต่ฝั่งเซิร์ฟเวอร์ยังรองรับอย่างจำกัด
HTTP/1.1 pipelining ก็ทำอะไรคล้ายกันได้ แต่การตอบกลับจะมาแบบตามลำดับ
แต่ในหลายวิชามหาวิทยาลัยกลับไม่เน้นจุดนี้ ทำให้หลายคนเข้าใจผิดว่า TCP มีอัลกอริทึมเดียว
อยากถามว่ามีใครชอบ SCTP เป็นพิเศษไหม
SCTP เป็นโปรโตคอลที่ผสานการสื่อสารแบบ message-oriented ของ UDP เข้ากับความเชื่อถือได้ของ TCP และรองรับ multistreaming กับ multihoming
มันส่งหลายสตรีมอิสระแบบขนานได้ จึงสามารถส่งทั้งข้อความและรูปภาพของเว็บเพจพร้อมกันได้
ดูรายละเอียดได้ที่ Wikipedia: Stream Control Transmission Protocol
ท้ายที่สุดคำตอบที่ดีที่สุดคือ ชั้นความเชื่อถือได้บน UDP หรือก็คือ QUIC
เคยสงสัยว่าสามารถส่งแพ็กเก็ตโดยใช้แค่ IP ตรง ๆ ได้ไหม
รู้สึกว่าระหว่างทางเราเตอร์น่าจะปฏิเสธแพ็กเก็ตที่ไม่ใช่ TCP หรือ UDP
ถ้าเป็น IPv4 ก็แค่ระบุหมายเลขหนึ่งในช่วง 0~255 จาก รายการหมายเลขโปรโตคอลของ IANA
เราเตอร์แกนหลักจะไม่ตรวจฟิลด์นี้ แต่ NAT หรืออุปกรณ์ของ ISP อาจตรวจ
ระหว่าง Linux server สองเครื่องก็สามารถสื่อสารด้วยหมายเลขทดลอง (253, 254) ได้
โปรโตคอลอย่าง IPsec, GRE, L2TP ก็ไม่ใช่ TCP/UDP
ในสภาพแวดล้อมที่เป็นไฟร์วอลล์หรือ NAT ของเครือข่ายองค์กร โปรโตคอลตามอำเภอใจอาจถูกบล็อกได้
NAT ได้ทำลาย หลักการ end-to-end และสุดท้ายผู้คนก็เริ่มวางทุกอย่างไว้บน TCP หรือ UDP หรือแม้แต่ บน HTTP
มีผลแค่ทำให้ entropy ของ ECMP hash ลดลงบ้างเท่านั้น
สุดท้ายประเด็นคือปลายทางเข้าใจโปรโตคอลนั้นหรือไม่
หมายเลขพอร์ตก็เป็นเพียง ตัวระบุบริการภายในโหนด เท่านั้น
RUDP(Plan9) เป็นทางสายกลางที่ยอดเยี่ยมระหว่าง TCP กับ UDP
ดู Reliable User Datagram Protocol
เพราะ TCP กลายเป็นค่าเริ่มต้น จึงถูกใช้แบบอัตโนมัติแม้ในกรณีที่ไม่ต้องการทั้งความเชื่อถือได้หรือการรับประกันลำดับ
ตอนนี้เมื่อ HTTP/3 (บนพื้นฐาน QUIC) เริ่มแพร่หลาย สถานการณ์อาจดีขึ้น
เพียงแต่ QUIC ซับซ้อนกว่ามาก และพลังของมันมีประโยชน์กับคนบางกลุ่มเท่านั้น
UDP + ชั้นเข้ารหัสแบบเรียบง่ายสไตล์ WireGuard อาจเป็นทางเลือกที่ดีกว่า
TCP เป็นหนึ่งในสิ่งประดิษฐ์อันยิ่งใหญ่ของมนุษยชาติ แต่ไม่ได้คาดการณ์การครอบงำของ เครือข่ายกึ่งเชื่อมต่อ (แบบอิง NAT)
วิศวกรในยุคนั้นก็คงถามว่าทำไมต้องทำให้ซับซ้อนแบบนั้น
ท้ายที่สุดโครงสร้างลิงก์แบบ อสมมาตร และการแบ่งฝั่ง client–server ในปัจจุบันก็มาจากแนวคิดแบบนี้
อัลกอริทึมควบคุมความคับคั่ง ของ TCP มีผลที่น่าสนใจซึ่งนักพัฒนาจำนวนมากไม่ค่อยรู้
เมื่อส่งข้อมูลบนการเชื่อมต่อใหม่ การส่งช่วงแรกจะช้า และการเพิ่มความเร็วขึ้นจะถูกกำหนดโดย latency
ในดาต้าเซ็นเตอร์ แค่ลด RTT ลงไม่กี่ไมโครวินาทีก็เพิ่มความเร็วได้มาก
TCP stack ส่วนใหญ่คำนวณการไต่ความเร็วเป็น หน่วยเซกเมนต์ ไม่ใช่ไบต์ ดังนั้นถ้าใช้ jumbo frame ก็อาจไต่ขึ้นได้เร็วกว่า 6 เท่า
ด้วยเหตุนี้ AWS จึงทุ่มเทอย่างมากกับ switching latency ต่ำ และ การรองรับ jumbo frame
ผู้เชี่ยวชาญปรับแต่งเรื่องนี้กัน แต่คนส่วนใหญ่กลับสงสัยว่าทำไมลิงก์ 10Gbps ถึงวิ่งไม่ได้ 10Gbps
การสร้างโปรโตคอลของตัวเองบน IP เคยเป็นเรื่องที่ ง่ายมาก
แค่เมื่อ 15 ปีก่อนก็ยังทดลองด้วย Python โดย ประกอบแพ็กเก็ตเองโดยตรง ได้