[置顶] 从0开始写一个高性能epoll网络库

前言

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字节的乒乓球在不停地转发。
测试数据如下(虚拟机中测试,仅供参考)
[置顶] 从0开始写一个高性能epoll网络库_第1张图片
平均一秒50k个数据包,每个连接上一秒钟可以收发50个乒乓球

你可能感兴趣的:(网络,epoll)