第9章 I/O复用

        应用程序通过I/O复用函数向内核注册一组I/O事件,内核通过I/O复用函数把就绪的事件通知给应用程序。常用的I/O复用函数是select、poll、epoll。I/O复用函数本身是阻塞的,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

        当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每-个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

9.1 select系统调用

        select实现多路复用的方式就是将已连接的socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式就是遍历文件描述符集合,当检查到有事件发生后,将此socket标记为可读或可写,接着再把整个文件描述符集合拷贝到用户空间,然后在用户空间遍历这些文件描述符,找到可读或可写的socket,然后在对其进行处理。

        因此,使用select需要进行2次遍历文件描述符集合,一次是在内核空间,一次是在用户空间,而且还会发生2次拷贝文件描述符集合,即先从用户空间拷贝到内核空间,由内核修改后,再拷贝到用户空间。

        select使用固定长度的BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在Linux系统中,由内核的FD_SETSIZE限制,默认最大值为1024,只能监听0~1023的文件描述符。

9.1.1 fd_set结构体

        select可以监视3中文件描述符集合,即:接收、传输、异常,每个文件描述符集合使用fd_set结构体存储。fd_set结构体使用一个bit位来描述文件描述符的状态,如果该位是1,则该文件描述符是监视对象。因此,可以认为fd_set是一个很大的字节数组(BitsMap)。fd_set能容纳的文件描述符的数量由FD_SETSIZE指定。

        首先介绍下面4种对fd_set操作的宏:(fd是指待操作的文件描述符)

// 这里的fd是文件描述符
FD_ZERO(fd_set *set);              // 将set清零
FD_SET(int fd, fd_set *set);       // 将fd加入set
FD_CLR(int fd, fd_set *set);       // 将fd从set中清除
FD_ISSET(int fd, fd_set *set);     // 检测fd是否在set中,不在则返回0

        接下来,将介绍如何fd_set是如何存储fd的。

        例:假设有文件描述符1、2、3,fd_set的长度只有1字节,即8bit,那么1个字节长的fd_set最大可以存放8个文件描述符(fd)。

//创建读文件描述符集合
fd_set set;
//先对set清零
FD_ZERO(&set);
int fd = 5;
//将fd加入set
FD_SET(fd, &set); 此时,set变成 0001 0000  (第5位是1)
在将fd = 1, fd = 2加入set, 执行FD_SET后,set变成 0001 0011
//执行select阻塞等待
select(5 + 1, &set, 0, 0, 0);
若任意fd发生可读事件,如fd=5发生可读事件,则set变成 0001 0000,也就是没有发生事件的fd=1,fd=2会被清零。因此,可以认为值仍为1的位置上的文件描述符发生了变化。此时的set会被拷贝到用户空间。

如果描述符fd=100怎么办呢?其实不难,fd_set并不是只有1个字节,可以有n个字节,可以拿n字节凑多个bit(也就是bitmap),如果fd是100,仍然可以执行 FD(100, &set),执行后,set的值为 1xxxxxxxxxxx............,其中“1”就是第99个bit(从0开始)。

9.1.2 select函数

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
参数解释:
maxfd:被监听的文件描述符总数。通常设为最大文件描述符编号加1。
readset:指向可读文件描述符集合,可为NULL
writeset:指向可写文件描述符集合,可为NULL
exceptset:指向异常文件描述符集合,可为NULL
timeout:用来设置select的超时时间。timeout成员都为0时,select立即返回;timeout=NULL,select一直阻塞,直到某个文件描述符就绪。
返回值:成功时返回就绪(可读、可写、异常)文件描述符的总数;如果在超时时间内没有任何文件描述符就绪,将返回0;失败时返回-1并设置errno;如果在select等待期间,程序即受到信号,则select立即返回-1并设置errno为EINTR。

struct timeval
{
    long tv_sec;    //秒数
    long tv_usec;   //微秒数
};

        下面是用select实现客户端向服务端发送什么,客户端就会收到什么的功能:

server:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#define BUF_SIZE 100

void error_handing(char* buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}


int main()
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    struct timeval timeout;
    fd_set readset, cpy_readset;

    socklen_t addr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE] = {0};
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))  == -1 )
    {
        error_handing("bind() error");
    }
    if ( listen(serv_sock, 5) == -1 )
    {
        error_handing("listen() error");
    }

    FD_ZERO(&readset);
    FD_SET(serv_sock, &readset);
    fd_max = serv_sock;

    while(1)
    {
        cpy_readset = readset;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        //出现错误,跳出循环
        /*cpy_readset先被拷贝到内核,里面保存有发生事件的socket,然后再被拷贝出内核,供用户使用.
          这也是为什么要执行cpy_readset = readset的原因
        */
        if ((fd_num = select(fd_max+1, &cpy_readset, 0, 0, &timeout)) == -1)
            break;

        //没有socket可读或可写
        if (fd_num == 0)
            continue;
        
        for(i = 0; i < fd_max + 1; i++)
        {
            if(FD_ISSET(i, &cpy_readset))   //i可读或可写
            {
                if(i == serv_sock)  //表明监听socket检测到有连接请求
                {
                    addr_sz = sizeof(clnt_addr);
                    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_sz);
                    //将clnt_sock加入readset监听集合
                    FD_SET(clnt_sock, &readset);
                    //看看用不用更新fa_max
                    if(fd_max < clnt_sock)
                        fd_max = clnt_sock;
                    printf("connect client: %d \n", clnt_sock);
                }
                else{                   //表明连接socket需要读或写
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0)   //客服端断开连接,需要将该客户端连接剔除
                    {
                        FD_CLR(i, &readset);
                        close(i);
                        printf("close client: %d \n", i);
                    }
                    else                //向客户端发送数据
                    {
                        write(i, buf, str_len);
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

 client:

#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>

using namespace std;

#define BUFFERSIZE 1024

int main()
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));

    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(12345);
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int ret = connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr));
    if (ret == -1)
    {
        cout <<"connect error"<<endl;
    }

    while(1)
    {
        char sendbuff[BUFFERSIZE] = {0};
        cout << "input send data:" <<endl;
        cin >> sendbuff;
        send(sock, sendbuff, sizeof(sendbuff), 0);
        
        char recvbuff[BUFFERSIZE] = {0};
        int ret = recv(sock, recvbuff, sizeof(recvbuff), 0);
        if (ret)
        {
            cout<<"recv serv:"<<recvbuff<<endl;
        }
        else if (ret == 0)
        {
            //连接断开
            cout << "link failure!" <<endl;
            close(sock);
            break;
        }
    }
    
    return 0;
}

