C++网络通信:基于IO多路复用实现的TCP并发服务器

IO多路复用的引入

上文提到,当主机没有操作系统时,或者说程序不能使用多进程或多线程完成任务的并发操作时,
我们可以引入IO多路复用的技术,完成多任务并发执行的操作。

一、select函数

功能:阻塞等待文件描述符集合中是否有事件产生,如果有事件产生,则解除阻塞。
#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数1:文件描述符集合中,最大的文件描述符加1
参数2、参数3、参数4:分别表示读集合、写集合、异常处理集合的起始地址
由于对于写操作而言,我们也可以转换读操作,所以,只需要使用一个集合就行
对于不使用的集合而言,直接填NULL即可
参数5:超时时间,如果填NULL表示永久等待,如果想要设置时间,需要定义一个如下结构体类型
的变量,并将地址传递进去
struct timeval {
    long tv_sec; /* 秒数 */
    long tv_usec; /* 微秒 */
};

struct timespec {
    long tv_sec; /* 秒数 */
    long tv_nsec; /* 纳秒 */
};

返回值:

        >0:成功返回解除本次阻塞的文件描述符的个数
        =0:表示设置的超时时间,时间已经到达,但是没有事件事件产生
        =-1:表示失败,置位错误码
注意:当该函数解除阻塞时,文件描述符集合中,就只剩下本次触发事件的文件描述符,其余的文
件描述符就被删除了 

专门针对于文件描述符集合fd_set提供的函数

void FD_CLR(int fd, fd_set *set); //将fd文件描述符从容器set中删除

int FD_ISSET(int fd, fd_set *set); //判断fd文件描述符,是否存在于set容器中

void FD_SET(int fd, fd_set *set); //将fd文件描述符,放入到set容器中

void FD_ZERO(fd_set *set); //清空set容器

select实现TCP并发服务器案例

#include<myhead.h>
#define SER_IP "192.168.137.140"
#define SER_PORT 8888
 
