阅读大概需要十分钟,绝对干货,看完还没搞懂你找我。


随便查一下,可以看到对FD_SET的说明如下:

一个long类型的数组,提供给select()机制使用的一种数据结构。主要功能是建立联系。其中每一个数组元素都能与任意一个打开的句柄(socket句柄、文件、命名管道、设备句柄等)建立联系。但是这种建立联系的工作是必须由程序员自己去完成的。

小白,比如像我这种就会纳闷,设置这种联系的目的是什么?

“可以理解为给打开的句柄添加了一种标识。( or or 异常 )的标识。暂且你就只需要知道我们可以通过fd_set(小写)去判断socket的操作即可。

在这里,我们提出以下几个问题,从简单的到稍微复杂的依次如下:

  1. fd_set是什么?
  2. FD_SET、FD_ZERO、FD_ISSET、FD_CLR的作用都是什么?
  3. 如何通过fd_set(结合select())判断句柄的状态?

本文就以上三个问题,回答和记录一下。实验环境(win10+vs2017+v141)

socket相关使用的文件头大致如下:

#include <iostream>
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")

1. fd_set是什么?

开篇我们就说了,fd_set是一个long类型的数组。我们可以认为这是一个很大的字节数组。

先来一小段代码理解一下fd_set这个数组。

代码1-1

int main()
{SOCKET socket = {0};  // 定义一个socket对象fd_set fdset = {0};     // 声明并定义,如果不赋初值,fd_set中存储的则是随机值FD_ZERO(&fdset);FD_SET(1, &fdset); // ’联系‘就是在这里产生的,以下4个操作会产生其他4个联系FD_SET(2, &fdset);FD_SET(3, &fdset);FD_SET(7, &fdset);FD_SET(socket, &fdset);int isset = FD_ISSET(socket, &fdset); // ’联系‘就是在这里产生的printf("isset = %d\n", isset); // isset = 1FD_CLR(socket, &fdset);isset = FD_ISSET(socket, &fdset); // isset = 0printf("isset = %d\n", isset);return 0;
}

调试截图如下:

可以看到,fd_set是一个长度为64的数组,由于代码进行了初始化,所以每一位都是0。在调用FD_SET的过程中,相当于vector.push_back的操作。

其中,到底有多少个set,则是通过fd_count来决定的。如上截图,虽然看似fd_array有效的值只有1、2、3、7,但实际上fd_count的值为5。这里不是没有绑定到scoket,而是因为socket被初始化为0了,所以实际上fdset变量保存的有效数组为[1,2,3,7,0]

2. FD_SET、FD_ZERO、FD_ISSET、FD_CLR的作用都是什么?

首先我们得知道,提供的以上四个宏接口(注意是宏接口)的作用肯定是用来操作fd_set的。具体作用如下所示:

代码2-1

// 这里的fd 实际使用都是以 句柄 传入
FD_ZERO(fd_set *fdset);              // 将set清零使集合中不含任何fd
FD_SET(int fd, fd_set *fdset);       // 将fd加入set集合
FD_CLR(int fd, fd_set *fdset);       // 将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset);     // 检测fd是否在set集合中,不在则返回0

正确的使用流程是:

调用FD_ZERO将一个 fd_set 变量的所有位设置为0。要开启描述符集中的一位,可以调用FD_SET。调用FD_CLR可以清除一位。最后,可以调用FD_ISSET测试描述符集中的一个指定位是否已打开。

还是结合1-1的代码:

  1. FD_ZERO就是把当前fd_set所有位的数字都置为0

  2. FD_SET实现了句柄和fd_set的联系,可以把fd(代码2-1),也就是句柄加入到fd_set中。

  3. FD_CLR清除所绑定的联系,注意注意:这里只清除你传进去的fdfd_set之间的联系。需要注意的是,FD_CLR的操作类似于链表节点的删除(后续节点会填补被删除节点)。例如第一个问题中的代码;

    代码2-2

     int isset = FD_ISSET(socket, &fdset); // printf("isset = %d\n", isset); // isset = 1FD_CLR(socket, &fdset);isset = FD_ISSET(socket, &fdset); // isset = 0printf("isset = %d\n", isset);
    

    代码2-2第3行,只是清除了前文FD_SET(socket, &fdset);绑定的联系,但是不涉及1、2、3、7fdset之间的联系。怎么判断这种联系?就是通过FD_ISSET

  4. FD_ISSET宏接口。如上代码(代码2-2)所示。如果绑定的联系在则返回1,反之,则返回0。

    • 在调用FD_ISSET之后,isset的值为 1 【LINE 2】
    • 调用FD_CLR之后,isset的值变为 0 【LINE 5】