9.2 poll系统调用

        poll系统调用和select类似,也是遍历文件描述符集合,检查其中是否有事件发生。

        但是,poll不再使用BitsMap存储所关注的文件描述符,而是使用动态数组,以链表的形式来保存,所以poll不再受文件描述符个数限制,当然还会收到系统文件描述符限制。

        poll和select的相同点就是,都使用线性结构存储进程关注的socket集合,因此都需要遍历文件描述符集合来找到可读或可写的socket,时间复杂度都为O(n),而且也需要再用户空间和内核空间之间拷贝文件描述符集合,这种方式随着并发数增加,性能的损耗会呈指数级增长。

#include <poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数解释:
fds:保存所有被监听的文件描述符及其对应的可读、可写和异常等事件
nfds:fds的大小。typedef unsigned long int nfds_t;
timeout:poll的超时值。单位是毫秒(1s = 1000ms)。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
返回值:
poll系统调用的返回值与select相同:
poll成功时返回就绪(可读、可写和异常)文件描述符的总数。
如果在超时时间内没有任何文件描述符就绪。poll将返回0。
失败时返回-1并设置errno。如果在poll等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。

struct pollfd
{
    int fd;//文件描述符
    short events;//fd上需要监听的事件,可以按位或
    short revents;//实际发生的事件,由内核填充
};

 pollfd结构体的事件类型如下表所示:

        poll工作过程总结下来就是:

  • poll 将描述符和事件fds传给内核
  • 内核实现轮询检测fds,时间效率相当O(n)
  • 找到就绪描述符,内核将fds返回给用户空间
  • 用户遍历fdsO(n),处理事件

        下面使用poll实现服务端收到什么就向客户端发送什么的功能:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUF_SIZE 100
#define MAXFD 10

