This is the fourteenth day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

Introduction to SELECT Model

When multiple clients connect to the server, the server cannot respond to multiple clients at the same time. The SELECT model is used to solve the problem of the server’s accept and RECV function waiting block (the client does not need to use this model). Note that the problem of blocking during execution is not solved, just that there is no waiting to block. (The difference between executing a block and waiting to block)

In other words, the recV execution process, the process of copying things from memory, is blocked, can not respond to other requests halfway; When recv is executed, if there is no message, the state is waiting for blocking. SELECT allows the SOCKET handle to respond to other SOCKET requests with messages while waiting for blocking.

The SELECT principle of

1. Each client has multiple sockets, and the server has its own SOCKET (single SOCKET), which is loaded into an array of sockets

The select function iterates through the array of sockets in 1. When a SOCKET has a response, the select parameter/return value is reflected back.

3. Perform different operations according to the SOCKET type:

  • If it is a server SOCKET, there is a client connection. Call Accept
  • If it is a client SOCKET, a client requests communication and invokes Send or RECV

SELECT code implementation

The server side

1, including network header file network library

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

Copy the code

2. Open the network library

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; }Copy the code

3. Verify the version

if (2 ! = HIBYTE(wdScoket.wVersion) || 2 ! = LOBYTE(wdscoke.wversion)) {printf(" Version problem! \n"); WSACleanup(); return 0; } printf(" Version verified! \n");Copy the code

4. Create a SOCKET

SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (INVALID_SOCKET == socketServer) { int err = WSAGetLastError(); Printf (" server failed to create SOCKET error code: %d\n", err); // Cleanup the network library without closing the handle WSACleanup(); return 0; } printf(" Server created SOCKET successfully! \n");Copy the code

5. Bind the IP address and port

if (SOCKET_ERROR == bind(socketServer, (const struct sockaddr*)&si, sizeof(si))) { int err = WSAGetLastError(); Printf (" server bind failed with error code: %d\n", err); closesocket(socketServer); / / WSACleanup release (); Return 0; } printf(" Server bind succeeded! \n");Copy the code

6. Start listening

if (SOCKET_ERROR == listen(socketServer, SOMAXCONN)) { int err = WSAGetLastError(); Printf (" server listening failed error code: %d\n", err); closesocket(socketServer); / / WSACleanup release (); Return 0; } printf(" Server listening successfully! \n");Copy the code

7, SELECT (key)

7.1 Loading server handles into fd_set

/ / reset FD_ZERO (& allSockets); FD_SET(socketServer, &allSockets);Copy the code

7.2 Starting a Polling Cycle

While (1) {outline}Copy the code

7.2.1 Querying signal Socket Handles Using Select

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

7.2.2 If no Socket handle is displayed, query the socket handle again

If (INVALID_SOCKET == socketClient) {int err = WSAGetLastError(); Printf ("accept client handle failed error code: %d\n", err); continue; }Copy the code

7.2.3 If the SELECT fails, error handling is performed

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 cannot get related information! \n"); int getopterr = WSAGetLastError(); Printf (" server getSocketopt failed with error code: %d\n", getopterr); } printf("%s\n", str); // Print the error message from getSocketopt}Copy the code

The complete code

//如果需要增加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;
}

Copy the code

The results

Warm prompt

The client can use the previous basic communication model code, but in the client receiving messages here:

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

Since the server sends a hello message with a length of 5, the client must receive the message with a length of 1500 characters.

Therefore, change the length of the code in which the client receives the message to the length of the specific message (20 in this case).

Int res = recv (socketServer, recvbuff, 20, 0).Copy the code

Or the server to send messages part of the code is also changed to the corresponding length of the character array. Call strlen() from the String library!!

summary

If this article helps you, remember the triple concave ~