概述
Windows操作系统提供了选择(Select)、异步选择 (WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O)和完成端口 (Completion Port)共五种I/O模型。每一种模型均适用于一种特定的应用场景。今天主要讲一下Select 模型。
涉及的API
int WSAAPI select(
int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
const timeval timeout
);
参数:nfds:忽略。
readnfds: 指向检查可读性的套接字集合的可选的指针。
writefds: 指向检查可写性的套接字集合的可选的指针。
exceptfds: 指向检查错误的套接字集合的可选的指针。
timeout: select函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null
返回值:成功返回 fd_set集合体已经准备好的socket句柄,失败返回SOCKET_ERROR,返回值为 0 表示是等待超时
示例:
int numb=select(0, &readset, NULL, NULL, &vt);
if (numb==0 ||numb==SOCKET_ERROR)
{
continue;
}
fd_set
fd_set结构由各种Windows套接字函数和服务提供者(如select函数)使用,用于将套接字放入“set”中,以实现各种目的,如使用select函数的readfds参数测试给定套接字的可读性,结构体原型为:
typedef struct fd_set {
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} FD_SET, *PFD_SET, *LPFD_SET;
参数:fd_count 集合中socket的个数
fd_array[FD_SETSIZE] 指套接字数组
主要利用4个宏定义操作fd_set 集合:
FD_ZERO(&set); 将set清零使集合中不含任何fd
FD_SET(fd, &set); 将fd加入set集合
FD_CLR(fd, &set); 将fd从set集合中清除
FD_ISSET(fd, &set); 在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)
timeval
时间间隔结构体,使用也比较简单,原型为:
typedef struct timeval {
long tv_sec;
long tv_usec;
} TIMEVAL, *PTIMEVAL, *LPTIMEVAL;
备注:tv_sec 时间间隔,单位秒
tv_usec 时间间隔 ,单位毫秒
示例:
timeval val={1,100};//表示1秒零100毫秒
关于TCP基础的知识,这里就不在过多的讲解,如果不太清楚,可以查看TCP通信相关知识点。
详细代码
由于客户端代码是通用的,这里就不再添加,如果需要可以直接调用TCP通信中的客户端代码即可。
服务端:
// TCP_Select.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <thread>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
fd_set pSet;
//SOCKET sockArray[64];
//int ntotal = 0;
void recvThread()
{
char recvBuf[1024];
while (1)
{
fd_set readset;
FD_ZERO(&readset);
readset=pSet;
/*for (int j=0;j!=ntotal;j++)
{
FD_SET(sockArray[j], &readset);
}*/
timeval vt = { 1,0 };// 前面是秒,后面是毫秒
int numb=select(0, &readset, NULL, NULL, &vt);
if (numb==SOCKET_ERROR ||numb==0)
{
continue;
}
for (int i=0;i!= readset.fd_count;i++)
{
if (FD_ISSET(readset.fd_array[i],&readset))
{
ZeroMemory(recvBuf,1024);
int len=recv(readset.fd_array[i],recvBuf,1024,0);
if (len>0)
{
cout << readset.fd_array[i] <<recvBuf<< endl;
}
if (len==0)//说明连接中断
{
cout << readset.fd_array[i] <<"断开连接"<< endl;
closesocket(readset.fd_array[i]);//关闭socket
FD_CLR(readset.fd_array[i], &pSet);//清空集合体
}
}
}
}
}
int main()
{
WSADATA wsData;
int nret = WSAStartup(MAKEWORD(2, 2), &wsData);
if (nret!=0)
{
return nret;
}
sockaddr_in sa, recvSa;
int nlen = sizeof(sa);
sa.sin_addr.S_un.S_addr = INADDR_ANY;
sa.sin_port = htons(9999);
sa.sin_family = AF_INET;
SOCKET asock = socket(AF_INET, SOCK_STREAM, 0);
if (asock==INVALID_SOCKET)
{
return WSAGetLastError();
}
if (bind(asock,(sockaddr*)&sa,nlen)!=0)
{
return WSAGetLastError();
}
if (listen(asock, SOMAXCONN)!=0)
{
return WSAGetLastError();
}
FD_ZERO(&pSet);//初始化
thread t(recvThread);
t.detach();
while (1)
{
SOCKET sock = accept(asock, (sockaddr*)&recvSa, &nlen);
if (sock != INVALID_SOCKET)
{
FD_SET(sock, &pSet);//加人集合体
//sockArray[ntotal] = sock;
//ntotal++;
}
}
}
注意事项:
- 这个小的Demo用的是全局变量 pSet 集合,那recvThread() 接收线程为什么还要重新定义一个临时变量 fd_set readset ,并赋值呢?
这是因为select() 返回后会把以前加入的但并无事件发生的fd清空,所以要每次循环都要重新对临时变量赋值,不能用直接使用全局变量。- 这个Demo 是直接使用全局 fd_set集合体,也可以使用SOCKET 数组的方式,这里我做了屏蔽,后面的连接断开处理也没写完,有兴趣可以自己试试。
- 使用全局变量涉及到线程同步,示例中没有给出,如果想了解可以再在使用全局变量的时候添加:
#include <mutex>//头文件
mutex mtx;//全局变量
........
unique_lock <mutex> lock(mtx);
FD_SET(sock, &pSet);
如有错误,欢迎各位大神指正,共勉!