void error_handing(char* buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

//将文件描述符fd及其事件信息加入到监听集合fds中
void add_fds(struct pollfd fds[], int fd, short events)
{
    int i = 0;
    for(; i < MAXFD; i++)
    {
        if (fds[i].fd == -1)
        {
            fds[i].fd = fd;
            fds[i].events = events;
            fds[i].revents = 0;
            break;
        }
    }
}

//删除fds数组中文件描述符fd和事件信息
void del_fds(struct pollfd fds[], int fd)
{
    int i = 0;
    for(; i < MAXFD; i++)
    {
        if (fds[i].fd == fd)
        {
            fds[i].fd = -1;
            fds[i].events = 0;
            fds[i].revents = 0;
            break;
        }
    }
}

//初始化fds数组
void init_fds(struct pollfd fds[])
{
    int i = 0;
    for (; i < MAXFD; i++)
    {
        fds[i].fd = -1;
        fds[i].events = 0;
        fds[i].revents = 0;
    }
}

int main()
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    fd_set readset, cpy_readset;

    socklen_t addr_sz;
    char buf[BUF_SIZE] = {0};
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(serv_sock != -1);
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))  == -1 )
    {
        error_handing("bind() error");
    }
    if ( listen(serv_sock, 5) == -1 )
    {
        error_handing("listen() error");
    }
    struct pollfd fds[MAXFD];   //定义pollfd类型的结构体数组fds
    init_fds(fds);  
    add_fds(fds, serv_sock, POLLIN);    //将serv_sock加入fds

    while(1)
    {
        int n = poll(fds, MAXFD, 5000);
        if (n == -1)    //失败
        {
            perror("poll error");
        }
        else if (n == 0)    //超时
        {
            printf("time out\n");
        }
        else        //有文件描述符就绪
        {
            int i;
            for (i = 0; i < MAXFD; i++)  //循环遍历监听集合
            {
                // printf("i = %d, revents = %d\n", i, fds[i].revents);
                if (fds[i].fd == -1)
                {
                    continue;
                }
                if (fds[i].revents & POLLIN)    //写事件发生
                {
                    /*
						此时有两种情况,若fds[i].fd == serv_sock
						说明监听队列中有连接待处理,使用accept建立一个连接;
						否则,说明没有新连接产生,是有客户端发来了数据,直接使
					    用recv接收客端数据,并打印,就ok
					*/
                    if (fds[i].fd == serv_sock)
                    {
                        //accept
                        struct sockaddr_in clnt_addr;
                        socklen_t len = sizeof(clnt_addr);
 
                        //接收一个套接字已建立的连接,得到连接套接字clnt_sock
                        int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &len);
                        if (clnt_sock < 0)
                        {
                            continue;
                        }
                        printf("accept clnt_sock = %d\n", clnt_sock);
 
                        add_fds(fds, clnt_sock, POLLIN);//将新的连接套接字加入fds数组                   
                    }
                    else
                    {
                        int str_len = read(fds[i].fd, buf, BUF_SIZE);//接受客户端发来的数据
                        if (str_len == 0)//说明客户端已经关闭
                        {
                            close(fds[i].fd);//先关闭文件描述符
                            del_fds(fds,fds[i].fd);//将此文件描述符在fds数组里删除
                            printf("one client over\n");
                        }
                        else
                        {
                            printf("recv(%d) = %s\n", fds[i].fd, buf);
                            write(fds[i].fd, buf, str_len);
                        }
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

        client的实现可复用9.1.2 的client代码。 

9.3 epoll系统调用

        epoll通过两个方面,很好的解决了select/poll的问题。

        (1)epoll在内核空间中使用红黑树来监控进程所有待检测的文件描述符(监听文件描述符和连接文件描述符),把需要监控的socket通过epoll_ctl()函数加入内核中的红黑树中(红黑树是个高效的数据结构,增删查一般时间复杂度是O(logn)),通过对这颗红黑树操作,就无需像select/poll那样每次操作时都要拷贝整个socket集合,这样就减少了内核空间和用户空间大量的数据拷贝和内存分配。

        (2)epoll在内核空间中使用一个链表来保存就绪事件,当某个socket可读或可写时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()函数时,只会向用户空间返回发生事件的文件描述符的个数,不需要像select/poll那样遍历整个socket集合,大大提高了检测效率。

        以上这2点可通过下图看出:

9.3.1 实现 epoll需要的3个函数:

(1)创建epoll文件描述符空间

#include <sys/epoll.h>

int epoll_create(int size);
参数解释:
size:现在并不起作用,只是提示内核事件表需要多大
返回值:
成功时返回epoll文件描述符,失败时返回-1

(2)向空间添加、修改、删除文件描述符

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数解释:
epfd:epoll_create()的返回值
op:指定对fd的操作类型
fd:被监听的文件描述符.实际作用是作为红黑树的key
event:指定fd的事件类型
返回值:
成功时返回0,失败时返回-1

struct epoll_event
{
    __uint32_t events;
    epoll_data_t data;
};

typedef union epoll_data
{
    void* ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
}epoll_data_t;

op参数的取值如下:
EPOLL_CTL_ADD:添加文件描述符到监听集合
EPOLL_CTL_DEL:从监听集合中删除文件描述符
EPOLL_CTL_MOD:修改文件描述符对应的事件类型

epoll_event结构体的成员events的取值如下(可通过或运算同时传递多个值):
EPOLLIN:需要读取数据情况
EPOLLOUT:输出缓冲区为空,可以立即发送数据的情况
EPOLLPRI:收到OOB数据的情况
EPOLLRDHUP:断开连接或半关闭情况,常使用在边缘触发方式中
EPOLLERR:发生错误情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLONESHOT:水平触发,即发生一次事件后,相应的文件描述符不再收到事件通知

(3)等待文件描述符发生变化

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数解释:
epfd:epoll_create()的返回值
events:保存发生事件的文件描述符集合的数组
maxevents:数组的大小
timeout:超时时间(毫秒),传递-1时,会一直等待直到事件发生
返回值:
成功时返回发生事件的文件描述符的个数,失败时返回-1

注意:随便往epoll_fd中加入一个int整数是无效的,虽然fd也是int类型,但是fd是文件描述符,是系统分配的,随便加入的整数是在系统中找不到的。

下面的添加操作是无效的:
int a = 10;
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = a;
epoll_ctl(epfd, EPOLL_CTL_ADD, a, &event);

下面使用epoll实现回声服务端:(客服端的实现见poll小结的客户端)

#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <stdio.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50

void error_handling(char* buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main()
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t addr_sz;
    int str_len, i;
    char buf[BUF_SIZE] = {0};

    struct epoll_event ep_events[EPOLL_SIZE];
    struct epoll_event event;
    int epfd, event_cnt;

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 
    if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))  == -1 )
    {
        error_handling("bind() error");
    }
    if ( listen(serv_sock, 5) == -1 )
    {
        error_handling("listen() error");
    }

    epfd = epoll_create(EPOLL_SIZE);    //相当于创建一颗红黑树的根节点

    //将serv_sock加入监听集合
    event.events = EPOLLIN;
    event.data.fd = serv_sock; 
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //将fd节点加入红黑树

    //轮询等待事件的发生
    while(1)
    {
        event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait error");
            break;
        }

        //遍历所有就绪的socket
        for (i = 0; i < event_cnt; i++)
        {
            //监听socket有事件发生,处理连接请求
            if (ep_events[i].data.fd == serv_sock)
            {
                addr_sz = sizeof(clnt_addr);
                clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_sz);
                event.events = EPOLLIN;
                event.data.fd = clnt_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                printf("connect client:%d\n", clnt_sock);
            }
            else
            {
                //连接socket有读事件发生
                str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                if (str_len == 0)   //客户端连接关闭
                {
                    //将连接socket从监听集合中删除且关闭
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                    close(ep_events[i].data.fd);
                    printf("close client:%d\n", ep_events[i].data.fd);
                }
                else
                {
                    //向客服端发送数据
                    write(ep_events[i].data.fd, buf, str_len);
                }
            }
        }
    }
    //最后要关闭监听socket和epoll文件描述符
    close(serv_sock);
    close(epfd);
    return 0;
}

