alin的学习之路(Linux网络编程:四)(多路IO转接:select、poll、epoll)

alin的学习之路(Linux网络编程:四)(多路IO转接:select、poll、epoll)

1. select

1. select优化思路

当select转接的文件描述符跨度过大时,每次都遍历文件描述符表显然会降低效率,因此要对其优化。

优化思路:添加一个数组来存储要监听的文件描述符,直接遍历这个数组即可

程序流程:

  1. 创建监听套接字lfd = Socket()
  2. 设置端口复用 setsockopt()
  3. Bind() 绑定ip和端口号
  4. Listen()设置最大监听个数
  5. 定义fd_set rset,allset; rset传出,allset用于存所有需要监听的文件描述符
  6. FD_SET(lfd,&allset);
  7. 定义一个存最大文件描述符的变量,暂时等于lfd, maxfd = lfd;
  8. 定义一个 client[1024] 数组存放所有需要监听的文件描述符
  9. 将 client 数组初始化为全 -1 ,-1代表没有文件描述符。定义一个maxindex为client数组的最大有效文件描述符下标
  10. while(1){
  11. rset = allset 初始化rset为全部要监听的文件描述符
  12. int nread = select(maxfd+1,&rset,NULL,NULL,NULL); 阻塞等待监听的文件描述符中有读事件产生
  13. if(FD_ISSET(lfd,&rset))
    • 说明有客户端请求接入,cfd = Accept() ,将 cfd 添加到client数组中的第一个不为-1的位置,并更新maxindex。
    • 然后判断 nread 如果等于1 的话,continue;
  14. else
    • 不为 lfd 即为 cfd 有读事件,遍历client数组,循环到最大下标maxindex,数组值为-1时continue。
    • 判断 cfd 是否在rset中,使用FD_ISSET(client[i]),如果存在的话调用Read()
    • Read()返回值为0:客户端退出,FD_CLR(client[i],&allset); ,client[i] 置为 -1,Close(client[i])
    • Read() 返回值大于0:小写转大写,Write()
  15. }
  16. Close(lfd);

2. 代码实现

#include "wrap.h"
#include <sys/select.h>
#include <strings.h>
#include <ctype.h>

#define SRV_PORT 6669
#define OPEN_MAX 1024

int main()
{
    int cfd,lfd,ret;
    struct sockaddr_in srv_addr,clt_addr;
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    socklen_t clt_addr_len;
    char buf[BUFSIZ] = {0};
    char cltip[16];

    lfd = Socket(AF_INET,SOCK_STREAM,0);

    int opt = 1;
    ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
    if(-1 == ret)
        sys_err("setsockopt error");

    Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));

    Listen(lfd,128);

    int maxfd = lfd;
    int maxindex = 0;
    fd_set rset,allset;
    FD_ZERO(&rset);
    FD_ZERO(&allset);
    FD_SET(lfd,&allset);

    int client[OPEN_MAX];
    for(int i=0 ;i<OPEN_MAX ;++i)
    {
        client[i] = -1;
    }

    int nread;
    while(1)
    {
        rset = allset;
        nread = select(maxfd+1,&rset,NULL,NULL,NULL);
        if(-1 == ret)
            sys_err("select error");

        if(nread == 0)
            continue;
        if(FD_ISSET(lfd,&rset))   //有新客户端接入
        {
            clt_addr_len = sizeof(clt_addr);
            cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);

            printf("客户端ip:%s,port:%d 接入\n",
                   inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,cltip,sizeof(cltip)),
                   ntohs(clt_addr.sin_port));

            int i;
            //将新文件描述符添加到数组中
            for(i=0 ;i<OPEN_MAX; ++i)
            {
                if(client[i] == -1)
                {
                    client[i] = cfd;
                    if(maxindex < i)
                        maxindex = i;
                    if(maxfd < cfd)
                        maxfd = cfd;
                    //将新文件描述符添加到allset
                    FD_SET(cfd,&allset);
                    break;
                }
            }
            if(i == OPEN_MAX)
            {
                printf("too much fd\n");
                exit(1);
            }
        }
        else    // 客户端通信
        {
            for(int i = 0 ;i <= maxindex ;++i)
            {
                if(client[i] != -1){
                    int sockfd = client[i];
                    if(FD_ISSET(sockfd,&rset))   //判断如果在rset中,通信
                    {
                        ret = Read(sockfd,buf,sizeof(buf));
                        if(0 == ret)
                        {
                            FD_CLR(sockfd,&allset);
                            client[i] = -1;
                            Close(sockfd);
                        }
                        else if(ret > 0)
                        {
                            for(int j = 0;j < ret ;++j)
                            {
                                buf[j] = toupper(buf[j]);
                            }
                            Write(sockfd,buf,ret);
                            Write(STDOUT_FILENO,buf,ret);
                        }
                    }
                }
            }
        }
    }
    Close(lfd);
    return 0;
}

