title: FD_SET的使用
description: FD_SET、FD_ZERO、FD_ISSET、FD_CLR 以及 select
随便查一下,可以看到对FD_SET
的说明如下:
本文就以上三个问题,回答和记录一下。实验环境(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 = 1
FD_CLR(socket, &fdset);
isset = FD_ISSET(socket, &fdset); // isset = 0
printf("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
正确的使用流程是:
还是结合1-1
的代码:
-
FD_ZERO
就是把当前fd_set
所有位的数字都置为0
。 -
FD_SET
实现了句柄和fd_set
的联系,可以把fd
,也就是句柄
加入到fd_set
中。 -
FD_CLR
清楚所绑定的联系,注意注意:这里只清楚你传进去的fd
和fd_set
之间的联系。需要注意的是,FD_CLR
的操作类似于链表节点的删除(后续节点会填补被删除节点)。例如第一个问题中的代码;代码
2-2
int isset = FD_ISSET(socket, &fdset); // printf("isset = %d\n", isset); // isset = 1 FD_CLR(socket, &fdset); isset = FD_ISSET(socket, &fdset); // isset = 0 printf("isset = %d\n", isset);
代码
2-2
第3行,只是清除了前文FD_SET(socket, &fdset);
绑定的联系,但是不涉及1、2、3、7
与fdset
之间的联系。怎么判断这种联系?就是通过FD_ISSET
-
FD_ISSET
宏接口。如上代码所示。如果绑定的联系在则返回1,反之,则返回0。- 在调用
FD_ISSET
之后,isset的值为 1 - 调用
FD_CLR
之后,isset的值变为 0
- 在调用
3. 如何通过fd_set
(结合select()
)判断句柄的状态?
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
本文主要关注的是select()
函数中间的三个参数readfds、writefds、exceptfds
(第一个参数也很重要)。这三个参数是指向描述符集的指针,描述符集说明了我们关心的 可读、可写、异常 的结合。如下图所示:
-
select()
的中间3个参数中的任意一个(或全部)可以是空指针,当你不需要进行操作判断读写异常的时候可以这么做。如果3个指针都是NULL
,则select
提供了比sleep
更精确的定时器。(什么意思?sleep等待整数秒,而 select 的等待时间则可以小于1秒,其实际精度取决于系统时钟。) -
select()
的第一个参数maxfdp1
的意思是“最大文件描述符编号加1”。还是得先明白一个概念,即fd_set
的每一位只能使用一次,只能标志一种状态。为避免发生重复应用的情况,如下代码,就需要通过第一个参数去控制。也就是第一个参数maxfdp1
。那么第一个参数值如何选取?代码
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
表示出错。这是可能发生的,例如,在所指定的描述符一个都没有准备好时捕捉到一个信号。在此种情况下,一个描述符集都不修改。代码3-2
就会返回-1
- 返回值
0
表示没有描述符准备好。若指定的描述符一个都没准备好,指定的时间就过了,那么就会发生这种情况。此时,所有描述符集都不修改。 - 一个
正返回值
说明了已经准备好的描述符数。该值时3个描述符集中已经准备好的描述符之和,所以如果通过描述符已准备好读和写,那么在返回值中会对其计两次数。在这种情况下,3个描述符集中仍旧打开的位对应于已准备好的描述符。
针对上述第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 = 0
printf("isset = %d\n", isset);
isset = FD_ISSET(Socket, &writeset); // isset = 0
printf("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
。
-
在使用前我们通过
FD_SET
去建立这种读写异常的联系 -
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; }
这个时候就可以判断句柄的操作了。
废话不多说,直接上现场:
因为没有发生读操作,所以只标志了写的操作。