3. 如何通过fd_set(结合select())判断句柄的状态?

要了解如何判断,还是得先回到select()函数
搬书《UNIX 环境高级编程》一书中 I/O多路转接 章节讲解的很清楚

select()函数原型:

代码3-1

int select(  int maxfdpl, fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict exceptfds,struct timeval *restrict typfr);
// 返回值∶准备就绪的描述符数目;若超时,返回0;若出错,返回-1

在所有POSIX 兼容的平台上,select 函数使我们可以执行I/O多路转接。传给 select 的参数告诉内核∶

  • 我们所关心的描述符;

  • 对于每个描述符我们所关心的条件(是否想从一个给定的描述符读,是否想写一个给定的描述符,是否关心一个给定描述符的异常条件);

  • 愿意等待多长时间(可以永远等待、等待一个固定的时间或者根本不等待)。

select 返回时,内核告诉我们∶

  • 已准备好的描述符的总数量;

  • 对于读、写或异常这3个条件中的每一个,哪些描述符已准备好。

使用这种返回信息,就可调用相应的 I/O函数(一般是 read 或 write),并且确知该函数不会阻塞。

  • socket非阻塞
    如果要设置socket为非阻塞的状态,则需要调用ioctlsocket(m_Socket, FIONBIO, &ul);来设置。其中的ul是一个unsigned long类型的变量,在此函数接口中,ul == 1表示设置当前的m_Socket为非阻塞状态。

本文主要关注的是select()函数中间的三个参数readfds、writefds、exceptfds(第一个参数也很重要)。这三个参数是指向描述符集的指针,描述符集说明了我们关心的 可读、可写、异常 的结合。如下图所示:

  1. select()的中间3个参数中的任意一个(或全部)可以是空指针,当你不需要进行操作判断读写异常的时候可以这么做。如果3个指针都是NULL,则select提供了比sleep更精确的定时器。(什么意思?sleep等待整数秒,而 select 的等待时间则可以小于1秒,其实际精度取决于系统时钟。)

  2. select()的第一个参数maxfdp1的意思是“最大文件描述符编号加1”。还是得先明白一个概念,即fd_set的每一位只能使用一次,只能标志一种状态。为避免发生重复应用的情况,如下代码,就需要通过第一个参数去控制。也就是第一个参数maxfdp1。那么第一个参数值如何选取?

    • 设置为FD_SETSIZE。这是<sys/select.h>的一个常量,它指定最大描述符数(通常是1024)。但是一般情况下,该数过于大,一般的程序也就是3~10个描述符。所以一般情况下,选择手动指定。
    • 手动指定,如下代码就属于手动指定。在所有的描述符集中,选择我们关注的最大的描述符数即可。下边代码中,指定的最大描述符数是3,因此select函数的第一个参数为4(= 3+1),即最大描述符编号值加1。

    代码3-2

    fd_set readset, writeset;FD_ZERO(&readset);FD_ZERO(&writeset);FD_SET(0, &readset);FD_SET(3, &readset);FD_SET(1, &writeset);FD_SET(2, &writeset);select(4, &readset, &writeset, NULL, NULL); // 该处的select就会返回-1

如代码3-2设置后的readset、writeset如下图所示:

select()有3个可能的返回值:

  1. 返回值-1表示出错。这是可能发生的,例如,在所指定的描述符一个都没有准备好时捕捉到一个信号。在此种情况下,一个描述符集都不修改。代码3-2就会返回-1
  2. 返回值0表示没有描述符准备好。若指定的描述符一个都没准备好,指定的时间就过了,那么就会发生这种情况。此时,所有描述符集都不修改。
  3. 一个正返回值说明了已经准备好的描述符数。该值时3个描述符集中已经准备好的描述符之和,所以如果通过描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3个描述符集中仍旧打开的位对应于已准备好的描述符。

