SELTCT模型(TCP)

385 阅读6分钟

这是我参与11月更文挑战的第十四天,活动详情查看:2021最后一次更文挑战

SELECT模型简介

针对多个客户端连接服务器时,服务器不能同时响应多个客户端的情况,SELECT模型就是用来解决服务器的accept、recv函数等待阻塞的问题的(客户端不需要使用这个模型)。注意这里函数执行时阻塞这个问题并没有解决,只是不等待阻塞了。(执行阻塞和等待阻塞的区别)

换句话解释,recv执行过程中,从内存拷贝东西的过程中,是执行阻塞状态,无法中途响应别的请求;但是执行recv函数的时候,如果没有消息,这个时候是等待阻塞状态,SELECT可以让SOCKET句柄在等待阻塞的时候响应别的有消息的SOCKET请求。

SELECT原理

1、每个客户端都有SOCKET(多个),服务器也有自己的SOCKET(单个),将所有的socket装进SOCKET数组里

2、通过select函数,遍历1中的SOCKET数组,当某个SOCKET有响应,SELECT就会通过其参数/返回值反馈出来。

3、根据SOCKET类型进行不同操作:

  • 如果是服务器SOCKET,说明有客户端连接,调用accept
  • 如果是客户端SOCKET,说明有客户端请求通信,调用send或者recv

SELECT代码实现

服务器端

1、包含网络头文件网络库

#include <WinSock2.h> #include <stdio.h> 
#pragma comment(lib, "Ws2_32.lib")

2、打开网络库

int nRes = WSAStartup(wdVersion, &wdScoket);

	if (0 != nRes)
	{
		switch (nRes)
		{
		case WSASYSNOTREADY:
			printf("解决方案:重启。。。\n");
			break;
		case WSAVERNOTSUPPORTED:
			break;
		case WSAEINPROGRESS:
			break;
		case WSAEPROCLIM:
			break;
		case WSAEFAULT:
			break;
		}
		return 0;

	}

3、校验版本

if (2 != HIBYTE(wdScoket.wVersion) || 2 != LOBYTE(wdScoket.wVersion))
	{
		printf("版本有问题!\n");
		WSACleanup();
		return 0;
	}
	printf("版本校验成功!\n");

4、创建SOCKET

SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (INVALID_SOCKET == socketServer)
	{
		int err = WSAGetLastError();
		printf("服务器创建SOCKET失败错误码为:%d\n", err);

		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}
	printf("服务器创建SOCKET成功!\n");

5、绑定地址与端口

if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	printf("服务器端bind成功!\n");

6、开始监听

if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器监听失败错误码为:%d\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}

	printf("服务器端监听成功!\n");

7、SELECT(重点)

7.1将服务器句柄装入fd_set

//清零 
FD_ZERO(&allSockets); 
//服务器装进去 
FD_SET(socketServer, &allSockets);

7.2开始轮询循环

while (1) {
略略略
}

7.2.1使用select查询有信号的socket句柄

//select int nRes = select(0, &recvSockets, &sendSockets, &errorSockets, &st);

7.2.2没有socket句柄由信号则重新查询

if (INVALID_SOCKET == socketClient)
	{
    //连接出错,取错误码后继续取下一个请求句柄
	int err = WSAGetLastError();//取错误码
	printf("accept客户端句柄失败错误码为:%d\n", err);
        continue;
	}

7.2.3select出错则进行错误处理

//循环处理有错误的句柄
for (u_int i = 0; i < errorSockets.fd_count; i++)
{
    char str[100] = { 0 };
    int len = 99;
    if (SOCKET_ERROR ==getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, str, &len)){
	printf("getsocketopt无法获取相关信息!\n");
	int getopterr = WSAGetLastError();//错误处理
	printf("服务器getsocketopt失败错误码为:%d\n", getopterr);
        }
	printf("%s\n", str);//打印getsocketopt获取的错误信息
}

完整代码

//如果需要增加Select模型处理客户端数量,可修改下面一行代码
//#define FD_SETSIZE 128
#include <WinSock2.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")

//用来保存所有客户端+服务器Socket句柄的集合
fd_set allSockets;


