Linux——I/O复用(1)——select

前言

TCP服务器的运行模型

在这里插入图片描述
TCP中,第一个客户端和服务器端建立连接,向服务器端不发数据,服务器端就在recv阻塞住,无法继续执行;如果有第二个客户端与服务器端建立连接,就在已完成三次握手的队列中放着,等着accept处理它,由于我们的代码阻塞在recv,没有机会去执行accept,导致第二个客户端得不到响应,我们之前的解决方法是accept之后的代码在子线程中执行。多线程多进程解决此问题。
根据我们对TCP编程流程的理解:
由图可知,客户端1将首先与服务器建立连接,此时客户端23也想和服务器建立连接,因为服务器已经与客户端1建立连接并进行交互,所以客户端23只能在listen()创建的内核等待队列中等待,因此就产生了一个问题

一个TCP服务器程序只能同时和一个客户端进行交互,其他链接上的客户端只能在TCP服务器的内核队列中等待。

I/O复用

IO复用使得程序能够同时监听多个文件描述符,这对于提升程序的性能至关重要
IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程,模型最大的优势是解决描述符过多的问题,系统开销小,不必创建也不必维护过多的线程或进程。

我们用I/O函数先去检测 所有的描述符,挑谁上面有数据,把这些检测出来,对这些有数据的描述符来个循环,依次处理,依次recv处理,都不会阻塞,因为都有数据,select检测没有数据的描述符我们不去recv处理

通常,网络程序在下列情况下需要使用I/O复用技术:

  • 客户端程序要同时处理多个socket。
  • 客户端程序要同时处理用户输入和网络连接。(聊天室程序)
  • TCP服务器要同时处理监听socket和连接socket,这是I/O复用使用最多的场合。
  • 服务器要同时处理TCP请求和UDP请求。
  • 服务器要同时监听多个端口,或者处理多种服务。

需要指出的是:

IO复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

下面就是添加了IO复用的TCP服务器模型:
在这里插入图片描述

如何做到的?在单个进程内同时处理多个描述符?
举个例子,假如学校要给你们发一本书,一种情况下,所有人到图书馆门前等,每个人就是一个线程,每个人都想要自己的数据,每个人都在等,阻塞住,如果谁的书到了,点名,谁就上去把书拿了,线程退出,线程往下执行。那么多人等,都阻塞住。还有一种情况,就留一个人在那等,谁的书发下来,那个人就给你打电话,你就去图书馆领取,这就是留一个人在那里等,就一个人阻塞住,先看有你没有数据,如果有数据,就通知你来处理这个数据,相当于代码执行recv,不会阻塞,因为你的数据已经到达了。

Linux上的I/O复用方式

  1. select
  2. poll
  3. epoll —— Linux独有的一种I/O复用方式

select原型

本质即一个Linux的系统调用方法

我们可以使用select函数实现I/O端口的复用,传递给 select函数的参数会告诉内核:

  • 我们所关心的文件描述符
  • 对每个描述符,我们所关心的状态。(我们是要想从一个文件描述符中读或者写,还是关注一个描述符中是否出现异常)
  • 我们要等待多长时间。(我们可以等待无限长的时间,等待固定的一段时间,或者根本就不等待)

从 select函数返回后,内核告诉我们一下信息:

  • 对我们的要求已经做好准备的描述符的个数
  • 对于三种条件哪些描述符已经做好准备.(读,写,异常)

有了这些返回信息,我们可以调用合适的I/O函数(通常是 read 或 write),并且这些函数不会再阻塞.

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, 
           fd_set *exceptfds, struct timeval *timeout);
  • nfds:所监听的所有文件描述符中的最大值 +1 ——> 提高内核执行效率,之后的描述符不用再访问了

  • 如果在超时时间内没有任何文件描述就绪,select将返回0;

  • select失败返回n>0,,集合上有n个事件就绪,到底是哪几个事件就绪需要用户自己检查,那么怎么检测哪几个描述符上有事件就绪呢?

  • 其实select返回以后,仅仅会返回有事件就绪的文件描述符(具体看下图),然后使用FD_ISSET方法(传入fd的值和集合的地址),以此从头遍历到末尾后,就会知道哪个文件描述符上有数据。

  • 出错返回负值

  • readfdswritefdsexceptfds

在这里插入图片描述

作用1:监听的文件描述符的集合,分别是 :

  1. 读事件的文件描述符集合;(以连接套接字为例:如果接收缓冲区中有数据,读事件就是就绪的)

  2. 写事件的文件描述符集合;(发送缓冲区有空间就就绪,一般一开始就是就绪状态)

  3. 异常事件的文件描述符集合.

