关于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 <WS2tcpip.h> #include <WinSock2.H> #include <iostream> #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 <iostream> #include <WS2tcpip.h> #include <WinSock2.H> #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编译通过。