目录
[1.2.参数二: readfds, writefds, exceptfds](#1.2.参数二: readfds, writefds, exceptfds)
[1.2.2.readfds, writefds, exceptfds](#1.2.2.readfds, writefds, exceptfds)
[1.2.3.怎么理解 readfds, writefds, exceptfds是输入输出型参数](#1.2.3.怎么理解 readfds, writefds, exceptfds是输入输出型参数)
[1.3.参数三: timeout](#1.3.参数三: timeout)
前言
我们今天要讲的select,select的原理就像下面的赵六一样。
赵六去钓鱼,他是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。
一,select函数
select是我们学习的第一个多路转接IO接口,我们知道IO是由等待和拷贝两部分组成的。select只负责IO过程中等待的这一步,也就是说,用户可能关心一些sock上的读事件,想要从sock中读取数据,直接读取,可能recv调用会阻塞,等待数据到来,而此时服务器进程就会被阻塞挂起,但服务器挂起就完蛋了,服务器就无法给客户提供服务,可能会产生很多无法预料的不好影响,万一客户正转账呢,服务器突然挂起了,客户的钱没了,但商家这里又没有收到钱,客户找谁说理去啊,所以服务器挂起是一个问题,我们要避免产生这样的问题。
select函数是I/O多路复用的经典实现,其基本原型如下:
select函数的功能
select的作用就是帮用户关心sock上的读事件,等sock中有数据时,select此时会返回,告知用户你所关心的sock上的读事件已经就绪了,用户你可以调用recv读取sock中的数据了!所以多路转接其实是把IO的过程分开来执行了,用多路复用接口来监视fd上的事件是否就绪,一旦就绪就会立马通知上层,让上层调用对应的接口进行数据的处理,等待和数据拷贝的工作分开执行,这样的IO效率一定是高的,因为像select这样的多路转接接口,一次能够等待多个fd,在返回时,它可以把多个fd中所有就绪的fd全部返回并通知给上层。
select()函数允许程序监视多个文件描述符,等待所监视的一个或者多个文件描述符变为"准备好"的状态。所谓的"准备好"状态是指:文件描述符不再是阻塞状态,可以用于某类IO操作了,包括可读,可写,发生异常三种。
我们使用select来监视文件描述符时,要向内核传递的信息包括:
- 1、我们要监视的文件描述符个数
- 2、每个文件描述符,我们可以监视它的一种或多种状态,包括:可读,可写,发生异常三种。
- 3、要等待的时间,监视是一个过程,我们希望内核监视多长时间,然后返回给我们监视结果呢?
- 4、监视结果包括:准备好了的文件描述符个数,对于读,写,异常,分别是哪儿个文件描述符准备好了。
参数详解
1.1.参数一:nfds
- nfds: 这个参数是监控的文件描述符集合中最大文件描述符的值加1。在使用select函数时,必须确保这个参数正确设置,以便函数能监视所有相关的文件描述符。
比如说我们的文件描述符有0,1,2,3,4,5,如果我们想要监视所有的文件描述符,我们这个nfds参数就该填6,也就是5+1.
当程序运行时,程序其实会在select这里进行等待,遍历一次底层的多个fd,看其中哪个fd就绪了,然后就将就绪的fd返回给上层,select的第一个参数nfds代表监视的fd中最大的fd值+1,其实就是select在底层需要遍历所有监视的fd,而这个nfds参数其实就是告知select底层遍历的范围是多大
1.2.参数二: readfds, writefds, exceptfds
1.2.1.fd_set类型和相关操作宏
fd_set是一个通过位图来管理文件描述符集合的数据结构,它允许高效地测试和修改集合中的成员。
- fd_set类型本质是一个位图,位图的位置 表示 相对应的文件描述符,内容表示该文件描述符是否有效,1代表该位置的文件描述符有效,0则表示该位置的文件描述符无效。
- 如果将文件描述符2,3设置位图当中,则位图表示的是为1100。
- fd_set的上限是1024个文件描述符。
由于文件描述符是整数,且通常范围有限(尤其是在UNIX和类UNIX系统中),因此使用位图来表示它们是一种非常有效的空间和时间优化方法。
- FD_SET(fd, &set):此宏将文件描述符fd添加到set集合中。它实际上是将set中与fd对应的位设置为1。
- FD_CLR(fd, &set):此宏从set集合中移除文件描述符fd。它实际上是将set中与fd对应的位清零。
- FD_ISSET(fd, &set):此宏检查文件描述符fd是否已经被加入到set集合中。如果set中与fd对应的位为1,则返回非零值(真),否则返回0(假)。
- FD_ZERO(&set):此宏用于清空set集合中的所有文件描述符,即将集合中的所有位都设置为0。这是在使用set之前的一个好习惯,以确保集合从一个已知的状态开始。
我们看个例子
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/socket.h>
int main() {
// 假设fd是一个已经打开的文件描述符,这里我们用socket作为示例
int fd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket,实际使用中需要设置地址并连接
if (fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// 创建一个文件描述符集合
fd_set readfds;
// 清空集合
FD_ZERO(&readfds);
// 将文件描述符fd添加到集合中
FD_SET(fd, &readfds);
// 假设我们想要等待这个fd变得可读,最长等待时间为5秒
struct timeval tv;
tv.tv_sec = 5; // 秒
tv.tv_usec = 0; // 微秒
// 使用select等待文件描述符变得可读
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret == -1) {
perror("select");
close(fd); // 不要忘记关闭文件描述符
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout occurred! No data after 5 seconds.\n");
close(fd); // 即使没有数据,也要关闭文件描述符
} else {
// 检查fd是否就绪
if (FD_ISSET(fd, &readfds)) {
printf("Data is available now on fd %d.\n", fd);
// 在这里处理数据,例如使用read()函数读取数据
// 假设处理完数据后,我们不再需要等待这个fd
// 可以在这里调用FD_CLR来从集合中移除它,但在这个简单的例子中我们直接关闭它
close(fd);
}
}
return 0;
}
1.2.2.readfds, writefds, exceptfds
这三个参数都是输入输出型参数
readfds, writefds, exceptfds: 这三个参数分别代表读、写和异常监视的文件描述符集合。它们使用fd_set类型表示,这是一种通过位图来管理文件描述符的数据结构。
readfds
- readfds:这是一个指向fd_set的指针,用于指定程序关心的、希望进行读操作的文件描述符集合。如果select函数返回时,某个文件描述符在该集合中被标记为就绪(即可以进行无阻塞的读操作),则可以通过FD_ISSET宏来检查。
- readfds是 等待读事件的文件描述符集合,.如果不关心读事件(缓冲区有数据),则可以传NULL值。
- 应用进程和内核都可以设置readfds,应用进程设置readfds是为了通知内核去等待readfds中的文件描述符的读事件,而内核设置readfds是为了告诉应用进程哪些读事件生效
writefds
- writefds:同样是一个指向fd_set的指针,但这次它用于指定程序希望进行写操作的文件描述符集合。如果select返回时某个文件描述符在该集合中被标记为就绪(即可以进行无阻塞的写操作),则同样可以通过FD_ISSET宏来检查。
- 与readfds类似,writefds是等待写事件(缓冲区中是否有空间)的集合,如果不关心写事件,则可以传值NULL。
exceptfds
- exceptfds:这个参数也是指向fd_set的指针,用于指定程序希望监视异常条件的文件描述符集合。这里的"异常"通常指的是网络套接字上的带外数据(out-of-band data)到达,或者其他一些非标准的I/O事件。
- 如果内核等待相应的文件描述符发生异常,则将失败的文件描述符设置进exceptfds中,如果不关心错误事件,可以传值NULL。
使用注意事项
- 在调用select之前,必须正确地使用FD_ZERO、FD_SET、FD_CLR等宏来初始化和修改readfds、writefds、exceptfds这三个集合。
- nfds参数的值应该设置为这三个集合中最大文件描述符值加1,以确保select能够正确地监视所有相关的文件描述符。
- select函数会阻塞调用它的线程(或进程),直到以下条件之一发生:
- 有一个或多个文件描述符在readfds集合中变得可读。
- 有一个或多个文件描述符在writefds集合中变得可写。
- 有一个或多个文件描述符在exceptfds集合中发生了异常条件。
- 超时时间到达(如果timeout参数非NULL且指定了超时时间)。
select函数返回后,应该使用FD_ISSET宏来检查哪些文件描述符已经就绪,并据此执行相应的I/O操作。
1.2.3.怎么理解 readfds, writefds, exceptfds是输入输出型参数
- 输入方面 :
- 在调用
select
之前,调用者会设置这三个参数指向的fd_set
集合,以指定哪些文件描述符(fd)是调用者感兴趣的。具体来说,readfds
集合包含了调用者想要检查是否有数据可读的文件描述符,writefds
集合包含了调用者想要检查是否可以写入数据的文件描述符,而exceptfds
集合则包含了调用者想要检查是否有异常条件(如带外数据、连接挂断等)的文件描述符。
- 在调用
- 输出影响方面 :
- 当
select
调用返回时,这三个集合会被select
函数内部修改,以反映哪些文件描述符在调用期间变得就绪或遇到异常条件。具体来说,如果某个文件描述符在select
等待期间变得可读、可写或出现异常,那么相应的集合中的该文件描述符的位将被设置(如果它之前没有被设置的话)。但是,这并不意味着select
在这些集合中添加了新的文件描述符或移除了原有的文件描述符;它只是在修改集合中文件描述符的"就绪"状态位。
- 当
1.3.参数三: timeout
1.3.1.timeval结构体
struct timeval 是一个在多种编程环境中,尤其是在 UNIX 和类 UNIX 系统(包括 Linux)的 C 语言标准库中定义的结构体,用于表示时间间隔或时间点。他的定义如下
cpp
struct timeval {
long tv_sec; // seconds
long tv_usec; // microseconds
};
它通常与需要精确到微秒(microseconds)的时间操作的函数一起使用,比如 select(), gettimeofday(), setitimer(), 和 utimes() 等。
这个结构体包含两个成员:
- long tv_sec;:这个成员表示自 Unix 纪元(即 1970 年 1 月 1 日 00:00:00 UTC)以来的秒数。它是一个长整型(long),通常可以存储非常大的数,足以表示从 Unix 纪元到现在的时间(以秒为单位)。
- long tv_usec;:这个成员表示秒之后的微秒数。它也是一个长整型(long),但用于存储 0 到 999999 之间的值,表示在 tv_sec 所表示的秒之后,再过去多少微秒。
这两个成员结合起来,就可以精确地表示一个时间点或时间间隔,精确到微秒级别。
例如,如果你想要表示一个从 Unix 纪元开始算起,经过了 123 秒又 456789 微秒的时间点,你可以这样设置 struct timeval 结构体:
cpp
struct timeval time;
time.tv_sec = 123;
time.tv_usec = 456789;
这个结构体经常与 gettimeofday() 函数一起使用,以获取当前时间(从 Unix 纪元开始的时间,精确到微秒)。例如:
cpp
#include <sys/time.h>
#include <stdio.h>
int main() {
struct timeval now;
gettimeofday(&now, NULL);
printf("Current time: %ld.%06ld\n", now.tv_sec, now.tv_usec);
return 0;
}
这段代码会输出当前的时间,格式为秒数和微秒数(微秒数前面补零至6位)。
1.3.1.timeout参数的设定
这是一个输入型参数!!
- timeout: 这是一个指向timeval结构的指针,该结构用于设定select等待I/O事件的超时时间。结构定义如下:
timeout的设定有三种情况:
1.当timeout为NULL时,select会无限等待,直到至少有一个文件描述符就绪。
cppfd_set fds; FD_ZERO(&fds); FD_SET(0, &fds); // 假设监听标准输入 int ret = select(1, &fds, NULL, NULL, NULL); // 无限期等待 // 检查ret和fds...
2.当timeout设置为0时(即tv_sec和tv_usec都为0),select会立即返回,用于轮询。这个就是非阻塞轮询。
cppstruct timeval tv = {0, 0}; fd_set fds; FD_ZERO(&fds); FD_SET(0, &fds); // 假设监听标准输入 int ret = select(1, &fds, NULL, NULL, &tv); // 立即返回 // 检查ret和fds...
3.设置具体的时间,select将等待直到该时间过去或者有文件描述符就绪。
cppstruct timeval tv = {2, 500000}; // 2秒500毫秒 fd_set fds; FD_ZERO(&fds); FD_SET(0, &fds); // 假设监听标准输入 int ret = select(1, &fds, NULL, NULL, &tv); // 等待2.5秒或直到文件描述符就绪 // 检查ret和fds...
1.4.返回值
select函数的返回值有三种可能:
- 大于0:返回值表示就绪的文件描述符数量,即有多少文件描述符已经准备好进行I/O操作。
- 等于0:表示超时,没有文件描述符在指定时间内就绪。
- 小于0:发生错误。错误发生时,应使用perror或strerror函数来获取具体的错误信息。
1.5.select的工作流程
应用进程 和内核都需要从readfds和writefds获取信息,其中,内核需要从readfds和writefds知道哪些文件描述符需要等待,应用进程需要从readfds和writefds中知道哪些文件描述符的事件就绪.
如果我们要不断轮询等待文件描述符,则应用进程需要不断的重新设置readfds和writefds ,因为每一次调用select,内核会修改readfds和writefds,所以我们需要在 应用程序 中 设置一个数组 来保存程序需要等待的文件描述符,保证调用 select 的时候readfds 和 writefds中的将如下:
二,select版TCP服务器
接下来我们将用select来重新编写一下我们的TCP服务器。
2.1.编写准备
还记得TCP服务器怎么写吗?
为了节约我们的时间,我们复制一下我们之前封装好的Socket.hpp
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定义一些错误代码
enum
{
SocketErr = 2, // 套接字创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列的长度
const int backlog = 10;
class Sock //服务器专门使用
{
public:
Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字
{
}
~Sock()
{
// 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源
}
// 创建套接字
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
printf("socket error, %s: %d", strerror(errno), errno); //错误
exit(SocketErr); // 发生错误时退出程序
}
int opt=1;
setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //关闭后快速重启
}
// 将套接字绑定到指定的端口上
void Bind(uint16_t port)
{
//让服务器绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));//清零
local.sin_family = AF_INET; // 网络
local.sin_port = htons(port); // 我设置为默认绑定任意可用IP地址
local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) //让自己绑定别人
{
printf("bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 监听端口上的连接请求
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
printf("listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受一个连接请求
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
printf("accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd; // 返回新的套接字文件描述符
}
// 连接到指定的IP和端口------客户端才会用的
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;//服务器的信息
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
// 关闭套接字
void Close()
{
close(sockfd_);
}
// 获取套接字的文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 套接字文件描述符
};
首先我们要创建一个SelectServer.hpp,main.cc,makefile
makefile
cpp
select_server:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf select_server
SelectServer.hpp
cpp
#pragma once
#include<iostream>
#include"Socket.hpp"
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
class SelectServer
{
public:
SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~SelectServer()
{
listensock_.Close();
}
bool Init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void Start()
{
}
private:
uint16_t port_;//绑定的端口号
Sock listensock_;//专门用来listen的
std::string ip_; // ip地址
};
cpp
#include"SelectServer.hpp"
#include<memory>
int main()
{
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
}
2.2.SelectServer.hpp的编写
接下来我们就只剩下
cpp
class SelectServer{
void Start()
{
}
};
没有编写了。
我们可以看看我们之前编写的TCP服务器是怎么编写的。
cpp
void Start()
{
while(true)
{
std::string clientip;
uint16_t clientport;
int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字
if(socket<0)
continue;
//提供服务
if(fork()==0)
{
listensock_.Close();
//通过sockfd使用提供服务
std::string inbuf;
while (1)
{
char buf[1024];
// 1.读取客户端发送的信息
ssize_t s = read(sockfd, buf, sizeof(buf) - 1);
if (s == 0)
{ // s == 0代表对方发送了空消息,视作客户端主动退出
printf("client quit: %s[%d]", clientip.c_str(), clientport);
break;
}
else if (s < 0)
{
// 出现了读取错误,打印错误后断开连接
printf("read err: %s[%d] = %s", clientip.c_str(), clientport, strerror(errno));
break;
}
else // 2.读取成功
{
}
}
exit(0);//子进程退出
}
close(sockfd);//
}
}
我们发现,我们首先进行的就是accept啊!!那我们这里能不能里面进行accept呢?答案是不能的。accept本质就是检测并建立listen上面有没有新连接的到来。
还记得我们最开始讲的例子吗?
赵六去钓鱼,他是个小土豪,赵六手里拿了一堆鱼竿,目测有几百根鱼竿,赵六到达河边,首先就把几百根鱼竿每隔几米插上去,总共插了好几百米的鱼竿,然后赵六就依次遍历这些鱼竿,哪个鱼竿上的鱼漂动了,赵六就把这根鱼竿上的鱼钓上来,然后接下来赵六就又继续重复之前遍历鱼竿的动作进行钓鱼了。
这个新链接就是鱼啊!!!新连接的到来就相当于鱼咬钩了。所以我们处理新连接的时候就得采用IO多路复用思想。
如果是一个select服务器进程,则服务器进程会不断的接收有新链接,每个链接对应一个文件描述符,如果想要我们的服务器能够同时等待多个链接的数据的到来,我们监听套接字listen_sock读取新链接的时候,我们需要将新链接的文件描述符保存到read_arrys数组中,下次轮询检测的就会将新链接的文件描述符设置进readfds中,如果有链接关闭,则将相对应的文件描述符从read_arrys数组中拿走。
一张图看懂select服务器:
按照上面的思路,我们暂且写出了下面这个
SelectServer.hpp
cpp
#pragma once
#include<iostream>
#include"Socket.hpp"
#include<sys/select.h>
#include<sys/time.h>
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
class SelectServer
{
public:
SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~SelectServer()
{
listensock_.Close();
}
bool Init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void Start()
{
int listensock=listensock_.Fd();
struct timeval timeout ={5,0};
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock,&rfds);
int n=select(listensock+1,&rfds,NULL,NULL,&timeout);//刚开始的时候只有1个连接啊!!!
switch(n)
{
case 0:
std::cout<<"time out"<<std::endl;
break;
case -1:
std::cout<<"select error"<<std::endl;
break;
default:
//有事件就绪
break;
}
}
}
private:
uint16_t port_;//绑定的端口号
Sock listensock_;//专门用来listen的
std::string ip_; // ip地址
};
这里需要补充一些知识:
当一个新的连接请求到达监听套接字时,操作系统会接受这个请求(但不在用户空间的应用程序中立即处理它),并将监听套接字的状态标记为可读。这是因为从技术上讲,新的连接请求会导致监听套接字上有一些数据可读------具体来说,是新的连接的信息(例如,客户端的地址和端口号),这些信息将用于后续的 accept 调用。
当一个新的连接请求到达监听套接字时,操作系统会在底层进行一系列的操作来处理这个请求,并使得在用户空间的应用程序能够检测到这个新连接的存在。这个过程涉及到TCP/IP协议栈的多个层次,但我们可以从高层角度来简化地理解它。
- 监听套接字上的"可读"数据
当应用程序通过
listen
函数将套接字设置为监听状态时,它实际上是在告诉操作系统:"我准备好了,可以开始接受来自这个套接字的新连接了。"但是,listen
函数本身并不涉及任何阻塞操作,它只是改变了套接字的状态。现在,当一个新的TCP连接请求(通常来自客户端的
connect
调用)到达时,操作系统会检查是否有相应的监听套接字在监听这个端口。如果有,操作系统会为该新连接创建一个新的套接字(通常称为"已连接套接字"或"子套接字"),并保存与该连接相关的所有信息,包括客户端的地址和端口号。**然而,从用户空间的应用程序角度来看,这个新创建的套接字并不是直接可见的。相反,监听套接字上的"可读"状态会被触发,以指示有新的连接请求到来。**这里的"可读"状态并不是说监听套接字本身有任何用户数据可读(尽管在某些上下文中,套接字被视为文件描述符,可以像文件一样读取数据),而是说它现在"准备好"被
accept
调用以接收新的连接。
- accept 调用
当
select
或类似的多路复用函数(如poll
或epoll
)指示监听套接字上有数据可读时,应用程序就会知道有一个或多个新的连接请求正在等待被接受。此时,应用程序可以调用accept
函数来尝试接受这些连接。
accept
函数会从监听套接字的"等待队列"中取出一个新的连接请求,并基于这个请求创建一个新的套接字(即已连接套接字)。这个新套接字包含了与客户端通信所需的所有信息,包括客户端的地址和端口号。然后,accept
将这个新套接字的文件描述符返回给应用程序,以便它可以与客户端进行数据传输。
- 总结
因此,从技术上讲,监听套接字上的"可读"状态并不是指套接字上有实际的数据可读,而是指有新的连接请求等待被
accept
函数处理。这种机制允许应用程序在多个连接请求同时到达时有效地管理它们,而无需为每个连接都创建一个单独的线程或进程。
我们编译运行一下
我们看看
我们回去再看看我们运行情况
嗯?什么情况?为什么一直在打印time out?这个是因为timeout参数是个输入输出型参数
- 事实上,select函数后四个参数全部是输入输出型参数,兼具用户告诉内核 和 内核告诉用户消息的作用,
- 比如timeout参数,输入时,代表用户告知内核select监视等待fd时的方式,nullptr代表select阻塞等待fd就绪,当有fd就绪时,select才会返回,传0代表非阻塞等待fd就绪,即select只会遍历检测一遍底层的fd,不管有没有fd就绪,select都会返回,传大于0的值,代表在该时间范围内select阻塞等待,超出该时间select直接非阻塞返回。
- 假设你输入的timeout参数值为5s,如果在第3时select检测到有fd就绪并且返回时,内核会在select调用内部将timeout的值修改为2s,这就是输出型参数的作用,内核告知用户,timeout值为2s,select等待的时间为3s。
- 所以对应timeout参数,需要周期性的进行重新设置
我们现在需要修改一下代码
SelectServer.hpp
cpp
void Start()
{
int listensock=listensock_.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock,&rfds);
struct timeval timeout ={5,0};//注意这里
int n=select(listensock+1,&rfds,NULL,NULL,&timeout);//刚开始的时候只有1个连接啊!!!
switch(n)
{
case 0:
std::cout<<"time out"<<std::endl;
break;
case -1:
std::cout<<"select error"<<std::endl;
break;
default:
//有事件就绪
break;
}
}
}
现在我们再去编译一下
现在就不会变了。 一直为5秒了。
我们可以把timeout参数设置为nullptr参数,这样子代表,会一直阻塞到有新连接到来.
SelectServer.hpp
cpp
#pragma once
#include<iostream>
#include"Socket.hpp"
#include<sys/select.h>
#include<sys/time.h>
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
class SelectServer
{
public:
SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port)
{
}
~SelectServer()
{
listensock_.Close();
}
bool Init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void Start()
{
int listensock=listensock_.Fd();
for(;;)
{
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock,&rfds);
int n=select(listensock+1,&rfds,NULL,NULL,nullptr);
switch(n)
{
case 0:
std::cout<<"time out"<<std::endl;
break;
case -1:
std::cout<<"select error"<<std::endl;
break;
default:
//有事件就绪
std::cout<<"get a new link"<<std::endl;
break;
}
}
}
private:
uint16_t port_;//绑定的端口号
Sock listensock_;//专门用来listen的
std::string ip_; // ip地址
};
我们编译运行一下
我们发现程序怎么一直打印get a new link啊?这是因为我们没有把连接处理。
其实这个是select的特点:如果事件就绪,上层不处理的话,select会一直通知你!!!
如果select告诉我们就绪,接下来的一次读取,我们读取fd的时候,不会被阻塞
接下来我们就要来处理这个连接了!!!
我们需要澄清一些细节,因为
select
函数本身并不直接"知道"一个监听套接字(listening socket)何时有新的连接请求。然而,它确实能够检测到在监听套接字上有数据可读,这通常意味着一个新的连接请求已经到达。这里是如何工作的:
监听套接字 :当你使用
socket
函数创建一个套接字,并用bind
和listen
函数将它设置为监听状态时,这个套接字就开始等待传入的连接请求(即客户端发起的连接)。但是,listen
函数本身并不阻塞,它只是将套接字设置为接受连接请求的状态。使用 select 监视监听套接字 :在你的代码中,你将监听套接字的文件描述符
listensock
添加到select
调用的可读文件描述符集合中。select
函数将阻塞(除非指定了超时时间),直到以下任一情况发生:
- 可读文件描述符集合中的某个文件描述符变得可读。
- 可写文件描述符集合中的某个文件描述符变得可写(但在这个例子中没有使用)。
- 异常文件描述符集合中的某个文件描述符有异常条件(同样,没有使用)。
- 指定的超时时间到达(如果提供了超时时间)。
新的连接请求 :当一个新的连接请求到达监听套接字时,操作系统会接受这个请求(但不在用户空间的应用程序中立即处理它),并将监听套接字的状态标记为可读。这是因为从技术上讲,新的连接请求会导致监听套接字上有一些数据可读------具体来说,是新的连接的信息(例如,客户端的地址和端口号),这些信息将用于后续的 accept 调用。
select 返回 :由于监听套接字现在被标记为可读,
select
函数会返回,并且返回值会大于 0(表示有文件描述符就绪)。然后,你可以通过FD_ISSET(listensock, &rfds)
检查监听套接字是否确实在就绪的文件描述符集合中。接受连接 :如果
FD_ISSET(listensock, &rfds)
返回真,你就可以使用accept
函数来接受这个新的连接请求。accept
函数将创建一个新的套接字来与客户端通信,并将监听套接字返回到等待新连接请求的状态。所以,虽然
select
函数本身并不"知道"有新的连接请求,但它能够检测到监听套接字上何时有数据可读(这通常意味着有新的连接请求),并允许你的程序在适当的时候调用accept
函数来接受这个连接。
为了让代码看起来更好看一点,我们将处理连接这部分封装起来。
cpp
void HandlerEvent(fd_set& rfds)
{
// 检查监听套接字是否就绪
if(FD_ISSET(listensock_.Fd(),&rfds))
// 监听套接字上有新的连接请求
// 调用accept来接受连接
{
//我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字
//请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
if(sockfd<0)
return;
}
}
注意:
- FD_ISSET(fd, &set):此宏检查文件描述符fd是否已经被加入到set集合中。如果set中与fd对应的位为1,则返回非零值(真),否则返回0(假)。
现在有一个问题,
我们现在可不可以在后面使用read对socked直接进行读数据呢?
答案是不可以。!!!!!
为什么呢?
因为我们一旦调用read,万一客户端没有发数据过来,服务器进程就会阻塞在read这里!!这样子就会导致HandlerEvent函数调用不会返回,继而导致Start函数的循环阻塞,无法调用select函数监视新加入的连接。
注意:socket函数返回的文件描述符和accept返回的文件描述符是不一样的。
cppvoid HandlerEvent(fd_set& rfds) { //1.判断哪个读事件就绪 if(FD_ISSET(listensock_.Fd(),&rfds))// { //我们的连接事件就绪了 std::string clientip; uint16_t clientport; int sockfd=listensock_.Accept(&clientip,&clientport);//这里会返回一个新的套接字 //请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可 if(sockfd<0) return; std::cout<<"accept's fd:"<<sockfd<<std::endl; } } void Start() { int listensock=listensock_.Fd(); std::cout<<"socket's fd:"<<listensock<<std::endl; for(;;) { fd_set rfds; FD_ZERO(&rfds); FD_SET(listensock,&rfds); int n=select(listensock+1,&rfds,NULL,NULL,nullptr);//注意这里会修改rfds,返回的rfds有效位代表着哪个位置的有效事件就绪了 switch(n) { case 0: std::cout<<"time out"<<std::endl; break; case -1: std::cout<<"select error"<<std::endl; break; default: //有事件就绪 std::cout<<"get a new link"<<std::endl; HandlerEvent(rfds);//处理事件 break; } }
我们使用telnet来测试一下
很明显了。
我们发现服务器只有一个文件描述符是来监听新的连接的,接受新连接的时候是会有新的文件描述符用来进行网络通信的
我们发现
所以在accept函数后面我们不能直接调用read函数,而是将新连接加入到select中。
可是我们发现,我们的select和我们的accept在不同的函数里面,我们怎么让select来设置我们的文件描述符呢?这个时候我们就要设置一个辅助数组了。
2.2.1.为什么要设置辅助数组
- 原因一
我们不能在accept这里调用新的select
为什么?
- 一般都是在主循环处持续调用select,高效且简洁
- 如果使用多个select,会导致代码逻辑复杂化,也难以管理
- 所以,需要我们把这个新套接字的fd设置进刚才的select的位图中
- 这一过程就相当于在不断增加自己鱼竿的数量
但是,这两个数据在不同的函数中(我们在处理函数中获取新连接,而select的使用在主逻辑函数中),如何传递呢?
因为这两个函数都在类中,所以我们搞一个类内变量 -- 辅助数组
cppint fds_[def_max_num]; // 辅助数组
这里补充一个知识点:fd_set有多少个比特位
cppstd::cout<<"fd_set:"<<sizeof(fd_set)*8<<std::endl;
由于fd_set每个比特位代表一个连接,fd_set有1024位比特位,所以最多可以同时处理1024个连接
- 原因二
我们知道select的rfds参数是个输入输出型参数,而且应用进程和内核都可以设置readfds,应用进程设置readfds是为了通知内核去等待readfds中的文件描述符的读事件,而内核设置readfds是为了告诉应用进程哪些读事件生效,就像下面这样子。
也就是说每调用一次select函数,参数rfds会变化!!!但是再看看我们的代码,我们可是把select放在一个循环里面,这意味着会多次调用select,这样子下去,只有第一次调用select时监听的是我们想要监听的,后续的rfds都变化了,调用select就不是我们想要监听的了!!! 这就意味着,每调用一次select函数,就要重新设定一次rfds参数!!!
- 原因三
我们select函数的第一个参数 nfds: 这个参数是监控的文件描述符集合中最大文件描述符的值加1。可是在我们的代码里面,我们却直接填了一个listensock+1,这就固定死了监听范围,可是我们是写服务器,当新连接到来的时候,会产生新的文件描述符,如果select的第一个参数不变的话,我们就不能监听到这些新的文件描述符了。
所以这个select的第一个参数要通过计算来进行动态设置!!!
我们可以让新增的fd都添加进辅助数组中,然后让select每次动态设置max_fd,以及三个位图(新增操作在"处理函数"中介绍)
- 可以固定监听套接字(也就是我们创建的第一个套接字)作为数组的第一项,方便我们后续区分[获取新连接] 和 [读写事件]。
- 因为在过程中,可能会陆陆续续关掉一些文件(断开连接时),所以原本添加进的连续fd,会变成零零星星的,所以需要我们每次都重新整理一下这个数组,把有效的fd统一放在左侧,我们每次在循环开头就处理数组中的值,合法的fd就让它设置进位图中
- 不仅如此,在这个过程中,我们还可以找到fd中的最大值,来填充select的第一个参数
接下来我们就修改一下Start函数
cpp
void Start()
{
int listensock = listensock_.Fd();
fd_arry[0] = listensock; // 将监听套接字加入辅助数组
for (;;)
{
fd_set rfds;//每调用一次select函数rfds需要重新设定
FD_ZERO(&rfds);
int maxfd = fd_arry[0]; // 最大有效数组下标
for (int i = 0; i < fd_num_max; ++i)
{
if (fd_arry[i] == default_fd)
{
continue;
}
FD_SET(fd_arry[i], &rfds);
//注意辅助数组第一个元素是listen套接字,所以最开始的时候一定会执行到这里
if (maxfd<fd_arry[i])//如果有更大的文件描述符,就替换掉maxfd
{
maxfd = fd_arry[i];
}
}
int n = select(maxfd + 1, &rfds, NULL, NULL, nullptr); // 注意这里会修改rfds,返回的rfds有效位代表着哪个位置的有效事件就绪了
switch (n)
{
case 0:
std::cout << "time out" << std::endl;
break;
case -1:
std::cout << "select error" << std::endl;
break;
default:
// 有事件就绪
std::cout << "get a new link" << std::endl;
HandlerEvent(rfds); // 处理事件
break;
}
}
}
接下来需要修改一下我们的HandlerEvent函数,我们accept新连接后不能直接读取,会阻塞,我们需要将这个新连接加入我们的select函数的范围,这就需要我们借助辅助数组了
当我们识别到有事件就绪,获取连接后获得新套接字fd,之后就该将该fd设置进辅助数组中了
- 需要我们遍历数组,找到空位(值为-1/其他你设定的[数组内的初始值]),然后添加进去
- 但是要注意位图还有没有空位置(别忘了位图是有上限的)
- 所以,还需要加个判断
HandlerEvent函数
cpp
void HandlerEvent(fd_set &rfds)
{
// 1.判断哪个读事件就绪
if (FD_ISSET(listensock_.Fd(), &rfds)) //
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
// 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
if (sockfd <0)
return;
else // 把新fd加入位图
{
int i = 1;
for (; i < fd_num_max; i++)//为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
{
if (fd_arry[i] !=default_fd ) // 没找到空位
{
continue;;
}
else{//找到空位,但不能直接添加
break;
}
}
if (i != fd_num_max)//没有满
{
fd_arry[i] = sockfd;//把新连接加入数组
}
else // 满了
{
close(sockfd);//处理不了了,直接关闭连接吧
}
}
}
}
一旦有新连接的到来,我们就是只先把连接放到辅助数组里面。
为了方便大家观察,我们写一个测试函数。
cppvoid Printfd() { std::cout<<"online fd list: "; for(int i=0;i<fd_num_max;i++) { if(fd_arry[i]==default_fd) continue; std::cout<<fd_arry[i]<<" "; } std::cout<<std::endl; }
大家有没有发现,这个辅助数组里面的事件有两类啊!!!!就是[新连接]和[读写事件],如何区分fd集上的事件就绪究竟是[新连接]还是[读写事件]呢?
如何区分fd集上的事件就绪究竟是[新连接]还是[读写事件]呢?
- 前面我们提到,将监听套接字固定在数组第一项,就是为了区分两者,所以写个判断语句就行
HandlerEvent函数
cpp
void HandlerEvent(fd_set &rfds)
{
for (int n = 0; n < fd_num_max; n++)
{
int fd = fd_arry[n];
if (fd == default_fd) // 无效的
continue;
if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
{
// 1.是listen套接字就绪了
if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
// 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
if (sockfd < 0)
continue;
else // 把新fd加入位图
{
int i = 1;
for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
{
if (fd_arry[i] != default_fd) // 没找到空位
{
continue;
}
else
{ // 找到空位,但不能直接添加
break;
}
}
if (i != fd_num_max) // 没有满
{
fd_arry[i] = sockfd; // 把新连接加入数组
Printfd();
}
else // 满了
{
close(sockfd); // 处理不了了,直接关闭连接吧
}
}
}
// 2.是通信的套接字就绪了,fd不是listen套接字
else // 读事件
{
char in_buff[1024];
int n = read(fd, in_buff, sizeof(in_buff) - 1);
if (n > 0)
{
in_buff[n] = 0;
std::cout << "get message: " << in_buff << std::endl;
}
else if (n == 0) // 客户端关闭连接
{
close(fd);//我服务器也要关闭
fd_arry[n] = default_fd; // 重置数组内的值
}
else
{
close(fd);//我服务器也要关闭
fd_arry[n] = default_fd; // 重置数组内的值
}
}
}
}
}
我们做个实验
很完美啊
很好!!
我们也可以把处理过程单拎出来封装成两个函数
- 就相当于我们把收到的事件根据类型不同,派发给不同的模块进行处理
HandlerEvent函数
cpp
void Accept()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
// 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
if (sockfd < 0)
return;
else // 把新fd加入位图
{
int i = 1;
for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
{
if (fd_arry[i] != default_fd) // 没找到空位
{
continue;
}
else
{ // 找到空位,但不能直接添加
break;
}
}
if (i != fd_num_max) // 没有满
{
fd_arry[i] = sockfd; // 把新连接加入数组
Printfd();
}
else // 满了
{
close(sockfd); // 处理不了了,直接关闭连接吧
}
}
}
void Receiver(int fd, int i)
{
char in_buff[1024];
int n = read(fd, in_buff, sizeof(in_buff) - 1);
if (n > 0)
{
in_buff[n] = 0;
std::cout << "get message: " << in_buff << std::endl;
}
else if (n == 0) // 客户端关闭连接
{
close(fd); // 我服务器也要关闭
fd_arry[i] = default_fd; // 重置数组内的值
}
else
{
close(fd); // 我服务器也要关闭
fd_arry[i] = default_fd; // 重置数组内的值
}
}
void HandlerEvent(fd_set &rfds)
{
for (int n = 0; n < fd_num_max; n++)
{
int fd = fd_arry[n];
if (fd == default_fd) // 无效的
continue;
if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
{
// 1.是listen套接字就绪了
if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
{
Accept();
}
// 2.是通信的套接字就绪了,fd不是listen套接字
else // 读事件
{
Receiver(fd,n);
}
}
}
}
2.2.2.select的优缺点
select并不是多路转接中好的一个方案,当然这并不代表他是有问题的,只不过他用起来成本较高,要关注的点也比较多,所以我们说他并不是一个好的方案。
总的来说,select最重要的就是思维方式 -- 我们要将所有等待的过程都交给select
并且优缺点很明显
优点
- 确实实现了多路转接,可以等待多个fd
- 代码简单明了
缺点
- 比如select监视的fd是有上限的,我的云服务器内核版本下最大上限是1024个fd,主要还是因为fd_set他是一个固定大小的位图结构,位图中的数组开辟之后不会在变化了,这是内核的数据结构,除非你修改内核参数,否则不会在变化了,所以一旦select监视的fd数量超过1024,则select会报错。
- 除此之外,select大部分的参数都是输入输出型参数,用户和内核都会不断的修改这些参数的值,导致每次调用select前,都需要重新设置fd_set位图中的内容,这在用户层面上会带来很多不必要的遍历+拷贝的成本。
- 同时select还需要借助第三方数组来维护用户需要关心的fd,这也是select使用不方便的一种体现。而上面的这些问题,正是其他多路转接接口所存在的意义,poll解决了很多select接口存在的问题。
2.3.源代码
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 定义一些错误代码
enum
{
SocketErr = 2, // 套接字创建错误
BindErr, // 绑定错误
ListenErr, // 监听错误
};
// 监听队列的长度
const int backlog = 10;
class Sock //服务器专门使用
{
public:
Sock() : sockfd_(-1) // 初始化时,将sockfd_设为-1,表示未初始化的套接字
{
}
~Sock()
{
// 析构函数中可以关闭套接字,但这里选择不在析构函数中关闭,因为有时需要手动管理资源
}
// 创建套接字
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
printf("socket error, %s: %d", strerror(errno), errno); //错误
exit(SocketErr); // 发生错误时退出程序
}
int opt=1;
setsockopt(sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //服务器主动关闭后快速重启
}
// 将套接字绑定到指定的端口上
void Bind(uint16_t port)
{
//让服务器绑定IP地址与端口号
struct sockaddr_in local;
memset(&local, 0, sizeof(local));//清零
local.sin_family = AF_INET; // 网络
local.sin_port = htons(port); // 我设置为默认绑定任意可用IP地址
local.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) //让自己绑定别人
{
printf("bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
// 监听端口上的连接请求
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
printf("listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
// 接受一个连接请求
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
printf("accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd; // 返回新的套接字文件描述符
}
// 连接到指定的IP和端口------客户端才会用的
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;//服务器的信息
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
// 关闭套接字
void Close()
{
close(sockfd_);
}
// 获取套接字的文件描述符
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 套接字文件描述符
};
SelectServer.hpp
cpp
#pragma once
#include <iostream>
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>
const uint16_t default_port = 8877; // 默认端口号
const std::string default_ip = "0.0.0.0"; // 默认IP
const int fd_num_max = sizeof(fd_set) * 8;
const int default_fd = -1;
class SelectServer
{
public:
SelectServer(const uint16_t port = default_port, const std::string ip = default_ip)
: ip_(ip), port_(port)
{
for (int i = 0; i < fd_num_max; i++)
{
fd_arry[i] = -1; // 辅助数组所有元素都是-1;
}
}
~SelectServer()
{
listensock_.Close();
}
bool Init()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
return true;
}
void Accept()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd = listensock_.Accept(&clientip, &clientport); // 这里会返回一个新的套接字
// 请问进程会阻塞在Accept这里吗?答案是不会的,因为上层的select已经完成的了等待部分,accept只需要完成建立连接即可
if (sockfd < 0)
return;
else // 把新fd加入位图
{
int i = 1;
for (; i < fd_num_max; i++) // 为什么从1开始,因为我们0号下标对应的是listen套接字,我们不要修改
{
if (fd_arry[i] != default_fd) // 没找到空位
{
continue;
}
else
{ // 找到空位,但不能直接添加
break;
}
}
if (i != fd_num_max) // 没有满
{
fd_arry[i] = sockfd; // 把新连接加入数组
Printfd();
}
else // 满了
{
close(sockfd); // 处理不了了,直接关闭连接吧
}
}
}
void Receiver(int fd, int i)
{
char in_buff[1024];
int n = read(fd, in_buff, sizeof(in_buff) - 1);
if (n > 0)
{
in_buff[n] = 0;
std::cout << "get message: " << in_buff << std::endl;
}
else if (n == 0) // 客户端关闭连接
{
close(fd); // 我服务器也要关闭
fd_arry[i] = default_fd; // 重置数组内的值
}
else
{
close(fd); // 我服务器也要关闭
fd_arry[i] = default_fd; // 重置数组内的值
}
}
void HandlerEvent(fd_set &rfds)
{
for (int n = 0; n < fd_num_max; n++)
{
int fd = fd_arry[n];
if (fd == default_fd) // 无效的
continue;
if (FD_ISSET(fd, &rfds)) // fd套接字就绪了
{
// 1.是listen套接字就绪了
if (fd == listensock_.Fd()) // 如果是listen套接字就绪了!!!
{
Accept();
}
// 2.是通信的套接字就绪了,fd不是listen套接字
else // 读事件
{
Receiver(fd,n);
}
}
}
}
void Printfd()
{
std::cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (fd_arry[i] == default_fd)
continue;
else
{
std::cout << fd_arry[i] << " ";
}
}
std::cout << std::endl;
}
void Start()
{
int listensock = listensock_.Fd();
fd_arry[0] = listensock; // 将监听套接字加入辅助数组
for (;;)
{
fd_set rfds; // 每调用一次select函数rfds需要重新设定
FD_ZERO(&rfds);
int maxfd = fd_arry[0]; // 最大有效数组下标
for (int i = 0; i < fd_num_max; ++i)
{
if (fd_arry[i] == default_fd)
{
continue;
}
FD_SET(fd_arry[i], &rfds);
// 注意辅助数组第一个元素是listen套接字,所以最开始的时候一定会执行到这里
if (maxfd < fd_arry[i]) // 如果有更大的文件描述符,就替换掉maxfd
{
maxfd = fd_arry[i];
std::cout << "max_fd:" << maxfd << std::endl;
}
}
int n = select(maxfd + 1, &rfds, NULL, NULL, nullptr); // 注意这里会修改rfds,返回的rfds有效位代表着哪个位置的有效事件就绪了
switch (n)
{
case 0:
std::cout << "time out" << std::endl;
break;
case -1:
std::cout << "select error" << std::endl;
break;
default:
// 有事件就绪
std::cout << "get a new link" << std::endl;
HandlerEvent(rfds); // 处理事件
break;
}
}
}
private:
uint16_t port_; // 绑定的端口号
Sock listensock_; // 专门用来listen的
std::string ip_; // ip地址
int fd_arry[fd_num_max]; // 辅助数组------方便文件描述符在不同函数间传递
};
cpp
#include"SelectServer.hpp"
#include<memory>
int main()
{
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
}