本文是我的学习笔记,学习路线跟随Github开源项目,链接地址:30dayMakeCppServer
文章目录
- select模型
- fd_set结构体
- timeval结构体
- 文件描述符的就绪条件
- 带外数据与普通数据
- socket的状态
select模型
select是Linux下的一个IO复用模型,同时,它也是Linux中一个系统函数的名称:
#include <sys/select.h>int select(int ndfs, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
select系统函数的用途是:
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件
我们先对这个函数的各个参数进行一个解释:
- ndfs:指定被监听的文件描述符的总数
- resdfds、writefds、exceptfds:分别指向可读、可写和异常事件所对应的文件描述符
- timeout:设置时间,若是NULL将会一直阻塞,知道某个文件描述符就绪
select成功时返回[[#文件描述符的就绪状态|就绪(可读、可写和异常)文件描述符]]的总数。如果在超时时间内没有任何文件描述符就绪,select将返回0。
select失败时返回-1并设置errno。
如果在程序等待期间,程序收到信号(例如Ctrl+C这种,可由函数kill发起,这点在Linux系统编程中有说到),则select会立即返回-1,并设置errno为EINTER(这是在errno.h中定义的一个错误代码,意思是:“Interrupted system call”,即:系统调用被中断
fd_set结构体
fd_set结构体的实质其实是:一个存放文件描述符状态的数组,它的定义类似于以下结构:
typedef struct {long fds_bits[FD_SETSIZE / (8 * sizeof(long))];
} fd_set;
这个数组的长度由FD_SETSIZE决定。现在,我们对这个数组进行一个更详细的解释。
这里给出fd_set的完整定义:
#include <typesizes.h>
/*文件描述符的最大数量
*/
#define __FD_SETSIZE 1024
#include <sys/select.h>
// 这个好像没啥用
#define FD_SETSIZE__FDFDSETSIZE
/*给long int取个别名叫__fd_mask这个类型的占用未4或8字节,具体看编译器和架构
*/
typedef long int __fd_mask;
// 取消宏定义,可能是为了防止宏定义冲突
#undef __NFDBITS
/*重新设定__NFDBITS————NFDBITS计算的是__fd_maks所占的位数
*/
#define __NFDBITS (8*(int)sizeof(__fd_mask))
typedef struct{#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE/__NFDBITS]#define __FDS_BITS(set((set)->fds_bits))#else__fd_mask fd_bits[__FD_SETSIZE/__NFDBITS];#define __FDS_BITS(set)((set)->__fds_bits)#endif
}fd_set
从上面这段代码可以知道:fd_set数组所占用的bit的数量其实就是__FD_SETSIZE/8。
这是因为:fd_set的本质是位图,即:它不直接存储文件描述符,而是根据其中每一位的存储情况来确定某一文件描述符的状态。
具体如下:
- 位图的索引(数组的下标)对应文件描述符的编号,即第0位对应文件描述符0,第1位对应文件描述符1,以此类推
- 位图的值表示对应的文件描述符的状态。如果位值位1,表示该文件描述符需要监视;若是0,则表示该文件描述符不需要监视
由于fd_set中的操作本质上是位操作,我们想要进行操作会十分的复杂,因此我们应当使用其提供的一系列宏来访问fd_set中的位:
#include <sys/select.h>
/*清除fdset中的所有位
*/
FD_ZERO(fd_set* fdset);
/*设置fd_set中的位fd
*/
FD_ZERO(int fd, fd_set* fdset);
/*清除fdset中的位fd
*/
FD_CLR(int fd, fd_set* fdset);
/*测试fdset的位fd是否被设置
*/
int FD_ISSET(int fd, fd_set* fdset);
timeval结构体
在上面的select系统函数中,最后一个参数就是一个timeval结构体指针,它用于设置select的超时时间:
struct timeval{long tv_sec; // 秒数long tv_usec;// 微秒数
};
从其定义我们可以看出:它提供了微秒级的定时方式,若是我们将其设置为0,即:监听时间为0,这会使得select立即返回。
至于为什么需要使用指针呢?这点应该很好理解,就是为了避免拷贝,需要将该参数提交给内核,内核会将其修改,以告诉应用程序select等待了多久。
但是在游双的《Linux高性能服务器编程》中写道:
不过,我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的。
文件描述符的就绪条件
此节摘抄与游双的《Linux高性能服务器编程》第九章,具体为9.1.2。
在网络编程中,下列情况的socket可读:
- socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时我们可以无阻塞地读取该socket,并且读操作返回地字节数大于0
- socket通信地对方关闭连接。此时对该socket读操作将返回0
- 监听socket上有新的连接请求
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误
下列情况下socket可写:
- socket内核发送缓存区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0
- socket的写操作被关闭。对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号
- wocket使用非阻塞的connect连接成功或者失败(超时)之后
- socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误
在网络程序中,select能处理的异常情况只有一种:socket上接收到[[#带外数据与普通数据|带外数据]]。
带外数据与普通数据
普通数据(Normal Data)是指正常的、按照通常顺序传输的数据,是正常状态。当进行网络通信时,普通数据是按照先进先出的原则进行传输的。发送方将数据逐个字节地发送给接收方,并确保接收方按照相同的顺序接收和重新组装数据。
带外数据(Out-of-Band Data)指的是具有高优先级的数据,被划分为与普通数据分开的数据通道,它也被称为”紧急数据“,是一种异常状态。带外数据可以在数据流中插入,即使在普通数据还未发送完毕的情况下,带外数据也可以及时传输并被接收方处理。带外数据的传输方式通常比普通数据的传输优先级更高。
带外数据通常用于发送一些紧急的控制信息或异常情况的通知,例如中断信号或连接关闭请求等。它们被用于提供紧急服务或在遇到特定事件时发送重要的控制信息。
socket的状态
在常见的套接字模型中,套接字的状态可分为以下几种:
- 未连接(unconnected):套接字没有与对方建立连接,处于初始状态或者已关闭状态
- 监听(listening):服务器套接字正在等待客户端连接请求。这通常用于服务端创建一个监听套接字,以接收客户端的连接请求
- 连接已建立(connected):套接字成功与远程对等体建立连接,并可以进行数据传输
- 关闭等待(closing):套接字已发送关闭请求,正在等待对方的相应或确认关闭
- 已关闭(closed):套接字连接已经关闭,并且不再可用(不可再次连接)
除了以上套接字状态,套接字在某一时刻拥有不同的状态指示符:
- 可读(readable):套接字上有数据可供读取。可以使用select等异步IO多路复用时检查该状态
- 可写(writable):套接字上可以写入数据。可以使用select等异步IO多路复用时检查该状态
- 异常(exceptional):表示套接字遇到异常情况,例如:带外数据到达或者发生错误等。可以使用select等异步IO多路复用时检查该状态
这些状态指示符是用于通知应用程序在某一时刻的特定状态,以进行相应的处理。