目录
selectServer的不完善版本的基础框架(构造函数、析构函数和类成员)
selectServer的不完善版本的start函数(如何调用accept,或者说如何看待监听套接字listen_sock)
selectServer的整体代码、即完善版本的基础框架(构造函数、析构函数和类成员)和完善版本的start函数
当网卡或者键盘这些外设中有数据时,OS如何知道外设中有数据呢?
缩减版的epoll服务器(当前epoll服务器只是为了演示epoll_create、epoll_ctl、epoll_wait的用法)
select函数

如上图所示,select是系统提供的一个多路转接接口。在<<高级IO的相关知识点>>一文中说过,IO = 等待 + 拷贝数据,多路转接IO可以支持一次等待多个文件描述符就绪,并且等待的时间还是重叠的,所以select作为多路转接IO中的一种函数,select系统调用可以让我们的程序同时等待(或者说监视)多个文件描述符的上的事件是否就绪。select只负责等待(或者说监视),当监视的多个文件描述符中有一个或多个文件描述符的事件就绪时,select就会成功返回并将对应文件描述符的对应就绪事件告知调用者。
头文件说明:
- 上图红框中的头文件是新版,蓝框中的头文件是旧版,我们可以直接使用新版的,因为更方便,只包一个<sys/select.h>文件即可。
参数说明:
- nfds:select系统调用可以让我们的程序同时等待(或者说监视)多个文件描述符的上的事件是否就绪,其中nfds-1就是这多个文件描述符中的最大值,比如说,如果想等待5号和8号文件描述符,因为在这多个文件描述符中最大值是8,又因为nfds-1也表示这多个文件描述符中的最大值,所以8 = nfds -1,所以如果想等待5号和8号文件描述符,就需要将nfds设置成9。其实也很好理解,文件描述符本质就是PCB中的文件描述符表(或者说数组)的下标,select底层在等待(或者说监视)多个文件描述符时肯定是要遍历数组的,nfds就表示遍历的最大范围,设置最大范围是为了避免select遍历整个数组浪费CPU资源。
- timeout:如下图红框处所示,timeval结构体中有两个成员,time_t类型的成员表示秒,suseconds_t类型的成员表示微秒,假如有struct timeval x,x.tv_sec=2,x.tv_usec=1,则x整体就表示2秒+1微秒,即2.000001秒。我们让select系统调用在同时等待(或者说监视)多个文件描述符时,也可以选择不同的等待策略,比如可以选择以阻塞模式等待(体现在代码上就是让timeout指针指向nullptr)、可以选择以非阻塞模式等待(体现在代码上就是让timeout指针指向一个值为{0,0}的timeval结构体变量,值为{0,0}表示等待0秒+0微妙=0秒,等待0秒也就是没有进行等待,也就表示非阻塞了)、可以选择定时,在规定时间内则以阻塞模式等待,超时就直接返回(比如想定时为5秒,则体现在代码上就是让timeout指针指向一个值为{5,0}的timeval结构体变量,5秒+0微妙=5秒)。如果选择定时5秒,但只过了2秒就有文件描述符就绪了进而导致select函数返回,则此时timeout指针指向的timeval结构体变量的值就为{3,0},表示还剩余3秒+0微妙=3秒;如果超时了,则此时timeout指针指向的timeval结构体变量的值就为{0,0},表示还剩余0秒+0微妙=0秒。所以可以看到,timeout是一个输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。使用timeout时要注意,因为timeout是一个输入输出型参数,所以如果一开始设置定时5秒,但后来select函数返回时timeout只剩下3秒或者0秒,这时如果想要timeout重新变成5秒,则需要对timeout重新进行设置。

