在做Socket编程时,当要处理一个server对应多个client,这种可以每个客户端用一个线程来处理,但是客户端太多,程序的性能会降低。Windows提供了select模型,很好的处理了一对多的模型。select的申明如下:
Int WSAAPI select(
_In_ int nfds, //0,无意义
_Inout_opt_ fd_set FAR * readfds, //检查可读性集合
_Inout_opt_ fd_set FAR * writefds, //检查可写性集合
_Inout_opt_ fd_set FAR * exceptfds,//进行异常检测的Socket
_In_opt_ const struct timeval FAR * timeout //非阻塞模式中设置最大等待时间;
从msdn可查看参数的详细解释:
Any two of the parameters, readfds, writefds, or exceptfds, can be given as null. At least one must be non-null, and any non-nulldescriptor set must contain at least one handle to a socket.
In summary, a socket will be identified in a particular set when select returns if:
其中任何两个参数readfds、writefds或exceptfds都可以被指定为null。至少一个必须是非空的,任何非空描述符集必须包含一个套接字的至少一个句柄。
总之,当select返回以下情况时,套接字将在特定集合中标识:
readfds:
If listen has been called and a connection is pending, accept will succeed.
如果监听已被调用,且连接正在挂起,此时调用accept函数会成功
Data is available for reading (includes OOB data if SO_OOBINLINE is enabled).
有数据可读,如果启用SO_OOBINLINE,则包含OOB数据
Connection has been closed/reset/terminated.
连接已经关闭,重置或终止
writefds:
If processing a connect call (nonblocking), connection has succeeded.
Data can be sent.
exceptfds:
If processing a connect call (nonblocking), connection attempt failed.
OOB data is available for reading (only if SO_OOBINLINE is disabled).
fd_set的定义如下
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
#define FD_SETSIZE 64
可知select模型最多可以处理64个套接字
Return Value
The select function returns the total number of socket handles that are ready and contained in the fd_set structures, zero if the time limit expired, or SOCKET_ERROR if an error occurred. If the return value is SOCKET_ERROR, WSAGetLastError can be used to retrieve a specific error code.
调用成功时select函数返回准备好并包含在fd_set结构中的套接字句柄的总数,如果时间限制过期,则返回零,如果发生错误,则返回SOCKET_ERROR。如果返回值是SOCKET_ERROR,则可以使用WSAGetLastError来检索特定的错误代码。
select模型紧密结合的四个宏来简化对socket的操作:
FD_CLR( s,*set) 从队列set删除句柄s。
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中。
FD_SET( s,*set )把句柄s添加到队列set中。
FD_ZERO( *set ) 把set队列初始化成空队列。
select判断sockets是否刻度的步骤:
1.将该套接字加入到readfds集合;
2.readfds作为第二个参数调用select函数;
3.当select函数返回时,应用程序判断该套接字是否任然存在于readfds集合;
4.如果该套接字存在于readfds集合,则表明该套接字刻度,此时可以调用recv函数接受数据,否则,
下面的代码是一个server对应多个client, 仅做测试demo, 如果是实际项目,需实际处理。
服务端代码
#include<winsock2.h>
#include<iostream>
using namespace std;
#pragma comment(lib,"Ws2_32.lib")
int main()
{
//初始化winsock的环境
WSADATA wd;
if (WSAStartup(MAKEWORD(2, 2, ), &wd) == SOCKET_ERROR)
{
cout << "WSAStartup error:" << GetLastError() << endl;
return 0;
}
//1.创建监听套接字
SOCKET sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET)
{
cout << "socket error:" << GetLastError() << endl;
return 0;
}
//2.绑定到ip与端口
sockaddr_in addr;
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8000);
addr.sin_family = AF_INET;
int len = sizeof(sockaddr_in);
if (bind(sListen, (SOCKADDR*)&addr, len) == SOCKET_ERROR)
{
cout << "bind error:" << GetLastError() << endl;
return 0;
}
//3.监听套接字
if (listen(sListen, 5) == SOCKET_ERROR)
{
cout << "listen error:" << GetLastError() << endl;
return 0;
}
//4. select开始了
fd_set readSet;//定义一个读(接受消息)的集合
FD_ZERO(&readSet);//初始化集合
FD_SET(sListen, &readSet);
//不停的select才可以读取套接字的状态改变
while (true)
{
fd_set tmpSet;//定义一个临时的集合
FD_ZERO(&tmpSet);//初始化集合
tmpSet = readSet;// 每次循环都是所有的套接字
//利用select选择出集合中可以读写的多个套接字,有点像筛选
int ret = select(0, &tmpSet, NULL, NULL, NULL);//最后一个参数为NULL,一直等待,直到有数据过来
if (ret == SOCKET_ERROR)
{
continue;
}
//成功筛选出来的tmpSet可以发送或者接收的socket
for (int i = 0; i < tmpSet.fd_count; ++i)
{
//获取到套接字
SOCKET s = tmpSet.fd_array[i];
//接收到客户端的链接
if (s == sListen)
{
SOCKET c = accept(s, NULL, NULL);
//fd_set集合最大值为64
if (readSet.fd_count < FD_SETSIZE)
{
//往集合中添加客户端套接字
FD_SET(c, &readSet);
cout << "欢迎" << c << "进入聊天室!" << endl;
//给客户端发送欢迎
char buf[100] = { 0 };
sprintf(buf, "欢迎%d进入聊天室!", c);
send(c, buf, 100, 0);
}
else
{
cout << "达到客户端容量上线!" << endl;
}
}
else//一定是客户端
{
//接收客户端的数据
char buf[100] = { 0 };
ret = recv(s, buf, 100, 0);
if (ret == SOCKET_ERROR || ret == 0)
{
closesocket(s);
FD_CLR(s, &readSet);
cout << s << "离开聊天室!" << endl;
}
else
{
cout << s << "说:" << buf << endl;
}
}
}
}
//关闭监听套接字
closesocket(sListen);
//清理winsock环境
WSACleanup();
return 0;
}
客户端代码
#include<winsock2.h>//winsock2的头文件
#include<iostream>
using namespace std;
//勿忘,链接dll的lib
#pragma comment(lib, "ws2_32.lib")
int main()
{
//加载winsock2的环境
WSADATA wd;
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0)
{
cout << "WSAStartup error:" << GetLastError() << endl;
return 0;
}
//1.创建流式套接字
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << "socket error:" << GetLastError() << endl;
return 0;
}
//2.连接服务器
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int len = sizeof(sockaddr_in);
if (connect(s, (SOCKADDR*)&addr, len) == SOCKET_ERROR)
{
cout << "connect error:" << GetLastError() << endl;
return 0;
}
//3接收服务端的消息
char buf[100] = { 0 };
recv(s, buf, 100, 0);
cout << buf << endl;
//3随时给服务端发消息
int ret = 0;
do
{
char buf[100] = { 0 };
cout << "请输入聊天内容:";
cin >> buf;
ret = send(s, buf, 100, 0);
} while (ret != SOCKET_ERROR && ret != 0);
//4.关闭监听套接字
closesocket(s);
//清理winsock2的环境
WSACleanup();
return 0;
}
例如,测试结果如下,在服务端窗口可以看到客户端的连接情况。
select模型只处理了64个socket, 如果多于64个如何处理呢,Hp-Socket, IOCP这两种可以试试, 暂时没有项目用到,以后再研究研究。
原文链接:https://blog.csdn.net/yao_hou/article/details/102728970