select模型是一种比较常用的IO模型。利用该模型可以使Windows socket应用程序可以同时管理多个套接字。 使用select模型,可以使当执行操作的套接字满足可读可写条件时,给应用程序发送通知,收到这个通知后,应用程序再去调用相应的Windows socket API去执行函数调用。
Select模型的核心是select函数。调用select函数检查当前各个套接字的状态。根据函数的返回值判断套接字的可读可写性。然后调用相应的Windows Sockets API完成数据的发送、接收等。
Select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入数据。 如:在调用recv函数之前,先调用select函数,如果系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就可以调用recv函数接收数据了。
select 函数
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval *timeout
);
参数说明:
- nfds 一般设置为0,可以忽略,主要是为了兼容其他系统参数兼容。
- readfds 准备接收数据的套接字集合,即可读性集合。
- writefds 准备发送数据的套接字集合,即可写性集合
- exceptfds,检查错误套接字集合指针
- timeout,等待时间,设置为NULL时,表示永久等待,直到有事件发生返回。
函数说明:
当程序执行select函数时,程序被阻塞,直至内核检测到有可读可写等套接字时才返回,并修改fd_set集合中数据,这些的数据都是可读可写socket集合,不存在的或没有完成IO操作的套接字会被‘删除’,返回值是这些可读可写集合的数量。
若设置超时,则超时时间达到后,函数返回值为0。
需要说明的是,select函数三个套接字指针集合,至少需要传入一个集合才可以 。
例如:
//检测可读可写
select(0,&read_set,&write_set,NULL,NULL);
//只检测可写套接字
select(0,&read_set,NULL,NULL,NULL);
//都检测
select(0,&read_set,&write_set,&except_set,NULL);
fd_set结构体
fd_se是一个结构体,其定义如下:
#ifndef FD_SETSIZE
#define FD_SETSIZE 64
#endif /* FD_SETSIZE */
typedef struct fd_set
{
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
fd_cout 表示该集合套接字数量。最大为64.
fd_array套接字数组。
readfds数组将包括满足以下条件的套接字:
1:有数据可读。此时在此套接字上调用recv,立即收到对方的数据。
2:连接已经关闭、重设或终止。
3:正在请求建立连接的套接字。此时调用accept函数会成功。
writefds数组包含满足下列条件的套接字:
1:有数据可以发出。此时在此套接字上调用send,可以向对方发送数据。
2:调用connect函数,并连接成功的套接字。
exceptfds数组将包括满足下列条件的套接字:
1:调用connection函数,但连接失败的套接字。
2:有带外(out of band)数据可读。
timeval 结构
timeval表示超时时间结构体,其定义如下:
structure timeval
{
long tv_sec;//秒。
long tv_usec;//毫秒。
};
当timeval为空指针时,select会一直等待,直到有符合条件的套接字时才返回。
当tv_sec和tv_usec之和为0时,无论是否有符合条件的套接字,select都会立即返回。
当tv_sec和tv_usec之和为非0时,如果在等待的时间内有套接字满足条件,则该函数将返回符合条件的套接字。如果在等待的时间内没有套接字满足设置的条件,则select会在时间用完时返回,并且返回值为0。
fd_set操作函数
为了方便使用,windows sockets提供了下列宏,用来对fd_set进行一系列操作。使用以下宏可以使编程工作简化。
FD_CLR(s,set);从set集合中删除s套接字。
FD_ISSET(s,set);检查s是否为set集合的成员。
FD_SET(s,set);将套接字加入到set集合中。
FD_ZERO(set);将set集合初始化为空集合。
FD宏源码如下:
//fd这个socket从set集合中‘移除’,并且set集合中的数量减一。
#define FD_CLR(fd, set) do { \
u_int __i; \
for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count ; __i++) { \
if (((fd_set FAR *)(set))->fd_array[__i] == fd) { \
while (__i < ((fd_set FAR *)(set))->fd_count-1) { \
((fd_set FAR *)(set))->fd_array[__i] = \
((fd_set FAR *)(set))->fd_array[__i+1]; \
__i++; \
} \
((fd_set FAR *)(set))->fd_count--; \
break; \
} \
} \
} while(0)
//检测fd是否已经在set中,不存在则新添加一个。
#define FD_SET(fd, set) do { \
u_int __i; \
for (__i = 0; __i < ((fd_set FAR *)(set))->fd_count; __i++) { \
if (((fd_set FAR *)(set))->fd_array[__i] == (fd)) { \
break; \
} \
} \
if (__i == ((fd_set FAR *)(set))->fd_count) { \
if (((fd_set FAR *)(set))->fd_count < FD_SETSIZE) { \
((fd_set FAR *)(set))->fd_array[__i] = (fd); \
((fd_set FAR *)(set))->fd_count++; \
} \
} \
} while(0)
//将集合数量设置为0
#define FD_ZERO(set) (((fd_set FAR *)(set))->fd_count=0)
//判断fd释放在set结合中
#define FD_ISSET(fd, set) __WSAFDIsSet((SOCKET)(fd), (fd_set FAR *)(set))
select和FD宏使用
在调用select函数对套接字进行监视之前,必须将要监视的套接字分配给上述三个数组中的一个。然后调用select函数,再次判断需要监视的套接字是否还在原来的集合中。就可以知道该集合是否正在发生IO操作
在开发Windows sockets应用程序时,通过下面的步骤,可以完成对套接字的可读写判断:
-
使用FD_ZERO初始化套接字集合。如FD_ZERO(&readfds);
-
使用FD_SET将某套接字放到readfds内,用于select检测,如: FD_SET(s,&readfds);
-
以readfds为第二个参数调用select函数。select在返回时会返回所有fd_set集合中套接字的总个数,并对每个集合进行相应的更新。将满足条件的套接字放在相应的集合中。
-
使用FD_ISSET判断s是否还在某个集合中。如: FD_ISSET(s,&readfds);
-
调用相应的Windows socket api 对某套接字进行操作。
select返回后会修改每个fd_set结构。删除不存在的或没有完成IO操作的套接字。这也正是在第四步中可以使用FD_ISSET来判断一个套接字是否仍在集合中的原因。
#include "stdafx.h"
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int _tmain(int argc, _TCHAR* argv[])
{
//加载套接字库
WORD wVersionRequested;
WSADATA wsaData = {0};
wVersionRequested = MAKEWORD(1,1);
if (WSAStartup(wVersionRequested, &wsaData) != 0 )
{
return 1;
}
if (LOBYTE(wsaData.wVersion) != 1 ||
HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup( );
return 1;
}
int nResult = 0; // used to return function results
//创建用于监听的套接字
SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM,0);
if (INVALID_SOCKET == ListenSocket)
{
printf("create socket error (%d)\n",::WSAGetLastError());
WSACleanup();
return 1;
}
//创建socket信息
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET;//地址族
addrSrv.sin_port = htons(6100);//端口号
//监听本机所有的主机地址,即不关心数据从哪个网卡过来
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
//将端口号/IP和套接字绑定在一起
nResult = bind(ListenSocket,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR_IN));
if (nResult == SOCKET_ERROR)
{
printf("bind socket error code = %d\n",::WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
//将套接字设为监听模式,准备接收客户请求
nResult = listen(ListenSocket,SOMAXCONN);
if (SOCKET_ERROR == nResult)
{
printf("listen socket error(%d)\n",::WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
fd_set allSocket;
FD_ZERO(&allSocket);
//监听套接字加入集合
FD_SET(ListenSocket,&allSocket);
printf("服务端启动监听...\n");
while (TRUE)
{
fd_set read_set;
FD_ZERO(&read_set);
//可读结合
read_set = allSocket;
//更新
nResult = select(0,&read_set,NULL,NULL,NULL);
if (SOCKET_ERROR == nResult)
{
printf("select error...\n");
break;
}
//判断监听套接字是否有连接请求
if (FD_ISSET(ListenSocket,&read_set))
{
SOCKADDR_IN ClientAddr;
int nLen = sizeof(ClientAddr);
SOCKET socketClient = accept(ListenSocket,(sockaddr*)&ClientAddr, &nLen);
if (INVALID_SOCKET == socketClient)
{
printf("accept socket is invalid...\n");
continue;
}
//新建连接
FD_SET(socketClient, &allSocket);
char* pszClientIP = inet_ntoa(ClientAddr.sin_addr);
if(NULL != pszClientIP)
{
printf("有新客户端[%s:%d]端请求连接\n",pszClientIP,ntohs(ClientAddr.sin_port));
printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);
}
continue;
}
for (unsigned i = 0; i < allSocket.fd_count; i++)
{
SOCKET socket = allSocket.fd_array[i];
SOCKADDR_IN addrClient;
int nLen = sizeof(addrClient);
getpeername(socket,(sockaddr*)&addrClient, &nLen);
char* pszClientIp = inet_ntoa(addrClient.sin_addr);
unsigned short usClientPort = ntohs(addrClient.sin_port);
//某个socket上友数据可以接收
if (FD_ISSET(socket,&read_set))
{
char szMsg[128] = {};
int nResult = recv(socket,szMsg,sizeof(szMsg),0);
if (nResult > 0)
{
printf("--------客户端[%s:%d]--------\n",pszClientIp,usClientPort);
printf("消息长度%d字节\n",nResult);
printf("消息内容: %s\n",szMsg);
}
else if (0 == nResult)
{
//对方关闭连接
printf("客户端[%s:%d]主动关闭连接...\n",pszClientIp,usClientPort);
closesocket(socket);
FD_CLR(socket,&allSocket);
printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);
continue;
}
else
{
DWORD err = WSAGetLastError();
// 客户端的socket没有被正常关闭,即没有调用closesocket
if (err == WSAECONNRESET)
{
printf("客户端[%s:%d]被强行关闭",pszClientIp,usClientPort);
}
else
{
printf("recv data error code(%d)...\n",::WSAGetLastError());
}
closesocket(socket);
FD_CLR(socket, &allSocket);
//监听socket不算客户端端
printf("目前客户端的数量为: %d\n",allSocket.fd_count - 1);
continue;
}
}//read end
}
}
//关闭监听套接字
closesocket(ListenSocket);
//清理套接字库的使用
WSACleanup();
return 0;
}
通过select模型服务端可以管理多个客户端请求消息和请求连接,不会因为recv函数没有数据造成阻塞。
以下是服务端运行结果:
作者:AncientCastle
来源:CSDN
原文:https://blog.csdn.net/hq354974212/article/details/76154849
版权声明:本文为博主原创文章,转载请附上博文链接!