Windows网络编程之Select模型学习笔记

来源:互联网 发布:电脑无法识别网络 编辑:程序博客网 时间:2024/05/20 18:46

关于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(){/// 初始化socketWSADATA 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_PORT8000#define SOCKET_NUM1/// 客户端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编译通过。


0 0