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

Bốn kiến trúc phục vụ nhiều client

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

  • Kể ưu nhược điểm của bốn kiến trúc
  • Cài bản một luồng cho mỗi client
  • Biết vì sao fork tốn hơn luồng
  • Chọn kiến trúc theo số kết nối dự kiến

Server ở Bài 46.3 phục vụ đúng một client tại một thời điểm. Bài này đo cái giá của điều đó bằng đồng hồ, rồi trình bày bốn cách khắc phục và cách chọn giữa chúng.

#Vấn đề, đo bằng đồng hồ

tuantu.c, vòng lặp phục vụ tuần tự
for (int i = 0; i < 2; ++i) {
    SOCKET c = accept(sv, NULL, NULL);
    printf("[%4lu ms] server: accept ket noi %d\n", tich()-t0, i+1);

    char dem[64];
    int n = recv(c, dem, 63, 0);       /* CHAN o day */
    if (n > 0) { dem[n] = 0; send(c, dem, n, 0); }

    printf("[%4lu ms] server: xong ket noi %d\n", tich()-t0, i+1);
    closesocket(c);
}

/* Hai client:
     CHAM  ket noi luc 100 ms, roi NGU 2 giay truoc khi gui
     NHANH ket noi luc 300 ms, gui ngay lap tuc            */
terminal
./tt.exe
[ 125 ms] client CHAM: da ket noi, ngu 2 giay roi moi gui
[ 125 ms] server: accept ket noi 1
[ 312 ms] client NHANH: da ket noi, gui ngay
[2140 ms] client CHAM: nhan "cham"
[2140 ms] server: xong ket noi 1
[2140 ms] server: accept ket noi 2
[2140 ms] client NHANH: nhan "nhanh"
[2140 ms] server: xong ket noi 2

#Bốn kiến trúc

Kiến trúcCách hoạt độngSố kết nối hợp lýChi phí mỗi kết nối
Tuần tựMột vòng lặp, phục vụ xong client này mới nhận client kia1Không có gì
forkMỗi kết nối một tiến trình conVài chục tới vài trămVài MB bộ nhớ, và một lần fork
LuồngMỗi kết nối một luồngVài trăm tới vài nghìnNgăn xếp 1 tới 8 MB, và một lần tạo luồng
Ghép kênh vào raMột luồng, một vòng lặp sự kiện với select, poll, hay epollHàng chục nghìnVài trăm byte trạng thái
Kiến trúc 2: fork, chỉ có trên POSIX
#include <sys/wait.h>
#include <signal.h>

/* Bo xu ly de thu hoac tien trinh con da chet.
   KHONG co dong nay thi moi client de lai mot tien trinh ZOMBIE. */
signal(SIGCHLD, SIG_IGN);

for (;;) {
    int c = accept(sv, NULL, NULL);
    if (c < 0) continue;

    pid_t con = fork();

    if (con == 0) {                 /* TIEN TRINH CON */
        close(sv);                  /* con khong can socket lang nghe */
        phuc_vu(c);
        close(c);
        _exit(0);                   /* _exit, KHONG phai exit */
    }

    close(c);                       /* CHA khong can socket ket noi */
    if (con < 0) { /* fork that bai */ }
}

#Một luồng cho mỗi client

luong.c
static volatile LONG stt = 0;

static DWORD WINAPI phuc_vu(LPVOID a) {
    SOCKET c = *(SOCKET *)a;
    free(a);                            /* giai phong NGAY, xem ben duoi */

    LONG id = InterlockedIncrement(&stt);

    char dem[64];
    int n = recv(c, dem, 63, 0);
    if (n > 0) { dem[n] = 0; send(c, dem, n, 0); }

    printf("[%4lu ms] luong %ld: xong\n", tich()-t0, id);
    closesocket(c);
    return 0;
}

