前言

网络编程代码实例:IO复用版。


代码仓库


内容

  • 使用传输控制协议(TCP)
  • 服务端多进程,一个服务端可连接多个客户端
  • 用户在客户端终端输入,可多次手动通信
  • 在上一份代码实例:多进程版的基础上,服务端增加获取客户端地址的逻辑;更新部分函数使用、错误处理、注释和Makefile文件;为保证代码简洁,部分输入输出和字符串处理函数未进行错误检测
  • 3个客户端代码实例分别使用IO复用的select、poll和epoll技术,同时监听用户输入和网络接收,可即时接收服务端进程终止和服务端主机关机消息
  • 客户端使用shutdown()而不是close()关闭连接,当客户端主动关闭写半部连接后,服务端仍能够接收而不是丢弃批量输入的缓冲区数据

代码(有详细注释)

server.c

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
// 头文件————————————————————
// #include <sys/socket.h> // socket()、sockaddr{}、bind()、listen()、accept()、recv()、send()
#include <stdio.h> // perror()、printf()、sprintf()
#include <stdlib.h> // exit()
// #include <netinet/in.h> // sockaddr_in{}、htons()、ntohs()
#include <string.h> // memset()、strcpy()、strcat()
#include <arpa/inet.h> // inet_pton()、inet_ntop()
// #include <unistd.h> // close()、fork()
#include <errno.h> // errno

// 全局常量————————————————————
const char g_serv_listen_ip[INET_ADDRSTRLEN] = "0.0.0.0"; // 服务端监听的IP地址
const uint16_t g_serv_listen_port = 6000; // 服务端监听的端口号
const int g_listen_max_count = 5; // 监听的最大连接数
const int g_buff_size = 32; // 消息缓冲区的大小。单位:字节

// 函数声明————————————————————
void handle_request(int, struct sockaddr_in); // 处理请求

// 主函数————————————————————
int main(int arg, char *argv[])
{
// 网络连接————————————————————
int listen_fd; // 监听套接字文件描述符
// 创建套接字并获取套接字文件描述符
if ((listen_fd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
perror("socket() error");
exit(EXIT_FAILURE);
}

struct sockaddr_in serv_addr; // 服务端网络信息结构体
// 初始化服务端网络信息结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(g_serv_listen_port);
if ((inet_pton(AF_INET, g_serv_listen_ip, &serv_addr.sin_addr)) != 1)
{
perror("inet_pton() error");
exit(EXIT_FAILURE);
}

// 绑定套接字与网络信息
if ((bind(listen_fd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr))) == -1)
{
if ((close(listen_fd)) == -1)
{
perror("bind() close() error");
exit(EXIT_FAILURE);
}

perror("bind() error");
exit(EXIT_FAILURE);
}

// 套接字设置被动监听状态
if ((listen(listen_fd, g_listen_max_count)) == -1)
{
if ((close(listen_fd)) == -1)
{
perror("listen() close() error");
exit(EXIT_FAILURE);
}

perror("listen() error");
exit(EXIT_FAILURE);
}

struct sockaddr_in clie_addr; // 客户端网络信息结构体
int clie_addr_size; // 客户端网络信息结构体大小
int connect_fd; // 连接套接字文件描述符
memset(&clie_addr, 0, sizeof(clie_addr));
clie_addr_size = sizeof(struct sockaddr);
pid_t pid; // 进程标识符

// 循环监听客户端请求
// 原则:父进程不能退出,子进程可以退出
while (1)
{
// 与客户端建立连接
if ((connect_fd = accept(listen_fd, (struct sockaddr *)(&clie_addr), &clie_addr_size)) == -1)
{
perror("accept() error");
continue; // 继续监听
}

// 创建子进程处理请求
pid = fork();
if (pid == -1) // 错误
{
perror("fork() error");
continue; // 继续监听
}
else if (pid == 0) // 子进程
{
if ((close(listen_fd)) == -1) // 1.关闭监听套接字文件描述符
{
if ((close(connect_fd)) == -1)
{
perror("fork() close() connect_fd child_process error");
exit(EXIT_FAILURE); // 子进程退出
}

perror("fork() close() listen_fd child_process error");
exit(EXIT_FAILURE); // 子进程退出
}

handle_request(connect_fd, clie_addr); // 2.处理请求

if ((close(connect_fd)) == -1) // 3.关闭连接套接字文件描述符
{
perror("fork() close() connect_fd2 child_process error");
exit(EXIT_FAILURE); // 子进程退出
}

exit(EXIT_SUCCESS);
}
else if (pid > 0) // 父进程
{
if ((close(connect_fd)) == -1) // 关闭连接套接字文件描述符
{
perror("fork() close() connect_fd parent_process error");
continue; // 继续监听
}
}
}

if ((close(listen_fd)) == -1) // 父进程关闭监听套接字文件描述符。实际不会执行
{
perror("close() listen_fd error");
exit(EXIT_FAILURE);
}

return 0;
}

