windows Socket编程之select网络模型以及fd_set介绍

本文介绍了一种提高网络服务效率的方法——select模型。通过使用fd_set结构体管理多个客户端连接,并利用select函数实现非阻塞编程,有效避免了大量线程开销。文章详细解释了select函数的工作原理,并给出了具体示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在此之前呢,介绍了TCP/UDP的服务端的实现。但是,它们有很大的缺点,比如说,效率很低,开销太大等。因此,接下来我们先介绍select网络模型。

我们在TCP的服务端里边,接收一个客户端的时候,我们调用accept函数,这个函数会返回一个客户端的socket,我们在主线程里边不停的接收客户的连接,每当有客户连接时,我们就会在开一个线程,用于对客户的服务。因此,如果有N个的客户进行连接的话,那么线程数量就会有N+1个(N个服务线程+主线程),若N比较大,则线程就会非常多,以至于将整个电脑都给拖垮掉。而我们的select模型呢,就是为了解决这个问题而设计的。
 

一、fd_set结构体

结构体:

typedef struct fd_set {
  u_int    fd_count;                 // 有多少个socket
  SOCKET   fd_array[FD_SETSIZE];     // 客户端socket数组,FD_SETSIZE为64
} fd_set;

 系统提供了FD_SET, FD_CLR, FD_ISSET, FD_ZERO进行操作,声明如下:

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 *fdset);              //将set清零使集合中不含任何fd

下面写一段程序探究一下这几个宏的工作:

#include <WINSOCK2.H>  
int main()
{
    fd_set fdset;
    FD_ZERO(&fdset);
    FD_SET(1, &fdset);
    FD_SET(2, &fdset);
    FD_SET(3, &fdset);
    FD_SET(7, &fdset);
    int isset = FD_ISSET(3, &fdset);
    printf("isset = %d\n", isset);
    FD_CLR(3, &fdset);
    isset = FD_ISSET(3, &fdset);
    printf("isset = %d\n", isset);
    return 0;
}

 当使用FD_SET添加完1、2、3、7后,fdset的值如下:

然后经过FD_CLR以后,fd_array[2]就被清除了,数组后面的数据依次往前提,即7被放到了fd_array[2]
所以isset前后两次打印的值分别为1和0

 

二、select函数

1.用途

在编程的过程中,经常会遇到许多阻塞的函数,好像read和网络编程时使用的recv, recvfrom函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这是就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。

 select函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。

2.大致原理

 select需要驱动程序的支持,驱动程序实现fops内的poll函数。select通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。详细的原理请看:https://blog.youkuaiyun.com/liitlefrogyyh/article/details/52104120

3.selec函数声明如下:

int select(  
  int nfds,                            //已经忽略了
  fd_set FAR *readfds,                 //可读fd_set的地址
  fd_set FAR *writefds,                //可写fd_set地址
  fd_set FAR *exceptfds,               //异常错误fd_set地址
  const struct timeval FAR *timeout    //timeval结构
);

使用select函数的一般过程:

先调用宏FD_ZERO将指定的fd_set清零,然后调用宏FD_SET将需要测试的fd加入fd_set,接着调用函数select测试fd_set中的所有fd,最后用宏FD_ISSET检查某个fd在函数select调用后,是否还在这个集合fd_set中。

 

接下来看下它是如何实现的

首先,TCP的服务端一样,它需要初始化环境,然后执行绑定,监听等操作。但是之后,我们会直接开一个线程来对客户进行服务,然后才是我们原来的一个循环,来接待客户的连接

当我们调用accept来获取客户端的连接之后,会调用FD_SET这个宏,它实际上是会将我们的客户那个socket保存到fd_array这个数组里边去,因为这个数组最大为64个,所以最多只能有64个客户端进行连接。
我们把服务客户的那个线程叫做工作者线程,在里边我们会调用一个函数叫做select。

这个函数会检查fd_array这个数组里边所有的socket是否有信号到来,如果有就成功返回,否则会阻塞在这里,不过我们在最后一个参数那里,传一个等待时间。

select的结果会对fd_set造成影响,select之后select将更新这个集合,把其中不可读的套节字去掉,只保留符合条件的套节字在这个集合里面 。

调用完select之后,我们可以在调用FD_ISSET这个宏来判断是fd_array这个数组里边的那个socket有信号了。之后我们就可以进行数据收发了。