int main(int argc, const char *argv[]){
    //创建套接字描述符
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(sfd==-1){
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd = %d\n",sfd);

    //填充服务端的地址信息结构体
    sockaddr_in sin;
    sin.sin_addr.s_addr=inet_addr(SER_IP);
    sin.sin_family=AF_INET;
    sin.sin_port=htons(SER_PORT);
    socklen_t socklen=sizeof(sin);

    //绑定IP地址和端口号
    if(bind(sfd,(sockaddr*)&sin,socklen)==-1){
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    //建立监听
    if(listen(sfd,128)==-1){
        perror("listen error");
        return -1;
    }
    printf("listen success\n");

    //定义文件描述符集合
    fd_set tempfds,readfds;
    FD_ZERO(&readfds);    //清空容器
    FD_SET(0,&readfds);   //将文件描述符(对应stdin)放入容器
    FD_SET(sfd,&readfds);  //将文件描述符sfd放入容器
    int maxfd=sfd;  //maxfd表示tempfds集合中文件描述符的最大值
    int newfd=-1;   //接收客户端的连接请求后,建立的通信套接字文件描述符
    sockaddr_in cin_arr[1024];  // 数组:[文件描述符:地址信息结构体]

    while(1){
        tempfds=readfds;  // 将reafds备份一份放入tempfds中

        // 调用阻塞函数,完成对文件描述符集合的管理工作
        int res=select(maxfd+1,&tempfds,NULL,NULL,NULL);
        if(res==-1){
            perror("select error");
            return -1;
        }else if(res==0){
            printf("time out\n");
            return -1;
        }

        //如果程序执行到这里,则说明有至少一个文件描述符产生了事件,其余没有触发事件的文件描述符被删除
        //此时我们只需要判断哪个文件描述符还在集合中,就说明该文件描述符触发了事件
        if(FD_ISSET(sfd,&tempfds)){  //如果是sfd触发了事件,说明有客户端请求建立连接
            sockaddr_in cin;
            socklen_t len=sizeof(cin);

            //接收客户端的连接请求
            newfd=accept(sfd,(sockaddr*)&cin,&len);
            if(newfd==-1){
                perror("accept error");
                return -1;
            }
            printf("[%s,%d]:connected\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));

            cin_arr[newfd]=cin;   //将该客户端对应的套接字地址信息结构体放入数组对应的位置上
            FD_SET(newfd,&readfds);  //将新的套接字描述符加入文件描述符集合
            maxfd = newfd>maxfd?newfd:maxfd;
        }
        if(FD_ISSET(0,&tempfds)){  //如果是stdin触发了事件,说明服务端要向客户端发送信息
            char sbuf[128]="";
            fgets(sbuf,sizeof(sbuf),stdin);
            for(int cli=4;cli<=maxfd;cli++){ 
                send(cli,sbuf,strlen(sbuf),0); //向客户端发送信息
            }
        }

        for(int cfd=4;cfd<=maxfd;cfd++){
            if(FD_ISSET(cfd,&tempfds)){  //如果客户端cfd有事件触发
                char buf[128]="";
                bzero(buf,sizeof(buf));

                //接收数据
                int res=recv(cfd,buf,sizeof(buf),0);
                if(res==0){   //连接断开
                    printf("[%s,%d]:leave\n",\
                        inet_ntoa(cin_arr[cfd].sin_addr),ntohs(cin_arr[cfd].sin_port));

                    //关闭与该客户端的连接请求
                    close(cfd);

                    //将该套接字从文件描述符中去除
                    FD_CLR(cfd, &readfds);

                    //重新更新maxfd的值
                    for(int k=maxfd;k>=0;k--){
                        //倒序遍历文件描述符,如果该文件描述符还在集合中,则说明该文件描述符的最大文件描述符
                        if(FD_ISSET(k,&readfds)){  
                            maxfd=k;
                            break;
                        }
                    }
                    continue;
                }

                printf("[%s:%d]:%s\n", inet_ntoa(cin_arr[cfd].sin_addr),\
                        ntohs(cin_arr[cfd].sin_port), buf);
                strcat(buf,"*_*");

                //数据发送到客户端
                if(send(cfd,buf,strlen(buf),0)==-1){
                    perror("send error");
                    return -1;
                }
            }
        }
    }

    //关闭套接字文件描述符
    close(sfd);
    return 0;
}

二、poll函数

功能:阻塞等待文件描述符集合中是否有事件产生,如果有,则解除阻塞,返回本次触发事件的文件描述符个数
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数1:文件描述符集合容器的起始地址,是一个结构体数组,结构体类型如下
struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 要等待的事件:由用户填写 */
    short revents; /* 实际发生的事件 :调用函数结束后,内核会自动设置*/
};

关于事件对应的位:

POLLIN:读事件
POLLOUT:写事件
参数2:集合中文件描述符的个数
参数3:超时时间,负数表示永久等待,0表示非阻塞
返回值:
        >0:表示触发本次解除阻塞事件的文件描述符的个数
        =0:表示超时
        =-1:出错,置位错误码
poll实现TCP客户端案例
#include<myhead.h>
#define SER_IP "192.168.137.142"
#define SER_PORT 8888
#define CLI_IP "192.168.137.142"
#define CLI_PORT 9999
 
int main(int argc, const char *argv[]){
    //创建套接字描述符
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(sfd==-1){
        perror("socket error");
        return -1;
    }

    //创建客户端地址信息结构体
    sockaddr_in cin;
    cin.sin_addr.s_addr=inet_addr(CLI_IP);
    cin.sin_family=AF_INET;
    cin.sin_port=htons(CLI_PORT);

    //绑定地址信息结构体
    if(bind(sfd,(sockaddr*)&cin,sizeof(cin))==-1){
        perror("bind error");
        return -1;
    }

    //创建服务器端地址信息结构体
    sockaddr_in sin;
    sin.sin_addr.s_addr=inet_addr(SER_IP);
    sin.sin_family=AF_INET;
    sin.sin_port=htons(SER_PORT);

    //请求连接
    if(connect(sfd,(sockaddr*)&sin,sizeof(sin))==-1){
        perror("connect error");
        return -1;
    }

    pollfd pfds[2]; //使用poll完成终端输入和套接字接收数据的并发执行
    pfds[0].fd=0;     //表示检测0号
    pfds[0].events=POLLIN; //表示检测的是读事件
    pfds[1].fd=sfd;
    pfds[1].events=POLLIN;

    char buf[128]="";
    while(1){
        int res=poll(pfds,2,-1);
        // 功能:阻塞等待文件描述符集合中是否有事件产生
        // 参数1:文件描述符集合起始地址
        // 参数2:文件描述符个数
        // 参数3:表示永久等待
        if(res==-1){
            perror("poll error");
            return -1;
        }

        //程序执行至此,说明pfds容器中有事件产生
        if(pfds[0].revents==POLLIN){ //0号文件描述符触发了事件
            //清空buf数组
            bzero(buf,sizeof(buf));

            //从终端读取数据
            fgets(buf,sizeof(buf),stdin);
            buf[strlen(buf)-1]=0;

            //发送数据
            if(send(sfd,buf,strlen(buf),0)==-1){
                perror("send error");
                return -1;
            }
        }

        if(pfds[1].revents==POLLIN){ //sfd文件描述符触发事件
            buf[strlen(buf)-1]=0;
            if(strcmp(buf,"quit")==0) break;

            //发送数据
            recv(sfd,buf,sizeof(buf),0);  
            printf("[%s,%d]:%s\n",inet_ntoa(sin.sin_addr),ntohs(sin.sin_port),buf);
        }
    }

    //关闭文件描述符
    close(sfd);
    return 0;
}

三、epoll函数

epoll全称为eventpoll,是内核实现IO多路复用的一种实现方式。在其中一个或多个事件的到满足
时,可以解除阻塞。
epollselectpoll的升级版,在嵌入式等领域用的更多,相比于selectpoll而言,epoll改进了工作方式(底层是红黑树),效率更高。
头文件:#include <sys/epoll.h>

(1)epoll_create函数

功能:创建一个epoll实例,并返回该实例的句柄,是一个文件描述符
int epoll_create(int size);
参数sizeepoll实例中能够容纳的最大节点个数,自从linux 2.6.8版本后,size可以忽略,但是
必须要是一个大于0的数字
返回值:成功返回控制epoll实例的文件描述符,失败返回-1并置位错误码

(2)epoll_ctl函数

功能:完成对epoll实例的各种控制
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数1:通过epoll_create创建的epoll实例文件描述符
参数2op表示要进行的操作3> epoll实现TCP并发服务器的模型
        EPOLL_CTL_ADD:向epoll树上添加新的要检测的文件描述符
        EPOLL_CTL_MOD:改变epoll树上的文件描述符检测的事件
        EPOLL_CTL_DEL:删除epoll树上的要检测的文件描述符,此时参数3可以省略填NULL
参数3:要检测的文件描述符
参数4:要检测的事件,是一个结构体变量地址,属于输入变量
typedef union epoll_data {
    void *ptr; //提供的解释性数据
    int fd; //文件描述符(常用)
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events; /* 要检测的事件 */
    epoll_data_t data; /* 用户有效数据,是一个共用体 */
};

要检测的事件:

EPOLLIN:读事件
EPOLLOUT:写事件
EPOLLERR:异常事件
EPOLLET:表示设置epoll的模式为边沿触发模式(默认方式是水平模式)
如何设置边沿触发:在将文件描述符放入到epoll树中时,需要加一个属性
        struct epoll_event ev;
        ev.event = EPOLLIN|EPOLLEV;

(3)epoll_wait函数

功能:阻塞检测epoll实例中是否有文件描述符准备就绪,如果准备就绪了,就解除阻塞
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数1epoll实例对于的文件描述符
参数2:文件描述符集合,当有文件描述符产生事件时,将所有产生事件的文件描述符,放入到该集合中
参数3:参数2的大小
参数4:超时时间,以毫秒为单位的超时时间,如果填-1表示永久阻塞
返回值:>0:表示解除本次操作的触发的文件描述符个数
              =0:表示超时,但是没有文件描述符产生事件
              =-1:失败,置位错误码
epoll实现TCP并发服务器案例
#include<myhead.h>
#include<map>
#define SER_IP "192.168.137.142"
#define SER_PORT 8888
 
int main(int argc, const char *argv[]){ 
    map<int,sockaddr_in> mp;
     
    //创建套接字描述符
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(sfd==-1){
        perror("socket error");
        return -1;
    }
    printf("socket success, sfd = %d\n",sfd);

    sockaddr_in sin;
    sin.sin_addr.s_addr=inet_addr(SER_IP);
    sin.sin_family=AF_INET;
    sin.sin_port=htons(SER_PORT);
    if(bind(sfd,(sockaddr*)&sin,sizeof(sin))==-1){
        perror("bind error");
        return -1;
    }
    printf("bind success\n");

    if(listen(sfd,128)==-1){
        perror("listen error");
        return -1;
    }
    printf("listen success\n");

    sockaddr_in cin;
    socklen_t socklen=sizeof(cin);

    //创建epoll实例,用于检测文件描述符
    int epfd=epoll_create(1);
    if(epfd==-1){
        perror("epoll create error");
        return -1;
    }
    
    //将sfd放入到检测集合中
    epoll_event ev;
    ev.data.fd=sfd;  //要检测的文件描述符
    ev.events=EPOLLIN;  //要检测读事件
    /*
        功能:将sfd放入到检测集合中
        参数1:epoll实例的文件描述符
        参数2:epoll操作,表示要添加文件描述符
        参数3:要检测文件描述符的值
        参数4:要检测的事件
    */
    epoll_ctl(epfd,EPOLL_CTL_ADD,sfd,&ev);

    //该数组用于接收返回事件集合
    epoll_event evs[1024];
    //数组大小
    int size=sizeof(evs)/sizeof(evs[0]);

    while(1){
        //阻塞检测文件描述符集合中是否有事件产生
        int num=epoll_wait(epfd,evs,size,-1);
        //参数1:epoll实例的文件描述符
        //参数2:返回触发事件的集合
        //参数3:集合的大小
        //参数4:是否阻塞,-1表示阻塞
        printf("num = %d\n",num);

        for(int i=0;i<num;i++){
            int fd=evs[i].data.fd;
            if(fd==sfd){
                int newfd=accept(sfd,(sockaddr*)&cin,&socklen);
                if(newfd==-1){
                    perror("accept error");
                    return -1;
                }
                mp[newfd]=cin;
                printf("[%s,%d]:connect\n",inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
                epoll_event ev;
                ev.data.fd=newfd;
                ev.events=EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ev);
            }else{
                char buf[128]="";
                bzero(buf,sizeof(buf));
                int res=recv(fd,buf,sizeof(buf),0);
                if(res==0){
                    printf("[%s,%d]:leave\n",
                        inet_ntoa(mp[fd].sin_addr),ntohs(mp[fd].sin_port));
                    
                    //将客户端从epoll树中删除
                    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);

                    close(fd);
                    break;
                }
                printf("[%s,%d]:%s\n",
                    inet_ntoa(mp[fd].sin_addr),ntohs(mp[fd].sin_port),buf);
                strcat(buf,"*_*\n");
                if(send(fd,buf,strlen(buf),0)==-1){
                    perror("send error");
                    return -1;
                }
            }
        }
    }
    
    close(sfd);  //关闭监听
    close(epfd); //关闭epoll实例
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值