// 函数定义————————————————————
// 处理请求
// 参数:连接套接字文件描述符,客户端网络信息结构体
void handle_request(int connect_fd, struct sockaddr_in clie_addr)
{
// 获取客户端的IP地址和端口————————————————————
char clie_ip[INET_ADDRSTRLEN]; // 客户端的IP地址 如:127.0.0.1
uint16_t clie_port; // 客户端的端口 如:42534
char clie_port_str[5]; // 客户端的端口,char[]类型 如:42534
char clie_ip_port[INET_ADDRSTRLEN + 5]; // 客户端的IP地址和端口 如:127.0.0.1:42534

if ((inet_ntop(AF_INET, &clie_addr.sin_addr, clie_ip, sizeof(clie_ip))) == NULL)
{
perror("inet_ntop() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束子进程
}
// 注意:返回值和第3个参数值相同,区别为一个是常量一个是变量
clie_port = ntohs(clie_addr.sin_port);
sprintf(clie_port_str, "%d", clie_port);
strcpy(clie_ip_port, clie_ip);
strcat(clie_ip_port, ":");
strcat(clie_ip_port, clie_port_str);
printf("Client connection: %s\n", clie_ip_port);

// 传输消息————————————————————
char msg_recv[g_buff_size]; // 从客户端接收的消息缓冲区
char msg_send[g_buff_size]; // 发送到客户端的消息缓冲区
int recv_byte; // 接收的消息字节数

while (1) // 循环接收和发送消息
{
memset(&msg_recv, 0, sizeof(*msg_recv));
memset(&msg_send, 0, sizeof(*msg_send));

recv_byte = recv(connect_fd, msg_recv, g_buff_size, 0); // 接收消息
if (recv_byte > 0) // 有消息
{
printf("From %s received message: %s", clie_ip_port, msg_recv); // 接收的消息

strcpy(msg_send, msg_recv); // 发送的消息
if ((send(connect_fd, msg_send, g_buff_size, 0)) == -1) // 发送消息
{
perror("send() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束子进程
}
}
else if (recv_byte == 0) // 文件末尾EOF:在客户端标准输入Ctrl+D或Ctrl+C
{
printf("From %s received the end of file\n", clie_ip_port);
return; // 函数返回后,关闭连接套接字文件描述符,结束子进程
}
else if ((recv_byte == -1) && (errno == EINTR)) // 信号或网络中断recv()
{
continue; // 继续接收消息
}
else if (recv_byte == -1) // 错误
{
perror("recv() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束子进程
}
}

return;
}

client_select.c

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// 头文件————————————————————
// #include <sys/socket.h> // socket()、sockaddr{}、connect()、send()、recv()、shutdown()
#include <stdio.h> // perror()、printf()、gets()
#include <stdlib.h> // exit()
// #include <netinet/in.h> // sockaddr_in{}、htons()
#include <string.h> // memset()、strcat()
#include <arpa/inet.h> // inet_pton()
#include <unistd.h> // close()、STDIN_FILENO
#include <errno.h> // errno
#include <sys/select.h> // select()

// 全局常量————————————————————
const char g_connect_serv_ip[INET_ADDRSTRLEN] = "127.0.0.1"; // 连接服务端的IP地址
const uint16_t g_connect_serv_port = 6000; // 连接服务端的端口号
const int g_buff_size = 32; // 消息缓冲区大小。单位:字节

// 函数声明————————————————————
void handle(int); // 处理

// 主函数————————————————————
int main(int argc, char *argv[])
{
// 网络连接————————————————————
int sock_fd; // 套接字文件描述符
// 创建套接字并获取套接字文件描述符
if ((sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
perror("socket() error");
exit(EXIT_FAILURE);
}

struct sockaddr_in serv_addr; // 服务端网络信息结构体
// 初始化服务端网络信息结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(g_connect_serv_port);
if ((inet_pton(AF_INET, g_connect_serv_ip, &serv_addr.sin_addr)) != 1)
{
perror("inet_pton() error");
exit(EXIT_FAILURE);
}

// 与服务端建立连接
if ((connect(sock_fd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr))) == -1)
{
if ((close(sock_fd)) == -1)
{
perror("connect() close() error");
exit(EXIT_FAILURE);
}

perror("connect() error");
exit(EXIT_FAILURE);
}

handle(sock_fd); // 处理

// 关闭套接字文件描述符
if ((close(sock_fd)) == -1)
{
perror("close() error");
exit(EXIT_FAILURE);
}

return 0;
}

// 函数定义————————————————————
// 处理
void handle(int sock_fd) // 参数:套接字文件描述符
{
// select()准备————————————————————
// 监听标准输入文件描述符和套接字文件描述符的可读条件
int maxfdp1; // 要监听描述符的最大值+1
fd_set read_fd_set; // 可读文件描述符集合
if (STDIN_FILENO >= sock_fd)
{
maxfdp1 = STDIN_FILENO + 1;
}
else
{
maxfdp1 = sock_fd + 1;
}
FD_ZERO(&read_fd_set);

// 传输消息————————————————————
char msg_send[g_buff_size]; // 发送到服务端的消息缓冲区
char msg_recv[g_buff_size]; // 从服务端接收的消息缓冲区
int recv_byte; // 接收的消息字节数

int stdin_eof = 0; // 标准输入文件描述符读到文件末尾的标志:0未读到EOF,1读到EOF

printf("Please input the message to be sent directly below:\n");
while (1) // 循环发送和接收消息
{
// 1.select()调用
// 因为select()返回会修改fd_set,所以每循环都要重新设置监听的fd_set
// 当stdin_eof == 1时,写半部关闭,不需要再设置监听标准输入文件描述符
if (stdin_eof == 0)
{
FD_SET(STDIN_FILENO, &read_fd_set);
}
FD_SET(sock_fd, &read_fd_set);
if ((select(maxfdp1, &read_fd_set, NULL, NULL, NULL)) == -1)
{
perror("select() error");
continue; // 若有错误在下个循环继续调用
}

memset(&msg_send, 0, sizeof(*msg_send));
memset(&msg_recv, 0, sizeof(*msg_recv));

// 2.select()检测
if (FD_ISSET(STDIN_FILENO, &read_fd_set)) // 标准输入文件描述符可读
{
if ((fgets(msg_send, g_buff_size, stdin)) == NULL)
// 从标准输入获取消息。错误或遇到文件结尾(EOF):在客户端标准输入Ctrl+D或Ctrl+C,相当于关闭连接
{
printf("End of connection\n");

stdin_eof = 1; // 设置标志
FD_CLR(STDIN_FILENO, &read_fd_set); // 清理fd_set
if ((shutdown(sock_fd, SHUT_WR)) == -1) // 写半部关闭
{
perror("shutdown() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
continue;
// 不是return,因为可能还需要从网络套接字文件描述符读
// 不需要进入下面的send(),服务端会recv()接收EOF
}

if ((send(sock_fd, msg_send, g_buff_size, 0)) == -1) // 发送消息
{
perror("send() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
printf("Send message: %s", msg_send);
}
else if (FD_ISSET(sock_fd, &read_fd_set)) // 套接字文件描述符可读
{
recv_byte = recv(sock_fd, msg_recv, g_buff_size, 0); // 接收消息
if (recv_byte > 0) // 有数据
{
printf("Received message: %s", msg_recv); // 接收的消息
}
else if (recv_byte == 0) // 服务端进程提前终止,在服务端标准输入Ctrl+C中断进程
{
// 如果已经调用shutdown()写半部关闭,当服务端recv()EOF后调用close()时,是正常的结束连接
// 否则,是服务端ctrl+c提前关闭连接
if (stdin_eof == 1)
{
return; // 函数返回后,关闭套接字文件描述符,结束进程
}

printf("Server terminated prematurely\n");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
else if ((recv_byte == -1) && (errno == EINTR)) // 信号或网络中断recv()
{
continue; // 继续发送和接收数据
}
else if (recv_byte == -1) // 错误
{
perror("recv() error");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
}
}

return;
}

client_poll.c

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// 头文件————————————————————
// #include <sys/socket.h> // socket()、sockaddr{}、connect()、send()、recv()、shutdown()
#include <stdio.h> // perror()、printf()、gets()
#include <stdlib.h> // exit()
// #include <netinet/in.h> // sockaddr_in{}、htons()
#include <string.h> // memset()、strcat()
#include <arpa/inet.h> // inet_pton()
#include <unistd.h> // close()、STDIN_FILENO
#include <errno.h> // errno
#include <poll.h> // poll()

// 全局常量————————————————————
const char g_connect_serv_ip[INET_ADDRSTRLEN] = "127.0.0.1"; // 连接服务端的IP地址
const uint16_t g_connect_serv_port = 6000; // 连接服务端的端口号
const int g_buff_size = 32; // 消息缓冲区大小。单位:字节

// 函数声明————————————————————
void handle(int); // 处理

// 主函数————————————————————
int main(int argc, char *argv[])
{
// 网络连接————————————————————
int sock_fd; // 套接字文件描述符
// 创建套接字并获取套接字文件描述符
if ((sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
perror("socket() error");
exit(EXIT_FAILURE);
}

struct sockaddr_in serv_addr; // 服务端网络信息结构体
// 初始化服务端网络信息结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(g_connect_serv_port);
if ((inet_pton(AF_INET, g_connect_serv_ip, &serv_addr.sin_addr)) != 1)
{
perror("inet_pton() error");
exit(EXIT_FAILURE);
}

// 与服务端建立连接
if ((connect(sock_fd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr))) == -1)
{
if ((close(sock_fd)) == -1)
{
perror("connect() close() error");
exit(EXIT_FAILURE);
}

perror("connect() error");
exit(EXIT_FAILURE);
}

handle(sock_fd); // 处理

// 关闭套接字文件描述符
if ((close(sock_fd)) == -1)
{
perror("close() error");
exit(EXIT_FAILURE);
}

return 0;
}

// 函数定义————————————————————
// 处理
void handle(int sock_fd) // 参数:套接字文件描述符
{
// poll()准备————————————————————
// 监听标准输入文件描述符和套接字文件描述符的可读条件
struct pollfd pollfd_array[2]; // pollfd{}结构数组
pollfd_array[0].fd = STDIN_FILENO;
pollfd_array[0].events = POLLIN;
pollfd_array[1].fd = sock_fd;
pollfd_array[1].events = POLLIN;

// 传输消息————————————————————
char msg_send[g_buff_size]; // 发送到服务端的消息缓冲区
char msg_recv[g_buff_size]; // 从服务端接收的消息缓冲区
int recv_byte; // 接收的消息字节数

int stdin_eof = 0; // 标准输入文件描述符读到文件末尾的标志:0未读到EOF,1读到EOF

printf("Please input the message to be sent directly below:\n");
while (1) // 循环发送和接收消息
{
// 1.poll()调用
// 当stdin_eof == 1时,写半部关闭,不需要再设置监听标准输入文件描述符
// 不监听的成员fd设置为-1,会忽略成员events,返回时将成员revents置0
if (stdin_eof == 1)
{
pollfd_array[0].fd = -1;
}
if ((poll(pollfd_array, 2, -1)) == -1)
{
perror("poll() error");
continue; // 若有错误在下个循环继续调用
}

memset(&msg_send, 0, sizeof(*msg_send));
memset(&msg_recv, 0, sizeof(*msg_recv));

// 2.poll()检测
if (pollfd_array[0].revents & (POLLIN | POLLERR)) // 标准输入文件描述符可读
{
if ((fgets(msg_send, g_buff_size, stdin)) == NULL)
// 从标准输入获取消息。错误或遇到文件结尾(EOF):在客户端标准输入Ctrl+D或Ctrl+C,相当于关闭连接
{
printf("End of connection\n");

stdin_eof = 1; // 设置标志
if ((shutdown(sock_fd, SHUT_WR)) == -1) // 写半部关闭
{
perror("shutdown() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
continue;
// 不是return,因为可能还需要从网络套接字文件描述符读
// 不需要进入下面的send(),服务端会recv()接收EOF
}

if ((send(sock_fd, msg_send, g_buff_size, 0)) == -1) // 发送消息
{
perror("send() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
printf("Send message: %s", msg_send);
}
else if (pollfd_array[1].revents & (POLLIN | POLLERR)) // 套接字文件描述符可读
{
recv_byte = recv(sock_fd, msg_recv, g_buff_size, 0); // 接收消息
if (recv_byte > 0) // 有数据
{
printf("Received message: %s", msg_recv); // 接收的消息
}
else if (recv_byte == 0) // 服务端进程提前终止,在服务端标准输入Ctrl+C中断进程
{
// 如果已经调用shutdown()写半部关闭,当服务端recv()EOF后调用close()时,是正常的结束连接
// 否则,是服务端ctrl+c提前关闭连接
if (stdin_eof == 1)
{
return; // 函数返回后,关闭套接字文件描述符,结束进程
}

printf("Server terminated prematurely\n");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
else if ((recv_byte == -1) && (errno == EINTR)) // 信号或网络中断recv()
{
continue; // 继续发送和接收数据
}
else if (recv_byte == -1) // 错误
{
perror("recv() error");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
}
}

return;
}

client_epoll.c

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// 头文件————————————————————
// #include <sys/socket.h> // socket()、sockaddr{}、connect()、send()、recv()、shutdown()
#include <stdio.h> // perror()、printf()、gets()
#include <stdlib.h> // exit()
// #include <netinet/in.h> // sockaddr_in{}、htons()
#include <string.h> // memset()、strcat()
#include <arpa/inet.h> // inet_pton()
#include <unistd.h> // close()、STDIN_FILENO
#include <errno.h> // errno
#include <sys/epoll.h> // epoll()

// 全局常量————————————————————
const char g_connect_serv_ip[INET_ADDRSTRLEN] = "127.0.0.1"; // 连接服务端的IP地址
const uint16_t g_connect_serv_port = 6000; // 连接服务端的端口号
const int g_buff_size = 32; // 消息缓冲区大小。单位:字节

// 函数声明————————————————————
void handle(int); // 处理

// 主函数————————————————————
int main(int argc, char *argv[])
{
// 网络连接————————————————————
int sock_fd; // 套接字文件描述符
// 创建套接字并获取套接字文件描述符
if ((sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
{
perror("socket() error");
exit(EXIT_FAILURE);
}

struct sockaddr_in serv_addr; // 服务端网络信息结构体
// 初始化服务端网络信息结构体
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(g_connect_serv_port);
if ((inet_pton(AF_INET, g_connect_serv_ip, &serv_addr.sin_addr)) != 1)
{
perror("inet_pton() error");
exit(EXIT_FAILURE);
}

// 与服务端建立连接
if ((connect(sock_fd, (struct sockaddr *)(&serv_addr), sizeof(serv_addr))) == -1)
{
if ((close(sock_fd)) == -1)
{
perror("connect() close() error");
exit(EXIT_FAILURE);
}

perror("connect() error");
exit(EXIT_FAILURE);
}

handle(sock_fd); // 处理

// 关闭套接字文件描述符
if ((close(sock_fd)) == -1)
{
perror("close() error");
exit(EXIT_FAILURE);
}

return 0;
}

// 函数定义————————————————————
// 处理
void handle(int sock_fd) // 参数:套接字文件描述符
{
// epoll()准备————————————————————
// 监听标准输入文件描述符和套接字文件描述符的可读条件
int epoll_fd; // epoll用的文件描述符
struct epoll_event epoll_listen_event; // epoll监听的事件
struct epoll_event epoll_wait_event[2]; // epoll等待的事件
int epoll_wait_event_num; // epoll_wait()调用返回的就绪事件数

if ((epoll_fd = epoll_create(2)) == -1)
{
perror("epoll_create() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
epoll_listen_event.data.fd = STDIN_FILENO;
epoll_listen_event.events = EPOLLIN; // 默认水平触发,未设置边缘触发
if ((epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &epoll_listen_event)) == -1)
{
if ((close(epoll_fd)) == -1) // 注意关闭epoll用的文件描述符
{
perror("epoll_ctl() STDIN_FILENO EPOLL_CTL_ADD close() error");
return;
}

perror("epoll_ctl() STDIN_FILENO EPOLL_CTL_ADD error");
return;
}
epoll_listen_event.data.fd = sock_fd; // 同一个变量可重复使用,修改后注册到epoll_ctl()即可
epoll_listen_event.events = EPOLLIN;
if ((epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &epoll_listen_event)) == -1)
{
if ((close(epoll_fd)) == -1)
{
perror("epoll_ctl() sock_fd close() error");
return;
}

perror("epoll_ctl() sock_fd error");
return;
}

// 传输消息————————————————————
char msg_send[g_buff_size]; // 发送到服务端的消息缓冲区
char msg_recv[g_buff_size]; // 从服务端接收的消息缓冲区
int recv_byte; // 接收的消息字节数

int stdin_eof = 0; // 标准输入文件描述符读到文件末尾的标志:0未读到EOF,1读到EOF

printf("Please input the message to be sent directly below:\n");
while (1) // 循环发送和接收消息
{
// 1.epoll()调用
// 当stdin_eof == 1时,写半部关闭,不需要再设置监听标准输入文件描述符
if (stdin_eof == 1)
{
if ((epoll_ctl(epoll_fd, EPOLL_CTL_DEL, STDIN_FILENO, NULL)) == -1) // 删除事件时,第4个参数设置为NULL
{
if ((close(epoll_fd)) == -1)
{
perror("epoll_ctl() STDIN_FILENO EPOLL_CTL_DEL close() error");
return;
}

perror("epoll_ctl() STDIN_FILENO EPOLL_CTL_DEL error");
return;
}
}
if ((epoll_wait_event_num = epoll_wait(epoll_fd, epoll_wait_event, 2, -1)) == -1)
{
if ((close(epoll_fd)) == -1)
{
perror("epoll_wait() close() error");
return;
}

perror("epoll_wait() error");
continue; // 若有错误在下个循环继续调用
}

memset(&msg_send, 0, sizeof(*msg_send));
memset(&msg_recv, 0, sizeof(*msg_recv));

// 2.epoll()检测
for (int i = 0; i < epoll_wait_event_num; ++i)
{
if ((epoll_wait_event[i].data.fd == STDIN_FILENO) && (epoll_wait_event[i].events & (EPOLLIN | EPOLLERR))) // 标准输入文件描述符可读
{
if ((fgets(msg_send, g_buff_size, stdin)) == NULL)
// 从标准输入获取消息。错误或遇到文件结尾(EOF):在客户端标准输入Ctrl+D或Ctrl+C,相当于关闭连接
{
printf("End of connection\n");

stdin_eof = 1; // 设置标志
if ((shutdown(sock_fd, SHUT_WR)) == -1) // 写半部关闭
{
perror("shutdown() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
continue;
// 不是return,因为可能还需要从网络套接字文件描述符读
// 不需要进入下面的send(),服务端会recv()接收EOF
}

if ((send(sock_fd, msg_send, g_buff_size, 0)) == -1) // 发送消息
{
perror("send() error");
return; // 函数返回后,关闭连接套接字文件描述符,结束进程
}
printf("Send message: %s", msg_send);
}
else if ((epoll_wait_event[i].data.fd == sock_fd) && (epoll_wait_event[i].events & (EPOLLIN | EPOLLERR))) // 套接字文件描述符可读
{
recv_byte = recv(sock_fd, msg_recv, g_buff_size, 0); // 接收消息
if (recv_byte > 0) // 有数据
{
printf("Received message: %s", msg_recv); // 接收的消息
}
else if (recv_byte == 0) // 服务端进程提前终止,在服务端标准输入Ctrl+C中断进程
{
// 如果已经调用shutdown()写半部关闭,当服务端recv()EOF后调用close()时,是正常的结束连接
// 否则,是服务端ctrl+c提前关闭连接
if (stdin_eof == 1)
{
return; // 函数返回后,关闭套接字文件描述符,结束进程
}

printf("Server terminated prematurely\n");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
else if ((recv_byte == -1) && (errno == EINTR)) // 信号或网络中断recv()
{
continue; // 继续发送和接收数据
}
else if (recv_byte == -1) // 错误
{
perror("recv() error");
return; // 函数返回后,关闭套接字文件描述符,结束进程
}
}
}
}

if ((close(epoll_fd)) == -1)
{
perror("close()");
return;
}

return;
}

结果

操作时序:

  1. 启动服务端
  2. 启动client_select,发送消息”client_select”
  3. 启动client_poll,发送消息”client_poll”
  4. 启动client_epoll,发送消息”client_epoll”
  5. 客户端client_select,使用Ctrl+C异常终止
  6. 客户端client_poll,使用Ctrl+D正常终止
  7. 服务端,使用Ctrl+C异常终止
  8. 客户端client_epoll,被迫终止

server:

client_select:

在这里插入图片描述

client_poll:

在这里插入图片描述

client_epoll:

在这里插入图片描述


总结

网络编程代码实例:IO复用版。


参考资料

  • 《UNIX环境高级编程(第3版)》作者:W.Richard Stevens,Stephen A.Rago
  • 《UNIX网络编程(第3版)》作者:W.Richard Stevens,Bill Fenner,Andrew M.Rudoff

作者的话

  • 感谢参考资料的作者/博主
  • 作者:夜悊
  • 版权所有,转载请注明出处,谢谢~
  • 如果文章对你有帮助,请点个赞或加个粉丝吧,你的支持就是作者的动力~
  • 文章在描述时有疑惑的地方,请留言,定会一一耐心讨论、解答
  • 文章在认识上有错误的地方, 敬请批评指正
  • 望读者们都能有所收获