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)
| POSIX | Windows | Nghĩa | Bạn làm gì |
|---|---|---|---|
| ECONNREFUSED | 10061 | Má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 |
| ETIMEDOUT | 10060 | SYN gửi đi mà không có hồi âm nào | Máy tắt, hoặc tường lửa lặng lẽ vứt gói |
| EHOSTUNREACH | 10065 | Bộ đị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 |
| ENETUNREACH | 10051 | Khô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ử
- Dịch và chạy
echo_kh.c, thử với ba thông điệp khác nhau. - Chạy client khi server chưa bật, và đọc mã lỗi bạn nhận được.
- Đo bằng đồng hồ xem
connecttới một địa chỉ không tồn tại mất bao lâu trên máy bạn. - Cài
ket_noi_chovà chứng minh nó dừng đúng sau ba giây. - Bỏ bước gọi
getsockopt(SO_ERROR)đi và xem lỗi lộ ra ở đâu. - 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
socketrồiconnect; hệ điều hành tự cấp cổng nguồn. ECONNREFUSEDnghĩa là mạng thông nhưng không có server;ETIMEDOUTnghĩ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ộngselect. - Sau
select, phải gọigetsockopt(SO_ERROR)để biết bắt tay thành công hay thất bại. - Dùng
getaddrinfochứ không dùnggethostbyname, và nhớfreeaddrinfo.