Bài 46.330 phút đọc
Server TCP đầu tiên
Sau bài này bạn sẽ làm được
- Viết server echo TCP chạy được
- Giải thích SO_REUSEADDR giải quyết vấn đề gì
- Xử lý lỗi và dọn dẹp ở mọi nhánh
- Đọc địa chỉ và cổng của bên kết nối tới
Server TCP nhỏ nhất chạy được là khoảng bốn mươi dòng. Bài này viết nó ra đầy đủ, kèm phần xử lý lỗi mà các hướng dẫn trên mạng hay lược bỏ, và giải thích cái cờ SO_REUSEADDR mà ai cũng chép nhưng ít người biết nó làm gì.
#Năm bước
socket
Tạo điểm cuối. Chưa gắn với địa chỉ nào. Trả về mô tả tệp, hoặcINVALID_SOCKET.setsockopt SO_REUSEADDR
Đặt trướcbind. Cho phép bind lại cổng đang ởTIME_WAIT. Không có bước này thì khởi động lại server thất bại trong một tới bốn phút.bind
Gắn socket vào một địa chỉ và cổng. Đây là bước hay thất bại nhất, và cũng là bước phải kiểm tra kết quả kỹ nhất.listen
Chuyển sang chế độ chờ và tạo hàng đợi kết nối. Tham số thứ hai là độ dài hàng đợi, không phải số client tối đa.accept trong vòng lặp
Lấy một kết nối ra. Chặn nếu hàng đợi rỗng. Trả về mô tả tệp mới cho riêng client đó.
#Mã nguồn đầy đủ
echo_sv.c
/* Server echo TCP, chay tren Windows va tren POSIX.
Windows: gcc echo_sv.c -o echo_sv.exe -D_WIN32_WINNT=0x0600 -lws2_32
Linux: gcc echo_sv.c -o echo_sv */
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#if defined(_WIN32)
#include <winsock2.h>
#include <ws2tcpip.h>
typedef int socklen_t;
typedef SOCKET Sock;
#define DONG(s) closesocket(s)
#define LOI() WSAGetLastError()
#define SOCK_XAU INVALID_SOCKET
#else
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef int Sock;
#define DONG(s) close(s)
#define LOI() errno
#define SOCK_XAU (-1)
#endif
#define CONG 18080
#define TON_DU 10
static int khoi_tao_mang(void) {
#if defined(_WIN32)
WSADATA d;
return WSAStartup(MAKEWORD(2, 2), &d) == 0 ? 0 : -1;
#else
return 0;
#endif
}
static void don_mang(void) {
#if defined(_WIN32)
WSACleanup();
#endif
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
if (khoi_tao_mang() != 0) {
fprintf(stderr, "khoi tao mang that bai\n");
return 1;
}
/* 1. socket */
Sock sv = socket(AF_INET, SOCK_STREAM, 0);
if (sv == SOCK_XAU) {
fprintf(stderr, "socket: %d\n", LOI());
don_mang();
return 1;
}
/* 2. SO_REUSEADDR, PHAI dat truoc bind */
int co = 1;
setsockopt(sv, SOL_SOCKET, SO_REUSEADDR, (const char *)&co, sizeof co);
/* 3. bind */
struct sockaddr_in dc;
memset(&dc, 0, sizeof dc);
dc.sin_family = AF_INET;
dc.sin_addr.s_addr = htonl(INADDR_ANY);
dc.sin_port = htons(CONG);
if (bind(sv, (struct sockaddr *)&dc, sizeof dc) < 0) {
fprintf(stderr, "bind: %d\n", LOI());
DONG(sv); don_mang();
return 1;
}
/* 4. listen */
if (listen(sv, TON_DU) < 0) {
fprintf(stderr, "listen: %d\n", LOI());
DONG(sv); don_mang();
return 1;
}
printf("server lang nghe tren cong %d\n", CONG);
/* 5. accept trong vong lap */
for (int lan = 0; lan < 2; ++lan) {
struct sockaddr_in kh;
socklen_t kh_co = sizeof kh;
Sock c = accept(sv, (struct sockaddr *)&kh, &kh_co);
if (c == SOCK_XAU) {
fprintf(stderr, "accept: %d\n", LOI());
continue; /* KHONG thoat, thu tiep */
}
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &kh.sin_addr, ip, sizeof ip);
printf("ket noi tu %s:%d\n", ip, ntohs(kh.sin_port));
char bo_dem[1024];
int n = recv(c, bo_dem, (int)sizeof bo_dem - 1, 0);
if (n > 0) {
bo_dem[n] = '\0';
printf("nhan %d byte: %s\n", n, bo_dem);
send(c, bo_dem, n, 0);
}
DONG(c); /* dong ket noi nay */
}
DONG(sv);
don_mang();
printf("server dong\n");
return 0;
}#SO_REUSEADDR
TIME_WAIT
Trạng thái mà bên đóng kết nối trước phải ở lại trong khoảng một tới bốn phút sau khi đóng, để mọi gói tin còn lang thang trong mạng kịp hết hạn. Trong thời gian đó, cặp địa chỉ và cổng vẫn bị coi là đang bận.
/* Vi sao TIME_WAIT ton tai:
1. Goi tin tre. Neu ban dong roi mo ngay mot ket noi moi voi
DUNG bon so cu, mot goi tin cu con lang thang co the toi noi
va bi hieu la du lieu cua ket noi moi.
2. ACK cuoi cung co the mat. Neu no mat, ben kia gui lai FIN,
va ban phai con song de tra loi. Neu ban da bien mat hoan
toan thi ben kia nhan duoc RST va bao loi.
Thoi gian: 2 x MSL (Maximum Segment Lifetime).
Linux 60 giay
Windows 240 giay theo mac dinh cu, thuong ngan hon ngay nay */terminal
# Khong co SO_REUSEADDR, khoi dong lai ngay sau khi dong
./echo_sv.exe
bind: 10048
netstat -an | findstr 18080
TCP 127.0.0.1:18080 127.0.0.1:64144 TIME_WAIT
Đặt cờ, và phải đặt TRƯỚC bind
int co = 1;
setsockopt(sv, SOL_SOCKET, SO_REUSEADDR, (const char *)&co, sizeof co);
/* ^^^^^^^^^^^^^ ep kieu nay
la vi Windows khai bao tham so la const char*, con POSIX la
const void*. Ep sang const char* chay dung tren ca hai. */
/* Dat SAU bind thi khong co tac dung gi ca. Trinh bien dich
khong canh bao, va setsockopt van tra ve 0. */Windows, cách nguy hiểm
int co = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
(const char *)&co, sizeof co);
/* Cho phep tien trinh khac cuop cong cua ban. */
Windows, cách đúng
int co = 1;
setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE,
(const char *)&co, sizeof co);
/* Windows: van bind lai duoc sau TIME_WAIT cua chinh minh,
nhung KHONG cho tien trinh khac chiem cong.
Trong ma da nen tang: */
#if defined(_WIN32)
setsockopt(s, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, (const char *)&co, sizeof co);
#else
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &co, sizeof co);
#endif
#Xử lý lỗi từng bước
| Bước | Lỗi hay gặp | Nguyên nhân | Cách chữa |
|---|---|---|---|
| socket | EAFNOSUPPORT | Sai họ địa chỉ | Kiểm tra AF_INET |
| bind | EADDRINUSE / 10048 | Cổng đang bận, hoặc TIME_WAIT | SO_REUSEADDR, hoặc đổi cổng |
| bind | EACCES / 10013 | Cổng dưới 1024, không đủ quyền | Dùng cổng trên 1024 |
| bind | EADDRNOTAVAIL | IP không thuộc máy này | Dùng INADDR_ANY |
| listen | EOPNOTSUPP | Gọi listen trên socket UDP | UDP không cần listen |
| accept | EINTR | Bị tín hiệu ngắt | Gọi lại, không coi là lỗi |
| accept | EMFILE / 10024 | Hết mô tả tệp | Kiểm tra rò rỉ, quên close |
| accept | ECONNABORTED | Client bỏ đi trước khi accept | Bỏ qua, accept tiếp |
#Chạy thử
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 echo_sv.c -o echo_sv.exe -lws2_32
# Cua so 1: khoi dong server
./echo_sv.exe
server lang nghe tren cong 18080
# Cua so 2: goi tin thu nhat
./echo_kh.exe
server tra loi: xin chao server
# Cua so 2: goi tin thu hai
./echo_kh.exe "goi tin thu hai"
server tra loi: goi tin thu hai
# Cua so 1, toan bo ban ghi
server lang nghe tren cong 18080 ket noi tu 127.0.0.1:64144 nhan 15 byte: xin chao server ket noi tu 127.0.0.1:64145 nhan 15 byte: goi tin thu hai server dong
Tự làm thử
- Dịch và chạy
echo_sv.c, kết nối bằng telnet hoặc netcat. - Bỏ
SO_REUSEADDRđi, khởi động lại ngay sau khi đóng, và đọc mã lỗi. - Đổi vòng lặp thành vô hạn và phục vụ được năm client liên tiếp.
- Cho server in ra tổng số byte đã echo kể từ khi khởi động.
- Viết lại phần
maintheo mẫugotodọn dẹp. - Kết nối bằng telnet rồi để đó không gõ gì, và mở một client thứ hai. Quan sát hiện tượng rồi giải thích.
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
- Năm bước:
socket,setsockopt,bind,listen,accepttrong vòng lặp. SO_REUSEADDRphải đặt trướcbind, và nó tồn tại để tránhTIME_WAITchặn khởi động lại.- Trên Windows,
SO_REUSEADDRcho phép cướp cổng; dùngSO_EXCLUSIVEADDRUSEthay thế. acceptthất bại thìcontinue, không được thoát; nếu không, một client xấu tính có thể giết server.- Server này là kiến trúc tuần tự: một client chậm khoá tất cả.