Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 46.630 phút đọc

send_all, recv_all và giao thức tiền tố độ dài

Sau bài này bạn sẽ làm được

  • Viết send_all và recv_all xử lý đúng mọi trường hợp
  • Xử lý EINTR và trả về từng phần
  • Cài giao thức tiền tố độ dài kèm kiểm tra kích thước
  • Giải thích vì sao gọi send một lần là sai

Hai hàm trong bài này ngắn, khoảng mười dòng mỗi hàm, và bạn sẽ chép lại chúng vào mọi dự án mạng bạn viết. Không có chúng thì mọi thứ ở trên đều sai một cách ngẫu nhiên.

#send_all

send_all, phiên bản đầy đủ
/* Gui DU n byte. Tra ve 0 thanh cong, -1 loi. */
static int send_all(Sock s, const void *dl, size_t n) {
    const char *p = (const char *)dl;
    size_t con = n;

    while (con > 0) {
        int g = send(s, p, (int)con, 0);

        if (g > 0) {                    /* gui duoc mot phan */
            p   += g;
            con -= (size_t)g;
            continue;
        }

        if (g == 0) return -1;          /* khong the xay ra voi n > 0,
                                           nhung phong xa */

#if defined(_WIN32)
        int e = WSAGetLastError();
        if (e == WSAEINTR) continue;    /* bi ngat, thu lai */
#else
        if (errno == EINTR) continue;   /* bi tin hieu ngat, thu lai */
#endif
        return -1;                      /* loi that su */
    }
    return 0;
}

#recv_all

recv_all, phiên bản đầy đủ
/* Doc DU n byte. Tra ve 0 thanh cong, -1 loi hoac ben kia dong. */
static int recv_all(Sock s, void *dl, size_t n) {
    char  *p   = (char *)dl;
    size_t con = n;

    while (con > 0) {
        int g = recv(s, p, (int)con, 0);

        if (g > 0) {
            p   += g;
            con -= (size_t)g;
            continue;
        }

        if (g == 0) return -1;          /* ben kia dong SACH giua chung */

#if defined(_WIN32)
        if (WSAGetLastError() == WSAEINTR) continue;
#else
        if (errno == EINTR) continue;
#endif
        return -1;
    }
    return 0;
}

#Giao thức tiền tố độ dài

Khung tin hoàn chỉnh
#include <stdint.h>

#define TIN_TOI_DA (1024u * 1024u)          /* 1 MB */

/* Khung: [do dai 4 byte, thu tu mang][du lieu N byte] */

static int gui_khung(Sock s, const void *dl, uint32_t n) {
    uint32_t d = htonl(n);
    if (send_all(s, &d, 4) != 0) return -1;
    if (n > 0 && send_all(s, dl, n) != 0) return -1;
    return 0;
}

/* Cap phat bo dem va tra ve qua *ra. Nguoi goi PHAI free. */
static int nhan_khung(Sock s, char **ra, uint32_t *ra_n) {
    uint32_t d;
    if (recv_all(s, &d, 4) != 0) return -1;
    d = ntohl(d);

    if (d > TIN_TOI_DA) {
        fprintf(stderr, "khung qua lon: %u\n", d);
        return -1;                          /* dong ket noi */
    }

    char *dem = malloc((size_t)d + 1);      /* +1 de them NUL */
    if (dem == NULL) return -1;

    if (d > 0 && recv_all(s, dem, d) != 0) { free(dem); return -1; }
    dem[d] = '\0';

    *ra   = dem;
    *ra_n = d;
    return 0;
}

#Kiểm chứng khắc nghiệt

Cách chắc chắn nhất để biết khung tin của bạn đúng là ép nó vào trường hợp xấu nhất: gửi từng byte một.

khung.c, phần send_all cố ý gửi một byte mỗi lần
static int send_all(SOCKET s, const void *dl, size_t n) {
    const char *p = (const char *)dl;
    size_t con = n;
    while (con > 0) {
        int g = send(s, p, (int)(con > 1 ? 1 : con), 0);   /* CO Y 1 byte */
        if (g <= 0) return -1;
        p += g; con -= (size_t)g;
    }
    return 0;
}

static const char *TIN[] = {
    "xin chao",
    "day la thong diep thu hai dai hon",
    "a",
    "",                                 /* khung RONG, truong hop bien */
    "ket thuc"
};
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 -D__USE_MINGW_ANSI_STDIO=1 khung.c -o khung.exe -lws2_32
./khung.exe
server: khung 1, 8 byte: "xin chao"  dung
server: khung 2, 33 byte: "day la thong diep thu hai dai hon"  dung
client: da gui 5 khung, tung byte mot
server: khung 3, 1 byte: "a"  dung
server: khung 4, 0 byte: ""  dung
server: khung 5, 8 byte: "ket thuc"  dung
server: nhan du 5 khung

