Bài 46.526 phút đọc
TCP là dòng byte
Sau bài này bạn sẽ làm được
- Tái hiện hiện tượng gộp gói
- Giải thích vì sao TCP không bảo toàn ranh giới thông điệp
- Kể ba cách tự định nghĩa ranh giới
- Nhận ra lỗi này trong mã người khác
Đây là bài quan trọng nhất của chương. Hầu hết lỗi mạng khó chịu nhất đều bắt nguồn từ một câu: TCP không bảo toàn ranh giới thông điệp. Bài này chứng minh điều đó bằng hai chương trình, rồi chỉ ba cách chữa.
#Ba lần gửi, một lần nhận
ranh_gioi.c, phần gửi
static DWORD WINAPI luong_gui(LPVOID a) {
(void)a;
Sleep(200);
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sv;
memset(&sv, 0, sizeof sv);
sv.sin_family = AF_INET;
sv.sin_port = htons(18081);
inet_pton(AF_INET, "127.0.0.1", &sv.sin_addr);
if (connect(s, (struct sockaddr *)&sv, sizeof sv) < 0) return 1;
/* BA lan send rieng biet, moi lan 5 byte */
send(s, "Hello", 5, 0);
send(s, "World", 5, 0);
send(s, "12345", 5, 0);
printf("client: da goi 3 lan, moi lan 5 byte\n");
Sleep(500);
closesocket(s);
return 0;
}ranh_gioi.c, phần nhận
SOCKET c = accept(sv, NULL, NULL);
Sleep(300); /* cho ca ba goi toi noi */
char bo_dem[64];
int lan = 0;
for (;;) {
int n = recv(c, bo_dem, (int)sizeof bo_dem - 1, 0);
if (n <= 0) break;
bo_dem[n] = '\0';
printf("server: recv lan %d duoc %d byte: \"%s\"\n", ++lan, n, bo_dem);
}
printf("server: tong cong %d lan recv cho 3 lan send\n", lan);terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 ranh_gioi.c -o ranh_gioi.exe -lws2_32
./ranh_gioi.exe
client: da goi 3 lan, moi lan 5 byte server: recv lan 1 duoc 15 byte: "HelloWorld12345" server: tong cong 1 lan recv cho 3 lan send
#Một lần gửi, hai mươi lăm lần nhận
Chiều ngược lại cũng đúng, và cũng hay bị quên: một lần send lớn sẽ được đọc ra bằng nhiều lần recv.
chia_nho.c
#define CO 200000
/* Ben gui: MOT lan send voi 200000 byte */
char *dem = malloc(CO);
memset(dem, 'A', CO);
int n = send(s, dem, CO, 0);
printf("client: goi send MOT lan voi %d byte, tra ve %d\n", CO, n);
/* Ben nhan: dem xem can bao nhieu lan recv */
char bo_dem[8192];
long tong = 0; int lan = 0;
int nho_nhat = 1 << 30, lon_nhat = 0;
for (;;) {
int n = recv(c, bo_dem, (int)sizeof bo_dem, 0);
if (n <= 0) break;
++lan; tong += n;
if (n < nho_nhat) nho_nhat = n;
if (n > lon_nhat) lon_nhat = n;
if (lan <= 3) printf("server: recv lan %d duoc %d byte\n", lan, n);
if (tong >= CO) break;
}
printf("server: tong %ld byte qua %d lan recv\n", tong, lan);
printf("server: lan nho nhat %d byte, lon nhat %d byte\n", nho_nhat, lon_nhat);terminal
./cn.exe
server: recv lan 1 duoc 8192 byte server: recv lan 2 duoc 8192 byte server: recv lan 3 duoc 8192 byte client: goi send MOT lan voi 200000 byte, tra ve 200000 server: tong 200000 byte qua 25 lan recv server: lan nho nhat 3392 byte, lon nhat 8192 byte
#Vì sao TCP làm vậy
Dòng byte
Trừu tượng mà TCP cung cấp: một chuỗi byte có thứ tự, không có cấu trúc. Nó giống một tệp hơn là một hộp thư. Bạn ghi byte vào một đầu, byte đó ra ở đầu kia đúng thứ tự, và không có gì đánh dấu chỗ nào là một lần ghi.
| Chỗ nào gộp hoặc chia | Vì sao |
|---|---|
| Bộ đệm gửi của hệ điều hành | send chỉ chép vào bộ đệm rồi trả về; tầng TCP quyết định gửi khi nào |
| Thuật toán Nagle | Cố tình giữ dữ liệu nhỏ lại để gộp, giảm số gói |
| MTU của đường truyền | Ethernet thường 1500 byte; dữ liệu lớn hơn bắt buộc bị chia |
| Bộ định tuyến trên đường | Có thể phân mảnh thêm nếu MTU chặng sau nhỏ hơn |
| Bộ đệm nhận | recv trả về bất kỳ lượng nào đang có sẵn, không chờ đủ |
#Ba cách tự đặt ranh giới
| Cách | Ví dụ thật | Ưu | Nhược |
|---|---|---|---|
| Tiền tố độ dài | Nhiều giao thức nhị phân, khung WebSocket | Đơn giản, nhanh, biết trước cần cấp bao nhiêu bộ nhớ | Phải chọn kích thước trường độ dài và kiểm tra giới hạn |
| Ký tự phân cách | HTTP dùng dòng trống, SMTP dùng dấu chấm | Đọc được bằng mắt, dễ gỡ lỗi bằng telnet | Phải thoát ký tự phân cách trong dữ liệu; chậm hơn |
| Thông điệp cố định | Một số giao thức công nghiệp | Đơn giản nhất | Lãng phí, và không mở rộng được |
Cách 1: tiền tố độ dài, cách nên dùng mặc định
/* Khung tin: [do dai 4 byte, thu tu mang][du lieu N byte] */
#define TIN_TOI_DA (1024 * 1024) /* GIOI HAN, xem ben duoi */
static int gui_khung(Sock s, const void *du_lieu, uint32_t n) {
uint32_t do_dai = htonl(n);
if (send_all(s, &do_dai, 4) != 0) return -1;
if (send_all(s, du_lieu, n) != 0) return -1;
return 0;
}
static int nhan_khung(Sock s, void **ra, uint32_t *ra_n) {
uint32_t do_dai;
if (recv_all(s, &do_dai, 4) != 0) return -1;
do_dai = ntohl(do_dai);
if (do_dai > TIN_TOI_DA) return -1; /* KHONG duoc bo dong nay */
char *dem = malloc(do_dai + 1);
if (dem == NULL) return -1;
if (recv_all(s, dem, do_dai) != 0) { free(dem); return -1; }
dem[do_dai] = '\0';
*ra = dem;
*ra_n = do_dai;
return 0;
}Cách 2: ký tự phân cách, giống HTTP
/* Doc toi khi gap '\n'. Phai co bo dem TICH LUY, vi mot lan recv
co the chua nua dong, hai dong ruoi, hay khong dong nao. */
typedef struct {
char dem[8192];
size_t co; /* so byte dang co trong dem */
} BoDem;
/* Tra ve 1 neu lay duoc mot dong, 0 neu can doc them, -1 neu loi. */
static int lay_dong(BoDem *b, char *ra, size_t ra_co) {
char *nl = memchr(b->dem, '\n', b->co);
if (nl == NULL) return 0; /* chua du, doc them */
size_t dai = (size_t)(nl - b->dem);
if (dai >= ra_co) return -1; /* dong qua dai */
memcpy(ra, b->dem, dai);
ra[dai] = '\0';
/* Don phan da dung ra khoi bo dem */
size_t con = b->co - dai - 1;
memmove(b->dem, nl + 1, con);
b->co = con;
return 1;
}
/* Vong lap dung: */
for (;;) {
char dong[1024];
int r = lay_dong(&b, dong, sizeof dong);
if (r == 1) { xu_ly(dong); continue; } /* con dong nua trong dem */
if (r < 0) break;
int n = recv(s, b.dem + b.co, sizeof b.dem - b.co, 0);
if (n <= 0) break;
b.co += (size_t)n;
}#Nhận ra lỗi này
Không nên
/* Bon dau hieu cua ma sai */
/* 1. Goi recv mot lan roi coi la ca thong diep */
char dem[1024];
int n = recv(s, dem, sizeof dem, 0);
dem[n] = '\0';
xu_ly(dem);
/* 2. Gia dinh moi recv la mot lenh */
recv(s, lenh, sizeof lenh, 0);
if (strcmp(lenh, "LIST") == 0) ...
/* 3. Gui thong diep bang nhieu lan send roi tuong ben kia
nhan duoc tung cai */
send(s, tieu_de, 8, 0);
send(s, than, than_co, 0);
/* 4. Doc "cho toi khi ket noi dong" cho giao thuc hoi dap */
while ((n = recv(s, dem, sizeof dem, 0)) > 0) tich_luy(dem, n);
/* Voi HTTP keep-alive thi vong lap nay khong bao gio ket thuc. */
Nên
/* Bon cach lam dung */
/* 1. Luon co ham doc DU N byte */
if (recv_all(s, dem, can) != 0) return -1;
/* 2. Luon co khung tin ro rang */
uint32_t n;
recv_all(s, &n, 4);
n = ntohl(n);
/* 3. Ghep thanh MOT lan gui, hoac dung send_all cho tung phan.
Ranh gioi do KHUNG quyet dinh, khong do so lan send. */
gui_khung(s, than, than_co);
/* 4. Doc dung so byte ma giao thuc noi la con lai */
long con = content_length;
while (con > 0) {
int n = recv(s, dem, min(sizeof dem, con), 0);
if (n <= 0) return -1;
con -= n;
}
Tự làm thử
- Chạy
ranh_gioi.c, rồi bỏSleep(300)và chạy lại mười lần, đếm xem có bao nhiêu kết quả khác nhau. - Sửa
chia_nho.cđể dùng bộ đệm 100 byte và đếm lại số lầnrecv. - Cài
lay_dongđầy đủ và test với dữ liệu chia nhỏ từng byte. - Cài
gui_khungvànhan_khung, gửi mười khung liên tiếp và kiểm tra bên nhận tách đúng mười. - Bỏ dòng kiểm tra
TIN_TOI_DArồi gửi một khung có độ dài0xFFFFFFFF, quan sát chuyện gì xảy ra. - Tìm trong một dự án mã nguồn mở bất kỳ một chỗ gọi
recvmột lần rồi coi là xong, và đánh giá xem đó có phải lỗi không.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- TCP là dòng byte: số lần
sendvà số lầnrecvkhông liên quan gì tới nhau. - Đã chứng minh cả hai chiều: ba lần gửi thành một lần nhận, và một lần gửi thành hai mươi lăm lần nhận.
TCP_NODELAYgiảm độ trễ nhưng không làm TCP giữ ranh giới.- Ba cách tự đặt ranh giới: tiền tố độ dài, ký tự phân cách, kích thước cố định. Mặc định chọn tiền tố độ dài.
- Luôn kiểm tra giới hạn độ dài trước khi
malloctheo số mà bên kia gửi.