9.3.2 边缘触发(ET)和水平触发(LT)

        epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET水平触发(level-triggered,LT

  • 边缘触发:当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
  • 水平触发:当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

        如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误-1,错误类型errno为 EAGAIN 或 EWOULDBLOCK

        默认情况下,文件描述符的读写操作都是阻塞的。但文件描述符可以被设置为非阻塞的状态,此时,文件描述符的 I/O 操作结果会立即返回。如果文件描述符没有就绪,那么会返回错误;否则会根据 I/O 操作的执行情况返回相应的结果(部分完成或全部完成)。下面两行代码可将文件描述符改为非阻塞状态:

int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

        下面是ET和LT模式的服务端实现: (注意:大数据块通常用LT,小数据块通常用ET,listen()函数监听的fd通常用LT。也有人总结成:ET+循环读=LT+一次性读)

#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10

//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

//将文件描述符fd添加到epoll的监听集合中
void addfd(int epollfd, int fd, bool enableET)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if (enableET)
    {
        event.events |= EPOLLET;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    //将要添加的文件描述符设为非阻塞
    setnonblocking(fd);
}

//1. LT模式的工作流程
void LT(epoll_event* events, int event_cnt, int epollfd, int listenfd)
{
    char buf[BUFFER_SIZE];
    for (int i = 0; i < event_cnt; i++)
    {
        int sockfd = events[i].data.fd;
        if (sockfd == listenfd)     //如果就绪的是监听文件描述符,接受连接
        {
            struct sockaddr_in clnt_addr;
            socklen_t clnt_addrlen = sizeof(clnt_addr);
            int connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
            addfd(epollfd, connfd, false);  //对连接socket要使用LT模式
        }
        else if (events[i].events & EPOLLIN)    //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
        {
            /*只要socket读缓存中还有未读出的数据,这段代码就被触发*/
            printf("event trigger once\n");
            memset(buf, '\0', BUFFER_SIZE);
            int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
            if (ret  <= 0)
            {
                close(sockfd);
                continue;
            }
            printf("get %d bytes of content: %s\n", ret, buf);
            send(sockfd, buf, ret, 0);
        }
        else
        {
            printf("something else happened \n");
        }
    }
}

//2。ET模式的工作流程
void ET(epoll_event* events, int event_cnt, int epollfd, int listenfd)
{
    char buf[BUFFER_SIZE];
    for (int i = 0; i < event_cnt; i++)
    {
        int sockfd = events[i].data.fd;
        if (sockfd == listenfd)     //如果就绪的是监听文件描述符,接受连接
        {
            struct sockaddr_in clnt_addr;
            socklen_t clnt_addrlen = sizeof(clnt_addr);
            int connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
            addfd(epollfd, connfd, false);  //对连接socket要使用LT模式
        }
        else if (events[i].events & EPOLLIN)    //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
        {
            /*这段代码不会被重复触发,所以我们循环读取数据,确保把socket读缓存中的所有数据读出*/
            printf("event trigger once\n");
            while(1)
            {
                memset(buf, '\0', BUFFER_SIZE);
                int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
                if (ret  < 0)
                {
                    /*对于非阻塞I/O,下面的条件成立表示数据已经全部读取完毕。此后,
                    epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作
                    */
                    if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
                    {
                       printf("read finished!\n");
                       break;
                    }
                    close(sockfd);
                    continue;
                }
                else if (ret == 0)
                {
                    //客户端关闭连接
                    close(sockfd);
                }
                else
                {
                    printf("get %d bytes of content: %s\n", ret, buf);
                    send(sockfd, buf, ret, 0);
                }
            }
        }
        else
        {
            printf("something else happened \n");
        }
}
}

int main()
{
    struct sockaddr_in serv_addr;
    socklen_t addr_sz;
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(serv_sock >= 0);

    int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    assert(ret != -1);

    ret = listen(serv_sock, 5);
    assert(ret != -1);
    
    //创建epoll监听集合
    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd != -1);
    //将监听文件描述符加入监听集合
    addfd(epollfd, serv_sock, true);
    
    while(1)
    {
        int event_cnt = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait error");
            break;
        }
        //LT(events, event_cnt, epollfd, serv_sock);  //使用LT模式
        ET(events, event_cnt, epollfd, serv_sock);  //使用ET模式
    }
    close(serv_sock);
    close(epollfd);
    return 0;
}

        客户端代码参见epoll的实现。 

        一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

        select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发。

        使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用。多路复用 API 返回的事件并不一定可读写的(例如异常),如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O。

9.3.3 EPOLLONESHOT事件

        假如一个socket使用ET模式,在并发程序中,一个线程读取完该socket上的数据后开始处理,而在处理过程中该socket上又有新数据可读,此时另外一个线程被唤醒来读取这些新数据,于是就出现两个线程同时操作一个socket的现象,显然我们不希望看到这样。我们希望该socket在任意时刻都只能被一个线程处理,这可通过EPOLLONESHOT事件实现

        如果一个文件描述符注册了EPOLLONESHOT事件,操作系统最多触发该文件描述符上注册的一个可读、可写、异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会处理该socket的。

        反之,注册了EPOLLONESHOT的socket一旦被某个线程处理完毕,该线程就应该立即重置该socket上的EPOLLONESHOT,使得该socket下一次就绪时,其他线程也能处理这个socket。

        下面的代码展示EPOLLONESHOT的使用:

#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <pthread.h>

#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024

struct  fds
{
    int epollfd;
    int sockfd;
};