作用2:内核监听到某些文件描述符有事件发生时,也是通过这三个参数告知应用程序的。 ——> 在线修改,因此每次调用select之前必须重新设置这三个fd_set

  • timeout:定时时间,在这段时间内监听所有关注的文件描述符,如果定时时间到了,依旧没有事件就绪,select也会返回。如果需要select永久阻塞,则将timeout置为NULL;

select的集合fd_set

typedef  long int  _fd_mask;
#define  _NFDBITS  (8 * (int)sizeof(_fd_mask))

typedef struct
{
	_fd_mask _fds_bits[32];
	#define _FDS_BITS(set)  ((set)->_fds_bits)
}fd_set;

fd_set记录文件描述符的方式:按位记录,来节省空间。
(eg:要记录的文件描述符是3,只需要1左移三位再进行或运算即可)

由于我们按照上述fdset的方式存放文件描述符,不太方便,所以还给我提供了以下的一些操作函数:

操作fd_set的宏函数:

#include <sys/select.h>

FD_SET(int  fd, fd_set  *fdset)       
 //设置fdset中的位fd, 即将fd加入到fdset
FD_CLR(int  fd, fd_set  *fdset)       
 //清除fdset的位fd
int  FD_ISSET(int  fd, fd_set  *fdset)
//测试fdset的位fd是否被设置。
FD_ZERO(int *fdset)                  
 //清除fdset中的所有位

timeval结构体如下

struct tmieval
{
	long tv_sec;  //秒数
	long tv_usec; //微秒数
};

小案例:select检测键盘是否有数据

在这里插入图片描述
在这里插入图片描述

select实现TCP服务器:并发处理客户端请求

客户端除了有自己的sockfd之外,在客户端请求之后,accept后也会产生新的文件描述符,所以服务器端的文件描述符会随着客户端的请求的增多而增多,随着连接的断开,服务端的文件描述符就减少了,所以服务器端的文件描述符一直是动态变化的。
我们的处理思路就是:
我们将所有的文件描述符收集起来存放在一个数组中,先将sockfd放入到集合中,检测集合中的文件描述符是否有事件发生,一旦有事件发生,accept后将新的文件描述符c放到集合中,接下来对集合中的sockfd进行accept操作,对c进行recv操作,如此循环,集合中的文件描述符的数量不断增加,由此用单个线程处理客户端的请求。

注意对于监听套接字sockfd我们执行accept,对于链接套接字我们执行recv操作

服务器端

// ser.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

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

#include <sys/select.h>

#define FDNUMBER 1024  //文件描述符数组的大小

// 根据ip地址与端口号创建套接字,返回创建的套接字的文件描述符
int  CreateSocket(char *ip, short port)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) return -1;

    struct sockaddr_in ser_addr;
    memset(&ser_addr, 0, sizeof(ser_addr));

    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(port);
    ser_addr.sin_addr.s_addr = inet_addr(ip);

    int res = bind(listenfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
    if(res == -1) return -1;

    res = listen(listenfd, 5);
    if(res == -1) return -1;

    return listenfd;
}

// 将所有的文件描述符设置到fd_set结构体变量上, 找到当前最大的文件描述符的值
int  InitFdSet(int *all_fd, fd_set *set)
{
    FD_ZERO(set);

    int maxfd = -1;

    int i = 0;
    for(; i < FDNUMBER; ++i)
    {
        if(all_fd[i] != -1)
        {
            FD_SET(all_fd[i], set);
            if(all_fd[i] > maxfd)
            {
                maxfd = all_fd[i];
            }
        }
    }

    return maxfd;
}


// 初始化文件描述符数组
void InitAllFd(int *all_fd)
{
    int i = 0;
    for(; i < FDNUMBER; ++i)
    {
        all_fd[i] = -1;
    }
}

// 向all_fd数组中插入fd
void InsertFd(int *all_fd, int fd)
{
    int i = 0;
    for(; i < FDNUMBER; ++i)
    {
        if(all_fd[i] == -1)
        {
            all_fd[i] = fd;
            return;
        }
    }
}

// 根据val在all_fd中进行删除,flag为1,则val为文件描述符, flag是0,则val是数组的下标
void DeleteFd(int *all_fd, int val, int flag)
{
    if(flag)
    {
        int i = 0;
        for(; i < FDNUMBER; ++i)
        {
            if(all_fd[i] == val)
            {
                all_fd[i] = -1;
                return;
            }
        }
    }
    else
    {
        all_fd[val] = -1;
    }
}