select函数剩下的3个参数readfds、writefds、exceptfds都是fd_set类型,同时也都是输入输入型参数。介绍一下fd_set类型和这3个参数,如下:
fd_set表示文件描述符集,和<<进程的信号>>一文中的信号集sigset_t类型一样,fd_set也是一个位图类型,在位图fd_set中,第一个位(把最低位称为第一个位)就表示0号文件描述符,第二个位就表示1号文件描述符....后序以此类推。
如果把readfds指针指向的fd_set位图变量中的第4个位设置为1,则表示需要让select函数关心3号文件描述符的读事件是否就绪(即关心3号文件描述符对应的struct file中的内核缓冲区中是否有数据可以让用户层拿走);如果把writefds指针指向的fd_set位图变量中的第4个位设置为1,则表示需要让select函数关心3号文件描述符的写事件是否就绪(即关心3号文件描述符对应的struct file中的内核缓冲区中是否有空间可以让用户层往其中输入数据);如果把exceptfds指针指向的fd_set位图变量中的第4个位设置为1,则表示需要让select函数关心3号文件描述符是否有异常事件,即是否发生了异常。说一下,一般来说调用select函数时是不会让select函数同时关心一个文件描述符的读事件和写事件的,举个例子,比如说把readfds指针指向的fd_set位图变量中的第4个位设置为1以让select函数关心3号文件描述符的读事件后,就不会再把writefds指针指向的fd_set位图变量中的第4个位设置为1以让select函数关心3号文件描述符的写事件。注意,虽然调用select函数时不会让select函数同时关心一个文件描述符的读事件和写事件,但能同时关心一个文件描述符的读事件和异常事件,也能同时关心一个文件描述符的写事件和异常事件。
注意, 和<<进程的信号>>一文中的信号集sigset_t类型一样,把fd_set位图变量中的第x个位设置为1时也不能自己编码进行位操作,而需要使用下图1红框处的系统提供的接口(这也是为了提高代码的跨平台性),其中FD_CLR函数表示把set指针指向的fd_set位图变量中的表示fd号文件描述符的位设置为0,比如fd为0时,就是把set指针指向的fd_set位图变量中的第1个位设置为0;FD_ISSET函数表示检查set指针指向的fd_set位图变量中的表示fd号文件描述符的位,如果位上为1,则返回真(即1),如果为0,则返回假(即0),如果给FD_ISSET函数传递的参数是错的,比如传给参数fd的值超出了fd_set位图中的比特位个数(如下图2所示,计算出的fd_set类型的大小为128字节,所以最多只有128*8=1024个比特位,如果传给参数fd的值等于1024,则1024号文件描述符需要在第1025个比特位上表示,此时就越界了,这就叫做传给参数fd的值超出了fd_set位图中的比特位个数;说一下,不同平台下的fd_set类型的大小可能不同),则返回一个负值;FD_SET函数表示把set指针指向的fd_set位图变量中的表示fd号文件描述符的位设置为1,比如fd为0时,就是把set指针指向的fd_set位图变量中的第1个位设置为1;FD_ZERO函数表示把set指针指向的fd_set位图变量中的所有比特位全设置为0。
- 图1如下。

- 图2如下。

