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

Socket API và thứ tự byte

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

  • Vẽ luồng gọi hàm của server và của client
  • Điền đúng mọi trường của struct sockaddr_in
  • Giải thích vì sao cần htons và htonl
  • Dùng inet_pton và inet_ntop thay cho các hàm cũ

Socket API có bảy hàm bạn dùng đến chín mươi phần trăm thời gian. Phần khó không phải là bảy hàm đó, mà là cái struct địa chỉ mà chúng nhận, vì trong đó có một cái bẫy tên là thứ tự byte.

#Bảy hàm cốt lõi

Server đi qua sáu bước, client chỉ ba. Client không cần bind.
HàmAi gọiLàm gìTrả về
socketCả haiTạo một điểm cuối, chưa gắn với aiMô tả tệp, hoặc -1
bindServerGắn socket vào một địa chỉ và cổng cụ thể0, hoặc -1
listenServerChuyển socket sang chế độ chờ, tạo hàng đợi0, hoặc -1
acceptServerLấy một kết nối ra khỏi hàng đợi, chặn nếu rỗngMô tả tệp MỚI
connectClientBắt tay ba bước tới server0, hoặc -1
send / recvCả haiGửi và nhận dữ liệuSố byte, 0, hoặc -1
closeCả haiĐóng và giải phóng0, hoặc -1

#struct sockaddr_in

Cấu trúc địa chỉ và bốn trường của nó
struct sockaddr_in {
    short          sin_family;   /* luon la AF_INET */
    unsigned short sin_port;     /* CONG, thu tu byte MANG */
    struct in_addr sin_addr;     /* DIA CHI IP, thu tu byte MANG */
    char           sin_zero[8];  /* dem cho bang sizeof sockaddr */
};

struct in_addr {
    uint32_t s_addr;             /* IPv4 dong goi thanh 32 bit */
};

/* Dien day du: */
struct sockaddr_in a;
memset(&a, 0, sizeof a);                    /* BAT BUOC, xem ben duoi */
a.sin_family      = AF_INET;
a.sin_port        = htons(18080);           /* htons, khong duoc quen */
a.sin_addr.s_addr = htonl(INADDR_ANY);      /* 0.0.0.0 */

#Thứ tự byte

Thứ tự byte mạng
Quy ước rằng byte có trọng số lớn nhất được gửi trước, tức là big-endian. Máy x86 và ARM thông thường lưu ngược lại, nên mọi số nhiều byte phải được đảo trước khi đưa vào struct địa chỉ.
byteorder.c
#include <stdio.h>
#include <stdint.h>
#include <winsock2.h>

static void in_byte(const char *ten, const void *p, size_t n) {
    const unsigned char *b = (const unsigned char *)p;
    printf("%-22s", ten);
    for (size_t i = 0; i < n; ++i) printf("%02X ", b[i]);
    printf("\n");
}

int main(void) {
    WSADATA d; WSAStartup(MAKEWORD(2,2), &d);

    uint16_t cong      = 18080;          /* 0x46A0 */
    uint16_t cong_mang = htons(cong);
    uint32_t ip        = 0x7F000001u;    /* 127.0.0.1 */
    uint32_t ip_mang   = htonl(ip);

    printf("cong = %u  (0x%04X)\n", cong, cong);
    in_byte("trong bo nho:", &cong, sizeof cong);
    in_byte("sau htons:",    &cong_mang, sizeof cong_mang);

    printf("\nip   = 0x%08X\n", ip);
    in_byte("trong bo nho:", &ip, sizeof ip);
    in_byte("sau htonl:",    &ip_mang, sizeof ip_mang);

    uint32_t thu = 1;
    printf("\nmay nay la %s-endian\n",
           (*(unsigned char *)&thu == 1) ? "little" : "big");
    printf("htons(18080) = %u\n", (unsigned)htons(18080));

    WSACleanup();
    return 0;
}
terminal
gcc -std=c11 -O2 -Wall -Wextra -D_WIN32_WINNT=0x0600 byteorder.c -o bo.exe -lws2_32
./bo.exe
cong = 18080  (0x46A0)
trong bo nho:         A0 46
sau htons:            46 A0

ip   = 0x7F000001
trong bo nho:         01 00 00 7F
sau htonl:            7F 00 00 01

