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

Project: máy chủ chat nhiều client

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

  • Thiết kế giao thức khung tin nhắn
  • Phục vụ năm mươi client đồng thời
  • Xử lý client ngắt đột ngột mà không sập server
  • Thoát sạch bằng tín hiệu và chạy sạch với công cụ kiểm tra bộ nhớ

Dự án cuối chương gộp mọi thứ: khung tiền tố độ dài của Bài 46.6, vòng lặp select của Bài 46.9, và phần xử lý lỗi của Bài 46.3. Kết quả là một máy chủ chat một luồng phục vụ năm mươi client, và nó chạy thật.

#Thiết kế giao thức

chat.h, phần giao thức
/* Khung: [do dai 4 byte, thu tu mang][loai 1 byte][than N byte]
   do dai = 1 + N, tuc la da tinh ca byte loai. */

#define CONG        18090
#define TEN_TOI_DA  24
#define TIN_TOI_DA  4096
#define KH_TOI_DA   50

enum {
    L_TEN   = 1,    /* client -> server: dat ten */
    L_TIN   = 2,    /* hai chieu: tin nhan */
    L_HE    = 3,    /* server -> client: thong bao he thong */
    L_DS    = 4,    /* client -> server: xin danh sach nguoi trong phong */
    L_THOAT = 5     /* client -> server: thoat */
};

#Thư viện dùng chung

chat.c, khung tin
int gui_khung(Sock s, uint8_t loai, const void *dl, uint32_t n) {
    if (n > TIN_TOI_DA) return -1;
    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;
}

