Giao thức Tcp: đóng mở kết nối, truyền dữ liệu

    0

    Trong bài học này chúng ta sẽ xem xét chi tiết hơn về giao thức Tcp để có thể hiểu rõ về cách thức lập trình với Tcp socket, bao gồm cấu trúc gói tin, thiết lập & hủy liên kết, cách truyền dữ liệu.

    Ở phần thực hành chúng ta đã thực hiện một chương trình client/server cơ bản sử dụng Tcp socket. Do Tcp là một giao thức rất phức tạp, trước khi đi vào phân tích chi tiết kỹ thuật lập trình Tcp socket, chúng ta sẽ xem xét các vấn đề chính của giao thức Tcp.

    Cấu trúc gói tin của giao thức Tcp

    Đóng gói dữ liệu

    Tương tự như Udp, Tcp cũng chỉ vận chuyển các chuỗi byte (mà ứng dụng tạo ra) mà không quan tâm tới ý nghĩa của chúng.

    Dữ liệu của ứng dụng sau khi chuyển qua socket tới tầng giao vận sẽ được giao thức TCP tách thành từng phần. Đối với mỗi phần dữ liệu này, TCP sẽ bổ sung thêm các thông tin điều khiển vào trước phần dữ liệu để tạo thành một đơn vị dữ liệu của tầng giao vận, gọi là TCP segment.

    Phần thông tin điều khiển mà TCP thêm vào trước khối dữ liệu ứng dụng gọi là TCP header. Phần dữ liệu ứng dụng lưu trong TCP segment được gọi là payload. TCP segment sau đó sẽ tiếp tục được chuyển xuống tầng mạng và tiến hành đóng với với giao thức IP để tạo thành một đơn vị dữ liệu của tầng mạng, gọi là IP datagram (hoặc IP package).

    Tại tầng liên kết IP package lại được đóng gói một lần nữa thành Ethernet frame trước khi đưa lên đường truyền vật lý. 

    Tcp header

    Cấu trúc của TCP header được trình bày trong sơ đồ dưới đây:

    Cấu trúc TCP header
    Cấu trúc TCP header

    Xem lại bài này để hiểu cách biểu diễn mảng byte của header

    Ý nghĩa các trường như sau:

    • Source port (16 bit): số cổng của tiến trình gửi.
    • Destination port (16 bit): số cổng của của tiến trình nhận.
    • Sequence number (32 bit): số thứ tự. Giá trị và ý nghĩa của số thứ tự phụ thuộc vào giá trị của bit SYN.
    • Các trường flag (6 trường, mỗi trường 1 bit):
      • Bit ACK dùng để xác nhận tính chính xác của giá trị trường Acknowledgement.
      • Bit RST, SYN, FIN dùng để thiết lập và hủy bỏ kết nối.
      • Bit PSH chỉ định máy nhận phải chuyển dữ liệu lên tầng trên ngay lập tức.
      • Bit URG chỉ định rằng có vùng dữ liệu được đánh dấu là “urgent” (dữ liệu khẩn). Nếu URG=1, trường Urgent pointer (16 bit) sẽ chứa vị trí của byte cuối cùng của dữ liệu khẩn; TCP phải thông báo về sự tồn tại của dữ liệu khẩn cho máy nhận, đồng thời chuyển cho máy nhận một con trỏ tới cuối của vùng dữ liệu khẩn. Trong thực tế, hai trường PSH và URG không được sử dụng.
    • Checksum (16 bit): sử dụng để kiểm tra lỗi.
    • Window size (16 bit): kích thước cửa sổ dùng trong kiểm soát luồng.
    • Reserved (3 bit): luôn chứa giá trị 000, để dành sử dụng trong tương lai.
    • Data offset (4 bit), còn gọi là Header length: kích thước của TCP header. Kích thước của TCP header có thể thay đổi do sự tồn tại của trường Options. Tuy nhiên, trường Options này thường để trống, và do đó, kích thước của TCP header thường là 20 byte.
    • Acknowledgement number (32 bit): số báo nhận. Trường số thứ tự và số báo nhận được sử dụng để đảm bảo dịch vụ truyền dữ liệu tin cậy.
    • Options (độ dài thay đổi): được sử dụng khi hai máy tham gia truyền thông thỏa thuận về kích thước tối đa của segment (TA: maximum segment size, MSS) hoặc hệ số tỉ lệ của cửa sổ trượt (dùng cho các mạng tốc độ cao).

    Quy trình hoạt động của giao thức Tcp

    Quy trình hoạt động của Tcp chia làm 3 giai đoạn: Thiết lập liên kết, Truyền dữ liệu và Đóng liên kết.

    Thiết lập liên kết Tcp

    Khi một tiến trình muốn tiến hành truyền thông với với một tiến trình khác qua mạng sử dụng giao thức TCP, nó phải thông báo cho tầng giao vận để khởi tạo một liên kết TCP. Trong quá trình này, tiến trình khởi tạo liên kết được gọi là tiến trình khách, tiến trình còn lại tiếp nhận liên kết TCP được gọi là tiến trình chủ.

    Bắt tay ba bước

    Quá trình thiết lập liên kết TCP giữa hai tiến trình được thực hiện trong ba bước và thường được gọi là quá trình bắt tay ba bước (three-way handshake).

    Bước 1. Tiến trình khách gửi một TCP segment không chứa dữ liệu tới tiến trình chủ, trong đó, bit SYN = 1. Tiến trình khách sẽ chọn một giá trị ngẫu nhiên cho trường Sequence (để tránh tấn công loại SYN). Segment loại này thường được gọi là SYN segment.

    Bước 2. Khi tiến trình chủ nhận được SYN segment, nó sẽ bố trí bộ nhớ cho TCP buffer và các biến trạng thái cho liên kết TCP. Tiến trình chủ sau đó sẽ gửi một segment trả lời cho tiến trình khách, trong segment này: bit SYN = 1; số báo nhận (ACK number) = Sequence của SYN segment (nhận từ tiến trình khách) + 1; số thứ tự Sequence được lựa chọn ngẫu nhiên. Segment trả lời này thường được gọi là SYNACK segment.  

    Bước 3. Khi nhận được SYNACK segment, tiến trình khách cũng thiết lập bộ nhớ đệm và các biến trạng thái cho liên kết TCP, sau đó gửi lại cho tiến trình chủ một segment, trong đó, trường số báo nhận ACK = số thứ tự Sequence của server + 1, SYN = 0, trường số thứ tự Sequence tăng thêm 1 đơn vị.

    Quá trình bắt tay ba bước của Tcp
    Quá trình bắt tay ba bước của Tcp

    Sau 3 bước này, liên kết TCP giữa hai tiến trình đã được thiết lập.

    Đặc điểm của liên kết Tcp

    Liên kết TCP là loại liên kết song công (TA: full-duplex). Tức là, một khi thiết lập được liên kết giữa tiến trình A và tiến trình B, dữ liệu tầng ứng dụng có thể đồng thời truyền từ A tới B và từ B tới A.

    Liên kết TCP thuộc loại liên kết điểm-điểm (TA: point-to-point) giữa duy nhất một tiến trình gửi và một tiến trình nhận. Trong TCP không tồn tại loại truyền thông kiểu quảng bá (TA: multicasting) giữa một tiến trình gửi với nhiều tiến trình nhận.

    Truyền dữ liệu

    Khi đã hình thành liên kết TCP, hai tiến trình tham gia liên kết có thể truyền dữ liệu cho nhau. Khi một tiến trình chuyển một chuỗi byte qua socket tới tầng giao vận, dữ liệu đó sẽ hoàn toàn do TCP trên máy đó quản lý và chịu trách nhiệm.

    Truyền dữ liệu qua liên kết Tcp
    Truyền dữ liệu qua liên kết Tcp: song công, segment, các buffer

    TCP có thể coi như một hệ thống con của hệ điều hành chịu trách nhiệm nhận dữ liệu từ ứng dụng cục bộ cũng như nhận dữ liệu từ máy ở xa.

    Tcp buffer

    TCP không lập tức đóng gói dữ liệu và chuyển lên mạng. Thay vào đó, Tcp chuyển dữ liệu tới một bộ nhớ đệm (buffer) được hình thành trong quá trình bắt tay ba bước. Theo thời gian, TCP sẽ lấy dữ liệu (dữ liệu của ứng dụng) từ bộ nhớ đệm, đóng gói thành segment, và chuyển lên đường truyền.

    Trong khoảng thời gian chờ ở bộ nhớ đệm, ứng dụng có thể gửi thêm dữ liệu và dữ liệu này sẽ tiếp tục được bổ sung vào bộ nhớ đệm. Tương tự, TCP trên máy đích cũng sử dụng một bộ nhớ đệm cục bộ của riêng mình để lưu dữ liệu nhận được cho đến khi nó nhận được đầy đủ tất cả các gói tin theo đúng thứ tự. Sau đó, dữ liệu này mới được chuyển lên cho ứng dụng.

    Tcp segment

    Đơn vị dữ liệu của Tcp được gọi là segment. Kích thước tối đa của segment (maximum segment size, MSS) được thiết lập tự động bằng cách xác định kích thước tối đa của frame dữ liệu của tầng liên kết. Việc thiết lập MSS như vậy đảm bảo cho TCP segment có thể nằm vừa khít trong một frame với kích thước đủ lớn để có hiệu suất gửi dữ liệu cao nhất.

    Khi TCP gửi đi dữ liệu kích thước lớn (ví dụ, một file ảnh lớn trong một trang web), dữ liệu này sẽ được cắt thành từng phần có kích thước bằng MSS (trừ mảnh cuối cùng có thể có kích thước nhỏ hơn).

    Ngược lại, các ứng dụng tương tác thường dùng các khối dữ liệu có kích thước nhỏ hơn MSS. Ví dụ, chương trình Telnet thường gửi đi dữ liệu có kích thước đúng 1 byte, do đó kích thước TCP segment dùng với Telnet thường chỉ có 21 byte.

    Acknowledgement

    Mỗi khi Tcp trên máy nguồn gửi đi một segment, nó sẽ chờ nhận được một gói tin phản hồi (ACK) từ máy đối tác thông báo về tình trạng của segment vừa nhận. Chỉ khi có ACK xác nhận đạt yêu cầu, Tcp trên máy nguồn mới tiếp tục gửi đi segment tiếp theo.

    Do những đặc điểm trên, khi tiến trình gửi dữ liệu đi (dưới dạng các thông điệp thuộc tầng ứng dụng), TCP đảm bảo việc chuyển được toàn bộ dữ liệu tới máy đích theo đúng thứ tự, không mất mát và không lỗi.

    Nhìn từ khía cạnh ứng dụng, liên kết và truyền dữ liệu qua Tcp giống như một luồng byte liên tục “chảy” từ buffer của máy nguồn tới buffer của máy đích. Khi tới buffer của máy đích, dữ liệu sẽ chờ được lấy vào chương trình và xử lý. Vì là một dòng byte liên tục, TCP không đảm bảo duy trì “ranh giới” giữa các thông điệp. Việc phân tách nội dung thông điệp tầng ứng dụng thuộc trách nhiệm của chương trình ứng dụng.

    Đóng liên kết

    Cả hai tiến trình tham gia vào liên kết TCP đều có thể yêu cầu đóng liên kết.

    Khi một trong hai tiến trình muốn kết thúc một liên kết, nó sẽ gửi đi một segment không chứa dữ liệu, trong đó bit FIN = 1.

    Khi tiến trình còn lại nhận được segment này, nó sẽ gửi lại một ACK segment. Tiếp theo tiến trình này sẽ gửi tiếp một segment, trong đó bit FIN = 1.

    Cuối cùng, tiến trình muốn kết thúc liên kết sẽ gửi một ACK segment. Ngay sau đó, bộ nhớ đệm và các biến trạng thái liên quan tời liên kết sẽ được giải phóng và liên kết được ngắt.

    Một số đặc điểm của lập trình với socket TCP

    Xử lý liên kết TCP trong chương trình

    Khi phát triên ứng dụng mạng sử dụng socket TCP, chúng ta sẽ phải trả lời câu hỏi: sau khi hoàn thành việc gửi nhận dữ liệu, chúng ta muốn giữ lại liên kết này để tiếp tục sử dụng trong tương lai hay ngắt bỏ liên kết. Việc lựa chọn này ảnh hưởng lớn đến cách thức viết chương trình.

    Điều này dẫn đến hai mô hình xử lý liên kết TCP.

    Trong mô hình thứ nhất, liên kết TCP sẽ bị hủy bỏ ngay sau khi hoàn thành việc trao đổi dữ liệu. Trong mô hình truyền thông điệp kiểu truy vấn / phản hồi, đây là khi kết thúc một vòng truy vấn – phản hồi. Đối với vòng truy vấn tiếp theo, một liên kết TCP mới sẽ được thiết lập. Mô hình này cũng tạo ra một đặc điểm khác của việc truyền thông tin: tính không duy trì trạng thái (stateless).

    Trong mô hình thứ hai, liên kết TCP sẽ được duy trì lâu dài để sử dụng trong nhiều vòng truy vấn – phản hồi cho đến khi một trong hai phía chính thức hủy bỏ nó. Mô hình này tạo ra tính duy trì trạng thái (stateful) của truyền thông.

    Vấn đề nằm ở chỗ, việc hủy bỏ liên kết và tái thiết lập liên kết TCP đều mất nhiều thời gian. Tuy nhiên, việc duy trì liên kết TCP lại đòi hỏi tài nguyên. Đối với client, tài nguyên ngày có thể không đáng kể. Tuy nhiên, đối với server, khi phải đồng thời phục vụ rất nhiều client, việc duy trì nhiều liên kết TCP cùng lúc gây tốn kém rất nhiều tài nguyên, nhất là nếu như liên kết đó không được khai thác.

    Trên thực tế, giao thức HTTP /1.1 mặc định sử dụng cơ chế ngắt kết nối TCP sau mỗi vòng truy vấn phản hồi. Trong khi đó kênh điều khiển của giao thức FTP lại duy trì kết nối TCP liên tục đến khi người dùng thoát hoặc không có dữ liệu trao đổi qua kênh điều khiển trong một khoảng thời gian nhất định.

    Như vậy, không thể nói là mô hình nào có ưu thế hơn. Việc lựa chọn mô hình nào phụ thuộc nhiều vào đặc điểm và yêu cầu của phần mềm ứng dụng.

    Truyền dữ liệu qua TCP

    Khi tiến trình nguồn phát lệnh Send, thực tế dữ liệu không được lập tức đóng gói vào truyền đi. Thay vào đó, dữ liệu được chuyển tới một bộ nhớ đệm của TCP (TCP buffer), được hình thành trong quá trình bắt tay ba bước. Theo thời gian, TCP sẽ lấy dữ liệu từ bộ nhớ đệm đó, đóng gói thành segment, và chuyển lên đường truyền. Trong khoảng thời gian chờ ở bộ nhớ đệm, ứng dụng có thể gửi thêm dữ liệu và dữ liệu này sẽ tiếp tục được bổ sung vào bộ nhớ đệm. Như vậy, số segment tạo ra không tương đương với số lệnh Send được gọi. Đây là một điểm khác biệt với UDP.

    Tương tự, TCP trên máy đích cũng sử dụng một bộ nhớ đệm cục bộ của riêng mình. Tất cả dữ liệu đi theo liên kết TCP tới máy đích sẽ được lưu trữ ở bộ nhớ đệm này. Một khi có dữ liệu trong bộ đệm, chương trình có thể đọc được bằng cách gọi phương thức Receive. Lệnh này sẽ di chuyển khối byte từ bộ đệm TCP vào chương trình và xóa bỏ khối byte tương ứng trong bộ đệm. Nếu chương trình đích chưa phát lệnh đọc dữ liệu thì dữ liệu vẫn tiếp tục nằm ở bộ nhớ đệm và không bị mất đi. Đây là điểm rất khác biệt với cách đọc dữ liệu của UDP.

    Do đặc điểm trên, các lệnh Send trên máy nguồn và Receive trên máy đích không đồng bộ nhau. Nhiều lệnh Send gửi dữ liệu đi có thể chỉ cần 1 lệnh Receive để nhận dữ liệu về. Đây là điểm khác biệt rất lớn với UDP cần lưu ý.

    Do sử dụng buffer, khi nhìn từ khía cạnh ứng dụng, liên kết và truyền dữ liệu qua Tcp giống như một luồng byte liên tục chạy từ buffer của máy nguồn tới buffer của máy đích. Vì vậy, khi sử dụng TCP socket chúng ta có thể áp dụng kỹ thuật lập trình luồng dữ liệu với lớp NetworkStream. UDP không áp dụng kỹ thuật luồng dữ liệu.

    Vì vận chuyển dữ liệu theo một dòng byte liên tục, TCP không đảm bảo duy trì “ranh giới” giữa các thông điệp. Việc phân tách nội dung thông điệp tầng ứng dụng thuộc trách nhiệm của chương trình ứng dụng. Đây cũng là một đặc điểm quan trọng cần lưu ý khi lập trình với TCP.

    Cắt ghép dữ liệu

    Khi TCP gửi đi dữ liệu kích thước lớn (ví dụ, một file video lớn), dữ liệu sẽ được tự động cắt thành từng phần để đóng gói vào các segment. Kích thước tối đa của segment MSS (Maximum Segment Size) được thiết lập tự động bằng cách xác định kích thước tối đa của frame dữ liệu của tầng liên kết. Việc thiết lập MSS như vậy đảm bảo cho TCP segment có thể nằm vừa khít trong một frame với kích thước đủ lớn để có hiệu suất gửi dữ liệu cao nhất. Mỗi segment đều được đánh số thứ tự, lưu trong trường Sequence Number của TCP header. Quá trình này được TCP làm tự động.

    Tại máy đích, TCP căn cứ vào số thứ tự này để tự động ghép nối các segment trở lại theo đúng thứ tự chúng được cắt và gửi đi. Khi chương trình phát lệnh Receive sẽ luôn nhận được phần dữ liệu đã ghép nối hoàn chỉnh.

    Đặc điểm này giúp chương trình ứng dụng có thể dễ dàng và an toàn gửi dữ liệu kích thước lớn qua TCP. Chương trình không cần thực hiện cắt ghép dữ liệu.

    Thực tế, khi truyền dữ liệu kích thước lớn qua TCP socket, người ta thường sử dụng kỹ thuật luồng dữ liệu. Luồng dữ liệu cho phép sử dụng một bộ nhớ đệm nhỏ trong chương trình để trung chuyển dữ liệu, ví dụ từ file tới TCP buffer, hoặc từ TCP buffer về file. Kỹ thuật này cho phép truyền khối lượng lớn dữ liệu nhưng không cần sử dụng quá nhiều bộ nhớ tạm để lưu dữ liệu. Kỹ thuật lập trình với luồng dữ liệu sẽ được xem xét trong một bài học riêng.

    + Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
    + Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
    + Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
    Cảm ơn bạn!

    Theo dõi
    Thông báo của
    guest

    0 Thảo luận
    Phản hồi nội tuyến
    Xem tất cả bình luận