//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

//fd添加到epoll的监听集合中
void addfd(int epollfd, int fd, bool enableOneshot)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET;
    if (enableOneshot)
    {
        event.events |= EPOLLONESHOT;
    }
    epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
    //将要添加的文件描述符设为非阻塞
    setnonblocking(fd);
}

/*
重置fd上的事件,使得fd被处理完后,下次就绪时,别的线程也能处理该fd
*/
void resetOnshot(int epollfd, int fd)
{
    epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}

//工作线程
void* worker(void* arg)
{
    int sockfd = ((fds*)arg)->sockfd;
    int epollfd = ((fds*)arg)->epollfd;
    printf("start new thread to recieve data on fd : %d\n", sockfd);
    char buf[BUFFER_SIZE] = {0};
    //循环读取sockfd上的数据,知道遇到EAGAIN错误
    while(1)
    {
        int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
        if (ret == 0)   //对端关闭连接
        {
            close(sockfd);
            printf("foreiner closed the connection \n");
            break;
        }
        else if (ret < 0)   //数据接收完毕
        {
            if (errno == EAGAIN)
            {
                resetOnshot(epollfd, sockfd);
                printf("read later\n");
                break;
            }
        }
        else
        {
            printf("get content: %s\n", buf);
            //休眠5s,模拟数据处理过程
            sleep(5);
        }
    }
    printf("finish thread receiving data on fd: %d\n", sockfd);
}

int main()
{
    struct sockaddr_in serv_addr;
    socklen_t addr_sz;
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(serv_sock >= 0);

    int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    assert(ret != -1);

    ret = listen(serv_sock, 5);
    assert(ret != -1);
    
    //创建epoll监听集合
    struct epoll_event events[MAX_EVENT_NUMBER];
    int epollfd = epoll_create(5);
    assert(epollfd != -1);
    /*将监听文件描述符加入监听集合
    注意:监听socket上不能注册EPOLLONESHOT事件,否则应用程序只能处理一个客户端连接,
        因为后续的客户连接请求将不再触发监听socket上的EPOLLIN事件。
    */
    addfd(epollfd, serv_sock, false);
    
    while(1)
    {
        int event_cnt = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
        if (event_cnt == -1)
        {
            puts("epoll_wait error");
            break;
        }
        for (int i = 0; i < event_cnt; i++)
        {
            int sockfd = events[i].data.fd;
            if (sockfd == serv_sock)     //如果就绪的是监听文件描述符,接受连接
            {
                struct sockaddr_in clnt_addr;
                socklen_t clnt_addrlen = sizeof(clnt_addr);
                int connfd = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
                //对每个连接socket都注册EPOLLONESHOT
                addfd(epollfd, connfd, true);  
            }
            else if (events[i].events & EPOLLIN)    //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
            {
                //新启动一个线程为sockfd服务,用完后要回收
                pthread_t thread;
                fds fds_for_new_worker;
                fds_for_new_worker.epollfd = epollfd;
                fds_for_new_worker.sockfd = sockfd;
                pthread_create(&thread, NULL, worker, (void*)&fds_for_new_worker);
            }
            else
            {
                printf("something else happened \n");
            }
        }
    }
    close(serv_sock);
    close(epollfd);
    return 0;
}

        尽管一个socket在不同事件可能被不同的线程处理,但是同一时刻肯定只有一个线程在为它服务。这就保证了连接的完整性,避免了很多可能的竟态条件。

9.3.4 引出Reactor

        从前面可以看出,epoll的代码模式一直都是下面这样:

event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
for (i = 0; i < event_cnt; i++)
{
	if (ep_events[i].data.fd == listenfd){
		int clntfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
		....
	}else{
		if (events[i].events & EPOLLIN){
			...recv...
		}
		if (events[i].events & EPOLLOUT){
			...send...
		}
	}
}

但是,这样会降低程序效率,而且也不整洁。由于listenfd对应的事件也属于写事件(EPOLLIN),因此可以设计一种结构体,保存fd和fd对应的处理函数。例如将处理listenfd的函数(accept)跟listenfd放在一个结构体中,处理clntfd的函数(recv/send)跟clntfd放在一个结构体中。

struct sockitem {
	int sockfd;
	int (*callback)(int fd, int events, void *arg);
}

int recv_cb(int fd, int events, void *arg) {

}

int accept_cb(int fd, int events, void *arg) {

	int clientfd = accept();

	struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
	si->sockfd = clientfd;
	si->callback = recv_cb;
	//
	epoll_ctl()
}

int main(int argc, char *argv[]) {
	socket();
	bind(...);
	listen(listenfd, 5);
	
	int epfd = epoll_create(1);
	
	struct epoll_event ev, events[512] = {0};
	ev.events = EPOLLIN;
	ev.data.fd = listenfd; //int idx = 2000;
	

	struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
	si->sockfd = listenfd;
	si->callback = accept_cb;
	ev.data.ptr = si;
	
	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
	while(1){
		int nready = epoll_wait(epfd, events, 512, -1);
		for (i = 0;i < nready;i ++) {
			if (events[i].events & EPOLLIN) {
				struct sockitem *si = (struct sockitem*)events[i].data.ptr;
				si->callback(events[i].data.fd, events[i].events, si);
			}
			if (events[i].events & EPOLLOUT) {
				struct sockitem *si = (struct sockitem*)events[i].data.ptr;
				si->callback(events[i].data.fd, events[i].events, si);
			}
		}	
	}
}

 注意要点:

