文章目录
1. 预备知识
1.1 SELECT模型介绍
Select模型是Windows Sockets中最常见的I/O模型。之所以称其为Select模型,是因为的核心是利用select()函数实现I/O管理。利用select()函数,Windows Sockets应用程序可以判断套接字上是否存在数据,或者能否向该套接字写入数据。
如图所示,在调用recv()函数接收数据之前,先调用select()函数。如果此时系统没有可读的数据,那么select()函数会阻塞在这里。当系统存在可读的数据时,该函数返回。此时应用程序就可以调用recv()函数接收数据了。
1.2 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函数三个套接字指针集合,至少需要传入一个集合才可以 。
1.3 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集合初始化为空集合。
1.4 select函数与宏的搭配使用
可通过以下步骤,来完成对套接字的可读可写判断。
- 使用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 对某套接字进行操作。
2. 关键代码
2.1 服务器端
- 在套接字处于监听状态后,使用select模型
//在套接字处于监听状态后,使用select模型
FD_SET socketSet;//服务器套接字集合
FD_SET writeSet;//可写套接字集合
FD_SET readSet;//可读套接字集合
FD_ZERO(&socketSet);//初始化套接字集合,即清空集合
FD_SET(listenSocket, &socketSet);//加入监听套接字
- 检测可读套接字,调用检查套接字状态
FD_ZERO(&writeSet);//清空可读套接字集合
FD_ZERO(&readSet);//清空可写套接字集合
readSet = socketSet;//赋值
writeSet = socketSet;
//只检测可读套接字,该函数返回处于就绪状态且已包含在FD_SET结构中的套接字总数
ret = select(0, &readSet, &writeSet, NULL, NULL);
if (SOCKET_ERROR == ret) {
//调用select()失败处理
cout << "select() returned with error:" << ::WSAGetLastError() << endl;
break;
}
- 判断是否存在客户端的连接请求
//判断是否存在客户端的连接请求
if (FD_ISSET(listenSocket, &readSet)) {
SOCKADDR_IN ClientAddr;//保存客户端IP地址端口
int nLen = sizeof(ClientAddr);
//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
acceptSocket = accept(listenSocket, (sockaddr*)&ClientAddr, &nLen);
if (INVALID_SOCKET == acceptSocket) {
cout<<"accept() returned with error:" << ::WSAGetLastError() << endl;
continue;
}
else {
//将该套接字加入服务器套接字结合
FD_SET(acceptSocket, &socketSet);
}
char* pszClientIP = inet_ntoa(ClientAddr.sin_addr); //返回点分十进制的字符串在静态内存中的指针
if (NULL != pszClientIP)
{
//ntohs主要是将网络字节转为主机字节
cout << "客户端[" << pszClientIP << ":" << ntohs(ClientAddr.sin_port) <<"]请求连接成功"<< endl;
cout << "目前客户端的数量为:" << (socketSet.fd_count - 1) << endl;
//等待其他客户端连接
Sleep(1000);
}
cout << endl;
continue;
}
- 遍历所有套接字,判断可读或可写
//遍历所有套接字
for (int i = 1; i < socketSet.fd_count; i++) {
SOCKET sAccept = socketSet.fd_array[i];//获取套接字
SOCKADDR_IN addrClient;
int nLen = sizeof(addrClient);
//获取当前连接的客户端的IP地址和端口号,即初始化addClient
getpeername(sAccept, (sockaddr*)&addrClient, &nLen);
//获取客户端地址以及它的主机字节
char* pszClientIp = inet_ntoa(addrClient.sin_addr);
unsigned short usClientPort = ntohs(addrClient.sin_port);
//该套接字可读
if (FD_ISSET(sAccept, &readSet)) {
char buf[6400] = {
'\0' }