int nhan_khung(Sock s, uint8_t *loai, char **ra, uint32_t *ra_n) {
    uint32_t d;
    if (recv_all(s, &d, 4) != 0) return -1;
    d = ntohl(d);

    if (d < 1 || d > TIN_TOI_DA + 1) return -1; /* CA can duoi lan can tren */

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

    uint32_t n = d - 1;
    char *dem = (char *)malloc((size_t)n + 1);
    if (dem == NULL) return -1;
    if (n > 0 && recv_all(s, dem, n) != 0) { free(dem); return -1; }
    dem[n] = '\0';

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

#Máy chủ

sv.c, cấu trúc dữ liệu và hai hàm trợ giúp
typedef struct {
    Sock s;
    char ten[TEN_TOI_DA + 1];
    int  da_dat_ten;
} Khach;

static Khach kh[KH_TOI_DA];
static int   kh_co = 0;
static volatile int chay = 1;

static void bat_thoat(int sig) { (void)sig; chay = 0; }

/* Gui toi tat ca, tru mot socket */
static void phat(const char *tin, Sock tru) {
    for (int i = 0; i < kh_co; ++i) {
        if (kh[i].s == tru) continue;
        gui_khung(kh[i].s, L_TIN, tin, (uint32_t)strlen(tin));
    }
}

static void he(Sock s, const char *tin) {
    gui_khung(s, L_HE, tin, (uint32_t)strlen(tin));
}

/* Bo mot khach ra khoi mang. Chu y THU TU cac buoc. */
static void bo(int i) {
    char tb[128];
    if (kh[i].da_dat_ten) {
        snprintf(tb, sizeof tb, "* %s da roi phong", kh[i].ten);
        DONG(kh[i].s);
        kh[i] = kh[--kh_co];        /* xoa TRUOC khi phat */
        phat(tb, SOCK_XAU);         /* nen no khong nhan lai chinh no */
        printf("server: %s\n", tb);
        return;
    }
    DONG(kh[i].s);                  /* chua dat ten: khong thong bao */
    kh[i] = kh[--kh_co];
}
sv.c, vòng lặp chính
while (chay) {
    fd_set doc;
    FD_ZERO(&doc);
    FD_SET(sv, &doc);
    int lon = (int)sv;
    for (int i = 0; i < kh_co; ++i) {
        FD_SET(kh[i].s, &doc);
        if ((int)kh[i].s > lon) lon = (int)kh[i].s;
    }

    struct timeval tv = { 1, 0 };       /* 1 giay, de kiem tra co "chay" */
    int r = select(lon + 1, &doc, NULL, NULL, &tv);
    if (r == 0) continue;               /* het gio: quay lai kiem tra co */
    if (r < 0) break;

    /* Ket noi moi */
    if (FD_ISSET(sv, &doc)) {
        struct sockaddr_in a; Solen ac = sizeof a;
        Sock c = accept(sv, (struct sockaddr *)&a, &ac);
        if (c != SOCK_XAU) {
            if (kh_co >= KH_TOI_DA) {
                he(c, "* phong day, thu lai sau");
                DONG(c);
            } else {
                kh[kh_co].s = c;
                kh[kh_co].da_dat_ten = 0;
                kh[kh_co].ten[0] = '\0';
                ++kh_co;
                char ip[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &a.sin_addr, ip, sizeof ip);
                printf("server: ket noi tu %s:%d, hien co %d khach\n",
                       ip, ntohs(a.sin_port), kh_co);
                he(c, "* hay gui ten cua ban");
            }
        }
    }

    /* Khach co du lieu */
    for (int i = 0; i < kh_co; ) {
        if (!FD_ISSET(kh[i].s, &doc)) { ++i; continue; }

        uint8_t loai; char *than; uint32_t n;
        if (nhan_khung(kh[i].s, &loai, &than, &n) != 0) { bo(i); continue; }

        if (loai == L_TEN) {
            if (n == 0 || n > TEN_TOI_DA) {
                he(kh[i].s, "* ten khong hop le");
            } else {
                memcpy(kh[i].ten, than, n);
                kh[i].ten[n] = '\0';
                kh[i].da_dat_ten = 1;
                char tb[128];
                snprintf(tb, sizeof tb, "* %s da vao phong", kh[i].ten);
                printf("server: %s\n", tb);
                phat(tb, kh[i].s);
                he(kh[i].s, "* chao mung");
            }
        } else if (loai == L_TIN) {
            if (!kh[i].da_dat_ten) {
                he(kh[i].s, "* hay dat ten truoc");
            } else {
                char tb[TIN_TOI_DA + TEN_TOI_DA + 8];
                snprintf(tb, sizeof tb, "%s: %s", kh[i].ten, than);
                printf("server: phat \"%s\" toi %d khach\n", tb, kh_co - 1);
                phat(tb, kh[i].s);
            }
        } else if (loai == L_DS) {
            char tb[512]; size_t o = 0;
            o += (size_t)snprintf(tb + o, sizeof tb - o, "* trong phong:");
            for (int j = 0; j < kh_co && o < sizeof tb - 1; ++j)
                if (kh[j].da_dat_ten)
                    o += (size_t)snprintf(tb + o, sizeof tb - o, " %s", kh[j].ten);
            he(kh[i].s, tb);
        } else if (loai == L_THOAT) {
            free(than);
            bo(i);
            continue;
        } else {
            he(kh[i].s, "* loai khong biet");
        }
        free(than);
        ++i;
    }
}

#Chạy thử

terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 -c chat.c -o chat.o
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 sv.c chat.o -o sv.exe -lws2_32
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 thu.c chat.o -o thu.exe -lws2_32

Kịch bản thử tự động: ba client vào phòng lệch nhau, hai trong số đó gửi tin nhắn.

thu.c, kịch bản
typedef struct { const char *ten; int cho_ms; int gui_tin; const char *tin; } Kb;

static Kb kb[3] = {
    { "an",   100, 1, "chao ca nha" },
    { "binh", 300, 0, NULL },
    { "chi",  500, 1, "toi den muon" },
};

static DWORD WINAPI chay(LPVOID a) {
    Kb *k = (Kb *)a;
    Sleep((DWORD)k->cho_ms);
    Sock s = noi();
    gui_khung(s, L_TEN, k->ten, (uint32_t)strlen(k->ten));

    if (k->gui_tin) {
        Sleep(600);
        gui_khung(s, L_TIN, k->tin, (uint32_t)strlen(k->tin));
    }

    DWORD ms = 2500;
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&ms, sizeof ms);
    for (;;) {
        uint8_t loai; char *than; uint32_t n;
        if (nhan_khung(s, &loai, &than, &n) != 0) break;
        in(k->ten, than);
        free(than);
    }
    gui_khung(s, L_THOAT, NULL, 0);
    DONG(s);
    return 0;
}
terminal
# Cua so 1
./sv.exe
server chat o cong 18090, toi da 50 khach
server: ket noi tu 127.0.0.1:58618, hien co 1 khach
server: * an da vao phong
server: ket noi tu 127.0.0.1:58619, hien co 2 khach
server: * binh da vao phong
server: ket noi tu 127.0.0.1:58620, hien co 3 khach
server: * chi da vao phong
server: phat "an: chao ca nha" toi 2 khach
server: phat "chi: toi den muon" toi 2 khach
server: * an da roi phong
server: * chi da roi phong
server: * binh da roi phong
# Cua so 2
./thu.exe
[ 329 ms] binh  <- * hay gui ten cua ban
[ 329 ms] binh  <- * chao mung
[ 532 ms] binh  <- * chi da vao phong
[ 719 ms] an    <- * hay gui ten cua ban
[ 719 ms] an    <- * chao mung
[ 719 ms] binh  <- an: chao ca nha
[ 719 ms] an    <- * binh da vao phong
[ 719 ms] an    <- * chi da vao phong
[1125 ms] chi   <- * hay gui ten cua ban
[1125 ms] an    <- chi: toi den muon
[1125 ms] chi   <- * chao mung
[1141 ms] binh  <- chi: toi den muon
[1141 ms] chi   <- an: chao ca nha
kich ban xong