3. select 函数优缺点

  • 优点:
    • 跨平台。Windows、Linux、MacOS、Unix、类Unix
  • 缺点:
    • 监听上限受文件描述符限制。 最大 1204
    • 检查满足条件 fd, 当fd 的跨度较大时,需自己添加业务逻辑提高检索效率。编码难度高。

4. 同步和异步的区分

同步:阻塞

异步:不阻塞

2. 突破文件描述符1024最大打开(Ubuntu 18.04以后)

  • 查看 当前 Linux 系统所能打开的 最大文件个数 —— 受硬件影响。

    cat /proc/sys/fs/file-max 
    
  • 获取当前 用户下的进程, 默认打开的文件描述符个数。 —— 默认值 1024

  • 使用命令ulimit -a

    itcast@ubuntu:~/classcode/itcode/4day/select_server$ ulimit -a
    core file size          (blocks, -c) 0
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 3472
    max locked memory       (kbytes, -l) 65536
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 65536
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 3472
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited
    
  1. sudo vi /etc/security/limits.conf 打开文件, 添加设置。 不能超过 cat /proc/sys/fs/file-
    max 命令查询结果。

    * soft nofile 65536 和 * hard nofile 65536
    
  2. sudo vi /etc/systemd/user.conf 打开文件, 添加设置

DefaultLimitNOFILE=65535 
  1. sudo vi /etc/systemd/system.conf 打开文件, 添加设置

    DefaultLimitNOFILE=65535
    
  2. 必须重启 Linux 系统,使配置文件生效。

  3. ulimit -a 查看到 修改后的 默认打开的文件数

  4. ulimit -n 4096。 设置的临时数量 < 65535 即可生效。

2. poll

1.相关函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds:【数组】监听的文件描述符
   struct pollfd {
       int  fd;  // 待监听的文件描述符  
       short events;   // 待监听的文件描述符的监听事件
       POLLIN、 POLLOUT、 POLLERR
       short revents;  // 传入时,赋值为0。 poll 函数返回后,如果满足对应事件,
       // 该成员变量的值变为非0。 POLLIN、 POLLOUT、POLLERR
     };

nfds: 监听数组的实际有效的监听个数。————不是数组的容量。
    
timeout:
	> 0: 超时时长。 单位:毫秒。
	-1: 阻塞等。
	0: 不阻塞。
返回值:
 成功:返回满足对应监听事件的文件描述符 【总个数】
 失败:-1, errno

2. 实现流程

在这里插入图片描述

3. poll优缺点

  • 优点:
    • 自带数组结构。 可以将 “监听事件集合” 和 “满足监听事件的集合” 分离。
    • 可以 拓展监听上限。 超出 1024。
  • 缺点:
    • 不能跨平台。Linux
    • 无法直接定位满足监听事件的文件描述符, 编码难度较大。

3. Read函数返回值

> 0: 	实际读到的字节数
= 0: 	socket 中 对端关闭。close()
- 1: 	如果 errno == EINTR 被异常中断。 需要重启。
  		如果 errno == EAGAIN 或 EWOULDBLOCK 以非阻塞方式读数据, 但没有数据。 需要,再次读。
  		如果 errno == ECONNRESET。 说明连接被重置。 需要 close,移除出监听队列。
  		其余为 异常。

4. epoll

1. 相关函数

  1. epoll_create()

    #include <sys/epoll.h>
    // 创建一棵监听红黑树
    int epoll_create(int size);
    	size:创建的红黑树的监听节点的数量。(仅供内核参考)
    	返回值:
    		成功:新创建的红黑树根节点的fd
    		失败:-1, errno
    
  2. epoll_ctl()

    // 操作监听红黑树
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    	参数epfd:epoll_create 函数的返回值。epfd
    	参数op:对监听红黑树所做的操作。
    		EPOLL_CTL_ADD: 添加 fd 到监听红黑树。(设置监听)
    		EPOLL_CTL_MOD: 修改 fd 在监听红黑树上的监听事件。
    		EPOLL_CTL_DEL: 将一个fd从监听红黑树上摘下。(取消监听)
      	参数fd:待监听的 fd。
     
      	参数event:【不是数组】本质是 struct epoll_event 结构体变量的地址。----传入参数。
     		成员 events:	EPOLLIN、 EPOLLOUT、 EPOLLERR
     		成员 data:联合体(共用体)
     			int fd: 对应监听事件的 fd
    			void *ptr:
    			uint32_t u32:
    			uint64_t u64:
    	返回值:
    		成功:0
    		失败:-1, errno
    
  3. epoll_wait()

    // 阻塞监听
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    	epfd:epoll_create 函数的返回值。epfd
    	events:传出参数【数组】,满足监听条件的那些 fd 结构体。
    	maxevents:数组元素的总个数。1024
    		举例:struct epoll_event events[1024];
    	timeout:
    		> 0: 超时时间,单位:毫秒
    		-1:阻塞监听。
    		0:非阻塞。
    	返回值:
     		> 0: 满足监听事件的文件描述符【总个数】。可以用作循环 events 数组的上限。
     		0:没有fd满足
     		-1:失败。 errno
    