1、使用event.data.ptr指针保存结构体指针,当有事件的时候就可以获取到fd和对应的cbFun函数。

2、固定的释放流程要写成原子操作:

epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
close(fd);
free(si);

3、read之后要将fd的事件改成EPOLLOUT,fd的回调函数设置为write;write之后要将fd的事件改成EPOLLIN,fd的回调函数设置为read。

        假如有大量的客户端请求连接服务器,那么就需要多次对epoll_wait的返回结果进行循环遍历。返回结果中既有clientfd又有listenfd,且clientfd占绝大多数。这显然会降低服务器处理连接请求的效率。因此,需要将clientfd和listenfd的处理分开,创建一个/多个线程专门处理listenfd,创建多个线程去处理clientfd。此时,单线程reactor和多线程reactor都被引出,即:
单线程reactor:一个线程处理listenfd和clientfd。(参考libevent/redis)
多线程reactor:A、多线程处理listenfd和clientfd。
               B、一个线程处理listenfd,多线程处理clientfd。(memcached)
多进程reactor:前提是多进程处理的数据是需要进程共享的。(ngix)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>

#include <errno.h>
#include <sys/epoll.h>


/*
保存单个fd对应的信息。
当接受/发送的数据非常大,一次发不完,可以借助recvbuffer/sendbuffer来保存.
*/
struct sockitem { //
	int sockfd;
	int (*callback)(int fd, int events, void *arg);

	char recvbuffer[1024]; //接受缓冲区
	char sendbuffer[1024]; //发送缓冲区
};

//保存所有fd公用的变量
struct reactor {
	int epfd;
	struct epoll_event events[512];
};


struct reactor *eventloop = NULL;


int recv_cb(int fd, int events, void *arg);


int send_cb(int fd, int events, void *arg) {

	struct sockitem *si = (struct sockitem*)arg;

	send(fd, "hello\n", 6, 0); //

	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;
	//ev.data.fd = clientfd;
	si->sockfd = fd;
	si->callback = recv_cb;
	ev.data.ptr = si;

	epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);

}

int recv_cb(int fd, int events, void *arg) {

	//int clientfd = events[i].data.fd;
	struct sockitem *si = (struct sockitem*)arg;
	struct epoll_event ev;

	char buffer[1024] = {0};
	int ret = recv(fd, buffer, 1024, 0);
	if (ret < 0) {

		if (errno == EAGAIN || errno == EWOULDBLOCK) { //
			return -1;
		} else {
			
		}

		ev.events = EPOLLIN;
		//ev.data.fd = fd;
		epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
		close(fd);
		free(si);
		
	} else if (ret == 0) { //

		// 
		printf("disconnect %d\n", fd);

		ev.events = EPOLLIN;
		//ev.data.fd = fd;
		epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
		close(fd);
		free(si);	
	} else {

		printf("Recv: %s, %d Bytes\n", buffer, ret);
		struct epoll_event ev;
		ev.events = EPOLLOUT | EPOLLET;
		//ev.data.fd = clientfd;
		si->sockfd = fd;
		si->callback = send_cb;
		ev.data.ptr = si;

		epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);

	}

}


int accept_cb(int fd, int events, void *arg) {

	struct sockaddr_in client_addr;
	memset(&client_addr, 0, sizeof(struct sockaddr_in));
	socklen_t client_len = sizeof(client_addr);
	
	int clientfd = accept(fd, (struct sockaddr*)&client_addr, &client_len);
	if (clientfd <= 0) return -1;

	char str[INET_ADDRSTRLEN] = {0};
	printf("recv from %s at port %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
		ntohs(client_addr.sin_port));

	struct epoll_event ev;
	ev.events = EPOLLIN | EPOLLET;
	//ev.data.fd = clientfd;

	struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
	si->sockfd = clientfd;
	si->callback = recv_cb;
	ev.data.ptr = si;
	
	epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, clientfd, &ev);
	
	return clientfd;
}

void *worker_thread(void *arg) {

	while (1) {

		int nready = epoll_wait(eventloop->epfd, eventloop->events, 512, -1);
		if (nready < -1) {
			break;
		}
// listen 
// 
		int i = 0;
		for (i = 0;i < nready;i ++) {

			if (eventloop->events[i].events & EPOLLIN) {
				//printf("sockitem\n");
				struct sockitem *si = (struct sockitem*)eventloop->events[i].data.ptr;
				si->callback(si->sockfd, eventloop->events[i].events, si);
			}

			if (eventloop->events[i].events & EPOLLOUT) {

				struct sockitem *si = (struct sockitem*)eventloop->events[i].data.ptr;
				si->callback(si->sockfd, eventloop->events[i].events, si);
			}
		}
	}

}

int main(int argc, char *argv[]) {

	if (argc < 2) {
		return -1;
	}

	int port = atoi(argv[1]);

	

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0) {
		return -1;
	}

	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));

	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = INADDR_ANY;

	if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
		return -2;
	}

	if (listen(sockfd, 5) < 0) {
		return -3;
	}

	
	eventloop = (struct reactor*)malloc(sizeof(struct reactor));
	// epoll opera

	eventloop->epfd = epoll_create(1);


	struct epoll_event ev;
	ev.events = EPOLLIN;
	//ev.data.fd = sockfd; //int idx = 2000;
	

	struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
	si->sockfd = sockfd;
	si->callback = accept_cb;
	ev.data.ptr = si;
	
	epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev);

	pthread_t id;
	pthread_create(&id, NULL, worker_thread, NULL);
}

后续内容请见第9-1章。

