文章目录

  • 一、I/O 多路复用概述
  • 二、fd_set结构体详解
  • 三、select函数
  • 四、select高并发服务器的流程
  • 五、select高并发服务器demo(tcp)
  • 六、select高并发服务器总结


一、I/O 多路复用概述

I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。

select,poll,epoll都是I/O多路复用的机制。I/O多路复用通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

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

【I/O多路复用使用的场合】:

  • 当客户处理多个描述符(通常是交互式输入、网络套接字)时,必须使用I/O多路复用;
  • tcp服务器既要处理监听套接字,又要处理已连接套接字,一般要使用I/O多路复用;
  • 如果一个服务器既要处理tcp又要处理udp,一般要使用I/O多路复用;
  • 如果一个服务器要处理多个服务时,一般要使用I/O多路复用。

二、fd_set结构体详解

typedef struct
{
#ifdef__USE_XOPEN
__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;

fd_set其实这是一个数组的宏定义,实际上是一个long类型的数组,每一个数组元素都与某一个打开的文件句柄(socket、文件、管道、设备等)相对应,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读

三、select函数

#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);
  • 功能:轮询监视并等待多个文件描述符的属性变化(可读、可写或错误异常);
  • 参数
  • nfds:要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024;
  • readfd:监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这;
  • writefds:监视的可写描述符集合;
  • exceptfds:监视的错误异常描述符集合;
  • timeout:超时时间,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其 timeval 结构用于指定这段时间的秒数和微秒数。
  • 返回值:成功:就绪描述符的数目,超时返回 0,出错:-1。

中间的三个参数 readfds、writefds 和 exceptfds 指定我们要让内核监测读、写和异常条件的描述字。如果不需要使用某一个的条件,就可以把它设为空指针( NULL )。集合fd_set 中存放的是文件描述符,可通过以下四个宏进行设置:

// 清空集合
void FD_ZERO(fd_set *fdset); 
// 将一个给定的文件描述符加入集合之中
void FD_SET(int fd, fd_set *fdset);
// 将一个给定的文件描述符从集合中删除
void FD_CLR(int fd, fd_set *fdset);
 // 检查集合中指定的文件描述符是否可以读写 
int FD_ISSET(int fd, fd_set *fdset);

四、select高并发服务器的流程

#include <头文件>

int main(int argc, char const *argv[])
{
	lfd = socket();
	bind();
	listen();
	fd_set rset, allset; // 读集合,所有描述符集合
	int maxfd = lfd; // 最大描述符
	FD_ZERO(&allset); // 所有描述符清零
	FD_SET(lfd, &allset); // 把listen返回的描述符置1
	vector<int> flag;
	while(1)
	{
		rset = allset; // allset是想监听的套接字描述符集合,rset是实际返回的套接字描述符集合
		// select IO多路复用
		// rset是传入传出参数,传入是想监听的文件描述符,返回是实际监听到的文件描述符
		int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		if (nready > 0)
		{
			if (FD_ISSET(lfd, &rset)) // 判断lfd是否在监听集合中
			{
				cfd = accept();
				// 把cfd加入到想监听的文件描述符集合中
				FD_SET(cfd, &allset);
				flag.push_back(cfd);
				if (maxfd < cfd)
					maxfd = cfd;
			}
			// 扫描所有文件描述符,看是否有读操作(最大不超过1024)
			for (int i = 0; i < flag.size(); ++i)
			{
				// i所在的文件描述符有读操作
				if (FD_ISSET(flag[i], &rset))
					/*事务处理*/
			}
		}
	}	
	close(lfd);
	return 0;
}

五、select高并发服务器demo(tcp)

#pragma GCC diagnostic error "-std=c++11"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <ctype.h>
#include <vector>
using namespace std;

void sys_err(const char *str)
{
    perror(str);
    exit(1);
}

int main(int argc, char **argv)
{
    int lfd, cfd;
    socklen_t clt_addr_len;
    struct sockaddr_in srv_addr, clt_addr;
    // 将地址结构清零(按字节),容易出错(后面两个参数容易颠倒)
    // memset(&srv_addr, 0, sizeof(srv_addr));
    // bzero也可以用来清零操作 
    bzero(&srv_addr, 0);
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(8080);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    int opt = 1;
    // 设置套接字选项
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    // 创建套接字
    lfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定套接字
    bind(lfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));
    
    // 监听客户端的连接
    listen(lfd, 128);
    
    fd_set rset, allset; // 读集合,所有描述符集合
    int maxfd = lfd; // 最大描述符
    FD_ZERO(&allset); // 所有描述符清零
    FD_SET(lfd, &allset); // 把listen返回的描述符置1

    char buf[512];
    vector<int> flag;

    while (1)
    {
        rset = allset; // allset是想监听的套接字描述符集合,rset是实际返回的套接字描述符集合
        // select IO多路转接
        // rset是传入传出参数,传入是想监听的文件描述符,返回是实际监听到的文件描述符
        int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
        if (nready < 0)
        {
            sys_err("select");
        }
        if (FD_ISSET(lfd, &rset)) // 判断lfd是否在监听集合中
        {
            clt_addr_len = sizeof(clt_addr);
            // 非阻塞接收客户端的连接
            cfd = accept(lfd, (struct sockaddr *)&clt_addr, &clt_addr_len);
            memset(buf, 0, 512);
            // 打印已经连接的客户端的信息
            cout << "客户端连接:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf)) 
                 << "," << ntohs(clt_addr.sin_port) << endl;
            // 把cfd加入到想监听的文件描述符集合中
            FD_SET(cfd, &allset);
            flag.push_back(cfd);

            // 更新最大描述符
            if (maxfd < cfd)
                maxfd = cfd;
            if (0 == --nready) // 说明select只返回一个lfd,即没有客户端连接上来,则无须执行后面的内容
                continue;
        }
        // 扫描所有文件描述符,看是否有读操作
        for (int i = 0; i < flag.size(); ++i)
        {
            if (FD_ISSET(flag[i], &rset)) // i所在的文件描述符有读操作
            {
                memset(buf, 0, 512);
                // 接收来自客户端的数据
                recv(flag[i], buf, sizeof(buf), 0);
                int ret = strlen(buf);
                if (ret == 0) // 读套接字返回零表明客户端关闭了
                {
                    close(flag[i]);
                    cout << "客户端关闭:" << inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, buf, sizeof(buf)) 
                        << "," << ntohs(clt_addr.sin_port) << endl;
                    FD_CLR(flag[i], &allset); // 解除select对此文件描述符的监听
                }
                for (int i = 0; i < ret; ++i)
                    buf[i] = toupper(buf[i]);
                // 回射到客户端
                send(flag[i], buf, ret, 0);
                // 客户端写到标准输出
                write(STDOUT_FILENO, buf, ret);
            }
        }
    }
    close(lfd);
    return 0;
}

六、select高并发服务器总结

【优点】:

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

【缺点】:

  • 每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大;
  • 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

为什么select只能监听1024个文件描述符?

IO 多路复用之select(高效并发服务器)_描述符


IO 多路复用之select(高效并发服务器)_文件描述符_02

内核定义了fd_set中1024为监听个数上限同时也是文件描述符上限,如果要扩大,只能重新编译内核。