关于select模型的理论讲解,网上随便一搜就有很多大神的精彩文章,这里就不重复造轮子了。不过要真正理解select模型,代码才是最好的文章。我在网上看了好多代码,可能是相互转载的原因,有些代码不是编译不过,就是逻辑不通,有些虽然可以正常运行,但是会导致CPU暴涨,还有个别大神只给出了关键代码,这对于我等新手菜鸟来说,简直苦不堪言。没办法了,自己动手,丰衣足食,先看一下下面的这行代码:
SOCKET clientSocket = accept(socketListen, (sockaddr*)&clientAddr, &len);
哈哈,能看到这篇文章的人肯定都知道,这是服务器用来等待客户端连接请求的,在默认的阻塞模式下的socket编程里,accept会一直阻塞在那里,直到有新的客户端连接请求到来,accept函数才会返回。在单线程的程序里出现这种情况一般是无法容忍的,因为如果永远都没有新的连接请求过来,那么整个程序就会一直被卡死在这里。很多人也许会想到可以用多线程来解决这个问题,那么我们再看下面的代码:
result = recv(socket, bufRecv, 1024, 0);
这是用来接收数据的,在默认的阻塞模式下,如果套接字连接上没有数据到来的话,recv同样会一直阻塞在那里。假设这是服务器上用来接收客户端数据的代码,在有多个客户端连接到服务器的情况下,我们要为每个客户端都分别创建一个线程来调用recv,否则一旦某个客户端的数据接收出现了阻塞,就会导致其他客户端的数据全部无法正常处理。当客户端有成百上千的时候,哈哈,你还敢使用多线程来解决这个问题吗?于是又有人会想到,既然socket编程默认是阻塞模式的,那么将其设置为非阻塞模式是不是也可以解决这个问题呢?我们还是用代码来试试吧:
u_long mode = 1;
ioctlsocket(socket, FIONBIO, &mode);///设置非阻塞模式
result = recv(socket, bufRecv, 1024, 0);
设置非阻塞模式之后可以发现,
不管套接字连接上有没有数据到来,recv的调用都会马上返回。不过在没有数据到来的情况下,recv虽然返回了,但是调用WSAGetLastError()你就会得到一个错误码:WSAEWOULDBLOCK,意思是请求的操作没有成功完成。所以为了能够完整地接收数据,就需要不断地循环调用recv并判断WSAGetLastError()返回值,直到成功为止。同样的,当客户端有成百上千的时候,程序的运行效率和资源开销将会变得让人崩溃……
为了解决上述问题,Winsock提供了五种I/O模型:select,WSAAsyncSelect,WSAEventSelect,Overlapped,Completion。下面是一个完整的基于TCP协议的socket编程,分为服务端和客户端,其中的服务器实现就使用了select模型。这只是一个入门级别的代码,比较适合在网络编程方面跟我水平一样的小伙伴,哈哈……
TCP服务端代码:
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main()
{
/// 初始化socket
WSADATA wsaData;
WORD version = MAKEWORD(2,2);
int result = 0;
result = WSAStartup(version, &wsaData);
if (result != 0)
{
std::cout << "WSAStartup() error." << std::endl;
return -1;
}
/// 创建socket
SOCKET socketListen;
socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (socketListen == INVALID_SOCKET)
{
WSACleanup();
std::cout << "socket() error." << std::endl;
return -1;
}
/// 服务器地址结构
sockaddr_in svrAddress;
svrAddress.sin_family = AF_INET;
svrAddress.sin_addr.s_addr = INADDR_ANY;
svrAddress.sin_port = htons(8000);
/// 绑定服务器套接字
result = bind(socketListen, (sockaddr*)&svrAddress, sizeof(svrAddress));
if (result == SOCKET_ERROR)
{
closesocket(socketListen);
WSACleanup();
std::cout << "bind() error." << std::endl;
return -1;
}
/// 开启监听
result = listen(socketListen, 5);
if (result == SOCKET_ERROR)
{
closesocket(socketListen);
WSACleanup();
std::cout << "listen() error." << std::endl;
return -1;
}
std::cout << "服务器启动成功,监听端口:" << ntohs(svrAddress.sin_port) << std::endl;
/// select模型
fd_set allSockSet;
FD_ZERO(&allSockSet);
FD_SET(socketListen, &allSockSet); // 将socketListen加入套接字集合中
while (true)
{
fd_set readSet;
FD_ZERO(&readSet);
readSet = allSockSet;
result = select(0, &readSet, NULL, NULL, NULL);
if (result == SOCKET_ERROR)
{
std::cout << "listen() error." << std::endl;
break;
}
if (FD_ISSET(socketListen, &readSet))
{
sockaddr_in clientAddr;
int len = sizeof(clientAddr);
SOCKET clientSocket = accept(socketListen, (sockaddr*)&clientAddr, &len);
if (clientSocket == INVALID_SOCKET)
{
std::cout << "accept() error." << std::endl;
break;
}
FD_SET(clientSocket, &allSockSet); /// 将新创建的套接字加入到集合中
char ipAddress[16] = { 0 };
inet_ntop(AF_INET, &clientAddr, ipAddress, 16);
std::cout << "有新的连接[" << ipAddress << ":" << ntohs(clientAddr.sin_port)
<< "], 目前客户端的数量为:" << allSockSet.fd_count - 1 << std::endl;
continue;
}
for (u_int i = 0; i < allSockSet.fd_count; ++i)
{
SOCKET socket = allSockSet.fd_array[i];
sockaddr_in clientAddr;
int len = sizeof(clientAddr);
getpeername(socket, (struct sockaddr *)&clientAddr, &len);
char ipAddress[16] = { 0 };
inet_ntop(AF_INET, &clientAddr, ipAddress, 16);
/// 可读性监视,可读性指有连接到来、有数据到来、连接已关闭、重置或终止
if (FD_ISSET(socket, &readSet))
{
char bufRecv[100];
result = recv(socket, bufRecv, 100, 0);
if (result == SOCKET_ERROR)
{
DWORD err = WSAGetLastError();
if (err == WSAECONNRESET) /// 客户端的socket没有被正常关闭,即没有调用closesocket
{
std::cout << "客户端[" << ipAddress << ":" << ntohs(clientAddr.sin_port) << "]被强行关闭, ";
}
else
{
std::cout << "recv() error," << std::endl;
}
closesocket(socket);
FD_CLR(socket, &allSockSet);
std::cout << "目前客户端的数量为:" << allSockSet.fd_count - 1 << std::endl;
break;
}
else if (result == 0) /// 客户端的socket调用closesocket正常关闭
{
closesocket(socket);
FD_CLR(socket, &allSockSet);
std::cout << "客户端[" << ipAddress << ":" << ntohs(clientAddr.sin_port)
<< "]已经退出,目前客户端的数量为:" << allSockSet.fd_count - 1 << std::endl;
break;
}
bufRecv[result] = '\0';
std::cout << "来自客户端[" << ipAddress << ":" << ntohs(clientAddr.sin_port)
<< "]的消息:" << bufRecv << std::endl;
}
}
}
for (u_int i = 0; i < allSockSet.fd_count; ++i)
{
SOCKET socket = allSockSet.fd_array[i];
closesocket(socket);
}
WSACleanup();
return 0;
}
在服务器的代码中,最好把监听的socket套接字也添加到select模型的套接字集合中,再通过判断具备可读性的套接字是否是监听的socket套接字来调用accept处理客户端的连接请求。网上有些示例代码就是没有这么做,而是只把客户端连接服务器的socket套接字放到select模型的套接字集合中,然后在创建一个线程循环调用select监视客户端socket套接字的可读性。采用这种方式的话,一旦始终都没有客户端连接到服务器,线程里的select就是在监视一个空的套接字集合,select会马上返回,再次重新循环,如此往复,线程就变成了一个死循环,CPU利用率肯定瞬间暴涨。
TCP客户端代码:
#include
#include
#include
#pragma comment(lib, "ws2_32.lib")
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 8000
#define SOCKET_NUM 1 /// 客户端socket的个数,修改该值可以改变连接到服务器的客户端个数
int main()
{
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
int err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0) return 1;
if (LOBYTE(wsaData.wVersion) != 2 ||
HIBYTE(wsaData.wVersion) != 2)
{
WSACleanup();
std::cout << "WSAStartup() error." << std::endl;
return -1;
}
SOCKET allSocketClients[SOCKET_NUM];
for (int i = 0; i < SOCKET_NUM; i ++)
{
SOCKET socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socketClient == INVALID_SOCKET)
{
WSACleanup();
std::cout << "socket() error." << std::endl;
return -1;
}
allSocketClients[i] = socketClient;
}
SOCKADDR_IN server;
memset(&server, 0, sizeof(SOCKADDR_IN));
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);
inet_pton(server.sin_family, SERVER_ADDRESS, &server.sin_addr);
for (int i = 0; i < SOCKET_NUM; i++)
{
SOCKET socketClient = allSocketClients[i];
err = connect(socketClient, (struct sockaddr *)&server, sizeof(SOCKADDR_IN));
if (err == SOCKET_ERROR)
{
std::cout << "connect() error." << std::endl;
closesocket(socketClient);
WSACleanup();
return -1;
}
std::cout << "第 " << i + 1 << " 个客户端连接服务器成功。" << std::endl;
}
for (int i = 0; i < SOCKET_NUM; i++)
{
SOCKET socketClient = allSocketClients[i];
char message[100] = { 0 };
sprintf_s(message, "我是第 %d 个客户端 ", i + 1);
send(socketClient, message, strlen(message), 0);
}
/// 按 q 退出程序
do
{
} while (getchar() != 'q');
for (int i = 0; i < SOCKET_NUM; i++)
{
SOCKET socketClient = allSocketClients[i];
closesocket(socketClient);
}
WSACleanup();
return 0;
}
输出结果:
在Winsock编程中使用select模型时,受限于轮询的套接字数量,这个数量由头文件WinSock2.h中定义FD_SETSIZE值来表示,默认值是64,所以上述客户端代码开启的socket个数不能超过64个。但事实上这个算不上真的限制,有很多方法可以解除这个限制,详情请自行搜索“突破select模型的FD_SETSIZE限制”。
提示:以上代码在Visual Studio 2013编译通过。