Bỏ qua điều hướng, tới nội dung chính
Học C
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ướcTCPUDP
socketSOCK_STREAMSOCK_DGRAM
bindServer cóServer có
listenCóKhông có khái niệm này
acceptCó, trả về fd mớiKhông. Chỉ một socket cho mọi client
connectClient bắt buộcTuỳ chọn, chỉ để đặt địa chỉ mặc định
Gửisend trên fd của kết nốisendto, kèm địa chỉ đích mỗi lần
Nhậnrecvrecvfrom, trả về cả địa chỉ người gửi
closeCó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ảiChi 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ớmBộ đệm sắp xếp lại, và độ trễ khi chờ gói thiếu
Loại gói trùngGhi 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ửiPhầ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ử

  1. Viết server và client UDP echo, chạy thử với ba thông điệp.
  2. Chạy client UDP khi server chưa bật, quan sát rằng sendto vẫn thành công.
  3. Thêm connect vào client UDP rồi thử lại, xem mã lỗi thay đổi thế nào.
  4. Tìm kích thước gói UDP lớn nhất mà máy bạn gửi được.
  5. Cài hoi_dap có lùi theo cấp số nhân và chứng minh nó bỏ cuộc sau bốn lần.
  6. 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ó listen và accept; một socket phục vụ mọi client, phân biệt bằng địa chỉ từ recvfrom.
  • Một sendto tương ứng đúng một recvfrom: 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.