4.select的示例代码

#include <winsock2.h>
#include <stdio.h>
#define PORT 6000
#pragma comment (lib, "Ws2_32.lib")
fd_set  g_fdClientSock;
int clientNum = 0;
 
BOOL WinSockInit()
{
	WSADATA data = {0};
	if(WSAStartup(MAKEWORD(2, 2), &data))
		return FALSE;
	if ( LOBYTE(data.wVersion) !=2 || HIBYTE(data.wVersion) != 2 ){
		WSACleanup();
		return FALSE;
	}
	return TRUE;
}
// 工作者线程 
DWORD WINAPI WorkThreadProc(LPARAM lparam)
{
	fd_set fdRead;
	FD_ZERO( &fdRead );
	int nRet = 0;
	char* recvBuffer =(char*)malloc( sizeof(char) * 1024 );
	if ( recvBuffer == NULL )
	    return -1;
	memset( recvBuffer, 0, sizeof(char) * 1024 );
	while ( true )
	{
	    fdRead = g_fdClientSock;
	    timeval tv;
		tv.tv_sec = 0;
		tv.tv_usec = 10;
	    //检查fd_arrray数组里边是否有信号到来
	    nRet = select( 0, &fdRead, NULL, NULL, &tv );
           //select之后select将更新fdRead这个集合,把其中不可读的套节字去掉 
           //只保留符合条件的套节字在这个集合里面 
	    if ( nRet != SOCKET_ERROR )
	    {
	    	
	        for ( int i = 0; i < g_fdClientSock.fd_count; i++ )
	        {
	        	// 遍历出来哪些SOCKET有信号
	            if ( FD_ISSET(g_fdClientSock.fd_array[i],&fdRead)  )
	            {
	            	// 下面是数据的收发
	                memset( recvBuffer, 0, sizeof(char) * 1024 );
	                nRet = recv( g_fdClientSock.fd_array[i], recvBuffer, 1024, 0);
	                if ( nRet == SOCKET_ERROR )
	                {
	                    closesocket( g_fdClientSock.fd_array[i]);
	                    clientNum--;
	                    FD_CLR( g_fdClientSock.fd_array[i], &g_fdClientSock );
	                }
	                else if ( nRet == 0 )
	                {
	                    closesocket( g_fdClientSock.fd_array[i]);
	                    clientNum--;
	                    FD_CLR( g_fdClientSock.fd_array[i], &g_fdClientSock );
	                }
	                else
	                {	                    
	                    printf("Recv msg:%s\n",recvBuffer);
	                    send(g_fdClientSock.fd_array[i], recvBuffer, strlen(recvBuffer), 0);
	                }
	            }
	        }
	    }
	}
 
	if ( recvBuffer != NULL )
	    free( recvBuffer );
	return 0;
}
int main()
{
	//初始化环境
	WinSockInit();
 
	SOCKET listenSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = htonl(INADDR_ANY);
	server.sin_port = htons(PORT);
	//绑定
	int ret = bind(listenSock, (sockaddr*)&server, sizeof(server));
	//监听
	ret = listen(listenSock, 4);
 
	sockaddr_in clientAddr;
	int nameLen = sizeof( clientAddr );
	// 先把工作者线程创建起来
	CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)WorkThreadProc, NULL, NULL, NULL);
 
	while( clientNum < FD_SETSIZE )//FD_SETSIZE==64
	{
		// 当有一个客户端进行连接时,主线程的accept会进行返回
		SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen );
 
		FD_SET(clientSock, &g_fdClientSock);
 
		clientNum++;
	}
	closesocket(listenSock);
	WSACleanup();
	return 0;
}

参考:

https://blog.youkuaiyun.com/liitlefrogyyh/article/details/52101999 select函数及fd_set介绍。

https://blog.youkuaiyun.com/Timmiy/article/details/52123755 windows Socket编程之select网络模型

https://blog.youkuaiyun.com/liitlefrogyyh/article/details/52104120 select函数实现原理分析

http://www.cnblogs.com/zhangshenghui/p/6097387.html  select()函数以及FD_ZERO、FD_SET、FD_CLR、FD_ISSET

https://blog.youkuaiyun.com/bzhxuexi/article/details/44833537 socket通信中select函数的使用和详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值