如果把readfds指针指向的fd_set位图变量中的第4、第6、第7个位设置为1,则表示需要让select函数关心3号、5号、6号文件描述符的读事件是否就绪(即关心3号、5号、6号文件描述符对应的struct file中的内核缓冲区中是否有数据可以让用户层拿走),如果此时在这多个文件描述符中真的有文件描述符的读事件就绪,则select函数会立刻返回,比如说当5号、6号文件描述符的读事件同时就绪,则select函数会立刻结束并返回,此时readfds指针指向的fd_set位图变量中的第6(表示5号文件描述符)、第7(表示6号文件描述符)个位的值就继续为1,表示5、6号文件描述符的读事件就绪,但第4个位(表示3号文件描述符)的值就不为1而是为0了,表示3号文件描述符的读事件没有就绪;通过本段前面的叙述,我们就能推而广之地脑补出如果把writefds指针指向的fd_set位图变量中的第4、第6、第7个位设置为1,则后面会发生什么,同理,我们也能推而广之地脑补出如果把exceptfds指针指向的fd_set位图变量中的第4、第6、第7个位设置为1,则后面会发生什么。
说一下,在网络通信中,对于服务端进程通过accept函数获取到的服务套接字(不以严格的视角上看,是可以把套接字当作文件的)对应的文件描述符A,如果服务端进程将该文件描述符A设置进了select函数以让select函数等待该文件描述符A的读事件和异常事件就绪,则如果客户端进程此时调用close函数主动断开连接,服务端进程的select函数是能检测到文件描述符A的读事件就绪的。是的你没看错,就是读事件就绪,而不是异常事件就绪,这是因为客户端调用close函数发起4次挥手时本质就是在给服务端发送FIN标志位为1的TCP报文,所以服务端进程的select函数能检测到文件描述符A的读事件就绪。
注意,和select函数的struct timeval*类型的参数timeout一样,因为select函数剩下的3个fd_set*指针类型的参数readfds、writefds、exceptfds都是输入输出型参数,所以在select函数调用完毕时这3个指针指向的fd_set位图变量可能已经被修改了,所以在进入下一次循环前,如果想要select函数继续关心某些文件描述符的读、写、异常事件是否就绪,一定别忘了重新对这3个指针指向的fd_set位图变量进行设置。
走到这里我们就对select函数剩下的3个fd_set*指针类型的参数readfds、writefds、exceptfds进行了深刻地理解,这里我们再对它们做一个简单的总结,如下:
- readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪。
- writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪。
- exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪。
返回值说明:
- select系统调用可以让我们的程序同时等待(或者说监视)多个文件描述符的上的事件就绪,在等待的这多个文件描述符中,如果只有一个就绪,则select函数就会调用成功并返回1,如果同时有多个文件描述符就绪,则有几个就绪,返回值就是几。
- 如果返回值是0,则说明select函数是以定时模式调用,并且超时了,这时返回值就是0。
- 如果返回值是-1,则说明select函数发生了某些异常,调用失败,一般发生这种情况都是传给select函数的参数非法造成的。
select服务器
selectServer的不完善版本的基础框架(构造函数、析构函数和类成员)
如下代码所示,selectServer的基础框架就是一个正常的TCP服务端的基础框架,写过很多次了,就不再赘述。
需要注意的是,select服务器我们只编写read读取的逻辑,写入和异常不做处理,这是为了图方便,但不必担心,因为select服务器并不常用,最常用的是epoll服务器,我们会在编写epoll服务器时将代码补充完整。
#include<iostream>
using namespace std;
#include <sys/select.h>
#include <string.h>
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体
//select服务器我们只编写read读取的逻辑,写入和异常不做处理,这是为了图方便,但不必担心,因为select服务器并不常用,最常用的是epoll服务器,我们会在编
//写epoll服务器时将代码补充完整。
class selectServer
{
public:
selectServer(uint16_t port)
{
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_listen_sock < 0)
exit(1);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr("0.0.0.0");
if (bind(_listen_sock, (sockaddr*)&local, sizeof(local)) < 0)
exit(2);
if (listen(_listen_sock, 10) < 0)
exit(3);
}
~selectServer()
{
if(_listen_sock > 0)
close(_listen_sock);
}
private:
int _listen_sock;
uint16_t _port;
};
selectServer的不完善版本的start函数(如何调用accept,或者说如何看待监听套接字listen_sock)
如何在selectServer的start函数中调用accept,或者说如何看待监听套接字listen_sock呢?
- 实际上监听套接字对应的文件描述符listen_sock和其他文件描述符的工作模式都是一样的。举个例子,对于其他普通文件的文件描述符,当内核缓冲区中没有数据时上层调用read就会阻塞,当内核缓冲区中有数据时上层调用read才能读取到数据;而对于监听套接字对应的文件描述符,虽然上层不是从该文件描述符对应的struct file中的内核缓冲区中读取数据,但上层是需要调用accept函数从该文件描述符对应的struct file中的连接队列里获取连接对象的,当连接队列中没有连接对象时上层调用accept就会阻塞,当连接队列中有连接对象时上层调用accept才能获取到连接对象。所以综上可以发现,进程调用accept函数获取新连接的过程本质也是IO的过程,在这一点上监听套接字listen_sock和其他文件描述符都是一样的。
- 然后注意,因为当前所编写的select服务器旨在让selectServer服务端进程能同时等待多个文件描述符,所以对于selectServer的start函数,是不能像普通的TcpServer的start函数一样直接调用accept函数的,这是因为如果此时并没有客户端和selectServer服务端3次握手建立连接成功,那么监听套接字listen_sock的连接队列里就没有连接对象,那么调用accept函数就会导致selectServer进程在accept函数处陷入阻塞,导致selectServer进程只能等待(或者说监视)监听套接字listen_sock的读事件就绪,而无法做任何其他事情,其中就包括等待(或者说监视)其他的文件描述符是否就绪(比如这时如果有某个文件描述符A对应的struct file中的内核缓冲区中有数据可以让上层拿走,但因为selectServer进程在accept函数处阻塞了,没有空闲等待或者说监视该文件描述符A,所以上层也就不会知道A的内核缓冲区就绪了),这就违背了select服务器的初衷了,所以对于selectServer的start函数而言不能像普通的TcpServer的start函数一样直接调用accept函数。
- 额外说一下,对于上一段的内容可能有人会说,【之所以不能让selectServer进程直接调用accept函数以避免该进程陷入阻塞进而避免该进程无法等待或者说监视其他的文件描述符是否就绪,就是因为当下的编程模式是单进程的,我们为什么不创建子进程或者新线程呢?比如创建后,我们就可以让主进程直接调用accept函数,这样即使主进程陷入阻塞了,因为有其他子进程或者新线程在调用read函数等待其他的文件描述符就绪,所以也能让selectServer服务端进程能同时等待多个文件描述符?】,这里笔者想说的是,如果selectServer服务端进程也按照多进程或者多线程的模式编写,那selectServer服务端进程和咱们以前编写的【多进程版版本、多线程版本的TcpServer服务端进程】就没有任何区别了,体现不出来selectServer的独特之处了。独特在哪呢?答案:selectServer是可以在不创建任何子进程或者新线程的情况下(即在单进程的情况下)同时等待多个文件描述符就绪的,并且还能同时为多个客户端提供服务,换言之,TcpServer服务器通过多个进程才能做到的事现在让selectServer服务器去做,selectServer服务器只需要一个进程即可做到,这就是selectServer的优势和独特之处。
既然如此,那如何在selectServer的start函数中调用accept呢?
因为select系统调用可以让我们的进程同时等待(或者说监视)多个文件描述符的上的事件是否就绪,所以我们可以把监听套接字listen_sock也添加进select函数中,让select函数帮当前进程等待(或者说监视)监听套接字对应的struct file中的连接队列就绪,在连接队列没有就绪时,因为select函数可以被设置成非阻塞模式或者定时模式,所以这时当前进程就可以趁机处理一些其他的业务,根据上面的理论,我们可以暂时编写出如下代码(注意该代码只是半成品):
#include<iostream>
using namespace std;
#include <sys/select.h>
#include <string.h>
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体
//select服务器我们只编写read读取的逻辑,写入和异常不做处理,这是为了图方便,但不必担心,因为select服务器并不常用,最常用的是epoll服务器,我们会在编
//写epoll服务器时将代码补充完整。
class selectServer
{
public:
selectServer(uint16_t port)
{
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if(_listen_sock < 0)
exit(1);
sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr("0.0.0.0");
if (bind(_listen_sock, (sockaddr*)&local, sizeof(local)) < 0)
exit(2);
if (listen(_listen_sock, 10) < 0)
exit(3);
}
~selectServer()
{
if(_listen_sock > 0)
close(_listen_sock);
}
void start()
{
fd_set fs;
FD_ZERO(&fs);
FD_SET(_listen_sock, &fs);
while(1)
{
//t作为select函数的输入输出型参数,在select函数结束后是有可能被改变的,所以如果想要select函数每次都等待5秒,即如果想要t一直等于5秒,则需要把定义t的代码
//放到while循环中。
timeval t = {5,0};
int x = select(_listen_sock+1, &fs, nullptr, nullptr, &t);
switch(x)
{
case 0:
cout<<"超时time out,没有文件描述符就绪,可以在这个分支里趁机处理一会其他的业务"<<endl;
break;
case -1:
cout<<"select error"<<endl;
break;
default:
cout<<"select success,有"<<x<<"个文件描述符就绪"<<endl;
if (FD_ISSET(_listen_sock, &fs) > 0)//如果是监听套接字的连接队列就绪,则accept获取连接对象(或者说获取服务套接字)
{
cout<<"get a new link"<<endl;
sockaddr_in peer;
socklen_t len = sizeof(peer);
int service_sock = accept(_listen_sock, (sockaddr*)&peer, &len);
if (service_sock < 0)
{
cout<<"accept error"<<endl;
//注意该break是终止switch的,而不是终止while循环的
break;
}
else
{
//accept获取一个服务套接字(或者说连接对象)后,可以直接对该服务套接字进行read/recv吗?
/*
未完待续
*/
}
}
//如果是服务套接字的内核缓冲区就绪,则直接进行read/recv,此时调用read/recv是不会被阻塞的。
/*
未完待续
*/
break;
}
}
}
private:
int _listen_sock;
uint16_t _port;
};
在上面代码的注释中提出了一个问题,我们需要先回答这个问题,然后根据问题的答案的思路才能继续完善上面的半成品代码,问题为:accept获取一个服务套接字(或者说连接对象)后,可以直接对该服务套接字进行read/recv吗?
答案是不行,因为服务套接字的内核缓冲区可能没有数据,这时调用read/recv就会导致当前进程陷入阻塞,这样一来当前进程就无法做任何其他事情了,其中就包括等待(或者说监视)其他的文件描述符是否就绪,这就违背了select服务器的初衷,所以我们不能冒然调用read/recv,而要在明确地知道该服务套接字的内核缓冲区中有数据时才能调用read/recv,这样一来才能避免read/recv导致当前进程陷入阻塞。那么谁才能让当前进程明确地知道该服务套接字的内核缓冲区中有数据呢?也只能靠select函数,我们可以在accept获取一个服务套接字后就立刻将该套接字设置进select函数中(即立刻让select函数关心该套接字的读事件是否就绪),然后下次循环时再通过select函数等待该服务套接字的内核缓冲区就绪。于是现在就有3个问题需要解决:
- 1、进入下一次循环时如何让当前进程知道该服务套接字的值是多少以将该套接字设置进select函数呢?
- 2、在不断循环的过程中,随着accept获取的服务套接字越来越多,我们就需要将越来越多的服务套接字文件描述符设置进select函数中,注意select函数的第一个参数nfds是等于用户往select函数中设置的文件描述符的最大值加一的,当服务套接字越来越多时,多个文件描述符中的最大值也是会越来越大的,这时传给select函数的参数nfds的值就也应该越来越大,换言之,传给nfds参数的值不能像上面的代码一样是个固定的值listen_sock+1,而应该动态计算出传给nfds的值(再举个例子证明这一点,比如如果最初需要让select函数监视的文件描述符是1、3、4、7,后来如果不需要监视7了,这时就需要将nfds的值设置成4+1=5),那么如何让当前进程知道在本次循环中需要让select函数监视的多个文件描述符中的最大值是多少从而计算出传给参数nfds的值呢?
- 3、在上文讲解select函数时说过,和select函数的struct timeval*类型的参数timeout一样,因为select函数剩下的3个fd_set*指针类型的参数readfds、writefds、exceptfds都是输入输出型参数,所以在select函数调用完毕时这3个指针指向的fd_set位图变量可能已经被修改了,所以在进入下一次循环前,如果想要select函数继续关心某些文件描述符的读、写、异常事件是否就绪,是需要重新对这3个指针指向的fd_set位图变量进行设置的,注意因为在上文中说过对于select服务器我们只编写read读取的逻辑,写入和异常不做处理(这是为了图方便,但不必担心,因为select服务器并不常用,最常用的是epoll服务器,我们会在编写epoll服务器时将代码补充完整),所以这里只需要重新对readfds指针指向的fd_set位图变量进行设置,那么如何让当前进程知道在本次循环中哪些文件描述符需要继续被设置进select函数的readfds中呢?
对于以上3个问题,我们的解决思路如下:
- 在selectServer类中设置一个数组成员int fd_arry[NUM],NUM是#define NUM ((sizeof(fd_set))*8),为什么数组上限是NUM呢?fd_set位图类型的变量只有NUM个比特位,每个比特位都用于映射一个文件描述符,比如第一个比特位(把最低位称为第一个比特位)映射0号文件描述符、第二个比特位映射1号文件描述符....以此类推,所以只有位于左闭右闭区间【0号,NUM-1号】中的文件描述符能被映射进fd_set位图类型的变量中,即最多有NUM个文件描述符能被映射进fd_set位图类型的变量中,既然fd_arry数组是用于保存应被设置进fd_set位图变量中的文件描述符的,而我们又知道最多有NUM个文件描述符能被映射进fd_set位图类型的变量中,那么当然数组fd_arry的上限是NUM了。注意虽然说数组的存储上限是能存NUM个文件描述符,但并不是说只要数组没有被存满,就能存储accept到的服务套接字service_sock,举个例子,如果进程在调用accept函数获取服务套接字前就open打开了许多文件,比如打开了100个文件(其中不包括自动打开的0、1、2号文件描述符对应的标准输入/输出/错误文件),则再调用accept函数获取到的文件描述符就会从104开始(从104开始是因为除了0、1、2,还有一个监听套接字占用了某个文件描述符,所以在accept之前共有104个文件描述符,那么accept获取到的文件描述符就是第105个文件描述符,所以该文件描述符就是104号了),那么很显然,如果数组从104号文件描述符开始往后存储NUM个文件描述符,一定会有很多超过区间【0号,NUM-1号】的非法文件描述符被存进数组中,注意因为这些非法文件描述符不在区间内,所以它们是无法被映射进位图中的,但编码的逻辑又是把数组中的所有文件描述符映射进位图中,所以这些非法文件描述符被存进数组中就有问题,所以不能只要数组没有被存满,就将accept获取的service_sock存进数组中,所以要注意在编码时设置好if判断处理这一点。
- 设置好数组成员fd_arry后,然后让该数组在每次循环结束前都把accpet获取到的服务套接字存起来,以让当前进程在进入下一次循环时知道该服务套接字的值是多少进而将该服务套接字设置进select函数的readfds中;同时让该数组在每次循环结束前都把需要继续被等待(或者说监视)的文件描述符给记录下来,这样一来,在进入下一次循环后,就能通过遍历数组计算出需要重新传给nfds参数的值,就能通过遍历数组知道哪些文件描述符需要重新被设置进select函数的readfds中。(至于为什么不用vector而用原生数组,是为了暴露一些select服务器的问题出来,如果使用vector,则存在封装,不容易把问题暴露出来,这一点会在讲解select服务器的缺点时说明)
- 对于这个fd_arry数组,我们首先将数组中的所有值都初始化成-1,规定如果某下标上存的值是-1,则表示该位置还没有存储任何文件描述符,如果某下标上存的值不是-1(因为文件描述符不可能小于0,所以如果某下标上的值不是-1,则一定是大于等于0的数字),则认为该下标上存了文件描述符,然后规定0号下标上的值永远是监听套接字。这两个规定体现在代码上就是,我们在调用socket函数创建监听套接字后要立刻将监听套接字对应的文件描述符存进数组的0号下标上,后序每accept成功获取到一个服务套接字后,就立刻遍历fd_arry数组将该服务套接字对应的文件描述符存进第一个遇到的值为-1的位置(即存进第一个表示没有存储文件描述符的位置),从这里我们也能看出来数组fd_arry的下标号和文件描述符的号数不具有任何映射关系(即0号下标不一定存0号文件描述符,1号下标不一定存1号文件描述符.....),而是该下标上存储的元素的值是几,就存储的是几号文件描述符。
selectServer的整体代码、即完善版本的基础框架(构造函数、析构函数和类成员)和完善版本的start函数
根据上面的思路,我们就能完善selectServer类的基础框架,并编写出完整的start函数的代码,如下所示。
#include<iostream>
using namespace std;
#include <sys/select.h>
#include <string.h>
#include <unistd.h>//提供close
//以下四个文件被称为网络四件套,包含后绝大多数网络接口就能使用了
#include<sys/types.h>//系统库,提供socket编程需要的相关的接口
#include<sys/socket.h>//系统库,提供socket编程需要的相关的接口
#include<netinet/in.h>//提供sockaddr_in结构体
#include<arpa/inet.h>//提供sockaddr_in结构体
#define NUM (sizeof(fd_set)*8)
//需要注意的是,select服务器我们只编写read读取的逻辑,写入和异常不做处理,这是为了图方便,但不必担心,因为select服务器并不常用,最常用的是epoll服务器,我们会在编
//写epoll服务器时将代码补充完整。
class selectServer
{
public:
selectServer(uint16_t port)
{
//初始化用于存储文件描述符的数组,将其中每个值都设置成-1,-1表示该位置上没有文件描述符,如果某个位置上为3,则表示该位置存储的是3号文件描述符
for(int i=0; i<NUM; i++)
_fd_arry[i] = -1;
_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
//在文中说过,规定0号下标上永远存储监听套接字对应的文件描述符
_fd_arry[0] = _listen_sock;
//让服务端进程能在TIME_WAIT状态下重复绑定同一个端口号
int opt = 1;
setsocko

本文详细介绍了select、poll和epoll三种服务器。阐述了select函数的参数、返回值等,给出selectServer的代码及测试,分析其优缺点;介绍pollServer的代码、测试和优缺点;深入讲解epoll原理、相关函数,给出缩减版和完整版epoll服务器代码及测试。
最低0.47元/天 解锁文章
1564

被折叠的 条评论
为什么被折叠?



