C++网络编程select函数原理详解

一.select功能的引出

1. 文件描述符lfd和cfd

1.1 lfd是服务器端调用socket()函数创建的

sock = socket(PF_INET, SOCK_STREAM, 0);

上面的sock会传入listen函数的第一个参数,使得sock成为了监听套接字lfd——所以也相当于是listen的作用使得服务器套接字成为了监听套接字,之前没有指定具体功能。

int listen(int sock, int backlog);//成功时返回0,失败返回-1

第一个参数就是lfd,即监听套接字;backlog 是连接等待队列请求的长度,若为5,则表示最多5个连接请求进入队列——p65-66

即有5个客户端等着连接,服务器会按顺序一个一个处理连接

1.2 客户端请求连接的函数是connect()

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);//成功时返回0,失败返回-1
//sock,是客户端程序socket()函数创建的套接字,传入这里
//servaddr是保存目标服务器端地址信息的结构体变量的地址serv_addr
//addrlen是sizeof(servaddr),即指针变量长度

这里创建的并不是cfd,因为cfd和lfd是针对服务器说的。客户端那边就一种文件描述符,服务器端有两种,所以加以区分

1.3 服务器接收客户端的请求,需要调用accept()

这个accept()返回cfd,即负责与客户端进行I/O的套接字文件描述符

int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);//成功时返回cfd,失败返回-1
//第一个参数传入lfd
//第二个参数指向保存了发起连接请求的客户端地址信息

第一个参数要把lfd传进去

总的来说,lfd就是服务器端用于监听有没有客户端连接的套接字,或者说监听有没有来自客户端的connect();

cfd就是负责连接客户端,看看有没有来自客户端的read() / write()这种I/O请求

只有先lfd监听到客户端的连接请求,然后答应连接accept(),才会创建出cfd

要补充一下文件描述符表:

文件描述符的前三个:0, 1, 2分别被定死为 标准输入、标准输出、标准错误,所以其它文件描述符都是从3开始编号的,每个进程会有一个1024长的文件描述符表

一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符进行管理;通常默认大小为1024,也即能容纳1024个文件描述符;

2023.04.23补充下accept()创建的子套接字和前面监听套接字的区分:

int cfd = accept(int lfd, struct sockaddr* clnt_addr, socklen_t* addrlen);
注意第一个参数就是传入监听套接字,这个监听套接字监听到了新客户端连接,这个新客户端的地址和端口号会被保存在第二个参数clnt_addr里,以后这个客户端的数据的收发就由现在accpet创建的这个子socket(文件描述符为cfd)负责了,accept里面调用soaccept()函数在协议层获取到这个客户端的地址(TCP详解卷II),所以这个新创建的套接字它是能和该客户端对接的。新创建的socket没有进行IP和端口的绑定(后面没有调用bind了),因此子socket并不占有端口号,实际上它的IP和端口是直接复制的监听socket的,这个可能和之前说的端口号用于区分不同套接字相冲突,但是这个子socket又不对外监听,所以其实也不会混淆,不然socket的数量要收到端口号的限制了。
所以,如果收到的是请求连接的数据包(新客户端),则传给监听着连接请求端口的监听套接字lfd,然后进行accept处理;
如果是已经建立过连接的客户端的数据包,则将数据放入接收缓冲区,由之前的cfd套接字负责收发,哪个套接字知道该收哪个数据包呢?因为每个TCP连接都是四元组标记,它数据包前面有源IP和源端口,根据这个就知道当前这个数据包该由哪个cfd接收。——————————————————————————————
基于UDP的服务器的话,他都没有调用listen和accept两个函数,但是它这个套接字同样有IP+端口号,也是自己设定,毕竟也调用了bind()函数;因为UDP是无连接的,就所有上面所谓的cfd负责对接哪个客户端,它服务器就一个sokcet,然后发送和接收的时候,sendto()/recvfrom()需要在函数参数里传入IP+端口号,就是sockaddr*变量里存的,无连接的也就没有所谓的监听,只要IP和端口号对着,就能收发,所以UDP的一个套接字能给多个主机发送,也能接收来自多个主机的消息

2. 没有I/O多路复用时的服务器

多进程也好,多进程也好,在基于TCP/IP的服务器-客户端模型中,服务器总是在用listen()监听有没有要连接的客户端,或者调用了accept()函数并一直在阻塞,直到客户端有连接请求connect()。
在这里插入图片描述
用前面的lfd和cfd来简述的话: 服务器用一个lfd去监听客户端,有连接的话,对请求队列里的客户端依次答应连接请求,并创建cfd,来一个客户端就创建一个cfd,然后cfd和客户端进行I/O,而lfd继续去监听。——所以TCP/IP那本书把lfd比喻做门卫
在这里插入图片描述
存在的问题:

任何事件都要服务器自己做:监听客户端,数据处理。

然后能不能让内核也帮着处理一些事情

3. select的引出

IO多路复用也叫 IO多路转接

select之于服务器server,相当于秘书之于老板,select是由内核提供的。之前没有select时,server要一直accept()阻塞,等待有客户端的连接。

有了select之后,相当于给各个客户端留电话,谁有事就给秘书打电话,然后秘书告诉老板去调用accept(),创建cfd,和客户端建立连接。

select自己不会创建出cfd,与客户端连接。

所以select做的就是监听,lfd。其实请求连接本质是一个读事件,只是读到的数据是连接请求

在这里插入图片描述
服务器不再像以前那样一直accept()阻塞,在等着有客户端连接而是让select监听到有连接请求事件之后再调用accept()。