// 处理就绪的事件
void DealReadyEvent(int *all_fd, fd_set *set, int listenfd)
{
    // 对所有就绪事件的处理还依旧是串行的
    int i = 0;
    for(; i < FDNUMBER; ++i)
    {
        if(all_fd[i] == -1) continue;
        if(FD_ISSET(all_fd[i], set))  // 如果返回为真,说明这个文件描述符在set中,也就是文件描述符事件就绪
        {
            /*
                tcp服务器程序的所有文件描述符可以分成两类: 监听客户端链接的文件描述符、 与一个客户端链接的文件描述符
                如果是监听文件描述符就绪,处理就是accept
                如果是链接文件描述符,处理就是recv/send
             */
            if(all_fd[i] == listenfd)
            {
                struct sockaddr_in cli_addr;
                socklen_t addr_len = sizeof(cli_addr);
                int c = accept(listenfd, (struct sockaddr*)&cli_addr, &addr_len);
                if(c < 0) continue;

                // 将接收客户端链接的文件描述符添加到all_fd数组中
                InsertFd(all_fd, c);
                printf("Get New Clinet Link\n");
            }
            else  
            {
                char buff[128] = {0};
                int n = recv(all_fd[i], buff, 127, 0);
                if(n <= 0)  //  recv失败,或者客户端关闭了链接
                {
                    close(all_fd[i]);
                    printf("%d Over\n", all_fd[i]);
                    DeleteFd(all_fd, i, 0);
                }
                else
                {
                    printf("%d: %s\n", all_fd[i], buff);
                    send(all_fd[i], "OK", 2, 0);
                }
            }
        }
    }
    printf("DealReadyEvent over\n");
}

int main()
{
    // 创建套接字
    int listenfd = CreateSocket("192.168.133.132", 6000);
    assert(listenfd != -1);

    //记录当前程序所打开的所有的文件描述符
    int  all_fd[FDNUMBER];
    InitAllFd(all_fd);
    InsertFd(all_fd, listenfd);

    while(1)
    {
        /*
            select的三个参数:最大文件描述符值+1,关注读、写、异常事件的文件描述符集合fd_set,超时时间
        */
        fd_set read_set;
        int maxfd = InitFdSet(all_fd, &read_set);
        struct timeval  timeout = {5, 0};

        int n = select(maxfd+1, &read_set, NULL, NULL, NULL);//&timeout);
        printf("select return\n");
        
        if(n < 0) 
        {
            printf("select error\n");
            continue;
        }
        if(n == 0)
        {
            printf("timeout\n");
            continue;
        }
        DealReadyEvent(all_fd, &read_set, listenfd);
    }
}

客户端

// cli.c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> //字节序的转换
#include <arpa/inet.h>  //IP地址转换

int main()
{
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
	assert(-1 != sockfd);
    
	struct sockaddr_in ser_addr;
	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(6000);
	ser_addr.sin_addr.s_addr = inet_addr("192.168.133.132"); 
    
	int res = connect(sockfd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));//指定连接的服务器端的 IP 地址和端口
	assert(-1 != res);
    
	while(1)
	{
		printf("input: ");
		char buff[128] = {0};
		fgets(buff, 127, stdin);
		if(strncmp(buff, "end", 3) == 0)
		{
			break;
		} 
        
        send(sockfd, buff, strlen(buff) - 1, 0);
        
		memset(buff, 0, 128);
		recv(sockfd, buff, 127, 0);
		printf("%s\n", buff);
	} 
    
    close(sockfd);
	exit(0);
}

select的相关总结

  • 最多只能监听1024个文件描述符,而且文件描述符的值最大为1023
  • 只能关注3种事件类型: 读事件、写事件、异常事件
  • 内核会在线修改用户传递关注事件的文件描述符的集合的变量(修改fd_set结构), 每次调用select之前都必须重新设置三个fd_set结构变量。
  • select返回后,只是告诉用户程序有几个文件描述符就绪,但是并没有指定是哪几个文件描述符。用户程序就需要遍历所有的文件描述符,在探测哪些文件描述符就绪,所以时间复杂度为O(n)。
  • 用户程序需要自己维护所有的文件描述符,每次调用select的时候,都需要将用户空间的fd_set集合传递给内核空间,select返回时,又需要将内核的fd_set传递给用户空间。这样调用select的时候,会存在两次数据的拷贝,效率不是很高。
  • select内核采用的是轮询的方式去监测哪些文件描述符上的事件就绪。
  • select的工作模式只能是LT模式。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值