9.4 三组I/O复用的区别总结

9.5 I/O复用的高级应用

9.5.1 非阻塞connect

        connect()默认为阻塞接口,超时时间在几十秒至几分钟。将一个socket设为非阻塞之后调用connect(),connect会立即返回EINPROGRESS错误,表示操作正在进行中,但仍未完成,三次握手继续进行。之后,调用select、poll等函数检查连接是否成功。

        处理非阻塞connect()的步骤:

第一步:创建socket,返回套接口描述符;
第二步:调用fcntl把套接口描述符设置成非阻塞;
第三步:调用connect开始建立连接;
第四步:判断连接是否成功建立;
A:如果connect返回0,表示连接建立成功(服务器可客户端在同一台机器上时就有可能发生这种情况);
B:调用select等待连接建立成功:
        如果select返回0,则表示建立连接超时;我们返回超时错误给用户,同时关闭连接,防止三次握手继续进行下去;
        如果select返回值大于0,则需要检查socket是否可读、可写。如果socket可读或可写,则调用getsockopt获取socket上待处理的错误(SO_ERROR);如果连接建立成功,errno值是0;如果连接建立失败,errno值不是0,如ECONNREFUSEDETIMEDOUT。

        下面是使用非阻塞connect的代码:

#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>

#define BUFFER_SIZE 1024

//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

/*
超时连接函数,参数分别为服务器的port、超时时间。
成功返回连接socket,失败返回-1
*/
int nonBlockConnect(int port, int time)
{
    struct sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(port);
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    setnonblocking(sockfd);
    int ret = connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr));
    //连接成功
    if (ret == 0)
    {
        printf("connect with server immediately!\n");
        return sockfd;
    }
    //连接失败
    else if (errno != EINPROGRESS)
    {
        printf("nonblock connect not supportQ!\n");
        return -1;
    }
    //errno==EINPROGRESS,连接还在进行中
    fd_set readfds;
    fd_set writefds;
    struct timeval timeout;
    timeout.tv_sec = time;
    timeout.tv_usec = 0;

    ret = select(sockfd+1, NULL, &writefds, NULL, &timeout);
    if (ret <= 0)
    {
        /*select超时或出错,立即返回-1*/
        printf("connection time out\n");
        close(sockfd);
        return -1;
    }

    //sockfd上没有事件发生
    if (!FD_ISSET(sockfd, &writefds))
    {
        printf("no events on sockfd found\n");
        close(sockfd);  //因为是测试,所以关不关毕无所谓
        return -1;
    }
    //sockfd上有事件发生
    int error = 0;
    socklen_t length = sizeof(errno);
    //调用getsockopt获取错误并清楚sockfd的错误
    if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
    {
        printf("get socket option failed\n");
        close(sockfd);
        return -1;
    }
    //连接错误
    if (error != 0)
    {
        printf("connection fail after select, error: %d\n", error);
        close(sockfd);
        return -1;
    }
    //连接成功
    printf("uconnection success after select, sockfd: %d\n", sockfd);
    return sockfd;
}

int main()
{
    int sockfd = nonBlockConnect(12345, 10);
    if (sockfd < 0)
    {
        return false;
    }
    close(sockfd);
    return 0;
}

9.5.2 聊天室程序

        该聊天室陈程序能让所有用户同时在线群聊,分为客户端和服务端两个部分。

客户端:

        使用poll同时监听用户输入和网络连接,并利用splice函数将用户输入内容直接定向到网络连接上并发送,实现零拷贝。

服务端:

        使用poll同时管理监听socke连接socket,并且使用牺牲空间换取时间的策略来提高服务器性能。服务端功能是接收客户端数据,并把该数据发给每一个登录到该服务器上的客户端。

        下面是服务端代码:

#define _GNU_SOURCE 1
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>

#define BUFFER_SIZE 64
#define USER_LIMIT 5    //最大用户数量
#define FD_LIMIT 65535  //文件描述符数量限制

//客户端数据:客户端socket地址、待发送给客户端的数据的位置、从客户端接收的数据
struct clientData
{
    sockaddr_in address;
    char* writeBuf;
    char reacvBuf[BUFFER_SIZE];
};

//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}