对于“准备好”的含义要作一些更具体的说明。

  • 若对读集(readfds)中的一个描述符进行的 read操作不会阻塞,则认为此描述符是准备好的。
  • 若对写集(writefds)中的一个描述符进行的write 操作不会阻塞,则认为此描述符是准备好的。
  • 若对异常条件集(exceptfds)中的一个描述符有一个未决异常条件,则认为此描述符是准备好的。现在,异常条件包括∶在网络连接上到达带外的数据,或者在处于数据包模式的伪终端上发生了某些条件。(Stevens【1990】的15.10 节中描述了后一种条件。)
  • 对于读、写和异常条件,普通文件的文件描述符总是返回准备好。

一个描述符阻塞与否并不影响 select 是否阻塞,理解这一点很重要。也就是说,如果希望读个非阻塞描述符,并且以超时值为5秒调用 select,则 select 最多阻塞5s。相类似,如果指定一个无限的超时值,则在该描述符数据准备好,或捕捉到一个信号之前,select会一直阻塞。

如果在一个描述符上碰到了文件尾端,则select 会认为该描述符是可读的。然后调用 read,它返回0,这是 UNIX系统指示到达文件尾端的方法。(很多人错误地认为,当到达文件尾端时,select会指示一个异常条件。)

针对上述第3中情况,完整代码如下:

这里需要远端开一个服务,可以使用华为的IPOP工具

代码3-3

#include <iostream>
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{WSADATA wsa;WSAStartup(MAKEWORD(2, 2), &wsa);SOCKADDR_IN addrServer;SOCKET Socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);fd_set readset, writeset;addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.3.16");addrServer.sin_family = AF_INET;addrServer.sin_port = htons(6000);DWORD dwResult = connect(Socket, (SOCKADDR*)&addrServer, sizeof(SOCKADDR));FD_ZERO(&readset);FD_ZERO(&writeset);FD_SET(Socket, &readset);FD_SET(Socket, &writeset);/*int isset = FD_ISSET(Socket, &readset); // isset = 0printf("isset = %d\n", isset);isset = FD_ISSET(Socket, &writeset); // isset = 0printf("isset = %d\n", isset);*/int nRet = select(0, &readset, &writeset, NULL, NULL);cout << "The Ret of Select is " << nRet << endl;return 0;
}

输出:

The Ret of Select is 1

那么全篇都在说的 读、写、异常 是怎么判断的呢?

答案是FD_ISSET

  1. 在使用前我们通过FD_SET去建立这种读写异常的联系

  2. select()的时候会修改fd_set的值,而这个修改完之后的值就是我们可以拿去判断的东西。如代码代码3-3,我们可以在select之后增加判断条件

    if (!FD_ISSET(m_Socket, &readset))
    {cout << "sock not in readset!" << endl;
    }
    if (FD_ISSET(m_Socket, &writeset))
    {cout << "sock not in writeset!" << endl;
    }
    if (FD_ISSET(m_Socket, &exceptset))
    {cout << "getsockopt fail!" << endl;
    }
    

    这个时候就可以判断句柄的操作了。

废话不多说,直接上现场:

因为没有发生读操作,所以只标志了写的操作。

补充:

参考资料:

  1. 《unix高级环境编程》
  2. win32官方文档

以上就是关于fd_set的详细说明。

为避免误人子弟,如有误,还望评论或私信指正。✌✌✌