2. 实现思路

  1. Socket()创建监听套接字
  2. 设置端口复用 setsockopt()
  3. Bind()绑定IP地址和端口号
  4. Listen() 设置最大监听数
  5. int epfd = epoll_create() 创建epoll树根节点
  6. struct epoll_event tep,ep[1024]; 创建一个中间量的epoll事件结构体,一个结构体数组。一个用于epoll_ctl()函数传参,一个用于epoll_wait()函数传参
  7. tep初始化:tep.events = EPOLLIN;tep.data.fd = lfd;
  8. epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep); 将lfd挂上ep树
  9. while(1){
  10. int ret = epoll_wait(epfd,ep,1024,-1); 监听ep树上的文件描述符
  11. for(int i=0 ;i<ret ;++i)
  12. if(ep[i].data.fd == lfd)
    • Accept() 连接客户端
    • 使用epoll_ctl()将cfd挂上ep树
  13. else
    • 非lfd即cfd ,Read()
    • Read() 返回值等于0:从ep树上摘下该文件描述符,使用epoll_ctl(),第二个参数是EPOLL_CTL_DEL,Close()
    • Read() 返回值大于0:小写转大写,Write()
  14. }
  15. Close(lfd);

3. 实现代码

#include "wrap.h"
#include <strings.h>
#include <ctype.h>
#include <sys/epoll.h>

#define SRV_PORT 6668
#define OPEN_MAX 1024

int main()
{
    int lfd,cfd;
    int ret;
    char cltip[16];
    char buf[BUFSIZ] = {0};
    socklen_t clt_addr_len;
    struct sockaddr_in srv_addr,clt_addr;
    bzero(&clt_addr,sizeof(clt_addr));
    bzero(&srv_addr,sizeof(srv_addr));
    
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(SRV_PORT);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    lfd = Socket(AF_INET,SOCK_STREAM,0);

    int opt = 1;
    ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));
    if(-1 == ret)
    {
        sys_err("setsockopt error");
    }
    
    Bind(lfd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));

    Listen(lfd,128);

    struct epoll_event tep,ep[OPEN_MAX];
    int epfd = epoll_create(OPEN_MAX);
    if(-1 == epfd)
        sys_err("epoll_create error");

    tep.events = EPOLLIN;
    tep.data.fd = lfd;
    ret = epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&tep);
    if(-1 == ret)
        sys_err("epoll_ctl error");

    while(1)
    {
        int nread = epoll_wait(epfd,ep,OPEN_MAX,-1);
        if(-1 == nread)
            sys_err("epoll_wait error");

        for(int i=0 ;i<nread ;++i)
        {
            if (!(ep[i].events & EPOLLIN))      //如果不是"读"事件, 继续循环
                continue;
            int sockfd = ep[i].data.fd;
            if(sockfd == lfd)     //有新的客户端接入
            {
                clt_addr_len = sizeof(clt_addr);
                cfd = Accept(lfd,(struct sockaddr*)&clt_addr,&clt_addr_len);
                
                printf("客户端ip:%s,port:%d 接入\n",
                       inet_ntop(AF_INET,&clt_addr.sin_addr.s_addr,cltip,sizeof(cltip)),
                       ntohs(clt_addr.sin_port));

                tep.events = EPOLLIN;
                tep.data.fd = cfd;
                ret = epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&tep);
                if(-1 == ret)
                    sys_err("epoll_ctl");

                
            }
            else    //与客户端通信
            {
                ret = Read(sockfd,buf,sizeof(buf));
                if(0 == ret)   //客户端退出
                {
                    int n = epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
                    if(-1 == n)
                        sys_err("epoll_ctl error");
                    Close(sockfd);
                }
                else if(ret > 0)
                {
                    for(int j = 0 ;j < ret ;++j)
                        buf[j] = toupper(buf[j]);
                    Write(sockfd,buf,ret);
                    Write(STDOUT_FILENO,buf,ret);
                }
            }
        }
    }
    Close(lfd);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41775886/article/details/107751306
今日推荐