int main(void) {
    /* ... socket, bind, listen nhu cu ... */

    HANDLE h[2];
    for (int i = 0; i < 2; ++i) {
        SOCKET c = accept(sv, NULL, NULL);
        printf("[%4lu ms] server: accept ket noi %d, giao cho mot luong\n",
               tich()-t0, i+1);

        SOCKET *pc = malloc(sizeof *pc);    /* MOT ban sao cho MOI luong */
        *pc = c;
        h[i] = CreateThread(NULL, 0, phuc_vu, pc, 0, NULL);
    }
    WaitForMultipleObjects(2, h, TRUE, INFINITE);
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 luong.c -o lu.exe -lws2_32
./lu.exe
[ 110 ms] client CHAM: da ket noi, ngu 2 giay roi moi gui
[ 110 ms] server: accept ket noi 1, giao cho mot luong
[ 297 ms] client NHANH: da ket noi, gui ngay
[ 297 ms] server: accept ket noi 2, giao cho mot luong
[ 313 ms] client NHANH: nhan "nhanh"
[ 313 ms] luong 2: xong
[2125 ms] client CHAM: nhan "cham"
[2125 ms] luong 1: xong

#Bể luồng

Bể luồng
Tạo sẵn N luồng lúc khởi động, và cho chúng lấy việc từ một hàng đợi chung. Tránh chi phí tạo và huỷ luồng cho mỗi kết nối, và đặt trần cứng cho số luồng đồng thời.
Cấu trúc, dùng pthread cho gọn
#define SO_LUONG   16
#define HANG_CO   256

typedef struct {
    int             viec[HANG_CO];      /* mo ta tep cua ket noi */
    int             dau, duoi, co;
    pthread_mutex_t khoa;
    pthread_cond_t  co_viec;
    pthread_cond_t  co_cho;
    int             dung;
} Be;

static void be_them(Be *b, int fd) {
    pthread_mutex_lock(&b->khoa);
    while (b->co == HANG_CO && !b->dung)
        pthread_cond_wait(&b->co_cho, &b->khoa);    /* hang doi day */
    if (b->dung) { pthread_mutex_unlock(&b->khoa); close(fd); return; }

    b->viec[b->duoi] = fd;
    b->duoi = (b->duoi + 1) % HANG_CO;
    b->co++;
    pthread_cond_signal(&b->co_viec);
    pthread_mutex_unlock(&b->khoa);
}

static void *tho(void *a) {
    Be *b = (Be *)a;
    for (;;) {
        pthread_mutex_lock(&b->khoa);
        while (b->co == 0 && !b->dung)
            pthread_cond_wait(&b->co_viec, &b->khoa);
        if (b->co == 0 && b->dung) { pthread_mutex_unlock(&b->khoa); return NULL; }

        int fd = b->viec[b->dau];
        b->dau = (b->dau + 1) % HANG_CO;
        b->co--;
        pthread_cond_signal(&b->co_cho);
        pthread_mutex_unlock(&b->khoa);

        phuc_vu(fd);                    /* NGOAI vung khoa */
        close(fd);
    }
}

#Chọn kiến trúc nào

Tình huốngChọnVì sao
Học, thử nghiệm, công cụ nội bộ một người dùngTuần tựĐơn giản nhất, ít lỗi nhất
Cần cách ly mạnh giữa các clientforkClient làm sập tiến trình con thì các client khác vẫn sống
Vài trăm kết nối, mỗi kết nối tính toán nhiềuLuồng hoặc bể luồngTận dụng nhiều lõi, chia sẻ bộ nhớ dễ
Hàng nghìn kết nối, chủ yếu là chờ vào raGhép kênh vào raChi phí mỗi kết nối rất nhỏ, không có chuyển ngữ cảnh
Hàng chục nghìn kết nối và nhiều lõiGhép kênh cộng nhiều tiến trìnhMỗi lõi một vòng lặp sự kiện, dùng SO_REUSEPORT

Tự làm thử

  1. Chạy tuantu.c và luong.c, so hai bản ghi thời gian.
  2. Sửa server tuần tự thành một luồng mỗi client và kiểm chứng lại.
  3. Truyền &c thay vì malloc cho luồng, chạy hai mươi client, và tìm bằng chứng của lỗi tranh chấp.
  4. Trên Linux, viết bản fork và quên close(c) ở tiến trình cha, rồi đếm mô tả tệp bằng ls /proc/PID/fd.
  5. Cài bể luồng mười sáu thợ và đo thông lượng so với một luồng mỗi client ở một nghìn kết nối ngắn.
  6. Với ứng dụng bạn định viết, chọn một trong bốn kiến trúc và viết ra ba lý do.

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

  • Server tuần tự bắt client nhanh chờ client chậm; đo được là 1828 ms chờ vô ích trong ví dụ.
  • Bốn kiến trúc: tuần tự, fork, luồng, ghép kênh vào ra.
  • Với fork: ba lệnh close và _exit, cộng với thu hoạch zombie.
  • Với luồng: malloc một bản sao mô tả tệp cho mỗi luồng, và tách luồng ra.
  • Bể luồng đặt trần cứng, nhưng không cứu bạn khỏi client chậm; cần thời gian chờ trên socket.