【一文搞懂】FD_SET的使用相关推荐

  1. 一文搞懂RNN(循环神经网络)

    基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...

  2. 一文搞懂 Python 的 import 机制

    一.前言 希望能够让读者一文搞懂 Python 的 import 机制 1.什么是 import 机制? 通常来讲,在一段 Python 代码中去执行引用另一个模块中的代码,就需要使用 Python ...

  3. python语言语句快的标记是什么_一文搞懂Python程序语句

    原标题:一文搞懂Python程序语句 程序流 Python 程序中常用的基本数据类型,包括: 内置的数值数据类型 Tuple 容器类型 String 容器类型 List 容器类型 自然的顺序是从页面或 ...

  4. 一文搞懂 Java 线程中断

    转载自   一文搞懂 Java 线程中断 在之前的一文<如何"优雅"地终止一个线程>中详细说明了 stop 终止线程的坏处及如何优雅地终止线程,那么还有别的可以终止线程 ...

  5. 一文搞懂HMM(隐马尔可夫模型)-Viterbi algorithm

    ***一文搞懂HMM(隐马尔可夫模型)*** 简单来说,熵是表示物质系统状态的一种度量,用它老表征系统的无序程度.熵越大,系统越无序,意味着系统结构和运动的不确定和无规则:反之,,熵越小,系统越有序, ...

  6. 一文搞懂如何使用Node.js进行TCP网络通信

    摘要: 网络是通信互联的基础,Node.js提供了net.http.dgram等模块,分别用来实现TCP.HTTP.UDP的通信,本文主要对使用Node.js的TCP通信部份进行实践记录. 本文分享自 ...

  7. 【UE·蓝图底层篇】一文搞懂NativeClass、GeneratedClass、BlueprintClass、ParentClass

    本文将对蓝图类UBlueprint的几个UClass成员变量NativeClass.GeneratedClass.BlueprintClass.ParentClass进行比较深入的讲解,看完之后对蓝图 ...

  8. 一文搞懂AWS EC2, IGW, RT, NAT, SG 基础篇下

    B站实操视频更新 跟着拉面学习AWS--EC2, IGW, RT, NAT, SG 简介 长文多图预警,看结论可以直接拖到"总结"部分 本文承接上一篇文章介绍以下 AWS 基础概念 ...

  9. 一文搞懂CAN FD总线协议帧格式

    目录 1.为什么会出现CAN FD? 2.什么是CAN FD? 3.CAN FD和CAN总线协议帧异同 4.解析CAN FD帧结构 4.1.帧起始 4.2.仲裁段 4.3.控制段 4.4.数据段 4. ...

  10. 一文搞懂 Traefik2.1 的使用

    原文链接:一文搞懂 Traefik2.1 的使用 一文搞懂 Traefik2.1 的使用 核心概念 安装 ACME 中间件 灰度发布 流量复制 TCP 简单 TCP 服务 带 TLS 证书的 TCP ...

最新文章

  1. 判断某数组是不是二叉树的前序遍历序列 python递归
  2. Hibernate(2)——Hibernate的实现原理总结和对其模仿的demo
  3. C#编写最小化时隐藏为任务栏图标的Window appllication
  4. ICCV 2019 《Robust Change Captioning》论文笔记(数据集)
  5. 怎么用python编贪吃蛇_少儿编程分享:手把手教你用PYTHON编写贪吃蛇(二)
  6. 用简单的方法构建一个高可用服务端
  7. Android应用的安全的攻防之战
  8. 企业实战_08_MyCat 搭建Mysql 一主二从复制环境
  9. java colormodel_ColorModel
  10. 计算机必备四大游戏,超大型游戏必备插件
  11. Centos 操作系统常用log日志
  12. GIS真正的魅力在哪?
  13. 和菜鸟一起学android4.0.3源码之vibrator振动器移植心得
  14. 面向对象程序设计C++学习之路2
  15. easyexcel实现代码生成xlsx文件并保存到云端
  16. 如何在Django中优雅的使用pyecharts设计可视化BI系统(多图表)
  17. 微信小程序生成普通网页的二维码
  18. 做微商可享社保补贴?微商的市场行情发展怎么样?
  19. 江门C语言培训,江门c语言编程学习,江门学c语言编程培训,江门学c语言编程效果怎么样...
  20. 传奇服务器充值系统,传奇服务端会员系统COM引擎传奇增加会员系统的方法

热门文章

  1. 一个留学中介的CRM案例
  2. STM32F103_study62_The punctual atoms(Clock system initialization function analysis)
  3. Elasticsearch:Script fields 及其调试
  4. Error: L6218E: Undefined symbol
  5. JeecgBoot商业版源码下载
  6. ORAN专题系列-12:从RIC中看传统电信设备商参与O-RAN的十大动机与机遇
  7. 针对ABCmouse的Xadmin管理端使用探究手册
  8. 链家租房数据抓取流程、分析
  9. 网站优化nofollow标签的作用,如何加nofollow标签
  10. html的nofollow标签,HTML中的A标签的nofollow属性解读