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
| Hàm | Ai gọi | Làm gì | Trả về |
|---|---|---|---|
| socket | Cả hai | Tạo một điểm cuối, chưa gắn với ai | Mô tả tệp, hoặc -1 |
| bind | Server | Gắn socket vào một địa chỉ và cổng cụ thể | 0, hoặc -1 |
| listen | Server | Chuyển socket sang chế độ chờ, tạo hàng đợi | 0, hoặc -1 |
| accept | Server | Lấy một kết nối ra khỏi hàng đợi, chặn nếu rỗng | Mô tả tệp MỚI |
| connect | Client | Bắt tay ba bước tới server | 0, hoặc -1 |
| send / recv | Cả hai | Gửi và nhận dữ liệu | Số byte, 0, hoặc -1 |
| close | Cả hai | Đóng và giải phóng | 0, 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àm | Nghĩa | Dùng cho |
|---|---|---|
| htons | host to network short | Cổng, 16 bit |
| htonl | host to network long | Địa chỉ IPv4, 32 bit |
| ntohs | network to host short | Đọc cổng ra để in |
| ntohl | network 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
| POSIX | Windows | |
|---|---|---|
| Header | sys/socket.h, netinet/in.h, arpa/inet.h | winsock2.h, ws2tcpip.h |
| Kiểu socket | int | SOCKET, thực chất là UINT_PTR |
| Giá trị lỗi | -1 | INVALID_SOCKET, tức (SOCKET)(~0) |
| Đóng | close(fd) | closesocket(s) |
| Mã lỗi | errno | WSAGetLastError() |
| Khởi tạo | Không cần | WSAStartup, WSACleanup |
| Liên kết | Không cần cờ | -lws2_32 |
| Kiểu độ dài | socklen_t | int |
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ử
- Chạy chương trình
byteorder.ctrên máy bạn và đọc từng dòng hex. - Viết chương trình in ra dạng hex của
htonl(0x01020304)và giải thích kết quả. - Cố ý quên
htonsrồi dùnggetsocknametìm ra cổng thật. - Viết hàm
in_dia_chi(const struct sockaddr *)làm việc với cả IPv4 lẫn IPv6. - Bỏ
memsetđi rồi chạy lại, xem có gì thay đổi trên máy bạn. - 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,sendvàrecv,close. accepttrả về một mô tả tệp mới; socket lắng nghe vẫn sống.- Luôn
memsetstruct địa chỉ, và luônhtonscho cổng. Quênhtonsthì chương trình chạy nhưng ở nhầm cổng. - Dùng
inet_ptonvàinet_ntop, không dùnginet_addrvàinet_ntoa. - Trên Windows,
SOCKETkhông dấu nêns < 0không bao giờ đúng; phải so vớiINVALID_SOCKET.