Bootstrap

Linux 多路转接 —— select

传统艺能😎

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

🎉🎉非科班转码社区诚邀您入驻🎉🎉
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


在这里插入图片描述

select😍

回顾一下上一篇,select 是系统提供的一个多路转接接口

select 系统调用可以让我们的程序同时监视多个文件描述符的事件是否就绪。我们知道select 的核心工作就是等,当监视的多个文件描述符中有一个或多个事件就绪时,select 才会成功返回并告知调用者

函数原型如下:

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

nfds 为需要监视的文件描述符中最大的文件描述符值 +1;readfds 即输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些事件已经就绪;同理 writefds 为写时间对应的参数;exceptfds 为用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些事件已经就绪;timeout 为输入输出型参数,由用户设置 select 等待时间,返回时表示 timeout 的剩余时间。

参数 timeout 的取值:

  1. NULL/nullptr:select 调用后进行阻塞等待,直到被监视的某个事件就绪。
  2. 0:selec 调用后进行非阻塞等待,无论被监视的事件是否就绪,select 检测后都会立即返回。
  3. 特定的时间值:select 在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,过时进行超时返回。

函数调用成功,则返回有事件就绪的文件描述符个数;如果 timeout 时间耗尽,则返回 0。调用失败则返回 -1,同时错误码会被设置。

select 调用失败时,错误码可能被设置为

EBADF:文件描述符为无效的或该文件已关闭
EINTR:此调用被信号所中断
EINVAL:nfds 参数为负值
ENOMEM:核心内存不足

fd_set 结构😒

fd_set 结构与 sigset_t 结构类似,fd_set 本质也是一个位图,用位图中对应的位来表示要监视的文件描述符:

在这里插入图片描述

在这里插入图片描述
调用 select 之前就需要用 fd_set 定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中,这个添加动作本质是进行位操作,但这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于 fd_set 位图进行各种操作:

void FD_CLR(int fd, fd_set *set); //清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //测试描述词组set中相关fd的位是否为真在这里插入图片描述

void FD_SET(int fd, fd_set *set); //设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); //清除描述词组set的全部位

timeval 结构🤣

select 函数的最后一个参数是 timeout,这是一个指向 timeval 结构的指针,timeval 用于描述一段时间长度,该结构当中包含两个成员,其中 tv_sec 表示的是秒,tv_usec 表示的是微秒:

在这里插入图片描述

socket 就绪条件😁

读条件🤣

  1. 接收缓冲区中的字节数,≥ 水位标记 SO_RCVLOWAT,此时可以无阻塞的读取该文件描述符,并且返回值大于0。
  2. socket TCP通信中,对端关闭连接,此时对该 socket 读,则返回 0。
  3. 监听的socket上有新的连接请求。
  4. socket 上有未处理的错误。

写就绪😍

  1. 发送缓冲区中的可用字节数,大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0。
  2. socket 的写操作被关闭(close或者shutdown),对一个关闭写操作的 socket 进行写操作,会触发SIGPIPE信号。
  3. socket 使用非阻塞connect连接成功或失败之后。
  4. socket 上有未读取的错误。

异常就绪😒

socket 上收到带外数据(带外数据和 TCP 紧急模式相关,TCP报头当中 URG 标志位和 16位 紧急指针搭配使用,就能够发送/接收带外数据)

select 工作流程😘

如果想实现一个 select 服务器,那么我们可以大概知道他的工作流程:

  1. 先初始化服务器,创建套接字,绑定和监听套接字
  2. 定义一个 fd_array 数组来保存监听套接字以及已经和客户端建立链接的套接字,监听套接字一开始就要加入数组
  3. 服务器循环进行 select 监测读事件是否就绪,如果就绪就可以执行对应操作
  4. 在 select 之前还应该创建一个 readfds 文件描述符集,将 fd_array 的文件描述符放入 readfds ,select 就会对这些文件描述符对应的读事件进行监视,在最后就会看到 在 readfds 里面会有一条条的记录
  5. select 检测到读事件就绪就会将其对应的文件描述符放进 readfds ,我们就能知道哪些事件已经就绪
  6. 如果是读事件的监听套接字就绪了,就用 accept 从底层全连接队列获取已建立的连接,并将对应连接的套接字添加到 fd_array 里
  7. 如果是读事件和客户端建立连接的套接字,就调用 read 将读取到的信息打印出来
  8. 如果读事件是和客户端建立连接的套接字就绪,也可能是因为客户端关闭了连接,此时服务器应该调用 close 关闭套接字,并将该套接字从 fd_array 数组中清除,因为下一次不需要再监视该读事件了。

因为传入 select 的 readfds、writefds 和 exceptfds都是输入输出型参数,当 select 返回时这些参数中的值已经修改了,因此每次调用 select 都需要重新设置,timeout 也是同理。

进行重新设置,就需要定义一个 fd_array 数组保存与客户端已经建立的若干连接和监听套接字,实际 fd_array 数组当中的文件描述符就是需要让 select 监视读事件的文件描述符。select 服务器只是读取客户端发来的数据,因此只需要让 select 帮我们监视特定文件描述符的读事件,如果要让 select 同时帮我们监视读事件和写事件,则需要分别定义 readfds 和 writefds,并定义两个数组分别保存需要被监视的文件描述符,便于每次调用 select 前对readfds 和 writefds 进行重新设置。

服务器刚开始运行时,fd_array 数组当中只有监听套接字,因此 select 第一次只需要告知监听套接字的读事件是否就绪,但每次调用accept 获取到新连接后,都会将对应的套接字添加到 fd_array 当中,后续 select 就需要监视监听套接字和连接套接字的读事件是否就绪。

由于调用 select 时还需要传入被监视的文件描述符中最大文件描述符值+1,因此每次在遍历 fd_array 对 readfds 进行重新设置时,还需要记录最大文件描述符值。

select 服务器实现😂

socket 类😒

首先编写一个 Socket 类,对套接字相关的接口进行一定程度的封装,为了能够直接调用 Socket 类中封装的函数,我们将这些函数定义成静态成员函数:

#pragma once

#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>

class Socket{
   
public:
	//创建套接字
	static int SocketCreate()
	{
   
		int sock = socket(AF_INET, SOCK_STREAM, 0);
		if (sock < 0){
   
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//设置端口复用
		int opt = 1;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
		return sock;
	}
	//绑定
	static void SocketBind(int sock, 
;