在多进程或多线程中,相当于开辟多个进程,每个都在那accept(),一个进程对应一个客户端,客户端有事就连接上了,客户端一直没事那该进程调用的accept()就一直在那阻塞,如下图左,有了IO多路复用,就可以一个进程连接多个客户端,如下右:
在这里插入图片描述
在这里插入图片描述

补充

服务器的服务相当于有三种:

阻塞:像前面说的调用了accept()一直在阻塞,有了连接请求就连上,结束阻塞,没有就一直阻塞

非阻塞忙轮询:不阻塞,一直去问有没有需要连接的,有就调用accept(),没有就一直问

响应式:别人有连接我再调用accept()——就是多路IO复用或者说多路IO转接

二. select函数参数和功能分析

#include<sys/select.h>
#include<sys/time.h>
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);//成功返回大于0的数,失败返回-1;这个大于0的数是发生事件的文件描述符数,返回0就是没有哪个文件描述符有事件
//maxfd,文件描述符数量,是最大文件描述符的值+1

前面说了,一个进程里文件描述符表是1024大的,而0, 1,2这几个文件描述符被标准输入、标准输出、标准错误占用了,比如我有三个客户端,首先服务器的lfd得占用3,客户端的三个cfd1, cfd2, cfd3则依次是 4, 5, 6,即最大文件描述符是6,那文件描述符的数量就是6 + 1 == 7

因为是文件描述符是从0开始的。
在这里插入图片描述

//fd_set是一个集合,fd_set的size值在linux下一般被定义为1024,意思是select管理的文件描述符数量不能大于1024,继而文件描述符取值为0~1023

//readset、 writeset、exceptset分别对应文件描述符的三种事件,分别是读事件,写事件,异常事件
//timeout是设置的超时时间,防止陷入无限阻塞

我们传入时,假如文件描述符3,5和 6 之前经常发生读事件,那我们把他们放入read_set里,监听他们的读(因为它们实际上并不是都会发生读事件)

同理,监听文件描述符6的写事件,放进write_set,监听文件描述符7的异常事件

他们每个集合,或者说fd_set,先暂且理解为就是一个数组,数组下标就是文件描述符0,1,2,3,4,…,如果要监听3和5的读事件,那就把下标为3,5和6处的值改为1,其余为0,然后作为readset传入select()函数第二个参数,同理,写事件,异常事件,也是这样:————位运算0和1
在这里插入图片描述
select()返回结果是这三个集合发生事件的总数,如下,文件描述符5和6发生了读事件,4发生了写事件,异常事件没有发生(即使我们监听了7,但它这回没发生异常):
在这里插入图片描述
这种情况,三个集合传入select后,返回整型 3(因为就3个文件描述符发生了事件)
显然,select()函数肯定会对我们传入的那几个集合进行修改,比如,我们传入的readset中,下标3,5和6对应的数是1,那fd3没发生,就将其改为0.
下面是fd2没发生,调用select()函数后将fd2所在数值改为0:
在这里插入图片描述
补充,如果我们只监听读事件,那除了readset,那另外两个参数我们传入空指针NULL即可,或者0

综上,select的使用流程如下:

在这里插入图片描述
即先将三种监视对应的fd_set写好,然后确定maxfd,然后设置timeout。
补充下第四个参数struct timeval* timeout计时的结构体:
在这里插入图片描述
实际上timeout的参数设置有三种情况:

1.传NULL,就是阻塞状态,一直等下去(不限时嘛)

2.设置一个大于0的时间timeval,就是等待固定时间

3.设置timeval为0,就是非阻塞,检查描述符集合后立刻返回,轮询。——即调用一下select,就去看看有没有事件,没有事件就返回,返回后过会又调用select,又去看有没有事件

三. select函数的相关函数来操作fd_set

fd_set这一集合并不是数组,而是位图(bitmap),这是一种数据结构。在网络编程中,对于位图的操作都会有相应的接口函数。因为他不像数组那样好操作。

如前所述,设置相应的fd_set是使用select的第一步

在fd_set中更改值的操作由下列****完成:

FD_ZERO(fd_set* fdset);//将fd_set变量的所有位初始化为0————zero
FD_SET(int fd, fd_set* fdset);//将待监听的文件描述符添加到监听集合中,其实就是将fd的值改为1————也就是所谓的注册文件描述符fd的信息
FD_CLR(int fd, fd_set* fdset);//清除文件描述符fd的信息,就是将1改为0————clear
FD_ISSET(int fd, fd_set* fdset);//判断一个文件描述符fd是否在监听集合fdset中,在则返回1,不在则返回0————is set

使用示例如下,首先我们要定义一个fd_set类型的变量set(它可以是readset,writeset、exceptset),将它的地址传入那些宏,所以注意取地址符
在这里插入图片描述
如果我们要监听fd1、fd2、fd4、fd5等四个文件描述符,那就需要调用四次FD_SET(),第一个参数就是对应的1, 2,4,5,会将这些位置的值改为1,这就是所谓的注册文件描述符的信息

FD_CLR本意就是clear,将fd的值清0,使用场景:比如我们监听文件描述符fd1,2,4,5,后来fd4断开连接了,我们读fd4发来的数据返回Null,就知道它断开连接了,那就需要把fd4从原来的监听集合中移除

FD_ISSET使用:FD_ISSET(4, &set);判断文件描述符fd4是否在监听集合set中,本意就是is set。——可以用来验证select函数的调用结果

  • 12
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值