#Mở rộng giao thức

Thêm trường loại, để giao thức có nhiều loại thông điệp
/* Khung: [do dai 4][loai 1][du lieu N-1] */

enum Loai {
    LOAI_TIN   = 1,     /* tin nhan van ban */
    LOAI_TEN   = 2,     /* dat ten */
    LOAI_LENH  = 3,     /* lenh dieu khien */
    LOAI_PING  = 4,     /* giu ket noi song */
    LOAI_PONG  = 5,
};

typedef struct {
    uint8_t  loai;
    uint32_t co;
    char    *du_lieu;   /* co the NULL neu co == 0 */
} Khung;

static int gui_khung2(Sock s, uint8_t loai, const void *dl, uint32_t n) {
    uint32_t d = htonl(n + 1);          /* +1 cho byte loai */
    if (send_all(s, &d, 4) != 0)     return -1;
    if (send_all(s, &loai, 1) != 0)  return -1;
    if (n > 0 && send_all(s, dl, n) != 0) return -1;
    return 0;
}

static int nhan_khung2(Sock s, Khung *k) {
    uint32_t d;
    if (recv_all(s, &d, 4) != 0) return -1;
    d = ntohl(d);

    if (d < 1 || d > TIN_TOI_DA) return -1;   /* phai co it nhat byte loai */

    if (recv_all(s, &k->loai, 1) != 0) return -1;

    k->co = d - 1;
    k->du_lieu = malloc((size_t)k->co + 1);
    if (k->du_lieu == NULL) return -1;

    if (k->co > 0 && recv_all(s, k->du_lieu, k->co) != 0) {
        free(k->du_lieu);
        k->du_lieu = NULL;
        return -1;
    }
    k->du_lieu[k->co] = '\0';
    return 0;
}
Giao thức mong manh
/* Gui struct nguyen khoi */ typedef struct { int loai; char ten[32]; double gia_tri; } TinNhan; TinNhan t = { 1, "an", 3.14 }; send(s, &t, sizeof t, 0); /* Ba van de: 1. Dem cau truc khac nhau giua trinh bien dich 2. Thu tu byte khac nhau giua kien truc 3. sizeof(double) va dinh dang so thuc co the khac Ma nay chi chay khi hai ben dung cung trinh bien dich, cung co, cung tuy chon. */
Giao thức bền
/* Tuan tu hoa tung truong */ static int gui_tin(Sock s, int loai, const char *ten, double gia_tri) { char goi[64]; size_t o = 0; uint8_t l = (uint8_t)loai; memcpy(goi + o, &l, 1); o += 1; uint8_t ten_co = (uint8_t)strlen(ten); memcpy(goi + o, &ten_co, 1); o += 1; memcpy(goi + o, ten, ten_co); o += ten_co; /* So thuc: gui duoi dang van ban, hoac dung IEEE754 da duoc chuan hoa. Cach don gian va an toan: */ char so[24]; int so_co = snprintf(so, sizeof so, "%.17g", gia_tri); memcpy(goi + o, &so_co, 1); o += 1; memcpy(goi + o, so, (size_t)so_co); o += (size_t)so_co; return gui_khung(s, goi, (uint32_t)o); }

Tự làm thử

  1. Cài send_all và recv_all vào một tệp mang.h dùng chung.
  2. Sửa send_all để gửi từng byte một và chứng minh khung tin vẫn đúng.
  3. Thêm một thông điệp một triệu byte vào bộ kiểm thử và xác nhận nó qua được.
  4. Bỏ kiểm tra d < 1 đi rồi gửi một khung có độ dài 0, xem chuyện gì xảy ra.
  5. Cài recv_all2 ba giá trị và phân biệt được đóng giữa khung với đóng giữa hai khung.
  6. Đo xem gui_khung hai lần send_all chậm hơn gui_khung_1 một lần bao nhiêu phần trăm với thông điệp mười byte.

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

  • send và recv đều có thể xử lý ít hơn số byte bạn yêu cầu; luôn dùng vòng lặp.
  • EINTR không phải lỗi, gọi lại. SIGPIPE trên POSIX giết chương trình, phải bỏ qua nó.
  • recv == 0 nghĩa là đóng sạch, khác hẳn recv < 0.
  • Khung tiền tố độ dài cần bốn quyết định: bốn byte, htonl, giới hạn trên, và kiểm tra cận dưới.
  • Kiểm chứng bằng cách gửi từng byte một và bằng khung rỗng; đó là hai trường hợp lộ ra hầu hết lỗi.