高级IO – 多路转接之 select
文章目录
- 高级IO -- 多路转接之 select
- 初识`select`
- select 函数原型
- 关于`fd_set`结构
- select使用示例
- 编写Sock
- 编写selectService
- 测试
- 理解select执行过程
- socket就绪条件
- 读就绪
- 写就绪
- select的特点
- select的缺点
- select的缺点
初识select
系统提供select
函数来实现多路复用 输入/输出 模型
select
系统调用是用来让我们的程序监视多个文件描述符的状态变化的- 程序会停在
select
这里等待,直到被监视的文件描述符有一个或多个发生了状态变化。
select 函数原型
select
的函数原型如下:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释:
nfds:
select
是用来等待多个文件描述符变化的,多个文件描述符在底层是用数组来保存的。
因此select
要有权力知道当前文件描述符的,因此第一个参数为最大文件描述符+1。
为什么是
maxfd+1
? 因为多个文件描述符是通过数组传来的,而文件描述符和数组的下标是一一对应的,都是从0开始的,因此我们需要让select知道当前管理的文件描述符的范围,因此传入最大的文件描述符+1。这里传入+1,是在循环的时候保证左闭右开罢了。
readfds/writefds/exceptfds :
这三个参数分别对应需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。
在此使用readfds
举例:
readfds
是一个输入输出型参数:
- 输入:用户告诉操作系统,你要帮我关心一下所设置的多个文件描述中读事件是否就绪,如果读事件就绪就立马告诉我。
- 输出:内核告诉用户,用户曾经让我关心的文件描述符有哪些已经就绪了
关于fd_set
结构
fd_set
:其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.
- 比特位的位置代表
fd
的编号- 比特位的内容代表 ‘是否’ 的概念,其中 1表示是,0表示否。
提供了一组操作
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的全部位
那么这里就会存在一个问题:输入和输出用的是同一张位图吗?
答案:是一张位图,只不过需要重新添加,再次设置。因为同一张位图是覆盖式的,每次重新使用都需要重新设置 [在下面select的特点中有提到]。
timeout:
的结构为 timeval
,是用来设置select()
的等待时间的
NULL
:则表示select()
没有timeout
,select
将一直被阻塞,直到某个文件描述符上发生了事件;- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,
select
将超时返回。
函数返回值:
- 执行成功则返回文件描述词状态已经改变的个数。
- 返回0代表在描述词状态改变前已超过
timeout
时间,没有返回。 - 当有错误发生时返回-1。错误原因存于
errno
,此时参数readfds
,writefds
,exceptfds
和timeout
的值变成不可预测。
select使用示例
编写Sock
基本的tcp
套接字代码,这里做了封装,不再赘述。
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cerrno>
#include <cassert>
class Sock
{
public:
static const int gbacklog = 20;
static int Socket()
{
int listenSock = socket(PF_INET,SOCK_STREAM,0);
if(listenSock < 0){
exit(1);
}
int opt = 1;
setsockopt(listenSock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));
return listenSock;
}
static void Bind(int socket,uint16_t port)
{
struct sockaddr_in local;//用户栈
memset(&local,0,sizeof(local));
local.sin_family = PF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(socket,(const struct sockaddr *)&local,sizeof(local))<0)
{
exit(2);
}
}
static void Listen(int socket)
{
if(listen(socket,gbacklog) < 0)
{
exit(3);
}
}
static int Accept(int socket,std::string * clientip,uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(socket,(struct sockaddr*)&peer,&len);
if(serviceSock <0){
return -1;//获取链接失败
}
if(clientport) *clientport = ntohs(peer.sin_port);
if(clientip) *clientip = inet_ntoa(peer.sin_addr);
return serviceSock;
}
};
编写selectService
#include <iostream>
#include <sys/select.h>
#include "Sock.hpp"
int fdsArray[sizeof(fd_set) * 8] = {0}; // 保存历史上所有的合法fd
int gnum = sizeof(fdsArray) / sizeof(fdsArray[0]);
#define DFL -1
using namespace std;
static void showArray(int arr[], int num)
{
cout << " 当前合法sock list# ";
for (int i = 0; i < num; i++)
{
if (arr[i] == DFL)
continue;
else
cout << arr[i] << " ";
}
cout << endl;
}
static void HandlerEvent(int listensock, fd_set &readfds)
{
for (int i = 0; i < gnum; i++)
{
if (fdsArray[i] == DFL)
continue;
if (i == 0 && fdsArray[i] == listensock)
{
if (FD_ISSET(listensock, &readfds))
{
cout << "已经有一个新连接到来了,需要进行获取了" << endl;
string clientip;
uint16_t clientport = 0;
int sock = Sock::Accept(listensock, &clientip, &clientport);
if (sock < 0)
return;
cout << "获取新连接成功: " << clientip << " : " << clientport << " | sock: " << sock << endl;
// read/write -- 不能 因为read不知道底层数据是否就绪 select知道
// 需要将fd托管给select
int i = 0;
for (; i < gnum; i++)
{
if (fdsArray[i] == DFL)
break;
}
if (i == gnum)
{
cerr << "我的服务器已经到达最大上限了,无法承载更多同时保持的链接了" << endl;
close(sock);
}
else
{
fdsArray[i] = sock; // 将sock添加到select中,进行进一步的监听就绪事件了
showArray(fdsArray, gnum); // 打印
}
}
}
else
{
// 处理普通sock的IO事件
if (FD_ISSET(fdsArray[i], &readfds))
{
char buffer[1024];
ssize_t s = recv(fdsArray[i], buffer, sizeof(buffer), 0); // 不会阻塞
if (s > 0)
{
buffer[s] = 0;
cout << "client[ " << fdsArray[i] << "]# " << buffer << endl;
}
else if (s == 0)
{
cout << " client[ " << fdsArray[i] << "] quit,server close " << fdsArray[i] << endl;
close(fdsArray[i]);
fdsArray[i] = DFL;
showArray(fdsArray, gnum);
}
else
{
cout << " client[ " << fdsArray[i] << "] error,server close " << fdsArray[i] << endl;
close(fdsArray[i]);
fdsArray[i] = DFL;
showArray(fdsArray, gnum);
}
}
}
}
}
static void usage(std::string process)
{
cerr << "\nUsage: " << process << " port\n"
<< endl;
}
// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(1);
}
int listensock = Sock::Socket();
Sock::Bind(listensock, atoi(argv[1]));
Sock::Listen(listensock);
// 初始化全为-1 表示均可用
for (int i = 0; i < gnum; i++)
{
fdsArray[i] = DFL;
}
fdsArray[0] = listensock;
while (true)
{
// 在每次进行select的时候进行参数的重新设定
int maxFd = DFL;
fd_set readfds;
FD_ZERO(&readfds);
for (int i = 0; i < gnum; i++)
{
if (fdsArray[i] == DFL)
continue; // 1.过滤掉不合法的fd
FD_SET(fdsArray[i], &readfds); // 2.添加所有的合法fd到readfds中,方便select统一进行就绪监听
if (maxFd < fdsArray[i])
{
maxFd = fdsArray[i]; // 3.更新maxFd
}
}
struct timeval timeout = {100, 0};
int n = select(maxFd + 1, &readfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
cout << "time out .... " << (unsigned long)time(nullptr) << endl;
break;
case -1:
cerr << errno << " : " << strerror(errno) << endl;
break;
default:
HandlerEvent(listensock, readfds);
break;
}
}
return 0;
}
测试
理解select执行过程
理解select
模型的关键在于理解fd_set
,为方便说明,取fd_set
长度为1字节,fd_set
中的每一个bit
可以对应一个文件描述符fd
,则1字节长的fd_set
最大可以对应8个fd
。
(1)执行
fd_set set
;FD_ZERO(&set)
;则set
用位图表示是0000,0000
。(2)若
fd=5
,执行FD_SET(fd,&set)
;后set
变为0001,0000
(第5位置为1)(3)若再加入
fd=2
,fd=1
,则set
变为0001,0011
(4)执行
select(6,&set,0,0,0)
阻塞等待(5)若
fd=1,fd=2
上都发生可读事件,则select
返回,此时set
变为0000,0011
。注意:没有事件发生的fd=5
被清空。
socket就绪条件
读就绪
socket
内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT
. 此时可以无阻塞的读该文件 描述符, 并且返回值大于0;socket
TCP通信中, 对端关闭连接, 此时对该socket
读, 则返回0;- 监听的
socket
上有新的连接请求; socket
上有未处理的错误;
写就绪
socket
内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT
, 此时可以无阻塞的写, 并且返回值大于0;socket
的写操作被关闭(close或者shutdown
). 对一个写操作被关闭的socket
进行写操作, 会触发SIGPIPE
信号;socket
使用非阻塞connect
连接成功或失败之后;socket
上有未读取的错误;
select的特点
- 可监控的文件描述符个数取决与
sizeof(fd_set)
的值. 每bit
表示一个文件描述符,假如一个服务器上sizeof(fd_set) = 512
则该服务器上支持的最大文件描述符是512*8=4096. - 将
fd
加入select
监控集的同时,还要再使用一个数据结构array
保存放到select
监控集中的fd
- 一是用于再
select
返回后,array
作为源数据和fd_set
进行FD_ISSET
判断。 - 二是
select
返回后会把以前加入的但并无事件发生的fd
清空,则每次开始select
前都要重新从array
取得fd
逐一加入(FD_ZERO
最先),扫描array
的同时取得fd
最大值maxfd
,用于select
的第一个参数。
- 一是用于再
备注: fd_set
的大小可以调整,可能涉及到重新编译内核…
select的缺点
- 每次调用select, 都需要手动设置
fd
集合, 从接口使用角度来说也非常不便. - 每次调用select,都需要把
fd
集合从用户态拷贝到内核态,这个开销在fd
很多时会很大 - 同时每次调用select都需要在内核遍历传递进来的所有
fd
,这个开销在fd
很多时也很大 - select支持的文件描述符数量太小
select
返回后会把以前加入的但并无事件发生的fd
清空,则每次开始select
前都要重新从array
取得fd
逐一加入(FD_ZERO
最先),扫描array
的同时取得fd
最大值maxfd
,用于select
的第一个参数。
备注: fd_set
的大小可以调整,可能涉及到重新编译内核…
select的缺点
- 每次调用select, 都需要手动设置
fd
集合, 从接口使用角度来说也非常不便. - 每次调用select,都需要把
fd
集合从用户态拷贝到内核态,这个开销在fd
很多时会很大 - 同时每次调用select都需要在内核遍历传递进来的所有
fd
,这个开销在fd
很多时也很大 - select支持的文件描述符数量太小
(本篇完)