文章目录
- 1. 概述
- 2. TCP 网络编程实例
- 2.1 服务器端
- 2.2 客户端
- 2.3 运行截图
- 3. I/O 模型
- 3.1 阻塞式I/O模型
- 3.2 非阻塞I/O模型
- 3.3 I/O 复用模型
- 3.4 信号驱动式I/O
- 3.5 异步I/O模型
- 4. I/O复用之 select
- 4.1 select 函数描述
- 4.2 服务端代码
- 4.3 客户端代码
- 4.4 运行截图
- 5. I/O复用之 poll
- 5.1 poll 函数描述
- 5.2 服务端代码
- 5.3 客户端代码
- 5.4 运行截图
- 6. I/O复用之 epoll
- 6.1 常用函数
- 6.2 服务端代码
- 参考文献
1. 概述
- 网络编程, 就是编写程序, 使两台联网的电脑可以交换数据,
- 套接字是网络数据传输用的软件设备, 用来连接网络的工具
- 在 linux中 socket被认为是文件中的一种, 在网络数据传输过程中, 使用文件I/O的相关函数
- 套接字常用网络协议: TCP、UDP
套接字进行网络连接流程, 如下图:
服务器端:
- 创建服务器套接字
socket()
- 绑定端口
bind()
- 监听端口
listen()
- 接受客户端请求
accept()
- 读取客户端请求的数据
read()
- 返回客户端要响应的数据
write()
- …
- 关闭与客户端的连接
close()
- 关闭服务器套接字
close()
客户端:
- 创建客户端套接字
socket()
- 连接服务端
connect()
- 请求服务端数据, 发送操作数和操作符到服务器
write()
- 从服务器读取操作结果
read()
- …
- 关闭客户端套接字
close()
流程图如下, 具体代码示例可以看下面的 2. TCP 网络编程实例
2. TCP 网络编程实例
2.1 服务器端
开放的端口号 6666:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
// 定义缓冲区大小和操作数大小
#define BUF_SIZE 1024
#define OPSZ 4
// 错误处理函数
void error_handling(char *message);
// 计算函数,根据操作数和操作符进行计算
int calculate(int opnum, int opnds[], char oprator);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
char opinfo[BUF_SIZE]; // 用于接收客户端发送的操作数和操作符
int result, opnd_cnt, i;
int recv_cnt, recv_len;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
// 创建服务器套接字
serv_sock = socket(PF_INET,SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling(
"socket() error"
);
// 初始化服务器地址结构体
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = 6666; // 设置端口号为 6666
// 绑定端口
if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling(
"bind() error"
);
// 监听端口
if (listen(serv_sock, 5) == -1)
error_handling(
"listen() error"
);
clnt_adr_sz = sizeof(clnt_adr); // 设置客户端地址结构体的大小
// 接受多个客户端连接并进行计算
for (i = 0; i < 5; i++) {
opnd_cnt = 0;
clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &clnt_adr_sz); // 接受客户端连接
read(clnt_sock, &opnd_cnt, 1); // 读取操作数个数
recv_len = 0;
// 循环接收操作数,直到接收完所有操作数
while ((opnd_cnt * OPSZ + 1) > recv_len) {
recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE - 1); // 从客户端接收数据
recv_len += recv_cnt;
// 计算结果并发送回客户端
result = calculate(opnd_cnt, (int *) opinfo, opinfo[recv_len - 1]);
write(clnt_sock, (char *) &result, sizeof(result));
close(clnt_sock); // 关闭与客户端的连接
}
}
close(serv_sock); // 关闭服务器套接字
return 0;
}
// 计算函数,根据操作数和操作符返回计算结果
int calculate(int opnum, int opnds[], char op) {
int result = opnds[0], i;
switch (op) { // 根据操作符进行不同的计算
case '+':
for (i = 1; i < opnum; i++) result += opnds[i];
break;
case '-':
for (i = 1; i < opnum; i++) result -= opnds[i];
break;
case '*':
for (i = 1; i < opnum; i++) result *= opnds[i];
break;
}
return result;
}
// 错误处理函数,在遇到错误时打印消息并退出程序
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
2.2 客户端
#include <stdio.h> // 标准输入输出
#include <stdlib.h> // 系统函数库
#include <string.h> // 字符串处理
#include <unistd.h> // UNIX标准函数
#include <arpa/inet.h> // Internet网络协议
#include <sys/socket.h> // Socket编程相关
// 定义缓冲区大小、结果大小和操作数字节大小
#define BUF_SIZE 1024
#define RLT_SIZE 4
#define OPSZ 4
// 错误处理函数
void error_handling(char *message);
int main(int argc, char *argv[]) {
int sock; // Socket描述符
char opmsg[BUF_SIZE]; // 用于存储操作数和操作符的消息
int result, opnd_cnt, i; // 结果、操作数个数和循环变量
struct sockaddr_in serv_adr; // 服务端地址结构体
// 创建Socket
sock = socket(PF_INET,SOCK_STREAM, 0);
if (sock == -1)
error_handling(
"socket() error"
);
// 初始化服务端地址
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = 6666;
// 连接服务端
if (connect(sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling(
"connect() error!"
);
else
puts("Connected ...........");
// 获取并存储操作数个数
fputs("Operand count: ",stdout);
scanf("%d", &opnd_cnt);
opmsg[0] = (char) opnd_cnt;
// 循环读取操作数
for (i = 0; i < opnd_cnt; i++) {
printf("Operand %d:", i + 1);
scanf("%d", (int *) &opmsg[i * OPSZ + 1]);
}
// 暂停,等待用户输入操作符
fgetc(stdin);
fputs("Operator: ",stdout);
scanf("%c", &opmsg[opnd_cnt * OPSZ + 1]);
// 发送操作数和操作符到服务器
write(sock, opmsg, opnd_cnt * OPSZ + 2);
// 从服务器读取操作结果
read(sock, &result, RLT_SIZE);
// 打印操作结果
printf("Operation result: %d \n", result);
// 关闭Socket
close(sock);
return 0;
}
// 错误处理函数:输出错误信息并退出程序
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
2.3 运行截图
3. I/O 模型
一个入门科普文章
3.1 阻塞式I/O模型
2. TCP 网络编程实例
就是一个阻塞式的模型, accept
和 read
都是阻塞的, 当 accept
到新连接, 或者 read
到数据程序才往下走
为了提高服务端处理能力, 一个客户端连接一个线程处理
不能一个线程处理多个客户端, 某个客户端会阻塞这个线程处理其他客户端
3.2 非阻塞I/O模型
不停的轮询, 看看有没有accept 到新连接, 没有连接不阻塞等待, 继续去看看已经建立的连接有没有read到客户端的新数据, read到新数据处理, read不到不处理
为了提高服务端处理能力, 可以一个客户端连接一个线程处理, 线程不停的轮询自己要处理的客户端
也可以一个线程处理多个客户端, 相较于上面的阻塞I/O模型, 非阻塞不至于某个客户端阻塞这个线程处理其他客户端
3.3 I/O 复用模型
可以调用 select/poll/epoll , 阻塞在select/poll/epoll, select/poll/epoll 监听多个客户端连接事件或写入的数据, 然后这些事件可再有多个线程分一分处理掉
3.4 信号驱动式I/O
让内核在描述符就绪时发送SINIO信号 通知我们
Linux 网络编程的5种IO模型:信号驱动IO模型 接受SINIO信号, 直接 recvfrom , 然后 sentTo 了, 不用 , 遍历socket
3.5 异步I/O模型
告诉内核启动某个操作, 并且把数据copy到用户缓冲区再通知我们, 和信号驱动的区别是, 信号驱动内核通知我们去I/O, 异步I/O, 通知我们I/O完成了, 送到了
[C++ 网络协议] 异步通知I/O模型
4. I/O复用之 select
4.1 select 函数描述
int select(int maxfdpl, fd_set *readSet, fd_set *writeSet,
fd_set *exceptSet, struct timeval *timeout)
- 最大描述符+1
fd_set *readSet
读事件描述符fd_set *writeSet
写事件描述符fd_set *exceptSet
异常事件描述符struct timeval *timeout
等待超时时间
fd_set 里数组初始化1024, 网上都说 select 最多监听 1024 个连接
- FD_SET: 将套接字添加到文件描述符集合中
- FD_ZERO: 用来清空文件描述符集合
- FD_CLR: 从套接字描述符集合中清除指定的套接字描述符
- FD_ISSET: 检查文件描述符集合中是否包含文件描述符
4.2 服务端代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>
// 定义缓冲区大小
#define BUF_SIZE 100
// 错误处理函数
void error_handling(char *message);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock; // 服务端和客户端套接字
struct sockaddr_in serv_adr, clnt_adr; // 服务端和客户端地址结构体
struct timeval timeout; // 超时时间结构体
fd_set reads, cpy_reads; // 文件描述符集合,用于select函数
socklen_t adr_sz; // 地址大小
int fd_max, str_len, fd_num, i; // 各种变量初始化
char buf[BUF_SIZE]; // 用于读取和写入数据的缓冲区
// 创建套接字
serv_sock = socket(PF_INET,SOCK_STREAM, 0);
// 初始化服务端地址结构体
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = 6666;
// 绑定端口
if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1)
error_handling(
"bind() error"
);
// 监听端口
if (listen(serv_sock, 5) == -1)
error_handling(
"listen() error"
);
// 用来清空文件描述符集合read
FD_ZERO(&reads);
// 将服务器套接字serv_sock添加到文件描述符集合reads中
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
// 无限循环,等待客户端连接和处理数据
while (1) {
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
// 使用select函数等待事件发生
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
// 没有客户端连接
if (fd_num == 0) {
continue;
}
// 遍历文件描述符集合,处理连接和数据传输
for (i = 0; i < fd_max + 1; i++) {
// 检查文件描述符集合cpy_reads中是否包含文件描述符i
if (FD_ISSET(i, &cpy_reads)) {
if (i == serv_sock) {
// 处理新连接
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if (fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
} else {
// 处理数据传输
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) {
// 从reads套接字描述符集合中清除指定的套接字描述符i
FD_CLR(i, &reads);
close(i);
printf("connected client: %d \n", i);
} else {
// 向客户端写入数据
write(i, buf, str_len);
}
}
}
}
}
// 关闭服务端套接字
close(serv_sock);
return 0;
}
// 错误处理函数
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
4.3 客户端代码
#include <stdio.h> // 标准输入输出头文件
#include <stdlib.h> // 通用实用程序头文件
#include <arpa/inet.h> // IPv4 Internet地址转换头文件
#include <sys/socket.h> // 用于创建和操作套接字的头文件
#include <string.h> // 字符串操作头文件
#include <unistd.h> // 提供一些与操作系统交互的函数
#include <sys/types.h> // 定义各种数据类型的头文件
#include <sys/time.h> // 时间处理头文件
#include <sys/select.h> // 用于文件描述符选择的头文件
#define BUF_SIZE 1024 // 定义缓冲区大小
// 错误处理函数
// 参数: buf - 存储错误信息的字符数组
void error_handling(char *buf);
int main(int argc, char *argv[]){ // 程序入口
int sock; // 套接字变量
char message[BUF_SIZE]; // 用于存储消息的缓冲区
int str_len; // 存储字符串长度的变量
struct sockaddr_in serv_adr; // 用于存储服务器地址信息的结构体
// 创建TCP套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock==-1)
{
error_handling("socket create error"); // 若创建失败,调用错误处理函数
}
// 初始化服务器地址结构体
memset(&serv_adr, 0,sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = 6666;
// 尝试连接到服务器
if (connect(sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
{
error_handling("connect error"); // 若连接失败,调用错误处理函数
}else{
puts("connected......"); // 连接成功,输出提示信息
}
// 进入消息收发循环
while (1)
{
fputs("input message (q to quit): ", stdout); // 提示用户输入消息
fgets(message, BUF_SIZE, stdin); // 从标准输入读取消息
if (!strcmp(message,"q\n")||!strcmp(message,"Q\n") )
{
break; // 如果用户输入的是 'q' 或 'Q',则退出循环
}
write(sock, message, strlen(message)); // 将消息写入套接字,发送给服务器
str_len=read(sock, message, BUF_SIZE-1); // 从套接字读取消息,存储到message中
message[str_len] = 0; // 在消息末尾添加null字符,以结束字符串
printf("Message from server: %s", message); // 输出服务器返回的消息
}
close(sock); // 关闭套接字
return 0; // 程序正常结束,返回0
}
// 错误处理函数
// 参数: buf - 存储错误信息的字符数组
// 说明: 该函数将错误信息输出到标准错误流,并退出程序
void error_handling(char* buf){
fputs(buf, stderr); // 将错误信息输出到标准错误流
fputc('\n', stderr); // 输出换行符
exit(1); // 退出程序,返回码为1,表示异常结束
}
4.4 运行截图
5. I/O复用之 poll
5.1 poll 函数描述
poll 提供的功能和 selcet 差不多, poll 可以自己声明最大长度, 还不用自己去 FD_SET、FD_ISSET 维护状态
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
struct pollfd {
int fd;
short events;
short revents;
};
fdarray
: pollfd数组nfds
: fd 数量timeout
: 超时时间
5.2 服务端代码
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/poll.h>
using namespace std;
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = 6666;
::bind(listenfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
listen(listenfd, 10);
/*
poll只是对select函数传入的参数的一个封装,底层实现还是select,好处就是需要传入的参数少了
*/
//pollfd数组, 一个元素为一个 描述符以及相对应的事件。元素的个数为可处理socket的最大个数,突破了select的1024的限制
pollfd pfds[2048] = {0};
pfds[listenfd].fd = listenfd;
pfds[listenfd].events = POLLIN; //可读
//pfds.revents //revents为poll返回的事件
int maxfd = listenfd;
while (1) {
int nready = poll(pfds, maxfd + 1, -1);
//监听socket有可读事件
if (pfds[listenfd].revents & POLLIN) {
sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
int clientfd = accept(pfds[listenfd].fd, (sockaddr*)&clientAddr, &len);
printf("accept==>> clientfd:%d %s:%d\n", clientfd, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
if (-1 == clientfd) {
continue;
}
//请注意这里的clientfd越界问题,这里就不做处理了
//需要注意的是,如果有客户端关闭连接了,再次有新的客户端连接进来时,accept返回的是没有使用的最小的描述符
//比如现在有4,5,6客户端建立了连接,4和5客户端断开了,再次有新的客户端建立连接后,accept会给分配4描述符,这点内核还是很人性化的。
pfds[clientfd].fd = clientfd;
pfds[clientfd].events = POLLIN;
maxfd = clientfd;
}
for (int i = listenfd + 1; i <= maxfd; i++) {
if (pfds[i].revents & POLLIN) {
char buf[128] = {0};
int count = recv(pfds[i].fd, buf, sizeof(buf), 0);
if (count == 0) {
printf("close\n");
close(pfds[i].fd);
pfds[i].fd = -1;
pfds[i].events = 0;
continue;
}
printf("clientfd:%d, count:%d, buf:%s\n", pfds[i].fd, count, buf);
send(pfds[i].fd, buf, count, 0);
}
}
}
close(listenfd);
return 0;
}
5.3 客户端代码
同 4.3
5.4 运行截图
6. I/O复用之 epoll
epoll 比 select、poll 性能都好, 获得就绪的文件描述符, select、poll O(n) 复杂度, epoll 是 O(1)
select,poll是基于轮询实现的,将fd_set从用户空间复制到内核空间,然后让内核空间以poll机制来进行轮询,一旦有其中一个fd对应的设备活跃了,那么就把整个fd_set返回给客户端(复制到用户空间),再由客户端来轮询每个fd的,找出发生了IO事件的fd
epoll是基于事件驱动实现的,加入一个新的fd,会调用epoll_ctr函数为该fd注册一个回调函数,然后将该fd结点注册到内核中的epoll红黑树中,当IO事件发生时,就会调用回调函数,将该fd结点放到就绪链表中,epoll_wait函数实际上就是从这个就绪链表中获取这些fd。
epoll很详细的文章
6.1 常用函数
poll操作过程需要三个接口,分别如下:
int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
- int epoll_create(int size); 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。 当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 等待epfd上的io事件,最多返回maxevents个事件。 参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
6.2 服务端代码
我用的MAC电脑, 不支持 epoll, 下面的代码没运行过:
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>
#include <sys/epoll.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);
/**
* 主程序:创建一个使用epoll模型的服务器,监听客户端连接,并处理客户端请求。
*
* @param argc 命令行参数个数
* @param argv 命令行参数数组
* @return 总是返回0,表示程序正常结束
*/
int main(int argc, char *argv[]) {
// 定义服务器套接字和客户端套接字
int serv_sock, clnt_sock;
// 定义服务器地址结构体和客户端地址结构体
struct sockaddr_in serv_adr, clnt_adr;
// 定义地址大小变量
socklen_t adr_sz;
// 定义字符串长度和循环索引
int str_len, i;
// 定义缓冲区,用于读取和写入数据
char buf[BUF_SIZE];
// 定义epoll事件结构体数组和epoll文件描述符
struct epoll_event *ep_events;
struct epoll_event event;
// 定义epoll等待事件个数变量
int epfd, event_cnt;
// 检查命令行参数是否正确
if (argc != 2) {
printf("usage error\n");
exit(1);
}
// 创建服务器套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
// 初始化服务器地址结构体
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = 6666;
// 绑定服务器套接字到指定地址和端口
if (bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) {
error_handling("bind error");
}
// 监听服务器套接字
if (listen(serv_sock, 5) == -1) {
error_handling("listen error");
}
// 创建epoll实例
epfd = epoll_create(EPOLL_SIZE);
// 分配存储epoll事件的内存
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
// 将服务器套接字添加到epoll监控列表中
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
// 主循环,等待和处理客户端连接和请求
while (1) {
// 等待epoll事件
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1) {
puts("epoll wait error\n");
break;
}
// 遍历所有触发的epoll事件
for (i = 0; i < event_cnt; i++) {
// 处理服务器套接字上的事件,即新的客户端连接
if (ep_events[i].data.fd == serv_sock) {
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *) &clnt_adr, &adr_sz);
// 将新连接的客户端套接字添加到epoll监控列表中
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connnected client: %d\n", clnt_sock);
} else {
// 处理客户端套接字上的事件,即客户端发送的数据
str_len = read(ep_events[i].data.fd, buf,BUF_SIZE);
if (str_len == 0) {
// 如果客户端关闭了连接,则从epoll监控列表中移除,并关闭套接字
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d \n", ep_events[i].data.fd);
} else {
// 如果客户端发送了数据,则将数据回传给客户端
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
// 关闭服务器套接字和epoll文件描述符
close(serv_sock);
close(epfd);
return 0;
}
/**
* 错误处理函数:输出错误信息并退出程序。
*
* @param buf 包含错误信息的字符串
*/
void error_handling(char *buf) {
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
参考文献
- UNIX 网络编程 卷1: 套接字联网API
- TCP/IP网络编程 尹圣雨 著 金国哲 译
- Linux IO模式及 select、poll、epoll详解
- 浅谈select,poll和epoll的区别