Linux网络编程【9】(IO模型及多路复用)

391 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

1 IO模型

1.1 分类

在UNIX/Linux下主要有4种I/O 模型:

  1. 阻塞I/O:最常用、最简单、效率最低
  2. 非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询
  3. I/O 多路复用:允许同时对多个I/O进行控制
  4. 信号驱动I/O:一种异步通信模型

1.2 阻塞IO

几乎所有的阻塞函数默认都是阻塞IO,

以读阻塞为例,

如果要读取的缓冲区中有数据,则正常执行

如果缓冲区中没有数据,则读函数会一直阻塞等待,当有数据的时候,==内核将会自动唤醒当前进程==,接着执行读操作

以写阻塞为例,

一般写操作是不会阻塞,只有当写操作对应的缓冲区写满时,会发生阻塞

#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    char buf[32] = {0};

    while(1)
    {
        fgets(buf, 32, stdin);
        //标准输入都有文件描述符0,1,2
        //没有这三个也可以操作,终端是设备文件

        sleep(1);

        printf("***************************\n");    
    }

    return 0;
}

1.3 非阻塞IO

如果将一个函数设置为非阻塞,意味着:

  • 如果要操作的缓冲区中有数据,则正常执行
  • 如果要操作的缓冲区中没有数据,则当前函数立即返回(当前函数执行失败),接着执行下面的代码

当一个应用程序使用了非阻塞模式套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称作polling),当应用程序不停地polling内核来检查是否I/O操作已经就绪,这是非常浪费CPU的资源的。

有一部分函数自带标志位,可以设置非阻塞,但是大多数函数都无法直接设置非阻塞,需要通过一些函数来设置

使用fcntl函数设置非阻塞IO

  • WNOHANG

  • MSG_DONTWAIT

  • O_NONBLOCK

  1. 头文件:
    1. #include <unistd.h>
    2. #include <fcntl.h>
  2. 原型:int fcntl(int fd, int cmd, ... /* arg */ );
  3. 功能:操作一个文件描述符
  4. 参数:
    1. fd:文件描述符
    2. cmd:命令选项
      1. F_GETFL 获取文件状态标志位
      2. F_SETFL 设置文件状态标志位
      3. O_NONBLOCK 非阻塞
    3. ...arg:可变参,是否需要由cmd后面括号里面内容决定,如果是int就需要,如果是void就不需要
  5. 返回值:
    1. 成功: F_GETFL 文件状态标志位 F_SETFL 0
    2. 失败:-1

读改写:一位一位的改

  • 第一步:读,获取之前标志位

  • 第二步:改,改变标志位

  • 第三步:写,将改后的标志位设置回去

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    char buf[32] = {0};

    //使用fcntl函数实现非阻塞
    //注意:对于标志位的操作,必须遵循只操作指定标志位而不改变其他标志位
    //所以对寄存器或者位的操作,一般执行读改写三步
    
    //第一步:读,获取之前标志位
    int flags;
    if((flags = fcntl(0, F_GETFL)) == -1)
    {
        perror("fcntl error");
        exit(1);
    }

    //第二步:改,改变标志位
    flags |= O_NONBLOCK;

    //第三步:写,将改后的标志位设置回去
    if(fcntl(0, F_SETFL, flags) == -1)
    {
        perror("fcntl error");
        exit(1);
    }
    
    while(1)
    {
        if(fgets(buf, 32, stdin) == NULL)
        {
            perror("fgets error");
        }

        sleep(1);

        printf("buf = [%s]\n", buf);
        printf("***************************\n");    
    }

    return 0;
}

2 IO多路复用

❓当一个代码中有多个阻塞函数时,因为代码默认都有先后执行顺序,所以无法做到每一个阻塞函数独立执行,相互没有影响,如何解决这个问题?

  • 如果按照默认阻塞形式,无法解决;
  • 如果设置为非阻塞,每一个函数都轮询查看缓冲区中是否有数据,可以解决这个问题,但是轮询比较消耗CPU资源,所以也不推荐;
  • 如果使用多进程或者多线程,需要考虑资源释放问题,也不推荐。

与多线程、多进程相比,I/O多路复用系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

  • 处理多个描述符
  • 服务器要处理多个服务或者多个协议

🀄相对比较好的方法是使用IO多路复用

IO多路复用的基本思想是:

  1. 先构造一张有关描述符的表,保存要操作的文件描述符;
  2. 然后调用一个函数,阻塞等待文件描述符准备就绪;
  3. 当有文件描述符准备就绪;
  4. 则函数立即返回;
  5. 执行相应的IO操作。

图片.png

调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。

2.1使用select实现IO多路复用

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
       fd_set *exceptfds, struct timeval *timeout);
功能:允许一个程序操作多个文件描述符,阻塞等待文件描述符
     准备就绪,如果有文件描述符准备就绪,函数立即返回,
     执行相应的IO操作
