点击上方【蓝字】关注博主
“ 本文详细介绍了如何使用Linux内核的epoll机制优化TCP服务器,包括epoll_create、epoll_ctl、epoll_event等API的使用,以及如何通过epoll处理并发连接,避免了select的性能限制。”
你被同事排挤了吗?
手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。
为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。
使用的API函数
2.1、epoll_create()函数
#include <sys/epoll.h>
int epoll_create(int size);
2.2、epoll_ctl()函数
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epoll_ctl对应系统调用sys_epoll_ctl
参数 | 含义 |
---|---|
epfd | 通过 epoll_create 创建的文件描述符 |
op | 对红黑树的操作,比如节点的增加、修改、删除,分别对应EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL |
fd | 需要添加监听的文件描述符 |
event | 事件信息 |
2.3、struct epoll_event结构体
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64
};
struct epoll_event{
uint32_t events;
epoll_data_t data;
}
成员变量 | 含义 |
---|---|
EPOLLIN | 监听fd的读事件 |
EPOLLOUT | 监听fd的写事件 |
EPOLLRI | 监听紧急数据可读事件(带外数据到来) |
EPOLLRDHUP | 监听套接字关闭或半关闭事件 |
EPOLLET | 将EPOLL设为边缘触发(Edge Triggered)模式 |
2.4、epoll_wait()函数
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
返回值 | 含义 |
---|---|
大于0 | 事件个数 |
等于0 | 超时时间timeout到了 |
小于0 | 出错,可通过errno查看出错原因 |
参数 | 含义 |
---|---|
epfd | 通过 epoll_create 创建的文件描述符 |
events | 存放就绪的事件集合,是输出参数 |
maxevents | 最大可存放事件数量,events数组大小 |
timeout | 阻塞等待的时间长短,单位是毫秒,-1表示一直阻塞等待 |
实现步骤
epoll的优点:
不需要轮询所有的文件描述符。
每次取就绪集合,都在固定位置。
事件的就绪和IO触发可以异步解耦。
(1)创建socket。
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
int epfd=epoll_create(1);
if(epfd==-1)
{
perror("epoll_create error");
return SOCKET_EPOLL_CREATE_FAILED;
}
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=listenfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev)==-1)
{
perror("epoll_ctl error");
return SOCKET_EPOLL_CTL_FAILED;
}
int nready=epoll_wait(epfd,evs,EVENTS_LENGTH,-1);
int curfd=evs[i].data.fd;
if(curfd==listenfd)
{
// accept
struct sockaddr_in client;
socklen_t clientlen=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&clientlen);
if(clientfd==-1)
{
perror("accept error");
continue;
}
//printf("client %s:%d connected\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
printf("client %s:%d connected\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
ev.events=EPOLLIN;
ev.data.fd=clientfd;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
if(evs[i].events&EPOLLIN)
{
//read
int ret=recv(curfd,rbuff,BUFFER_LENGTH,0);
if(ret>0)
{
printf("recv from %d: %s\n",curfd,rbuff);
rbuff[ret]='\0';
memcpy(wbuff,rbuff,BUFFER_LENGTH);
ev.events=EPOLLOUT;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
else if(ret==0)// 连接关闭
{
printf("client %d disconnected\n", evs[i].data.fd);
// 将连接从epoll实例中删除
if(epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, NULL)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
close(evs[i].data.fd);
}
else if (ret == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
continue; // 数据已读完
}
perror("read error");
break;
}
else{
printf("read error,unknow type %d\n",ret);
}
}
else if(evs[i].events&EPOLLOUT)
{
//write
send(curfd,wbuff,BUFFER_LENGTH,0);
ev.events=EPOLLIN;
ev.data.fd=curfd;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,curfd,&ev)==-1)
{
perror("epoll_ctl error");
exit(SOCKET_EPOLL_CTL_FAILED);
}
}
完整代码
为了方便读者更完整地理解代码的实现,这里仅展示了全部核心代码片段,完整的代码文件已经整理完毕,并已上传至我的GitHub仓库。您可以联系作者获取完整的代码。
如果您在获取代码或者运行代码的过程中遇到问题,欢迎在评论区留言或私信我,我会尽力解答您的疑问。
此外,为了帮助您更好地理解代码的结构和功能,我将在接下来的文章中对代码进行详细的解读,并分享一些实际应用的案例。
期待与您共同学习和进步!
TCP客户端
5.1、自己实现一个TCP客户端
自己实现一个TCP客户端连接TCP服务器的代码:
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_CONN_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char** argv)
{
if(argc<3)
{
printf("Please enter the server IP and port.");
return 0;
}
printf("connect to %s, port=%s\n",argv[1],argv[2]);
int connfd=socket(AF_INET,SOCK_STREAM,0);
if(connfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=inet_addr(argv[1]);
serv.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(serv);
int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
if(rwfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
close(rwfd);
return SOCKET_CONN_FAILED;
}
int ret=1;
while(ret>0)
{
char buf[BUFFER_LENGTH]={0};
printf("Please enter the string to send:\n");
scanf("%s",buf);
send(connfd,buf,strlen(buf),0);
memset(buf,0,BUFFER_LENGTH);
printf("recv:\n");
ret=recv(connfd,buf,BUFFER_LENGTH,0);
printf("%s\n",buf);
}
close(rwfd);
return 0;
}
gcc -o client client.c
5.2、Windows下可以使用NetAssist的网络助手工具
下载地址:http://old.tpyboard.com/downloads/NetAssist.exe
总结
至此,我们最终确定使用IO多路复用器epoll处理高并发。但是,上面的epoll实现的TCP服务器存在一些问题:
所有的连接都是使用相同的读写缓存(rbuff和wbuff),这会导致数据覆盖。
没有分包能力。
下一章节会解决这些问题,构建一个reactor网络模型。
公众号: Lion 莱恩呀
微信号: 关注获取
扫码关注 了解更多内容
点个 在看 你最好看