• Linux 下网络 IO 的多路复用


    2019-10-20

    关键字:select 与 poll


    在 Linux 系统下,IO 总共可以分为以下四种:

    1、阻塞 IO;

    2、非阻塞 IO;

    3、IO多路复用;

    允许同时对多个 IO 进行控制。

    4、信号驱动 IO;

    一种异步通信模型。前面三种 IO 都是同步型的,唯这一种是异步型的。

    阻塞 IO

    所谓阻塞 IO 就是在调用相关函数时,程序的运行指针会暂停往下执行,直至这个 IO 操作有结果返回为止。简单来说就是我发起一个 IO 操作请求,你有数据就返回给我,没数据我就等你到有数据为止。

    阻塞型 IO 是最普遍使用的 IO 模式,大部分的程序都是使用这一模式的。套接字在缺省情况下使用的就是阻塞 IO 模式。

    常见的阻塞模式函数有:read, recv, recvfrom, write, send, accept, connect。 这里需要强调,sendto 函数是非阻塞型的。

    非阻塞 IO

    非阻塞型 IO 比较干脆。当它发起一个 IO 请求时,若 IO 有数据就返回结果,若无数据则程序指针就继续往下执行了,不会死等的。

    我们可以通过两个函数来切换阻塞型与非阻塞型 IO。

    1、fcntl()

    假设我们需要将阻塞型 IO 设置为非阻塞型 IO。

    int flag;

    flag = fcntl(sockfd, F_GETFL, 0);

    flag |= O_NONBLOCK;

    fcntl(sockfd, F_SETFL, flag);

    这样一来一去,就将 IO 形式给更改过来了。

    2、ioctl()

    int b_on = 1;

    ioctl(sock_fd, FIONBIO,  &b_on);

    IO 多路复用

    IO 多路利用其实就是 C 编程中的 select/poll 模型和  epoll 模型。

    Select 函数:

    select 函数的作用就是将原本需要单独分别监听阻塞资源统一交由 select 来监听。每当被 select 所监听的资源有数据波动时,select 会采用轮询的方式去找出哪个阻塞资源有数据过来了。并将这些有数据波动的资源保存起来,然后中断 select 函数的阻塞态,执行后面的代码。select() 函数的原型如下:

    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

    这个函数是一个阻塞函数。当它返回值 0 时表示等待超时。返回值 -1 时表示等待失败。返回值大于 0 时表示成功监测到信号。

    参数 nfds 是指 fd 的个数。由于在 Linux 中,文件描述符总是顺序递增增长的,因此这个参数也可以填入最大文件描述符号的值再加一。即 maxfd + 1。

    参数 readfds 是读集合。

    参数 writefds 是写集合。

    参数 exceptfds 是异常集合。

    参数 timeout 是超时时长。这个参数没特殊需求的话建议填 NULL。NULL值会让 select() 一直处于等待状态永不超时。

     

    fd_set 是系统定义的用于存放文件描述符的信息的结构体,可以通过以下几个函数来设定:

    1、void FD_ZERO(fd_set *fdset);

    对指定集合清零。

    2、void FD_SET(int fd, fd_set *fdset);

    将 fd 加入到 fdset 集合中去。

    3、void FD_CLR(int fd, fd_set *fdset);

    从集合中清除指定的 fd。

    4、int FD_ISSET(int fd, fd_set *fdset);

    判断指定的 fd 是否可以读写。

     

    对于 select 函数中,writefds 与 exceptfds 通常都填 NULL。同时,对于 select() 之前与之后的 fd_set 集合,它所包含的内容是不一样的。在 select() 之前,我们会往 fd_set 集合中填入所有我们想监听的资源 fd,但在 select() 之后,对应的 fd_set 集合中的数据就已经发生了变化。通常可以理解为只有有数据波动的资源集合才会出现在 select() 以后的代码中。

     

    以下是一个运用 select() 来复用 TCP 编程的伪代码:

    以下贴出一个运用 seletct() 模型监听2个UDP连接的客户端与服务端的代码:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <arpa/inet.h>
    
    static int sockfd1;
    static int sockfd2;
    
    void main()
    {
        printf("hello world.!
    ");
        
        struct sockaddr_in sin1;
        int bre = 1;
        int ret;
        
        bzero(&sin1, sizeof(sin1));
        
        //准备监听第1个UDP端口。
        sockfd1 = socket(AF_INET, SOCK_DGRAM, 0);
        printf("sockfd1:%d
    ", sockfd1);
        
        setsockopt(sockfd1, SOL_SOCKET, SO_REUSEADDR, &bre, sizeof(int));
        
        sin1.sin_addr.s_addr = htonl(INADDR_ANY);
        sin1.sin_port = htons(17166);
        
        ret = bind(sockfd1, (struct sockaddr *)&sin1, sizeof(sin1));
        printf("sockfd1 bind ret:%d
    ", ret);
        
    
        //准备监听第2个UDP端口。
        sockfd2 = socket(AF_INET, SOCK_DGRAM, 0);
        printf("sockfd2:%d
    ", sockfd2);
        
        setsockopt(sockfd2, SOL_SOCKET, SO_REUSEADDR, &bre, sizeof(int));
        
        sin1.sin_port = htons(17177);
        ret = bind(sockfd2, (struct sockaddr *)&sin1, sizeof(sin1));
        printf("sockfd2 bind ret:%d
    ", ret);
        
        
        // select prepare.
        fd_set fdset;
        char bufff[32];
        
        while(1)
        {
            FD_ZERO(&fdset);
            FD_SET(sockfd1, &fdset);
            FD_SET(sockfd2, &fdset); //clear all fd in fd_set.
            
            ret = select(sockfd2 + 1, &fdset, NULL, NULL, NULL);
            printf("select ret:%d
    ", ret);
            if(FD_ISSET(sockfd1, &fdset))
            {
                printf("sockfd1 recevied.
    ");
                bzero(bufff, 32);
                ret = read(sockfd1, bufff, 32);
                printf("sockfd1 read len:%d,read:%s
    ", ret, bufff);
            }
            else if(FD_ISSET(sockfd2, &fdset))
            {
                printf("sockfd2 recevied.
    ");
                bzero(bufff, 32);
                ret = read(sockfd2, bufff, 32);
                printf("sockfd2 read len:%d,read:%s
    ", ret, bufff);
            }
            else
            {
                printf("unknown recevied.
    ");
            }
        }
    }
    select监听UDP服务端
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h>
    #include <arpa/inet.h>
    #include <linux/input.h>
    
    void main()
    {
        printf("hello world!
    ");
        
        int fd1 = -1;
        int fd2 = -1;
        int p1 = 17166;
        int p2 = 17177;
        
        struct sockaddr_in sin1;
        struct sockaddr_in sin2;
        
        fd1 = socket(AF_INET, SOCK_DGRAM, 0);
        fd2 = socket(AF_INET, SOCK_DGRAM, 0);
        printf("fd1:%d, fd2:%d
    ", fd1, fd2);
        
        bzero(&sin1, sizeof(sin1));
        bzero(&sin2, sizeof(sin2));
        
        sin1.sin_family = AF_INET;
        sin1.sin_port = htons(p1);
        
        if(inet_pton(AF_INET, "127.0.0.1", (void *)&sin1.sin_addr) != 1)
        {
            printf("cannot prepare udp ip address.
    ");
            return 0;
        }
        
        sin2.sin_family = AF_INET;
        sin2.sin_port = htons(p2);
        if(inet_pton(AF_INET, "127.0.0.1", (void *)&sin2.sin_addr) != 1)
        {
            printf("cannot prepare udp ip address.
    ");
            return 0;
        }
        
        char *buf;
        char cmd[5];
        int fd;
        struct socketaddr *sin;
        while(1)
        {
            bzero(cmd, 5);
            if(fgets(cmd, 4, stdin) == NULL)
            {
                printf("
    ");
                continue;
            }
    
            if(strlen(cmd) == 2)
            {
                if(*cmd == '1')
                {
                    printf("send to sockfd1
    ");
                    fd = fd1;
                    buf = "This is from sockfd1.";
                    sin = (struct socketaddr *)&sin1;
                }
                else if(*cmd == '2')
                {
                    printf("send to sockfd2
    ");
                    fd = fd2;
                    buf = "Hi,lemontea.";
                    sin = (struct socketaddr *)&sin2;
                }
                else
                {
                    continue;
                }
            }
            else if(strlen(cmd) == 1)
            {
                continue;
            }
            
            sendto(fd, buf, strlen(buf), 0, sin, sizeof(sin1));
        }
        
    }
    select监听UDP客户端

    以上示例代码的功能是:服务端利用 select() 监听2个UDP端口。客户端通过键盘输入决定给哪个端口发送数据,以此演示 select() 模型。

    这里额外提一点:当 select() 模型监听到有事件发生时,最好将这一事件消费掉,即上例中在监听到有 UDP 消息过来时,一定要通过 read() 将消息读出来。否则 select() 函数将在下一次监听时立即返回。

    poll 函数:

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);

    函数执行成功时返回大于0的值,失败时返回 EOF,超时返回值0。

    参数 pollfd 是一个系统结体体,它的原型如下:

    struct pollfd {

        int fd;  //要监听的文件描述符号。

        short events;  // 请求的事件。

        short revents;  // 返回的事件。

    };

    参数 nfds 与 timeout 就不再赘述了。

    以下是一个示例程序,程序来自于 https://www.jianshu.com/p/6a6845464770

    epoll 函数:

    略。

    信号驱动 IO

    略。


  • 相关阅读:
    学生排队 201703-2
    让动画停在最后一帧 forwards animation-fill-mode
    新版本的charles代理本地接口
    移动端调试插件Tencent / vConsole
    HDU
    Codeforces Round #668 (Div. 2)(A B C D)
    The 13th Chinese Northeast Collegiate Programming Contest
    2020, XIII Samara Regional Intercollegiate Programming Contest (B D)
    tarjan算法 双连通分量
    Codeforces Round #666 (Div. 2) (A B C D)
  • 原文地址:https://www.cnblogs.com/chorm590/p/11688916.html
Copyright © 2020-2023  润新知