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úc | Cách hoạt động | Số 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 kia | 1 | Không có gì |
| fork | Mỗi kết nối một tiến trình con | Vài chục tới vài trăm | Vài MB bộ nhớ, và một lần fork |
| Luồng | Mỗi kết nối một luồng | Vài trăm tới vài nghìn | Ngăn xếp 1 tới 8 MB, và một lần tạo luồng |
| Ghép kênh vào ra | Một luồng, một vòng lặp sự kiện với select, poll, hay epoll | Hàng chục nghìn | Và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ống | Chọn | Vì sao |
|---|---|---|
| Học, thử nghiệm, công cụ nội bộ một người dùng | Tuần tự | Đơn giản nhất, ít lỗi nhất |
| Cần cách ly mạnh giữa các client | fork | Client 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ều | Luồng hoặc bể luồng | Tậ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 ra | Ghép kênh vào ra | Chi 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õi | Ghép kênh cộng nhiều tiến trình | Mỗi lõi một vòng lặp sự kiện, dùng SO_REUSEPORT |
Tự làm thử
- Chạy
tuantu.cvàluong.c, so hai bản ghi thời gian. - Sửa server tuần tự thành một luồng mỗi client và kiểm chứng lại.
- Truyền
&cthay vìmalloccho luồng, chạy hai mươi client, và tìm bằng chứng của lỗi tranh chấp. - Trên Linux, viết bản
forkvà quênclose(c)ở tiến trình cha, rồi đếm mô tả tệp bằngls /proc/PID/fd. - 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.
- 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ệnhclosevà_exit, cộng với thu hoạch zombie. - Với luồng:
mallocmộ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.