int main()
{
    struct sockaddr_in serv_addr;
    socklen_t addr_sz;
 
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(12345);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(serv_sock >= 0);
 
    int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    assert(ret != -1);
 
    ret = listen(serv_sock, 5);
    assert(ret != -1);
    /*创建users数组,分配65535个clientData对象。每个可能的socket连接都能获得一个这样的对象,
    并且socket的值可以直接用来索引(作为数组的下标)socket连接对应的clientData对象,
    这是将socket和客户端数据关联的简单而高效的方式
    */
    clientData* users = new clientData[FD_LIMIT];
    
    //虽然分配了足够多的clientData对象,但为了提高poll性能,仍然有必要限制用户的数量
    pollfd fds[USER_LIMIT + 1];
    int userCnt = 0;
    //初始化fds数组
    for (int i = 1; i <= USER_LIMIT; i++)
    {
        fds[i].fd = -1;
        fds[i].events = 0;
    }
    //将监听socket加入fds,对应的事件是可读或错误
    fds[0].fd = serv_sock;
    fds[0].events = POLLIN | POLLERR;
    fds[0].revents = 0;

    while(1)
    {
        ret = poll(fds, userCnt + 1, -1);
        if (ret < 0)
        {
            printf("poll failuer\n");
            break;
        }

        for(int i = 0; i < userCnt + 1; i++)
        {
            //如果监听socket有连接请求事件发生,接收连接
            if ((fds[i].fd == serv_sock) && (fds[i].revents & POLLIN))
            {
                struct sockaddr_in clnt_addr;
                socklen_t clnt_addrLen = sizeof(clnt_addr);
                int connfd = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addrLen);
                if (connfd < 0)     //接收连接失败
                {
                    printf("errno is: %d\n", errno);
                    continue;
                }
                //如果请求太多,则关闭新到的连接
                if (userCnt >= USER_LIMIT)
                {
                    const char* info = "too many users\n";
                    printf("%s", info);
                    send(connfd, info, strlen(info), 0);
                    close(connfd);
                    continue;
                }
                /*对于新的连接,将connfd加入fds和users数组。
                users[connfd]对应新连接connfd的客户端数据
                */
                userCnt++;
                users[connfd].address = clnt_addr;
                setnonblocking(connfd);
                fds[userCnt].fd = connfd;
                fds[userCnt].events = POLLIN | POLLRDHUP | POLLERR;
                fds[userCnt].revents = 0;
                printf("comes a new user, now have %d users\n", userCnt);
            }
            //如果连接socket有错误事件发生
            else if (fds[i].revents & POLLERR)
            {
                printf("get an error from %d\n", fds[i].fd);
                char errors[100];
                memset(errors, '\0', 100);
                socklen_t length = sizeof(errors);
                //获取并清除socket错误状态
                if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0)
                {
                    printf("get socket option failed\n");
                }
                continue;
            }
            //客户端关闭连接,则服务端也关闭连接,userCnt减1
            else if (fds[i].revents & POLLRDHUP)
            {
                //随便为users[fds[i].fd]赋一个值
                users[fds[i].fd] = users[fds[userCnt].fd];
                close(fds[i].fd);
                i--;
                userCnt--;
                printf("a client left\n");
            }
            //连接socket上有读事件发生
            else if (fds[i].revents & POLLIN)
            {
                int connfd = fds[i].fd;
                memset(users[connfd].reacvBuf, '\0', BUFFER_SIZE);
                ret = recv(connfd, users[connfd].reacvBuf, BUFFER_SIZE-1, 0);
                printf("get %d bytes of client data %s from %d", ret, users[connfd].reacvBuf, connfd);
                
                if(ret < 0)
                {
                    /*对于非阻塞I/O,errno==EAGAIN表示数据已经全部读取完毕*/

                    //读出错,关闭连接
                    if (errno != EAGAIN)
                    {
                        close(connfd);
                        users[fds[i].fd] = users[fds[userCnt].fd];
                        fds[i] = fds[userCnt];
                        i--;
                        userCnt--;
                    }
                }
                else if (ret == 0)
                {
                    //客户端关闭连接,前面已经做过检测处理
                }
                else    //服务端成功接收数据
                {
                    //如果接收到客户数据,则通知其他socket连接准备写数据
                    for (int j = 1; j <= userCnt; j++)
                    {
                        if (fds[j].fd == connfd)
                        {
                            continue;
                        }
                        fds[j].events |= ~POLLIN;
                        fds[j].events |= POLLOUT;
                        //将收到的数据作为发给每个客户端的数据
                        users[fds[j].fd].writeBuf = users[connfd].reacvBuf;
                    }
                }
            }
            //连接socket上有写事件发生
            else if (fds[i].revents & POLLOUT)
            {
                int connfd = fds[i].fd;
                if (!users[connfd].writeBuf)
                {
                    continue;
                }
                ret = send(connfd, users[connfd].writeBuf, strlen(users[connfd].writeBuf), 0);
                users[connfd].writeBuf = NULL;
                //发送完数据后需要将fds[i]的事件更改为可读事件
                fds[i].events |= ~POLLOUT;
                fds[i].events |= POLLIN;
            }
        }
    }
    delete []users;
    close(serv_sock);
    return 0;
}

        下面是客户端代码:

#define _GNU_SOURCE 1
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>

#define BUFFER_SIZE 64

int main()
{
    struct sockaddr_in servAddr;
    memset(&servAddr, 0, sizeof(servAddr));

    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(12345);
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0);
    if (connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0)
    {
        printf("connection failed\n");
        close(sockfd);
        return 1;
    }
    
    struct pollfd fds[2];
    /*注册文件描述符0(标准输入)和sockfd上的可读事件*/
    fds[0].fd = 0;
    fds[0].events = POLLIN;
    fds[0].revents = 0;

    fds[1].fd = sockfd;
    fds[1].events = POLLIN | POLLRDHUP;
    fds[1].revents = 0;

    char readBuf[BUFFER_SIZE];
    int pipefd[2];
    int ret = pipe(pipefd);
    assert(ret != -1);

    while(1)
    {
        ret = poll(fds, 2, -1);
        //创建poll失败
        if (ret < 0)
        {
            printf("poll failure\n");
            break;
        }
        //sockfd上发生对方关闭连接
        if (fds[1].revents & POLLRDHUP)
        {
            printf("server close the connection\n");
            break;
        }
        //sockfd上发生可读事件,读取服务端发来的数据
        else if (fds[1].revents & POLLIN)
        {
            memset(readBuf, '\0', BUFFER_SIZE);
            recv(fds[1].fd, readBuf, BUFFER_SIZE-1, 0);
            printf("%s\n", readBuf);
        }

        if (fds[0].revents & POLLIN)
        {
            //使用splice将用户输入的数据直接写到sockfd上(零拷贝)
            ret = splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
            ret = splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
        }
    }
    close(sockfd);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值