#Năm mươi client

tai.c, tải thử
#define SO_KH 50
static volatile LONG nhan_duoc = 0, noi_duoc = 0;

static DWORD WINAPI mot(LPVOID a) {
    int id = (int)(intptr_t)a;
    Sock s = socket(AF_INET, SOCK_STREAM, 0);
    /* ... connect ... */
    InterlockedIncrement(&noi_duoc);

    char ten[TEN_TOI_DA + 1];
    int tc = snprintf(ten, sizeof ten, "kh%02d", id);
    gui_khung(s, L_TEN, ten, (uint32_t)tc);

    Sleep(800);                             /* cho moi nguoi vao het */
    char tin[64];
    int n = snprintf(tin, sizeof tin, "tin tu %s", ten);
    gui_khung(s, L_TIN, tin, (uint32_t)n);

    DWORD ms = 2000;
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char *)&ms, sizeof ms);
    for (;;) {
        uint8_t loai; char *than; uint32_t m;
        if (nhan_khung(s, &loai, &than, &m) != 0) break;
        if (loai == L_TIN) InterlockedIncrement(&nhan_duoc);
        free(than);
    }
    gui_khung(s, L_THOAT, NULL, 0);
    DONG(s);
    return 0;
}
terminal
./tai.exe
noi duoc      : 50 / 50
tin nhan phat : 4360  (ky vong 2450)
tong thoi gian: 7468 ms
# Dem so lan server phat tin nhan nguoi dung
grep -c "phat" sv2.log
50

#Thoát sạch

Bắt tín hiệu, và vì sao select có thời gian chờ
static volatile int chay = 1;

static void bat_thoat(int sig) { (void)sig; chay = 0; }

