在此之前呢,介绍了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函数的使用和详解