IO复用的相关知识select,epoll

IO复用的相关知识

计算机如何接受网络数据

网卡接受到网络数据,写入到计算机内存的某个地址

socket网络编程

socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。客户端和服务端就通过socket来连接,交互。

所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。

socket工作流程

先看服务器端

//创建socket 
int fd = socket(AF_INET, SOCK_STREAM, 0);    
//绑定 
bind(fd, ...) 
//监听 
listen(fd, ...) 
//接受客户端连接,一直阻塞进程,有连接事件才返回。 
int clientfd = accept(fd, ...) 
//接收客户端数据
while(true){
    if (read(clientfd,acbuf,20) > 0)
    {
        printf("receive: %s\n",acbuf);
    }
}
​

Linux的socket标识符

socket包含:输入缓冲区,输出缓冲区,等待队列(它指向所有需要等待该 Socket 事件的进程)

在linux万物皆文件的思想中,客户端连接tcp服务的时候,会创建一个"socket文件",socket文件标识符clientfd就可以操作这个"socket文件"。clientfd->read(),就可以读取这个socket的输入缓冲区的数据(就是客户端的请求数据),clientfd->write就可以把输出缓冲区写入数据(操作系统会把这些数据发送到客户端)。

上面这个服务会一直阻塞在aceept函数中,一直到有客户端连接事件发生,触发中断,然后系统会把这个socket等待队列中的进程加入到就绪队列中(就是唤醒进程)。这里一个进程只能处理一个clientfd,一个进程处理一个客户端。

服务端两种socket,监听socket和连接socket

在服务端,首先用函数socket创建一个socket,经过bind,listen后就是监听socket,负责处理客户端的连接事件。也就是clientfd = accept(fd, ...)。fd就是监听socket,只有一个,没次客户端连接事件发生,就会返回一个clientfd。

clientfd就是连接socket,每一个客户端连接,都有一个clientfd。主要处理接受数据事件,客户端断开连接事件,服务端还可以通过clientfd发送数据给对应的客户端。

上面的select模式中,fds[]包含了监听socket和连接socket。epoll模式中,监视的所有socket也包括这两种socket.

数据流程和步骤

img

  • 计算机收到了对端传送的数据(步骤 ①)

  • 数据经由网卡传送到内存(步骤 ②)

  • 然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序(步骤 ③)

此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入工作队列中。

以上是内核接收数据全过程,这里我们可能会思考两个问题:

  • 操作系统如何知道网络数据对应于哪个 Socket?

  • 如何同时监视多个 Socket 的数据?

第一个问题:因为一个 Socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的 Socket。

当然,为了提高处理速度,操作系统会维护端口号到 Socket 的索引结构,以快速读取。

第二个问题是多路复用的重中之重,也正是本文后半部分的重点。

select模式

Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。

当其中一个sock接受到数据后,会触发中断程序

select函数的作用:

1.将进程从sock1,sock2,sock3的等待队列中移除,并且把进程唤醒。

2.进程遍历所有socket得到发生事件的socket

