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

关于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;
}

输出结果:

Windows网络编程之Select模型学习笔记_第1张图片


在Winsock编程中使用select模型时,受限于轮询的套接字数量,这个数量由头文件WinSock2.h中定义FD_SETSIZE值来表示,默认值是64,所以上述客户端代码开启的socket个数不能超过64个。但事实上这个算不上真的限制,有很多方法可以解除这个限制,详情请自行搜索“突破select模型的FD_SETSIZE限制”。



提示:以上代码在Visual Studio 2013编译通过。


你可能感兴趣的:(C++点滴记录)