参数:
    nfds:最大的文件描述符加1
    readfds:保存读操作文件描述符的集合
    writefds:保存写操作文件描述符的集合
    exceptfds:保存其他或者异常的文件描述符的集合
    timeout:超时
        NULL 阻塞
返回值:
    成功:准备就绪的文件描述符的个数
    失败:-1
    
清空集合set
void FD_ZERO(fd_set *set);

将文件描述符fd添加到集合setvoid FD_SET(int fd, fd_set *set);

将文件描述符fd从集合set中移除
void FD_CLR(int fd, fd_set *set);

判断文件描述符fd是否在集合setint FD_ISSET(int fd, fd_set *set);
返回值:
    存在:1
    不存在:0

2.1.2 服务器

//TCP网络编程之服务器

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/types.h> 
#include <string.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

#define N 128
#define ERRLOG(errmsg) do{\
                            perror(errmsg);\
                            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
                            exit(1);\
                            }while(0)

int main(int argc, char const *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in serveraddr, clientaddr;
    socklen_t addrlen = sizeof(serveraddr);
    char buf[N] = {0};

    //第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        ERRLOG("socket error");
    }

    //第二步:填充服务器网络信息结构体
    //inet_addr:将点分十进制ip地址转换为网络字节序的无符号4字节整数
    //atoi:将数字型字符串转换为整形数据
    //htons:将主机字节序转化为网络字节序
    serveraddr.sin_family = AF_INET;
    //注意:ip地址不能随便写,服务器在那个主机中运行,ip地址就是这个主机的
    //如果是自己主机的客户端服务器测试,可以使用127网段的
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    //第三步:将套接字与服务器网络信息结构体绑定
    if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
    {
        ERRLOG("bind error");
    }

    //第四步:将套接字设置为被动监听状态
    if(listen(sockfd, 5) == -1)
    {
        ERRLOG("listen error");
    }

    //使用select实现IO多路服用

    //第一步:创建一个保存要操作的文件描述符集合并清空
    fd_set readfds;
    FD_ZERO(&readfds);

    int maxfd = sockfd;

    while(1)
    {
        //第二步:将要操作的文件描述符添加到集合中
        FD_SET(0, &readfds);
        FD_SET(sockfd, &readfds);

        //第三步:调用select函数,阻塞等待文件描述符准备就绪
        if(select(maxfd+1, &readfds, NULL, NULL, NULL) == -1)
        {
            ERRLOG("select error");
        }

        //第四步:如果有文件描述符准备就绪,则select函数立即返回执行对应的IO操作
        //注意:如果有文件描述符准备就绪,select函数返回之后,会自动将集合中没有准备就绪的文件描述符移除
        //所以select函数返回之后,判断哪个文件描述符还在集合中,在的就是准备就绪的

        if(FD_ISSET(0, &readfds) == 1)
        {
            fgets(buf, N, stdin);
            buf[strlen(buf) - 1] = '\0';
            printf("buf = %s\n", buf);
        }

        if(FD_ISSET(sockfd, &readfds) == 1)
        {
            if(accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen) == -1)
            {
                ERRLOG("accept error");
            }
            printf("客户端%s:%d连接了\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
        }
    }

    return 0;
}

2.1.3客户端

//TCP网络编程之客户端

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

#define N 128
#define ERRLOG(errmsg) do{\
                            perror(errmsg);\
                            printf("%s - %s - %d\n", __FILE__, __func__, __LINE__);\
                            exit(1);\
                            }while(0)

int main(int argc, char const *argv[])
{
    if(argc < 3)
    {
        fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(serveraddr);

    //第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        ERRLOG("socket error");
    }

    //第二步:填充服务器网络信息结构体
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    serveraddr.sin_port = htons(atoi(argv[2]));

    //第三步:给服务器发送客户端的连接请求
    if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
    {
        ERRLOG("connect error");
    }

    //进行通信
    char buf[N] = {0};
    while(1)
    {
        fgets(buf, N, stdin);
        buf[strlen(buf) - 1] = '\0';

        if(send(sockfd, buf, N, 0) == -1)
        {
            ERRLOG("send error");
        }

        if(strcmp(buf, "quit") == 0)
        {
            printf("客户端退出了\n");
            exit(0);
        }

        memset(buf, 0, N);

        if(recv(sockfd, buf, N, 0) == -1)
        {
            ERRLOG("recv error");
        }

        printf("服务器:%s\n", buf);
    }

    return 0;
}

2.2poll

1.6.1 poll

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:同select,阻塞等待文件描述符准备就绪,如果有文件描述符
      准备就绪,则函数立即返回并执行相应的IO操作
参数:
    fds:结构体数组,有多少个元素由要操作的文件描述符的个数决定
        struct pollfd {
               int   fd;         文件描述符
               short events;     请求的事件
                   POLLIN 有数据可读
               short revents;    返回的事件
           };
    nfds:要操作的文件描述符的个数
    timeout:超时检测
        >0  设置超时毫秒数
        0  非阻塞
        <0 阻塞
返回值:
    成功:准备就绪的文件描述符的个数
    失败:-1