Bỏ qua điều hướng, tới nội dung chính
Học C
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

  1. socket

    Tạo điểm cuối. Chưa gắn với địa chỉ nào. Trả về mô tả tệp, hoặc INVALID_SOCKET.
  2. setsockopt SO_REUSEADDR

    Đặt trước bind. 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.
  3. 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.
  4. 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.
  5. 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ướcLỗi hay gặpNguyên nhânCách chữa
socketEAFNOSUPPORTSai họ địa chỉKiểm tra AF_INET
bindEADDRINUSE / 10048Cổng đang bận, hoặc TIME_WAITSO_REUSEADDR, hoặc đổi cổng
bindEACCES / 10013Cổng dưới 1024, không đủ quyềnDùng cổng trên 1024
bindEADDRNOTAVAILIP không thuộc máy nàyDùng INADDR_ANY
listenEOPNOTSUPPGọi listen trên socket UDPUDP không cần listen
acceptEINTRBị tín hiệu ngắtGọi lại, không coi là lỗi
acceptEMFILE / 10024Hết mô tả tệpKiểm tra rò rỉ, quên close
acceptECONNABORTEDClient bỏ đi trước khi acceptBỏ 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ử

  1. Dịch và chạy echo_sv.c, kết nối bằng telnet hoặc netcat.
  2. Bỏ SO_REUSEADDR đi, khởi động lại ngay sau khi đóng, và đọc mã lỗi.
  3. Đổi vòng lặp thành vô hạn và phục vụ được năm client liên tiếp.
  4. Cho server in ra tổng số byte đã echo kể từ khi khởi động.
  5. Viết lại phần main theo mẫu goto dọn dẹp.
  6. 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, accept trong vòng lặp.
  • SO_REUSEADDR phải đặt trước bind, và nó tồn tại để tránh TIME_WAIT chặn khởi động lại.
  • Trên Windows, SO_REUSEADDR cho phép cướp cổng; dùng SO_EXCLUSIVEADDRUSE thay thế.
  • accept thấ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ả.