BOOL WINAPI cls(DWORD dwCtrlType)
{
	switch (dwCtrlType)
	{
	case CTRL_CLOSE_EVENT:
		//释放所有句柄
		for (u_int i = 0; i < allSockets.fd_count; i++)
		{
			closesocket(allSockets.fd_array[i]);
		}
		WSACleanup();//清理网络库

	}

	return TRUE;
}

int main(void)
{
	SetConsoleCtrlHandler(cls, TRUE);
	/* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
	WORD wdVersion = MAKEWORD(2, 2);
	int a = *((char*)&wdVersion);
	int b = *((char*)&wdVersion + 1);

	//LPWSADATA lpw = (WSADATA*)malloc(sizeof(WSADATA));手动分配内存还要自己释放,太麻烦
	WSADATA wdScoket;
	//1、打开网络库
	int nRes = WSAStartup(wdVersion, &wdScoket);

	if (0 != nRes)
	{
		switch (nRes)
		{
		case WSASYSNOTREADY:
			printf("解决方案:重启。。。\n");
			break;
		case WSAVERNOTSUPPORTED:
			break;
		case WSAEINPROGRESS:
			break;
		case WSAEPROCLIM:
			break;
		case WSAEFAULT:
			break;
		}
		return 0;

	}
	printf("打开网络库成功!\n");

	//2、校验版本	
	if (2 != HIBYTE(wdScoket.wVersion) || 2 != LOBYTE(wdScoket.wVersion))
	{
		printf("版本有问题!\n");
		WSACleanup();
		return 0;
	}
	printf("版本校验成功!\n");

	//3、创建SOCKET
	SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (INVALID_SOCKET == socketServer)
	{
		int err = WSAGetLastError();
		printf("服务器创建SOCKET失败错误码为:%d\n", err);

		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}
	printf("服务器创建SOCKET成功!\n");

	struct sockaddr_in si;
	si.sin_family = AF_INET;
	si.sin_port = htons(12345);//用htons宏将整型转为端口号的无符号整型

	si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	//4、绑定地址与端口
	if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si)))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}
	printf("服务器端bind成功!\n");

	//5、开始监听
	if (SOCKET_ERROR == listen(socketServer, SOMAXCONN))
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器监听失败错误码为:%d\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库

		return 0;
	}

	printf("服务器端监听成功!\n");




	//清零
	FD_ZERO(&allSockets);
	//服务器装进去
	FD_SET(socketServer, &allSockets);

	while (1)
	{

		//防止轮询数组被传址引用清空
		fd_set recvSockets = allSockets;
		fd_set sendSockets = allSockets;
		//由于服务器不会自己和自己send消息,因此这里要把服务器句柄从发送列表里面去掉
		FD_CLR(socketServer, &sendSockets);
		fd_set errorSockets = allSockets;


		//时间段
		struct timeval st;
		st.tv_sec = 3;
		st.tv_usec = 4;

		//select
		int nRes = select(0, &recvSockets, &sendSockets, &errorSockets, &st);

		if (0 == nRes) //没有响应的socket
		{
			continue;
		}
		else if (nRes > 0)
		{	
			//循环处理有读请求的句柄:可能是服务器句柄,可能是客户端句柄
			for (u_int i = 0; i < recvSockets.fd_count; i++)
			{
				//如果是服务器句柄有请求,则表明有新客户端需要进行accept
				if (recvSockets.fd_array[i] == socketServer)
				{
					//accept新客户端
					SOCKET socketClient = accept(socketServer, NULL, NULL);
					if (INVALID_SOCKET == socketClient)
					{
						//连接出错,取错误码后继续取下一个请求句柄
						int err = WSAGetLastError();//取错误码
						printf("accept客户端句柄失败错误码为:%d\n", err);
						continue;
					}

					//将新加入的客户端丢进fd_set集合
					FD_SET(socketClient, &allSockets);
					//这里可以send
					printf("服务器获取客户端句柄成功,fd_set中共有%d个句柄!\n", allSockets.fd_count);
				}
				else//处理客户端,recv消息
				{
					char strBuf[1500] = { 0 };
					//接收客户端消息
					int nRecv = recv(recvSockets.fd_array[i], strBuf, 1500, 0);
					//send
					if (0 == nRecv)
					{
						//客户端已经掉线
						//记录fd_set中掉线的客户端句柄
						//要从fd_set中去掉该客户端句柄
						//手工释放记录的客户端句柄
						SOCKET socketTemp = recvSockets.fd_array[i];
						FD_CLR(recvSockets.fd_array[i], &allSockets);
						//释放
						closesocket(socketTemp);
						printf("客户端已经掉线,fd_set中共有%d个句柄!\n", allSockets.fd_count);
					}
					else if (0 < nRecv)
					{
						//打印收到的消息
						printf("客户端收到消息为:%s\n", strBuf);
					}
					else ///SOCKET_ERROR处理,强行关闭客户端也会走这里10054
					{
						//强制下线也叫出错 10054
						//有时候也只会触发10053
						int recverr = WSAGetLastError();
						switch (recverr)
						{
						case 10053:
						{
							//记录轮询数组中客户端句柄
							//要从轮询数组中去掉该客户端句柄
							//释放记录的客户端句柄
							SOCKET socketTemp = recvSockets.fd_array[i];
							FD_CLR(recvSockets.fd_array[i], &allSockets);
							//释放
							closesocket(socketTemp);
							printf("10053客户端已经强关,fd_set中共有%d个句柄!\n", allSockets.fd_count);
						}
						case 10054:
						{
							//记录轮询数组中客户端句柄
							//要从轮询数组中去掉该客户端句柄
							//释放记录的客户端句柄
							SOCKET socketTemp = recvSockets.fd_array[i];
							FD_CLR(recvSockets.fd_array[i], &allSockets);
							//释放
							closesocket(socketTemp);
							printf("10054客户端已经强关,fd_set中共有%d个句柄!\n", allSockets.fd_count);
						}
						}
					}
				}
			}

			//循环处理有错误的句柄
			for (u_int i = 0; i < errorSockets.fd_count; i++)
			{
				char str[100] = { 0 };
				int len = 99;
				if (SOCKET_ERROR == getsockopt(errorSockets.fd_array[i], SOL_SOCKET, SO_ERROR, str, &len))
				{
					printf("getsocketopt无法获取相关信息!\n");
					int getopterr = WSAGetLastError();
					//错误处理
					printf("服务器getsocketopt失败错误码为:%d\n", getopterr);

				}
				printf("%s\n", str);//打印getsocketopt获取的错误信息
			}

			//循环处理有写请求的句柄
			for (u_int i = 0; i < sendSockets.fd_count; i++)
			{
				//由于服务器不会自己和自己send消息
				//因此,在sendSockets集合中是没有服务器句柄的
				
				//printf("服务器%d,%d:可写\n", socketServer, writeSockets.fd_array[i]);

				//处理send,注意这里接收信息的客户端要设置接收长度和hello一致
				if (SOCKET_ERROR == send(sendSockets.fd_array[i], "hello", 5, 0))
				{
					int senderr = WSAGetLastError();
					//这里最好不打印错误码
					//printf("服务器send失败错误码为:%d\n", senderr);
				}
			}
		}
		else//发送错误
		{
			//获取错误码,根据错误码基分类处理

			//这里简单退出一下,不详细写
			break;
		}

		//可以定义异步输入标志,代替while循环判断条件,满足该条件即可退出循环

	}

	//删除指定SOCKET句柄
	//FD_CLR(socketServer,&allSockets);

	//判断某个SOCKET句柄是否在数组中
	//FD_ISSET(socketServer,&allSockets);


	//释放所有句柄
	for (u_int i = 0; i < allSockets.fd_count; i++)
	{
		closesocket(allSockets.fd_array[i]);
	}

	//+closesocket(socketServer);
	WSACleanup();

	//free(lpw);

	system("pause");
	return 0;
}

运行结果

image.png

温馨提示

客户端可以使用之前的基本通信模型的代码,但是在客户端接收消息这里:

char recvbuff[1500]={0};
int res = recv(socketServer,recvbuff,sizeof(recvbuff),0);

由于服务器端发送过来的消息是:hello,长度只有5,客户端非要用长度为1500的长度的字符数据进行接收,就会出现:

image.png

因此要把客户端接收消息的代码中,长度改成具体消息的长度(这里是20)

int res = recv(socketServer,recvbuff,20,0);

或者把服务器发送消息部分代码也改成对应长度的字符数组。调用string库中的strlen()!!!

小结

如果这篇文章对你有帮助的话,记得三连凹~