Bài 46.726 phút đọc
UDP
Sau bài này bạn sẽ làm được
- Viết server và client UDP
- Giải thích vì sao UDP không cần listen và accept
- Kể bốn thứ TCP làm hộ mà UDP thì không
- Biết giới hạn kích thước một gói UDP
UDP là socket API rút gọn: không listen, không accept, không connect bắt buộc. Đổi lại, mọi thứ TCP làm hộ bạn thì bây giờ bạn phải tự làm, hoặc chấp nhận không có.
#Khác gì so với TCP
| Bước | TCP | UDP |
|---|---|---|
| socket | SOCK_STREAM | SOCK_DGRAM |
| bind | Server có | Server có |
| listen | Có | Không có khái niệm này |
| accept | Có, trả về fd mới | Không. Chỉ một socket cho mọi client |
| connect | Client bắt buộc | Tuỳ chọn, chỉ để đặt địa chỉ mặc định |
| Gửi | send trên fd của kết nối | sendto, kèm địa chỉ đích mỗi lần |
| Nhận | recv | recvfrom, trả về cả địa chỉ người gửi |
| close | Có | Có |
#Server và client UDP
Server UDP, chỉ ba bước
SOCKET u = socket(AF_INET, SOCK_DGRAM, 0); /* 1. SOCK_DGRAM */
struct sockaddr_in dc; /* 2. bind */
memset(&dc, 0, sizeof dc);
dc.sin_family = AF_INET;
dc.sin_addr.s_addr = htonl(INADDR_ANY);
dc.sin_port = htons(18085);
if (bind(u, (struct sockaddr *)&dc, sizeof dc) < 0) { ... }
/* KHONG co listen. KHONG co accept. */
for (;;) { /* 3. recvfrom */
char dem[2048];
struct sockaddr_in kh;
socklen_t kh_co = sizeof kh;
int n = recvfrom(u, dem, (int)sizeof dem, 0,
(struct sockaddr *)&kh, &kh_co);
if (n < 0) continue;
/* Tra loi: sendto voi DUNG dia chi vua nhan */
sendto(u, dem, n, 0, (struct sockaddr *)&kh, kh_co);
}Client UDP, chỉ hai bước
SOCKET u = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in sv;
memset(&sv, 0, sizeof sv);
sv.sin_family = AF_INET;
sv.sin_port = htons(18085);
inet_pton(AF_INET, "127.0.0.1", &sv.sin_addr);
/* KHONG connect. Dia chi dich di kem tung lan gui. */
sendto(u, "xin chao", 8, 0, (struct sockaddr *)&sv, sizeof sv);
char dem[2048];
struct sockaddr_in tu;
socklen_t tu_co = sizeof tu;
int n = recvfrom(u, dem, (int)sizeof dem, 0,
(struct sockaddr *)&tu, &tu_co);#Ranh giới được bảo toàn
udp.c, cùng thí nghiệm như Bài 46.5
/* Ben gui: bon lan sendto */
sendto(u, "Hello", 5, 0, (struct sockaddr *)&sv, sizeof sv);
sendto(u, "World", 5, 0, (struct sockaddr *)&sv, sizeof sv);
sendto(u, "12345", 5, 0, (struct sockaddr *)&sv, sizeof sv);
sendto(u, "MOT-GOI-DAI-HON-BO-DEM", 22, 0, (struct sockaddr *)&sv, sizeof sv);
/* Ben nhan: bo dem CO Y nho, 10 byte */
for (int i = 0; i < 4; ++i) {
char dem[10];
struct sockaddr_in kh;
int kh_co = sizeof kh;
int n = recvfrom(u, dem, (int)sizeof dem, 0,
(struct sockaddr *)&kh, &kh_co);
if (n < 0) {
printf("server: recvfrom loi %d\n", WSAGetLastError());
continue;
}
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &kh.sin_addr, ip, sizeof ip);
printf("server: goi %d, %d byte tu %s:%d\n",
i + 1, n, ip, ntohs(kh.sin_port));
}terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 udp.c -o udp.exe -lws2_32
./udp.exe
server UDP o cong 18085 (khong goi listen, khong goi accept) server: goi 1, 5 byte tu 127.0.0.1:65325 client: da sendto 4 lan server: goi 2, 5 byte tu 127.0.0.1:65325 server: goi 3, 5 byte tu 127.0.0.1:65325 server: recvfrom loi 10040
#Kích thước tối đa
udp_to.c
SOCKET u = socket(AF_INET, SOCK_DGRAM, 0);
int co_gui = 0, co = sizeof co_gui;
getsockopt(u, SOL_SOCKET, SO_SNDBUF, (char *)&co_gui, &co);
printf("SO_SNDBUF mac dinh = %d byte\n", co_gui);
int mx = 0; co = sizeof mx;
if (getsockopt(u, SOL_SOCKET, SO_MAX_MSG_SIZE, (char *)&mx, &co) == 0)
printf("SO_MAX_MSG_SIZE = %d byte\n", mx);
int co_thu[] = { 1024, 8192, 65507, 65508, 70000 };
char *dem = malloc(70000);
memset(dem, 'x', 70000);
for (int i = 0; i < 5; ++i) {
int n = sendto(u, dem, co_thu[i], 0, (struct sockaddr *)&sv, sizeof sv);
if (n < 0) printf("sendto %6d byte -> loi %d\n", co_thu[i], WSAGetLastError());
else printf("sendto %6d byte -> gui %d\n", co_thu[i], n);
}terminal
./ut.exe
SO_SNDBUF mac dinh = 65536 byte SO_MAX_MSG_SIZE = 65507 byte sendto 1024 byte -> gui 1024 sendto 8192 byte -> gui 8192 sendto 65507 byte -> gui 65507 sendto 65508 byte -> loi 10040 sendto 70000 byte -> loi 10040
#Bốn thứ bạn phải tự làm
| TCP làm hộ | Với UDP bạn phải | Chi phí |
|---|---|---|
| Gửi lại gói mất | Đánh số gói, bên nhận gửi ACK, bên gửi giữ bản sao và đặt hẹn giờ | Bộ nhớ giữ gói chưa xác nhận, và một bộ hẹn giờ |
| Sắp đúng thứ tự | Đánh số thứ tự, bên nhận đệm lại gói tới sớm | Bộ đệm sắp xếp lại, và độ trễ khi chờ gói thiếu |
| Loại gói trùng | Ghi nhớ các số thứ tự đã nhận trong một cửa sổ | Một tập hoặc bitmap của cửa sổ gần đây |
| Kiểm soát tắc nghẽn | Đo mất gói và tự giảm tốc độ gửi | Phần khó nhất, và phần hay bị bỏ nhất |
Mức tối thiểu: hẹn giờ và gửi lại
/* Mot vong hoi dap don gian co gui lai. Day KHONG phai la TCP,
chi la muc toi thieu cho giao thuc hoi mot cau tra loi mot cau. */
#define SO_LAN_THU 4
#define CHO_DAU_MS 200
static int hoi_dap(SOCKET u, const struct sockaddr *dc, int dc_co,
const void *hoi, int hoi_co,
void *dap, int dap_co) {
int cho = CHO_DAU_MS;
for (int lan = 0; lan < SO_LAN_THU; ++lan) {
if (sendto(u, hoi, hoi_co, 0, dc, dc_co) < 0) return -1;
DWORD ms = (DWORD)cho;
setsockopt(u, SOL_SOCKET, SO_RCVTIMEO, (const char *)&ms, sizeof ms);
int n = recvfrom(u, dap, dap_co, 0, NULL, NULL);
if (n >= 0) return n; /* co tra loi */
if (WSAGetLastError() != WSAETIMEDOUT) return -1;
cho *= 2; /* LUI THEO CAP SO NHAN */
printf("het gio, thu lai lan %d, cho %d ms\n", lan + 2, cho);
}
return -1; /* bo cuoc */
}
/* Ba y trong 20 dong tren:
1. Thu lai co GIOI HAN. Thu mai la vong lap vo han khi may
kia tat han.
2. Thoi gian cho TANG GAP DOI moi lan. Neu mang dang nghen
thi thu lai ngay lam nghen them. Day la "lui theo cap so
nhan", va no la co che chong nghen re tien nhat.
3. Nen them mot luong NGAU NHIEN vao thoi gian cho. Neu mot
nghin client cung mat ket noi va cung thu lai sau dung
200 ms thi ho tao ra mot con bao dong bo. */Dùng UDP sai chỗ
/* Truyen mot tep 10 MB bang UDP vi "UDP nhanh hon" */
for (long o = 0; o < co_tep; o += 1400) {
sendto(u, tep + o, 1400, 0, dc, dc_co);
}
/* Ket qua:
- mat vai goi, tep hong, khong biet cho nao
- gui het toc luc, lam nghen mang cua chinh minh
- khong co thu tu, ben nhan phai tu sap
Ban vua tu viet mot TCP te hon TCP. */
Dùng UDP đúng chỗ
/* Do luong: mat vai mau khong sao,
va KHONG duoc lam cham ung dung chinh */
static void gui_do(SOCKET u, const char *ten, double gt) {
char goi[128];
int n = snprintf(goi, sizeof goi, "%s %.3f %lld",
ten, gt, (long long)time(NULL));
sendto(u, goi, n, 0, dc, dc_co);
/* Khong cho tra loi. Khong thu lai. Mat thi thoi.
Do la dung y do: mot he do luong khong duoc lam
ung dung cham lai. StatsD hoat dong dung the nay. */
}
Tự làm thử
- Viết server và client UDP echo, chạy thử với ba thông điệp.
- Chạy client UDP khi server chưa bật, quan sát rằng
sendtovẫn thành công. - Thêm
connectvào client UDP rồi thử lại, xem mã lỗi thay đổi thế nào. - Tìm kích thước gói UDP lớn nhất mà máy bạn gửi được.
- Cài
hoi_dapcó lùi theo cấp số nhân và chứng minh nó bỏ cuộc sau bốn lần. - Gửi một trăm gói UDP liên tiếp qua loopback và đếm xem mất bao nhiêu. Rồi thử qua mạng thật nếu bạn có hai máy.
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
- UDP không có
listenvàaccept; một socket phục vụ mọi client, phân biệt bằng địa chỉ từrecvfrom. - Một
sendtotương ứng đúng mộtrecvfrom: ranh giới được bảo toàn. - Bộ đệm nhỏ hơn gói thì Windows báo
WSAEMSGSIZE, POSIX cắt cụt im lặng. Cấp bộ đệm 65536 byte là cách chữa đơn giản. - Tối đa 65507 byte, nhưng thực tế nên giữ dưới 1200 byte để tránh phân mảnh.
- Bốn thứ bạn phải tự làm; nếu bạn làm đủ cả bốn thì bạn đang viết lại TCP.