int main(void) {
    signal(SIGINT, bat_thoat);
#if !defined(_WIN32)
    signal(SIGPIPE, SIG_IGN);       /* BAT BUOC tren POSIX */
#endif
    ...

    while (chay) {
        ...
        struct timeval tv = { 1, 0 };       /* 1 giay */
        int r = select(lon + 1, &doc, NULL, NULL, &tv);
        if (r == 0) continue;               /* quay lai kiem tra "chay" */
        ...
    }

    /* Don dep */
    printf("server: dang dong, %d khach con lai\n", kh_co);
    for (int i = 0; i < kh_co; ++i) {
        he(kh[i].s, "* server dong");       /* bao truoc */
        DONG(kh[i].s);
    }
    DONG(sv);
    mang_tat();
    printf("server: da thoat sach\n");
    return 0;
}
Thoát bẩn
static void bat_thoat(int sig) { printf("dang thoat...\n"); /* khong an toan tin hieu */ for (int i = 0; i < kh_co; ++i) closesocket(kh[i].s); /* sua trang thai dung chung */ exit(0); /* khong an toan */ } /* Ba van de: - printf co the khoa chet neu tin hieu toi giua mot printf khac - kh_co co the dang bi sua o luong chinh - client thay ket noi dut dot ngot, khong biet vi sao */
Thoát sạch
static void bat_thoat(int sig) { (void)sig; chay = 0; } /* Don dep o LUONG CHINH, sau vong lap: */ for (int i = 0; i < kh_co; ++i) { he(kh[i].s, "* server dong"); /* client biet ly do */ DONG(kh[i].s); } DONG(sv); mang_tat(); /* Client nhan duoc thong bao, hien thi cho nguoi dung, va co the tu ket noi lai sau. */
  1. Bước 1: chạy được ba client

    Dịch chat.c, sv.c, và một client tương tác đơn giản. Vào phòng bằng ba cửa sổ terminal và nói chuyện.
  2. Bước 2: chống client xấu tính

    Chuyển sang socket không chặn và bộ đệm tích luỹ, để một client gửi bốn byte rồi im lặng không khoá được server.
  3. Bước 3: thêm phòng

    Thêm loại L_PHONG, cho mỗi khách một trường phong[24], và sửa phat để chỉ gửi trong cùng phòng.
  4. Bước 4: chạy sạch dưới công cụ

    Không rò rỉ dưới Valgrind, không cảnh báo dưới -fsanitize=address,undefined, không cảnh báo với -Wall -Wextra -Wconversion.

Tự làm thử

  1. Viết client tương tác đọc từ bàn phím và hiển thị tin nhắn đến, dùng select trên cả stdin lẫn socket.
  2. Sửa phat để thông báo vào và rời phòng dùng L_HE, rồi chạy lại tai.exe và xác nhận con số thành 2450.
  3. Thêm lệnh L_RIENG để gửi tin nhắn riêng cho một tên cụ thể.
  4. Chuyển server sang socket không chặn với bộ đệm tích luỹ, rồi chứng minh một client gửi bốn byte rồi im lặng không khoá được server.
  5. Thêm trường hoat_dong và đóng các kết nối im lặng quá sáu mươi giây.
  6. Trên Linux, viết lại vòng lặp bằng epoll và nâng KH_TOI_DA lên một nghìn, rồi tải thử.

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

  • Giao thức: khung tiền tố độ dài cộng một byte loại, với giới hạn kiểm tra ở cả cận dưới lẫn cận trên.
  • Một luồng, một vòng select, năm mươi client đồng thời, đã đo được 50 trên 50 kết nối thành công.
  • KH_TOI_DA phải nhỏ hơn FD_SETSIZE; kiểm tra trước khi thêm chứ không để tràn mảng.
  • Gọi recv_all trực tiếp trong vòng lặp sự kiện là một lỗ hổng; server thật cần bộ đệm tích luỹ.
  • Thoát sạch: bộ xử lý tín hiệu chỉ đặt một cờ, dọn dẹp ở luồng chính, và select phải có thời gian chờ.