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

Client TCP

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

  • Viết client TCP chạy được
  • Phân biệt các mã lỗi của connect
  • Đặt thời gian chờ cho connect
  • Viết mã chạy được trên cả Windows lẫn POSIX

Client đơn giản hơn server: không bind, không listen, không accept. Chỉ socket, connect, rồi trao đổi. Phần khó nằm ở chỗ connect có thể treo hai mươi giây trước khi báo lỗi, và mặc định không có cách nào đặt thời gian chờ.

#Ba bước

/* 1. Tao socket */
Sock s = socket(AF_INET, SOCK_STREAM, 0);

/* 2. Dien dia chi CUA SERVER */
struct sockaddr_in sv;
memset(&sv, 0, sizeof sv);
sv.sin_family = AF_INET;
sv.sin_port   = htons(18080);
inet_pton(AF_INET, "127.0.0.1", &sv.sin_addr);

/* 3. Ket noi */
connect(s, (struct sockaddr *)&sv, sizeof sv);

/* Xong. Tu day tro di s dung duoc voi send va recv. */

#Mã nguồn đầy đủ

echo_kh.c
/* Client echo TCP.
   Windows:  gcc echo_kh.c -o echo_kh.exe -D_WIN32_WINNT=0x0600 -lws2_32
   Linux:    gcc echo_kh.c -o echo_kh                              */

#include <stdio.h>
#include <string.h>

#if defined(_WIN32)
  #include <winsock2.h>
  #include <ws2tcpip.h>
  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

int main(int argc, char **argv) {
    setvbuf(stdout, NULL, _IONBF, 0);

    const char *tin = (argc > 1) ? argv[1] : "xin chao server";

#if defined(_WIN32)
    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d) != 0) return 1;
#endif

    Sock s = socket(AF_INET, SOCK_STREAM, 0);
    if (s == SOCK_XAU) {
        fprintf(stderr, "socket: %d\n", LOI());
        return 1;
    }

    struct sockaddr_in sv;
    memset(&sv, 0, sizeof sv);
    sv.sin_family = AF_INET;
    sv.sin_port   = htons(CONG);

    if (inet_pton(AF_INET, "127.0.0.1", &sv.sin_addr) <= 0) {
        fprintf(stderr, "dia chi khong hop le\n");
        DONG(s);
        return 1;
    }

    if (connect(s, (struct sockaddr *)&sv, sizeof sv) < 0) {
        fprintf(stderr, "connect: %d\n", LOI());
        DONG(s);
        return 1;
    }

    send(s, tin, (int)strlen(tin), 0);

    char bo_dem[1024];
    int n = recv(s, bo_dem, (int)sizeof bo_dem - 1, 0);
    if (n > 0) {
        bo_dem[n] = '\0';
        printf("server tra loi: %s\n", bo_dem);
    }

    DONG(s);
#if defined(_WIN32)
    WSACleanup();
#endif
    return 0;
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 echo_kh.c -o echo_kh.exe -lws2_32
./echo_kh.exe
server tra loi: xin chao server
./echo_kh.exe "goi tin thu hai"
server tra loi: goi tin thu hai

#Bốn lỗi hay gặp

loi_ket.c
static void thu(const char *ip, int cong, const char *ghi_chu) {
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in a;
    memset(&a, 0, sizeof a);
    a.sin_family = AF_INET;
    a.sin_port   = htons((unsigned short)cong);
    inet_pton(AF_INET, ip, &a.sin_addr);

    printf("%-28s %s:%d -> ", ghi_chu, ip, cong);
    fflush(stdout);
    if (connect(s, (struct sockaddr *)&a, sizeof a) == 0) {
        printf("thanh cong\n");
    } else {
        int e = WSAGetLastError();
        const char *ten = "?";
        switch (e) {
            case WSAECONNREFUSED: ten = "WSAECONNREFUSED"; break;
            case WSAETIMEDOUT:    ten = "WSAETIMEDOUT";    break;
            case WSAEHOSTUNREACH: ten = "WSAEHOSTUNREACH"; break;
            case WSAENETUNREACH:  ten = "WSAENETUNREACH";  break;
        }
        printf("loi %d (%s)\n", e, ten);
    }
    closesocket(s);
}
terminal
./lk.exe
khong ai lang nghe:          127.0.0.1:19999 -> loi 10061 (WSAECONNREFUSED)
dia chi khong toi duoc:      10.255.255.1:80 -> loi 10060 (WSAETIMEDOUT)
POSIXWindowsNghĩaBạn làm gì
ECONNREFUSED10061Máy tồn tại nhưng không có ai lắng nghe ở cổng đóKiểm tra server đã chạy chưa, đúng cổng chưa
ETIMEDOUT10060SYN gửi đi mà không có hồi âm nàoMáy tắt, hoặc tường lửa lặng lẽ vứt gói
EHOSTUNREACH10065Bộ định tuyến báo không tới được máy đóSai địa chỉ, hoặc máy không cùng mạng
ENETUNREACH10051Không có tuyến đường tới mạng đóMáy bạn chưa có kết nối mạng

#Thời gian chờ cho connect

