1、epoll诞生的原因
问大家一个问题,如果要设计一款有着千万级别并发的系统,你的客户端和服务端的网络通信底层该怎么设计?我在上一篇文章( )中有说到用select可以实现 IO 多路复用,但是select的设计有瓶颈所在,超过十万的并发效率就非常慢。那么又该怎么办呢?
2、epoll是什么
什么是epoll呢?epoll和select一样,也是为IO多路复用而生的。而epoll最大的优点也是select的不足之处,我们知道,select的最大连接数被限制在了1024个,而且select是通过轮询所有的连接的方式寻找需要的那个连接,所以对select来说,连接数越多,耗费的资源就越大,这是一个无法调和的矛盾。
而epoll就是无需通过轮询就是可以找到那个发生IO事件的连接,它通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,epoll在内核初始化的时候向内核注册了一个文件系统,用于存储上述被监控的socket,所以无需轮询所有的socket连接,有点类似用空间换时间的意思。至于epoll底层的实现原理暂时不在本文的讨论范围,以后我会弄个章节出来特别讨论下。
3、具体实现
首先,还是先不扯其他的,我先扔出代码,然后结合代码讲解epoll,带着代码的疑问去思考,这样子感觉学起来更加的有效率。如果大家不习惯的话,可以先跳过以下的代码,先看代码下方的讲解部分。
3.1、服务端代码
# include
#include
#include
#include
#include
#include
#include
#include
#define EPOLL_SIZE 1023 //epoll监听客户端的最大数目
#define MAX_EVENTS 64
#define BUF_SIZE 512
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
//创建套接字
int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (m_sockfd < 0)
{
ERR_EXIT("create socket fail");
}
//初始化socket元素
struct sockaddr_in server_addr;
int server_len = sizeof (server_addr);
memset (&server_addr, 0, server_len);
server_addr.sin_family = AF_INET;
//server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); //用这个写法也可以
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(39002);
//绑定文件描述符和服务器的ip和端口号
int m_bindfd = bind(m_sockfd, (struct sockaddr *)&server_addr, server_len);
if (m_bindfd < 0)
{
ERR_EXIT("bind ip and port fail");
}
//进入监听状态,等待用户发起请求
int m_listenfd = listen(m_sockfd, 20);
if (m_listenfd < 0)
{
ERR_EXIT("listen client fail");
}
//定义客户端的套接字,这里返回一个新的套接字,后面通信时,就用这个m_connfd进行通信
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
//int m_connfd;
printf("client accept success\n");
//创建一个监听描述符epoll,并将监听套接字加入监听列表
int epollfd = epoll_create(EPOLL_SIZE);
if (epollfd < 0)
{
ERR_EXIT("epoll create fail");
}
struct epoll_event eve;
eve.events = EPOLLIN;
eve.data.fd = m_sockfd;
//控制epoll文件描述符上的动作
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, m_sockfd, &eve) < 0)
{
ERR_EXIT("epoll control fail");
}
struct epoll_event evelist[MAX_EVENTS];
//接收客户端数据,并相应
char buffer [BUF_SIZE];
while (1)
{
int ret = epoll_wait(epollfd, evelist, MAX_EVENTS, -1);
if (ret < 0)
{
ERR_EXIT("epoll fail");
}
else if (ret == 0)
{
printf("epoll timeout\n");
continue;
}
for (int i = 0; i < ret; i++)
{
//客户端请求连接
if (evelist[i].data.fd == m_sockfd)
{
int m_connfd = accept(m_sockfd, (struct sockaddr *)&client_addr, &client_len);
if (m_connfd < 0)
{
ERR_EXIT("server accept fail");
}
//把客户端新建立的连接添加到epoll的监听中
struct epoll_event eve;
eve.events = EPOLLIN | EPOLLRDHUP; //监听连接套接字的可读和退出
eve.data.fd = m_connfd;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, m_connfd, &eve) < 0) //将新连接的套接字加入监听
{
ERR_EXIT("epoll control fail, accept client");
}
printf("we got a new connection, client_socket=%d, ip=%s, port=%d\n", m_connfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
//客户端发来数据
else if (evelist[i].events & EPOLLIN)
{
memset(buffer, 0, sizeof(buffer)); //重置缓冲区
int recv _len = recv(evelist[i].data.fd, buffer, sizeof(buffer) - 1, 0);
if (recv_len < 0)
{
ERR_EXIT("recv data fail");
}
//客户端断开连接
else if (recv_len == 0)
{
//打印断开的客户端数据
printf("a client close , ip=%s, port=%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
epoll_ctl(epollfd, EPOLL_CTL_DEL, evelist[i].data.fd, &eve);
close(evelist[i].data.fd);
}
else
{
printf("server recv:%s\n", buffer);
strcat(buffer, "+ACK");
send(evelist[i].data.fd, buffer, sizeof(buffer) - 1, 0);
}
}
//客户端退出
else if (evelist[i].events & EPOLLRDHUP)
{
printf("a client is quit, ip=%s, port=%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
epoll_ctl(epollfd, EPOLL_CTL_DEL, evelist[i].data.fd, &eve);
close(evelist[i].data.fd);
}
}
}
//关闭套接字
close(m_sockfd);
printf("server socket closed!!!\n");
return 0;
}
3.2、客户端代码
#include
#include
#include
#include
#include
#include
#define BUF_SIZE 512
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
//创建套接字
int m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (m_sockfd < 0)
{
ERR_EXIT("create socket fail");
}
//服务器的ip为本地,端口号
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("81.68.140.74");
server_addr.sin_port = htons(39002);
//向服务器发送连接请求
int m_connectfd = connect(m_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (m_connectfd < 0)
{
ERR_EXIT("connect server fail");
}
//发送并接收数据
char buffer[BUF_SIZE];
while (1)
{
memset(buffer, 0, sizeof(buffer)); //重置缓冲区
printf("client send:");
scanf("%s", buffer);
send(m_sockfd, buffer, sizeof(buffer) - 1, MSG_NOSIGNAL);
recv(m_sockfd, buffer, sizeof(buffer) - 1, 0);
printf("client recv:%s\n", buffer);
}
//断开连接
close(m_sockfd);
printf("client socket closed!!!\n");
return 0;
}
4、epoll结构剖析
int epoll_create(int size);
首先利用创建一个epoll句柄,参数size用来告诉内核监听的数目,size为epoll所支持的最大句柄数。它其实是在内核申请空间,用来存放你想监听的套接字描述符上是否有读、写或者异常的各类事件。不管是什么样的写法,只要是用到了epoll,就必须要有这个函数。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
最后一个函数就像是阻塞函数,等待着客户端发来请求或者数据,只要有连接发来,这个函数就会响应。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd : epoll特有的文件描述符
events :从内核中的就绪队列中拷贝出就绪的文件描述符,不可以是空指针,内核只负责将数据拷贝到这里,不会为我们开辟空间。
maxevent : 高速内核events有多大,一般不能超过epoll_create传递的size,
timeout : 函数超时时间,0表示非阻塞式等待,-1表示阻塞式等待,函数返回0表示已经超时。
说实话,epoll的代码写法上会比select容易理解得多,基本上就是围绕着那三个函数展开的,一个创建,一个注册,一个等待。至于底层的具体部分,都已经封装好了,属于操作系统层面。对于想自己搭建底层通信协议的童鞋来说懂得如何用epoll已经足够了,如果要深入了解epoll的话,需要更加阅读深入的教程,但已不属于本文所讨论的范围。后期如果有时间,我一定一点点和大家剖析底层的实现原理。
更多精彩内容,请关注同名公众:一点笔记alittle
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~