Unix Network Programming - 1. 阻塞式I/O模型 EchoClient/EchoServer

1

基本套接字

int socket(int family, int type, int protocal);
family 协议域 type套接字类型 protocal 协议类型常值z

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include "Common.h"

void str_echo(int fd) {
char buf[MAXLINE];

while (1) {
int n = read(fd, buf, MAXLINE); // 子进程阻塞与从相应fd读取数据
if(n > 0) write(fd, buf, n);

if (n <= 0 && errno == EINTR) {
std::cout << "read errno -> EINTR" << std::endl;
}
else if (n <= 0)
{
std::cout << "read return <= 0" << std::endl;
break;
}
}
}

void sig_chld(int signo) {
pid_t pid;
int stat;

// WHOHANG选项, 告知waitpid在有尚未终止的子进程在运行时不要阻塞
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
std::cout << "child " << pid << " terminated\n" << std::endl;
}
return;
}

int main() {
int listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sockfd < 0) {
std::cout << "create blank socket failure." << std::endl;
}

sockaddr_in socket_addr;
bzero(&socket_addr, sizeof(socket_addr));
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 等到TCP连接成功后选择IP地址
socket_addr.sin_port = htons(SERVER_PORT);

if (bind(listen_sockfd, (sockaddr*)& socket_addr, sizeof(socket_addr)) < 0) {
std::cout << "bind socket address failure." << std::endl;
}

if (listen(listen_sockfd, LISTENQ) < 0) {
std::cout << "listening socket create failure." << std::endl;
}

signal(SIGCHLD, sig_chld); // 处理SIGCHLD信号, 避免进程成为僵尸进程

sockaddr_in client_addr;
socklen_t clilen = sizeof(client_addr);
while (1) {
int connect_sockfd = accept(listen_sockfd, (sockaddr*)&client_addr,&clilen); // 父进程将再次阻塞与accept
if (connect_sockfd < 0) {
if (errno == EINTR) continue;
else std::cout << "connected socket create failure." << std::endl;
}

pid_t childpid = fork();
if (childpid == 0) {
close(listen_sockfd); // 子进程不需要这个监听套接字
str_echo(connect_sockfd);
exit(0); // 子进程退出后, 向父进程发送SIGCHLD, 同时触发四次挥手后两个分节
}
close(connect_sockfd); // 父进程不需要这个连接套接字
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void str_cli(FILE* fp, int sockfd) {
char sendbuffer[MAXLINE], recvbuffer[MAXLINE];
while (fgets(sendbuffer, MAXLINE, fp) != NULL) { // 客户端阻塞于从标准输入读取数据
write(sockfd, sendbuffer, strlen(sendbuffer));

if (read(sockfd, recvbuffer, MAXLINE) == 0) { // 客户端再次阻塞于从socket读取数据
std::cout << "server terminated prematurely" << std::endl;
}

fputs(recvbuffer, stdout);
}
}

int main(int argc, char** argv) {
if(argc != 2) return 0;

int sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4, TCP
if (sockfd < 0) {
std::cout << "create blank socket failure." << std::endl;
}

sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT); // 将端口转换为网络字节序
inet_pton(AF_INET, argv[1], &server_addr.sin_addr); // 点分十进制到IP地址结构

// 发起与服务器建立连接, 触发三次握手
// C: CLOSED --[connect(), send:SYN]--> SYNSEND --[recv: SYN,ACK, send: ACK ]--> ESTABLISHED
// S: CLOSED --[listen()]--> LISTENING --[recv: SYN, send: SYN,ACK]--> SYNRECV --[recv: ACK]--> ESTABLISHED
//
// 该函数在连接成功或出错时才会返回, 其中可能会遇到的错误:
// 1. 客户端的SYN没有收到服务器任何响应, 返回ETIMEOUT错误
// 2. 服务器相应的端口没有进程在等待客户端与之建立连接, 此时响应SYN一个RST, 触发ECONNREFUSED错误
// 3. 目的不可达ICMP错误, 但客户端会继续进行尝试, 一段时间后将保存的ICMP错误转换为EHOSTUNREACH或ENEWUNREACH错误
if (connect(sockfd, (sockaddr*)& server_addr, sizeof(server_addr)) < 0) {
std::cout << "connect to server failure." << std::endl;
}

str_cli(stdin, sockfd);

exit(0);
// 客户端进程正常退出, 将关闭进程打开的所有描述符, 在关闭sockfd时, 将会向服务器发送一个FIN, 开始TCP断开连接四次挥手的前两个分节
// C: ESTABLISHED --[close(), send:FIN]--> FIN_WAIT_1 --[recv:ACK]--> FIN_WAIT_2 --[recv:FIN, send:ACK]--> TIME_WAIT --[2MSL timeout]--> CLOSED
// S: ESTABLISHED --[recv:FIN, send:ACK]--> CLOSE_WAIT --[close(), send:FIN]--> LAST_ACK --[recv:ACK]--> CLOSED

return 0;
}

3