int s = socket(AF_INET, SOCK_STREAM, 0);   
bind(s, ...) 
listen(s, ...)  
int fds[] =  存放需要监听的socket,包括fd和所有的clientfd。
while(1){ 
    int n = select(..., fds, ...) //阻塞进程,等有事件发生,引发中断唤醒进程。    
    for(int i=0; i < fds.count; i++){ //遍历所有可能的socket
        if(FD_ISSET(fds[i], ...)){//只有发生事件的socket才进入
            if(i==s){
                //处理监听socket的事件,也就是连接事件
                clientfd = accept(s,...);
                FD_SET(clientfd,&fds);//把新加入的clientfd加入到fds中
            } else{
                //处理clientfd的事件,也就是客户端发送数据,或者客户端结束连接
            }
        } 
    } 

fds在select前,保存了所有监听的socket fds在select后,只保存发生事件的socket

select模式服务端执行流程

img

优点:

我们一个进程就可以监视和处理多个socket。

缺点:

每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。

Epoll 的设计思路

先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据:

int s = socket(AF_INET, SOCK_STREAM, 0);    
bind(s, ...) 
listen(s, ...)  
int eventpoll  = epoll_create(...); //创建eventpoll对象,并且把当前进程放入到eventpoll的等待队列
epoll_ctl(eventpoll , ...); //将所有需要监听的socket添加到eventpoll中
​
 while(1){ 
    int n = epoll_wait(...)//阻塞进程,一直到socket发生事件才唤醒
    for(接收到数据的socket){ 
        //处理 同上
    } 

Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。

img

rdlist就保存了接收到数据的clientfd。所以epoll_wait只需要返回rdlist里面的clientfd就行。

所以说,epoll方式其实就是多了一个 eventpoll对象,通过epoll_ctl可以把需要监听的clientfd加入到 eventpoll 对象中,同时 eventpoll 对象中还有一个队列rdlist,只要哪个clientfd接收到数据,就会加入到rdlist中。

eventpoll对象认识

rdlist: 双向链表,维护着有接收到数据的clientfd

rbr:红黑树,保存监视的 保存监视的clientfd, 通过epoll_ctl添加

epoll模式服务端执行里流程

img

 

select服务端代码demo

#include < sys/types.h>     
    #include < sys/socket.h>
    #include < netinet/in.h>    //sockaddr_in
    #include < stdio.h>
    #include < string.h>
    #include < signal.h>
    #include < sys/select.h>
    #include < unistd.h>
    #include < sys/time.h>
    //TCP
    int main()
    {
        int fd;
        int clientfd;
        int ret;
        pid_t pid;
​
        int i;
        int maxfd;          //当前最大套接字
        int nEvent;
        fd_set set = {0};   //监听集合
        fd_set oldset = {0};    //存放所有要监听的文件描述符
        struct timeval time = {0};
​
        int reuse = 0;
        char acbuf[20] = "";
        char client_addr[100] = "";
        struct sockaddr_in addr = {0};  //自己的地址
        struct sockaddr_in clientAddr = {0};    //连上的客户端的地址
        int addrLen = sizeof(struct sockaddr_in);
​
        signal(SIGCHLD,SIG_IGN);
​
        //1.socket()
        fd = socket(PF_INET,SOCK_STREAM,0);
        if(fd == -1)
        {
            perror("socket");
            return -1;
        }
​
        //会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
        //由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
        if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
            {
            perror("setsockopet error\n");
            return -1;
            }
​
        //2.bind()
        addr.sin_family = AF_INET;
        addr.sin_port = htons(1234);
        addr.sin_addr.s_addr = inet_addr("192.168.159.5");
        ret = bind(fd,(struct sockaddr *)&addr,addrLen);
        if(ret == -1)
        {
            perror("bind");
            return -1;
        }
​
        //3.listen()
        ret = listen(fd,10);
        if(ret == -1)
        {
            perror("listen");
            return -1;
        }
​
        //创建监听集合
        FD_ZERO(&oldset);
        FD_SET(fd,&oldset);
        //maxfdp1:当前等待的最大套接字。比如:当前fd的值=3,则最大的套接字就是3
        //所以每当有客户端连接进来,就比较一下文件描述符
        maxfd = fd;
        //select
        //select之前,set放的是所有要监听的文件描述符;{3,4,5}
        //select之后,set只剩下有发生事件的文件描述符。{3}
​
        while(1)
        {
            set = oldset;
            printf("before accept.\n");
            time.tv_sec = 5;
            nEvent = select(maxfd + 1,&set,NULL,NULL,&time);//返回文件描述符的个数(即事件的个数)
            printf("after accept.%d\n",nEvent);
            if(nEvent == -1)
            {
                perror("select");
                return -1;
            }
            else if(nEvent == 0)    //超时
            {
                printf("time out");
                return 1;
            }
            else
            {           
                //有事件发生,遍历每一个fd,既要处理监听套接口,又要处理连接套接口
                //判断是否是客户端产生的事件
                for(i = 0 ; i <= maxfd ; i++)
                {
                    if(FD_ISSET(i,&set))//select后,set只剩下发生事件的文件描述符,
                    {
                        if(i == fd)//对于fd,只能是连接事件
                        {
                            clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
                            FD_SET(clientfd,&oldset);//有新的连接,把新的clientfd加入到oldset中
                            printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
                            if(clientfd > maxfd)
                            {
                                maxfd = clientfd;
                            }
                        }
                        else //对于clientfd,只能是发送数据或者客户端退出
                        {
                            memset(acbuf,0,20);
                            if(read(i,acbuf,20) == 0) //客户端退出
                            {
                                close(i);
                                //还要从集合里删除
                                FD_CLR(i,&oldset);
                            }
                            else
                                printf("receive: %s\n",acbuf);
                        }
                    }
                }
            }
        }
        return 0;
    }

 

epoll模式服务端demo

 #include <sys/types.h>     
        #include <sys/socket.h>
        #include <netinet/in.h> //sockaddr_in
        #include <stdio.h>
        #include <string.h>
        #include <signal.h>
        #include <sys/epoll.h>
        //epoll
        //epoll_wait() epoll_creat() epoll_ctl()
        //TCP
        int main()
        {
            int fd;
            int clientfd;
            int ret;
            pid_t pid;
​
            int i;
            int epfd;
            int nEvent;
            struct epoll_event event = {0};
            struct epoll_event rtl_events[20] = {0};    //事件结果集
​
            int reuse = 0;
            char acbuf[20] = "";
            char client_addr[100] = "";
            struct sockaddr_in addr = {0};  //自己的地址
            struct sockaddr_in clientAddr = {0};    //连上的客户端的地址
            int addrLen = sizeof(struct sockaddr_in);
​
            signal(SIGCHLD,SIG_IGN);
​
            //1.socket()
            fd = socket(PF_INET,SOCK_STREAM,0);
            if(fd == -1)
            {
                perror("socket");
                return -1;
            }
​
            //会出现没有活动的套接字仍然存在,会禁止绑定端口,出现错误:address already in use .
            //由TCP套接字TIME_WAIT引起,bind 返回 EADDRINUSE,该状态会保留2-4分钟
            if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
                {
                perror("setsockopet error\n");
                return -1;
                }
​
            //2.bind()
            addr.sin_family = AF_INET;
            addr.sin_port = htons(1234);
            addr.sin_addr.s_addr = inet_addr("192.168.159.5");
            ret = bind(fd,(struct sockaddr *)&addr,addrLen);
            if(ret == -1)
            {
                perror("bind");
                return -1;
            }
​
            //3.listen()
            ret = listen(fd,10);
            if(ret == -1)
            {
                perror("listen");
                return -1;
            }
​
            epfd = epoll_create(1000);  //同时监听的文件描述符
            event.data.fd = fd;
            event.events = EPOLLIN;  //读
            epoll_ctl(epfd,EPOLL_CTL_ADD,fd, &event);
            while(1)
            {
        //      nEvent = epoll_wait(epfd,rtl_events,20,-1);  //-1:阻塞    0:非阻塞
                nEvent = epoll_wait(epfd,rtl_events,20,5000);
                if(nEvent == -1)
                {
                    perror("epoll_wait");
                    return -1;
                }
                else if(nEvent == 0)
                {
                    printf("time out.");
                }
                else
                {
                    //有事件发生,立即处理
                    for(i = 0; i < nEvent;i++)
                    {
                        //如果是 服务器fd
                        if( rtl_events[i].data.fd == fd )
                        {
                            clientfd = accept(fd,(struct sockaddr *)&clientAddr,&addrLen);
                            //添加
                            event.data.fd = clientfd;
                            event.events = EPOLLIN;  //读
                            epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&event);
                            printf("client ip:%s ,port:%u\n",inet_ntoa(clientAddr.sin_addr),ntohs(clientAddr.sin_port));
                        }
                        else
                        {
                            //否则 客户端fd 
                            memset(acbuf,0,20);
                            ret = read(rtl_events[i].data.fd,acbuf,20);
                            printf("%d\n",ret);
                            if( ret == 0) //客户端退出
                            {
                                close(rtl_events[i].data.fd);
                                //从集合里删除
                                epoll_ctl(epfd,EPOLL_CTL_DEL,rtl_events[i].data.fd,NULL);
                            }
                            else
                                printf("receive: %s\n",acbuf);
                        }
​
                    }
                }
            }
​
            return 0;
        }

参考:https://blog.csdn.net/armlinuxww/article/details/92803381

https://blog.51cto.com/13097817/2054397

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

runtoweb3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值