may nay la little-endian
htons(18080) = 41030
HàmNghĩaDùng cho
htonshost to network shortCổng, 16 bit
htonlhost to network longĐịa chỉ IPv4, 32 bit
ntohsnetwork to host shortĐọc cổng ra để in
ntohlnetwork to host longĐọc địa chỉ ra

#Chuyển đổi địa chỉ

Không nên
/* Ba ham cu, dung KHONG duoc dung nua */ a.sin_addr.s_addr = inet_addr("127.0.0.1"); /* Tra ve INADDR_NONE (0xFFFFFFFF) khi loi. Nhung 0xFFFFFFFF cung LA 255.255.255.255 hop le. Nen ban khong phan biet duoc loi voi dia chi quang ba. */ char *s = inet_ntoa(a.sin_addr); /* Tra ve con tro toi bo dem TINH dung chung. Goi lan hai ghi de len ket qua lan mot. Va khong an toan voi da luong. */ struct hostent *h = gethostbyname("example.com"); /* Chi IPv4, khong an toan da luong, da bi loai khoi POSIX. */
Nên
/* Ba ham thay the, dung ba cai nay */ if (inet_pton(AF_INET, "127.0.0.1", &a.sin_addr) != 1) { /* tra ve 1 thanh cong, 0 neu chuoi sai, -1 neu ho sai */ fprintf(stderr, "dia chi khong hop le\n"); } char ip[INET_ADDRSTRLEN]; /* hang so = 16 */ inet_ntop(AF_INET, &a.sin_addr, ip, sizeof ip); /* Ban tu cap bo dem, nen an toan da luong. */ struct addrinfo *kq; getaddrinfo("example.com", "80", &goi_y, &kq); /* Ho tro ca IPv4 lan IPv6, tra ve DANH SACH dia chi. */ freeaddrinfo(kq);
Đọc ra địa chỉ của client vừa kết nối
struct sockaddr_in kh;
socklen_t kh_co = sizeof kh;

Sock c = accept(sv, (struct sockaddr *)&kh, &kh_co);
if (c == SOCK_XAU) { /* xu ly loi */ }

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));
/*                                ^^^^^ ntohs, khong duoc quen */
terminal
./echo_sv.exe
server lang nghe tren cong 18080
ket noi tu 127.0.0.1:64144
nhan 15 byte: xin chao server

#Windows và POSIX

POSIXWindows
Headersys/socket.h, netinet/in.h, arpa/inet.hwinsock2.h, ws2tcpip.h
Kiểu socketintSOCKET, thực chất là UINT_PTR
Giá trị lỗi-1INVALID_SOCKET, tức (SOCKET)(~0)
Đóngclose(fd)closesocket(s)
Mã lỗierrnoWSAGetLastError()
Khởi tạoKhông cầnWSAStartup, WSACleanup
Liên kếtKhông cần cờ-lws2_32
Kiểu độ dàisocklen_tint
Lớp đệm chạy được trên cả hai
#include <stdio.h>
#include <string.h>

#if defined(_WIN32)
  #include <winsock2.h>
  #include <ws2tcpip.h>
  typedef SOCKET Sock;
  typedef int    socklen_t;
  #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

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
}

Tự làm thử

  1. Chạy chương trình byteorder.c trên máy bạn và đọc từng dòng hex.
  2. Viết chương trình in ra dạng hex của htonl(0x01020304) và giải thích kết quả.
  3. Cố ý quên htons rồi dùng getsockname tìm ra cổng thật.
  4. Viết hàm in_dia_chi(const struct sockaddr *) làm việc với cả IPv4 lẫn IPv6.
  5. Bỏ memset đi rồi chạy lại, xem có gì thay đổi trên máy bạn.
  6. Viết lớp đệm ở mục cuối thành một tệp mang.h để dùng lại cho các bài sau.

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

  • Bảy hàm: socket, bind, listen, accept, connect, send và recv, close.
  • accept trả về một mô tả tệp mới; socket lắng nghe vẫn sống.
  • Luôn memset struct địa chỉ, và luôn htons cho cổng. Quên htons thì chương trình chạy nhưng ở nhầm cổng.
  • Dùng inet_pton và inet_ntop, không dùng inet_addr và inet_ntoa.
  • Trên Windows, SOCKET không dấu nên s < 0 không bao giờ đúng; phải so với INVALID_SOCKET.