epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
本文介绍如何从0开始写一个高性能的epoll网络库,设计目标是
1、简单、易用
2、无内存碎片,启动时申请好所需所有内存
3、高性能,至少不比其它网络库性能差
本文介绍的代码和demo可以从github上下载:
https://github.com/eleventest/easynet
使用cmake构建,仅支持linux
在功能模块的组织上分3部分
1、用于监听的套接字管理
2、用于连接的套接字管理
3、epoll的异步io管理
前两部分比较简单,只需几行代码就可以完成
为了保持设计上的简单,我们单独处理套接字监听,一共需要3个函数:打开监听套接字、关闭监听套接字和接收新连接,功能比较简单,注释也都有。其中用到的辅助函数在net_sock.h都可以找到,只是对socket函数的一层简单封装。
/*打开一个监听套接字*/
int32_t net_open_listener(const char* ip, int32_t port)
{
/*创建tcp套接字*/
int32_t listener = sock_tcp();
UT_CHECK_TRUE(listener != -1);
/*设置可复用地址*/
UT_VERIFY_TRUE(sock_reuseaddr(listener) == 0);
/*绑定地址*/
UT_CHECK_TRUE(sock_bind(listener, ip, port) == 0);
/*设置为非阻塞*/
UT_CHECK_TRUE(sock_set_nonblock(listener) == 0);
/*开始监听*/
UT_CHECK_TRUE(sock_listen(listener, 128) == 0);
return listener;
FAIL:
/*如果创建过程发生错误,则跳到这里关闭套接字*/
net_close_listener(listener);
return -1;
}
/*关闭监听套接字*/
void net_close_listener(int32_t listener)
{
sock_close(listener);
}
/*接收新的连接*/
int32_t net_accept_newsock(int32_t listener)
{
return accept(listener, 0, 0);
}
/*创建到远程目标机的连接*/
int32_t net_connect(const char* ip, int32_t port)
{
/*创建tcp套接字*/
int32_t target = sock_tcp();
UT_CHECK_TRUE(target != -1);
/*连接到目标机器*/
UT_CHECK_TRUE(sock_connect(target, ip, port) == 0);
return target;
FAIL:
/*如果创建过程发生错误,则跳到这里关闭套接字*/
sock_close(target);
return -1;
}
套接字建立后就需要把他投递到epoll中,开始异步处理数据,在此之前,先介绍几个相关的数据结构:
/*网络管理器设置*/
struct net_config_t
{
/*最大连接数限制*/
int32_t num_connection_limit;
/*用户态接收缓冲区大小*/
int32_t recv_buf_size;
/*epoll的epoll_event的大小*/
int32_t num_wait_event;
};
struct net_t;
/*每个连接对应一个channel_t*/
struct channel_t
{
/*加速索引,也可用户网络连接的唯一标识*/
int32_t index;
/*所属网络模块*/
struct net_t* net;
/*套接字句柄*/
int32_t sock; // 网络套接字
/*数据接收回调函数*/
void(*connection_data)(struct net_t* net, int32_t channel, const char* data
, int32_t datalen, void* netdata, void* channeldata);
/*绑定的用户数据*/
void* channel_data;
};
typedef struct channel_t* pchannel_t;
/*网络管理器*/
struct net_t
{
/*用户配置数据*/
struct net_config_t cfg;
/*epoll 句柄*/
int32_t epoll_handle;
/*epoll事件缓存*/
struct epoll_event* wait_event;
/*接收缓存*/
char* rcv_buf;
/*连接数据*/
struct channel_t* channels;
/*空闲连接所在栈*/
pchannel_t* queue_free;
int32_t free_top;
/*建立连接回调函数*/
void(*new_func)(struct net_t* net, int32_t channel, void* netdata, void* channeldata);
/*丢失连接回调函数*/
void(*lost_func)(struct net_t* net, int32_t channel, void* netdata, void* channeldata);
/*用户绑定数据*/
void* net_data;
};
net_config_t是对网络库进行设置的数据结构
channel_t对应每一个建立的连接
net_t 对应整个网络管理器
为了高性能,我们需要设置3个回调函数,在系统有对应事件发生时会调用回调函数
/*连接建立回调函数,net是网络句柄,channel是新建立的连接通道的id,netdata是绑定到net上的数据,channeldata是绑定到channel上的数据*/
typedef void(*new_connection) (struct net_t* net, int32_t channel, void* netdata, void* channeldata);
/*连接断开回调函数*/
typedef void(*lost_connection) (struct net_t* net, int32_t channel, void* netdata, void* channeldata);
/*连接上有数据到达的回调函数,data是数据缓冲区起始地址,datalen是有效数据长度*/
typedef void(*recv_data) (struct net_t* net, int32_t channel, const char* data, int32_t datalen, void* netdata, void* channeldata);
1、网络配置接口
/*网络库配置数据*/
struct net_config_t* net_config_create();
void net_config_delete(struct net_config_t* cfg);
/*设置最大连接数*/
int32_t net_config_set_connectionlimit(struct net_config_t* cfg, int32_t limit);
2、网络管理器创建删除接口
/*创建一个网络句柄,netdata是绑定到网络句柄上的用户数据*/
struct net_t* net_create(struct net_config_t* cfg, new_connection newfunc,
lost_connection lostfunc, void* netdata);
/*删除网络句柄,绑定到网络句柄上的所有连接都会被关闭*/
void net_delete(struct net_t* net);
/*接收数据处理,如果接收到数据,回调函数会被调用*/
int32_t net_pool_once(struct net_t* net, int32_t weight, int32_t waittime);
3、网络套接字管理接口
/*在一个通道上发送数据*/
int32_t net_send(struct net_t* net, int32_t channel, const char* data, int32_t datalen);
/*关闭一个连接通道*/
void net_channel_close(struct net_t* net, int32_t channel);
/*投递一个已经建立好的连接到网络中,channeldata是绑定到通道上的数据*/
int32_t net_deliver_socket(struct net_t* net, int32_t sock,
recv_data rcvfunc, void* channeldata);
上面通过listen或者connect建立好连接后,就可以投递到网络引擎中,由网络引擎管理其后的io操作,这是通过net_deliver_socket实现的。
/*投递一个已经建立好的连接到网络中,channeldata是绑定到通道上的数据*/
int32_t net_deliver_socket(struct net_t* net,
int32_t sock, recv_data rcvfunc, void* channeldata)
{
struct channel_t* channel = 0;
/*入参检查*/
UT_VERIFY_TRUE(net != 0);
UT_VERIFY_TRUE(rcvfunc != 0);
/*设置保活定时器*/
UT_CHECK_TRUE(0 == sock_set_keepalive(sock));
/*设置为非阻塞*/
UT_CHECK_TRUE(0 == sock_set_nonblock(sock));
/*申请空闲通道资源*/
channel = channel_malloc(net);
UT_CHECK_TRUE(channel != 0);
/*初始化通道数据*/
channel->sock = sock;
channel->connection_data = rcvfunc;
channel->channel_data = channeldata;
UT_CHECK_TRUE(0 == do_deliver_socket(net, channel));
/*回调连接建立函数*/
net->new_func(net, channel->index, net->net_data, channel->channel_data);
/*返回通道的id*/
return channel->index;
FAIL:
/*如果发生意外,到这里关闭套接字*/
sock_close(sock);
if (channel != 0)
{
/*释放通道*/
channel_free(net, channel);
}
return -1;
}
int32_t do_deliver_socket(struct net_t* net, struct channel_t* channel)
{
int32_t err = 0;
struct epoll_event eevent;
eevent.data.ptr = channel;
eevent.events = EPOLLIN;
/*投递socket到epoll中,使用水平触发方式,只有接收数据事件,发送不需要事件通知*/
return epoll_ctl(net->epoll_handle, EPOLL_CTL_ADD, channel->sock, &eevent);
}
当有数据发送时,我们可以用net_send发送,实际上我们的引擎中并未管理发送事件,始终认为数据可以发送。当数据发送缓冲区已满无法继续发送怎么办?除了直接关闭连接还有更好的处理办法么?
/*在一个通道上发送数据*/
int32_t net_send(struct net_t* net, int32_t channel, const char* data, int32_t datalen)
{
/*入参检查*/
UT_CHECK_TRUE(net != 0);
UT_CHECK_TRUE(channel >= 0 && channel < net->cfg.num_connection_limit);
UT_VERIFY_TRUE(data != 0);
UT_VERIFY_TRUE(datalen > 0);
/*调用底层socket的send发送数据*/
return sock_send(net->channels[channel].sock, data, datalen);
FAIL: return 0;
}
使用net_close关闭一个连接,注意,这里只是关闭tcp连接,并未关闭文件句柄。
/*关闭一个连接通道*/
void net_channel_close(struct net_t* net, int32_t channel)
{
/*入参检查*/
UT_CHECK_TRUE(net != 0);
UT_CHECK_TRUE(channel >= 0 && channel < net->cfg.num_connection_limit);
/*关闭socket,注意不是close是shutdown*/
sock_shutdown(net->channels[channel].sock);
FAIL: return;
}
当套接字投递到epoll中后,我们就可以通过调用net_pool_once来获取接收到的数据,如果没有数据,这个函数什么也不会做,如果有数据就会调用回调函数处理数据。
int32_t net_pool_once(struct net_t* net, int32_t weight, int32_t waittime)
{
int32_t i = 0;
int32_t wait_num = 0;
/*入参范围调整*/
if (weight < 1)
{
weight = 1;
}
if (weight > net->cfg.num_wait_event)
{
weight = net->cfg.num_wait_event;
}
/*查看是否有事件发生*/
wait_num = epoll_wait(net->epoll_handle, net->wait_event
, weight, waittime);
for ( ; i < wait_num; i++)
{
struct channel_t* channel = (struct channel_t*)net->wait_event[i].data.ptr;
if ((net->wait_event[i].events & ~EPOLLIN) == 0)
{
/*处理接收事件,所有套接字共享接收缓冲区*/
int32_t nrcvl = sock_recv(channel->sock, net->rcv_buf,
net->cfg.recv_buf_size);
if (nrcvl > 0)
{
/*如果收到数据则调用数据接收回调函数*/
channel->connection_data(net, channel->index, net->rcv_buf,
nrcvl, net->net_data, channel->channel_data);
}
else if (nrcvl == 0 || (nrcvl != EINTR && nrcvl != EAGAIN))
{
/*接收错误,或对方关闭则关闭此通道*/
do_lost_connection(channel);
}
}
else
{
/*处理错误,关闭通道*/
do_lost_connection(channel);
}
}
return wait_num;
}
至此,整个引擎的重要工作都完成了,在net_test.c里面有一个简单的乒乓测试:建立1000个连接,每个连接上有一个1024字节的乒乓球在不停地转发。
测试数据如下(虚拟机中测试,仅供参考)
平均一秒50k个数据包,每个连接上一秒钟可以收发50个乒乓球