cho_ket.c
/* Tra ve 0 thanh cong, -1 loi, -2 het gio. */
static int ket_noi_cho(SOCKET s, const struct sockaddr *dc, int dc_co, int giay) {
    /* 1. Chuyen socket sang che do KHONG CHAN */
    u_long khong_chan = 1;
    ioctlsocket(s, FIONBIO, &khong_chan);

    /* 2. connect tra ve ngay lap tuc voi WSAEWOULDBLOCK */
    int r = connect(s, dc, dc_co);
    if (r == 0) goto xong;                          /* xong ngay, loopback */
    if (WSAGetLastError() != WSAEWOULDBLOCK) return -1;

    /* 3. Cho socket san sang GHI, hoac co ngoai le, hoac het gio */
    fd_set ghi, ngoai;
    FD_ZERO(&ghi);   FD_SET(s, &ghi);
    FD_ZERO(&ngoai); FD_SET(s, &ngoai);

    struct timeval tv;
    tv.tv_sec  = giay;
    tv.tv_usec = 0;

    r = select(0, NULL, &ghi, &ngoai, &tv);
    if (r == 0) return -2;                          /* HET GIO */
    if (r <  0) return -1;

    /* 4. San sang ghi KHONG co nghia la thanh cong.
          Phai hoi SO_ERROR de biet ket qua that. */
    int err = 0; int err_co = sizeof err;
    getsockopt(s, SOL_SOCKET, SO_ERROR, (char *)&err, &err_co);
    if (err != 0) { WSASetLastError(err); return -1; }

xong: ;
    /* 5. Tra socket ve che do chan cho de dung tiep */
    u_long chan = 0;
    ioctlsocket(s, FIONBIO, &chan);
    return 0;
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 cho_ket.c -o ck.exe -lws2_32
./ck.exe
connect 10.255.255.1:80, cho 3 giay -> HET GIO
connect 127.0.0.1:19999, cho 3 giay -> loi 10061

#Từ tên miền tới địa chỉ

getaddrinfo, cách hiện đại
#include <stdio.h>
#include <string.h>

struct addrinfo goi_y, *kq, *p;
memset(&goi_y, 0, sizeof goi_y);
goi_y.ai_family   = AF_UNSPEC;      /* AF_INET, AF_INET6, hay ca hai */
goi_y.ai_socktype = SOCK_STREAM;    /* TCP */

int r = getaddrinfo("example.com", "80", &goi_y, &kq);
if (r != 0) {
    fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(r));
    return 1;
}

/* getaddrinfo tra ve mot DANH SACH. Thu tung cai cho toi khi
   mot cai thanh cong. Do la cach chuong trinh that lam viec. */
Sock s = SOCK_XAU;
for (p = kq; p != NULL; p = p->ai_next) {
    s = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
    if (s == SOCK_XAU) continue;
    if (connect(s, p->ai_addr, (int)p->ai_addrlen) == 0) break;   /* xong */
    DONG(s);
    s = SOCK_XAU;
}

freeaddrinfo(kq);                   /* BAT BUOC, neu khong thi ro ri */

if (s == SOCK_XAU) {
    fprintf(stderr, "khong ket noi duoc toi may nao\n");
    return 1;
}
Không nên
/* Cu, chi IPv4, khong an toan da luong */ struct hostent *h = gethostbyname("example.com"); if (h == NULL) { herror("gethostbyname"); return 1; } memcpy(&sv.sin_addr, h->h_addr, h->h_length); /* gethostbyname da bi loai khoi POSIX tu 2008. */
Nên
/* Moi, ca IPv4 lan IPv6, an toan da luong */ struct addrinfo goi_y = {0}, *kq; goi_y.ai_family = AF_UNSPEC; goi_y.ai_socktype = SOCK_STREAM; if (getaddrinfo("example.com", "80", &goi_y, &kq) != 0) return 1; /* ... duyet danh sach ... */ freeaddrinfo(kq);

Tự làm thử

  1. Dịch và chạy echo_kh.c, thử với ba thông điệp khác nhau.
  2. Chạy client khi server chưa bật, và đọc mã lỗi bạn nhận được.
  3. Đo bằng đồng hồ xem connect tới một địa chỉ không tồn tại mất bao lâu trên máy bạn.
  4. Cài ket_noi_cho và chứng minh nó dừng đúng sau ba giây.
  5. Bỏ bước gọi getsockopt(SO_ERROR) đi và xem lỗi lộ ra ở đâu.
  6. Viết client dùng getaddrinfo để kết nối tới một tên miền thật, và in ra mọi địa chỉ mà nó trả về.

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

  • Client chỉ cần socket rồi connect; hệ điều hành tự cấp cổng nguồn.
  • ECONNREFUSED nghĩa là mạng thông nhưng không có server; ETIMEDOUT nghĩa là gói tin biến mất.
  • Không có tuỳ chọn socket nào đặt thời gian chờ cho connect; phải dùng socket không chặn cộng select.
  • Sau select, phải gọi getsockopt(SO_ERROR) để biết bắt tay thành công hay thất bại.
  • Dùng getaddrinfo chứ không dùng gethostbyname, và nhớ freeaddrinfo.