- TCP (Transmission Control Protocol) là giao thức cốt lõi của Internet, cho phép truyền dữ liệu tin cậy và đúng thứ tự ngay cả trong môi trường mạng không ổn định
- Trong khi IP chỉ đảm nhiệm việc chuyển dữ liệu giữa các host, TCP thực hiện giao tiếp giữa các tiến trình dựa trên cổng cùng với khôi phục lỗi, truyền lại và kiểm soát thứ tự
- Thông qua điều khiển luồng (flow control) và điều khiển tắc nghẽn (congestion control), TCP điều chỉnh để không vượt quá giới hạn của bộ đệm nhận hoặc băng thông mạng
- Qua ví dụ máy chủ TCP đơn giản và máy chủ HTTP được triển khai bằng ngôn ngữ C, bài viết giải thích cụ thể quá trình tạo socket, bind, listen, chấp nhận kết nối, gửi và nhận dữ liệu
- Cấu trúc bên trong của TCP như số thứ tự, số ACK, window, checksum, cờ (SYN/ACK/FIN/RST) là nền tảng cốt lõi giúp Internet vận hành ổn định ngày nay
Sự cần thiết và vai trò của TCP
- IP chỉ đảm nhiệm việc chuyển gói tin giữa các host, còn để giao tiếp giữa các tiến trình thì cần tầng vận chuyển như TCP/UDP
- Địa chỉ IP được ví như “tòa nhà”, còn cổng được ví như “căn hộ”, và mỗi ứng dụng giao tiếp bằng cách bind vào một cổng
- TCP che giấu sự bất ổn của mạng như mất gói, trùng lặp, đảo thứ tự gói tin, đồng thời bảo đảm độ tin cậy bằng truyền lại, checksum
- Router được giữ ở mức đơn giản, còn độ tin cậy được xử lý tại hai đầu cuối của kết nối, từ đó giảm độ phức tạp của hạ tầng mạng
- Nhờ cấu trúc này, các dịch vụ Internet chính như HTTP, SMTP, SSH có thể hoạt động ổn định
Điều khiển luồng và điều khiển tắc nghẽn
- Phía nhận tạm thời lưu dữ liệu thông qua bộ đệm nhận của kernel (receive buffer), và kích thước bộ đệm được cấu hình bằng
net.ipv4.tcp_rmem
- Phía gửi nhận lượng dữ liệu mà phía nhận cho phép thông qua trường window, rồi điều chỉnh lượng truyền tương ứng
- Để ngăn tắc nghẽn (congestion) do chênh lệch băng thông trên toàn mạng, TCP áp dụng thuật toán điều khiển tắc nghẽn
- Sau sự kiện congestion collapse xảy ra vào năm 1986, cơ chế ‘back-off’ đã được bổ sung
Ví dụ về máy chủ TCP và máy chủ HTTP
- Máy chủ echo TCP cơ bản viết bằng ngôn ngữ C nhận dữ liệu đầu vào từ client, thêm “you sent:” rồi gửi lại
- Sử dụng Berkeley sockets API như
socket(), bind(), listen(), accept(), send(), recv()
- Khi server đang
sleep(), dữ liệu từ client sẽ chờ trong bộ đệm nhận và sau đó được xử lý tuần tự
- Trong ví dụ máy chủ HTTP/1.1 đơn giản, server chấp nhận yêu cầu qua kết nối TCP rồi gửi header
HTTP/1.1 200 OK và phần nội dung
- Số lần yêu cầu được đếm bằng
i, và khi gọi curl localhost:8080 sẽ trả về phản hồi dạng “[1] Yo, I am a legit web server”
Cấu trúc segment TCP và các trường cốt lõi
- Một segment TCP gồm cổng nguồn/đích, số thứ tự, số ACK, kích thước window, checksum, cờ
- Mỗi cổng được cấp phát 16 bit nên có thể dùng tối đa 64K cổng
- Kết nối được định danh bằng bộ 5 (5-tuple) gồm
(protocol, source IP, source port, destination IP, destination port)
- Số thứ tự biểu thị phạm vi byte đã được truyền, còn số ACK biểu thị các byte đã được nhận xong
- Nếu có dữ liệu bị thiếu, ACK sẽ dừng tại điểm đó và sau khi truyền lại sẽ được cập nhật bằng ACK cộng dồn
- Các bit cờ điều khiển trạng thái kết nối
SYN/ACK thiết lập kết nối thông qua 3-way handshake
FIN kết thúc kết nối bằng 4-way handshake
RST ngắt kết nối ngay lập tức khi có lỗi hoặc kết thúc bất thường
- Trường window biểu thị lượng dữ liệu có thể nhận, và có thể kiểm tra trạng thái bộ đệm (
rb131072, tb16384) bằng lệnh ss
- Checksum thực hiện phát hiện lỗi bằng cách cộng theo đơn vị 16 bit bên trong segment
Kết luận
- TCP bảo đảm độ tin cậy, thứ tự và tính toàn vẹn, giúp ứng dụng hoạt động bình thường ngay cả trong môi trường Internet không ổn định
- Vài chục năm trước, việc truyền chỉ vài KB còn khó khăn, nhưng ngày nay đã phát triển đến mức streaming 4K trở nên phổ biến
- Sự tinh vi trong thiết kế và triển khai TCP chính là nền tảng cho sự tăng trưởng liên tục của Internet
1 bình luận
Ý kiến trên Hacker News
Nếu cố tạo một luồng dữ liệu đáng tin cậy trên một lớp datagram không đáng tin cậy, thì kết quả cuối cùng sẽ có hình dạng gần như giống hệt TCP
Những giới hạn ban đầu của TCP là kích thước cửa sổ nhỏ, xử lý gói bị mất chưa tốt, và chỉ quản lý một luồng duy nhất
Để giải quyết các vấn đề này, SCTP và QUIC đã ra đời
Thuật toán kiểm soát tắc nghẽn không phải là một phần của giao thức mà là đoạn mã chạy ở hai đầu của mỗi kết nối
Các thuật toán ban đầu (Reno, Vegas, v.v.) tuy đơn giản nhưng đủ hiệu quả, và từ đó đến nay nghiên cứu vẫn tiếp tục để xử lý bộ đệm lớn, RTT dài, tính công bằng, v.v.
Trước đây tôi từng viết một thư viện bằng JavaScript cho phép điều khiển độ ưu tiên và khả năng hủy của nhiều lượt tải xuống trong một luồng duy nhất
Tôi còn dùng script GreaseMonkey để tải trước ảnh thu nhỏ của một trang web hẹn hò ở chế độ nền, và cho phép preload theo vị trí cuộn
Kết quả là vừa giảm tải cho máy chủ vừa cải thiện trải nghiệm người dùng
Thú vị là tôi đã chia sẻ script đó với một người match, và chúng tôi vẫn ở bên nhau cho đến giờ — gần như là Tinder trước cả khi có Tinder
TCP là một cấu trúc cung cấp mạch ảo trên mạng chuyển mạch gói, và ý tưởng hiện thực hóa độ tin cậy bằng truyền lại bắt nguồn từ mạng Cylades của Pháp
Kẻ tấn công có thể chèn (inject) dữ liệu ở bất kỳ đâu trên mạng hoặc cắt kết nối bằng gói RST
Chặn RST bằng tường lửa sẽ tăng độ ổn định, nhưng tấn công mất đồng bộ hóa do số thứ tự bị giả mạo vẫn có thể xảy ra
Vì vậy mọi ứng dụng đều phải tự triển khai tính năng resume trên một kết nối riêng, đồng thời phải gánh cả vấn đề slow start của TCP
Ngoài ra, bản thân khái niệm tách biệt địa chỉ và cổng cũng bị xem là kém hiệu quả
Ví dụ trong DNS over TLS(DoT), có thể gửi đồng thời nhiều truy vấn trên một kết nối TCP và nhận phản hồi không theo thứ tự
Cách này hiệu quả và lịch sự hơn là mở nhiều kết nối
Không rõ QUIC có nhanh hơn không, nhưng hỗ trợ phía máy chủ vẫn còn hạn chế
HTTP/1.1 pipelining cũng làm được điều tương tự nhưng phản hồi sẽ đến theo thứ tự
Nhưng nhiều khóa học đại học không nhấn mạnh điều này, khiến người ta thường hiểu lầm rằng TCP chỉ có một thuật toán duy nhất
Muốn hỏi xem có ai đặc biệt yêu thích SCTP không
SCTP là giao thức kết hợp tính hướng thông điệp của UDP với độ tin cậy của TCP, hỗ trợ multi-streaming và multi-homing
Nó có thể truyền song song nhiều luồng độc lập, nên có thể gửi đồng thời văn bản và hình ảnh của một trang web
Xem thêm tại Wikipedia: Stream Control Transmission Protocol
Rốt cuộc lời giải tốt nhất là một lớp độ tin cậy trên UDP, tức QUIC
Tôi từng thắc mắc liệu có thể gửi trực tiếp gói chỉ bằng IP hay không
Có vẻ như các router trung gian sẽ từ chối những gói không phải TCP hay UDP
Với IPv4, chỉ cần chỉ định một giá trị từ 0 đến 255 trong danh sách số giao thức IANA
Router lõi không kiểm tra trường này, nhưng thiết bị NAT hoặc hạ tầng của ISP có thể kiểm tra
Giữa hai máy chủ Linux, thậm chí có thể giao tiếp bằng các số thử nghiệm (253, 254)
Các giao thức như IPsec, GRE, L2TP cũng không phải TCP/UDP
Trong môi trường tường lửa hoặc NAT của mạng doanh nghiệp, các giao thức tùy ý có thể bị chặn
NAT đã phá vỡ nguyên tắc end-to-end, và cuối cùng mọi người bắt đầu đặt mọi thứ lên trên TCP hoặc UDP, hoặc thậm chí trên HTTP
Chỉ có ảnh hưởng ở mức làm giảm entropy của băm ECMP
Cuối cùng điều quan trọng là phía bên kia có hiểu giao thức đó hay không
Số cổng chỉ đơn thuần là định danh dịch vụ bên trong một nút
RUDP(Plan9) là một giải pháp thỏa hiệp tuyệt vời giữa TCP và UDP
Xem Reliable User Datagram Protocol
Nhờ TCP trở thành mặc định, nó bị dùng một cách vô điều kiện ngay cả khi không cần độ tin cậy hay đảm bảo thứ tự
Giờ đây khi HTTP/3 (dựa trên QUIC) đang lan rộng, tình hình có thể sẽ được cải thiện
Tuy nhiên QUIC phức tạp hơn rất nhiều, và sức mạnh đó chỉ hữu ích với một số người
UDP + một lớp mã hóa đơn giản kiểu WireGuard có thể là phương án thay thế tốt hơn
TCP là một trong những phát minh vĩ đại của nhân loại, nhưng nó đã không lường trước được sự thống trị của mạng bán kết nối (dựa trên NAT)
Các kỹ sư khi đó hẳn sẽ hỏi tại sao lại cố tình làm mọi thứ phức tạp đến vậy
Rốt cuộc cấu trúc liên kết bất đối xứng ngày nay và sự phân tách client–server đều bắt nguồn từ kiểu tư duy đó
Thuật toán kiểm soát tắc nghẽn của TCP tạo ra những hiệu ứng thú vị mà nhiều lập trình viên không biết
Khi gửi dữ liệu trên một kết nối mới, truyền ban đầu sẽ chậm, và tốc độ tăng lên được quyết định bởi độ trễ (latency)
Trong trung tâm dữ liệu, chỉ cần giảm RTT vài micro giây cũng có thể tăng tốc rất lớn
Hầu hết TCP stack tính mức tăng tốc theo đơn vị segment chứ không phải byte, nên nếu dùng jumbo frame thì tốc độ tăng có thể nhanh hơn gấp 6 lần
Vì lý do này AWS đầu tư rất nhiều vào độ trễ chuyển mạch thấp và hỗ trợ jumbo frame
Các chuyên gia có thể tinh chỉnh những thứ này, nhưng đa số mọi người chỉ thắc mắc vì sao đường truyền 10Gbps lại không đạt được 10Gbps
Tự tạo giao thức riêng trên IP từng là việc rất đơn giản
Chỉ khoảng 15 năm trước, người ta còn có thể dùng Python để tự lắp ráp gói tin và thử nghiệm