• 基于Select模型的Windows TCP服务端和客户端程序示例


    最近跟着刘远东老师的《C++百万并发网络通信引擎架构与实现(服务端、客户端、跨平台)》,Bilibili视频地址为C++百万并发网络通信引擎架构与实现(服务端、客户端、跨平台),重新复习下Windows以及Linux、MacOS下的C++网络编程。另外因为最近自己使用boost写了一个TCP服务器压力测试工具,模拟多个客户端设备连接指定的服务器,并定时向服务器推送数据,以测试服务器的并发连接数等,感觉看这个视频收货还蛮大的。
    下面是Windows下使用Select模型实现的一个简易TCP服务端和客户端,客户端添加了一个命令输入线程,代码如下:
    一、服务端程序代码如下:
    // Server.cpp

    #include <stdio.h>
    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <WinSock2.h>
    
    #pragma comment(lib, "ws2_32.lib")
    
    using namespace std;
    
    // 要考虑字节对齐问题(32位和64位,平台和系统)
    enum CMDTYPE
    {
     CMD_LOGIN,			// 登录
     CMD_LOGIN_RESULT,	// 登录返回结果
     CMD_LOGOUT,			// 登出
     CMD_LOGOUT_RESULT,	// 登出返回结果
     CMD_NEW_USER_JOIN,	// 新用户加入
     CMD_ERROR			// 错误
    };
    
    // 消息头
    struct DataHeader
    {
     int	cmd;			// 命令类型
     int dataLength;		// 消息体的数据长度
    };
    
    // 消息体
    // DataPackage
    // 登录
    struct Login : public DataHeader 
    {
     Login()
     {
     	dataLength = sizeof(Login);
     	cmd = CMD_LOGIN;
     }
     char userName[32];
     char passWord[32];
    };
    
    // 登录结果
    struct LoginResult : public DataHeader
    {
     LoginResult()
     {
     	dataLength = sizeof(Login);
     	cmd = CMD_LOGIN_RESULT;
     	result = 0;
     }
     int result;
    };
    
    // 登出
    struct Logout : public DataHeader
    {
     Logout()
     {
     	dataLength = sizeof(Logout);
     	cmd = CMD_LOGOUT;
     }
     char userName[32];
    };
    
    // 登出结果
    struct LogoutResult : public DataHeader
    {
     LogoutResult()
     {
     	dataLength = sizeof(LogoutResult);
     	cmd = CMD_LOGOUT_RESULT;
     	result = 0;
     }
     int result;
    };
    
    // 新用户加入
    // 登出
    struct NewUserJoin : public DataHeader
    {
     NewUserJoin()
     {
     	dataLength = sizeof(NewUserJoin);
     	cmd = CMD_NEW_USER_JOIN;
     	sock = 0;
     }
     int sock;
    };
    
    std::vector<SOCKET> g_clientList;	// 客户端套接字列表
    
    int processor(SOCKET sock)
    {
     // 缓冲区(4096字节)
     char szRecv[4096] = {};
     // 5、接收客户端的请求
     // 先接收消息头
     int recvLen = recv(sock, szRecv, sizeof(DataHeader), 0);
     DataHeader *pHeader = (DataHeader*)szRecv;
     if (recvLen <= 0)
     {
     	printf("客户端<Socket=%d>已退出,任务结束...", sock);
     	return -1;
     }
    
     // 6、处理请求
     switch (pHeader->cmd)
     {
     case CMD_LOGIN:
     {
     	Login *login = (Login*)szRecv;
    
     	recv(sock, szRecv + sizeof(DataHeader), pHeader->dataLength - sizeof(DataHeader), 0);
     	printf("收到客户端<Socket=%d>请求:CMD_LOGIN, 数据长度:%d, userName:%s Password: %s
    ",
     		sock, login->dataLength, login->userName, login->passWord);
     	// 忽略判断用户名和密码是否正确的过程
     	LoginResult ret;
     	send(sock, (char*)&ret, sizeof(LoginResult), 0);
     }
     break;
     case CMD_LOGOUT:
     {
     	Logout *logout = (Logout*)szRecv;
    
     	recv(sock, szRecv + sizeof(DataHeader), pHeader->dataLength - sizeof(DataHeader), 0);
     	printf("收到客户端<Socket=%d>请求:CMD_LOGOUT, 数据长度:%d, userName:%s
    ",
     		sock, logout->dataLength, logout->userName);
     	LogoutResult ret;
     	send(sock, (char*)&ret, sizeof(LogoutResult), 0);
     }
     break;
     default:
     {
     	DataHeader header = { 0, CMD_ERROR };
     	send(sock, (char*)&header, sizeof(header), 0);
     	break;
     }
     }
    
     return 0;
    }
    
    int main(int argc, char *agrv[])
    {
     // 加载套接字库
     WORD wVersionRequested;
     WSADATA wsaData;
     int err;
    
     wVersionRequested = MAKEWORD(2, 2);
     // 启动Windows Socket 2.x环境
     err = WSAStartup(wVersionRequested, &wsaData);
    
     // 使用Socket API建立简易的TCP服务端
     if (err != 0) {
     	/* Tell the user that we could not find a usable */
     	/* WinSock DLL.                                  */
     	return 1;
     }
    
     /* Confirm that the WinSock DLL supports 2.2.*/
     /* Note that if the DLL supports versions greater    */
     /* than 2.2 in addition to 2.2, it will still return */
     /* 2.2 in wVersion since that is the version we      */
     /* requested.                                        */
    
     if (LOBYTE(wsaData.wVersion) != 2 ||
     	HIBYTE(wsaData.wVersion) != 2) {
     	/* Tell the user that we could not find a usable */
     	/* WinSock DLL.                                  */
     	WSACleanup();
     	return 1;
     }
    
     /* The WinSock DLL is acceptable. Proceed. */
      //----------------------
    // Create a SOCKET for listening for
    // incoming connection requests.
     SOCKET ListenSocket;
     ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
     if (ListenSocket == INVALID_SOCKET) {
     	printf("Error at socket(): %ld
    ", WSAGetLastError());
     	WSACleanup();
     	return 1;
     }
    
     //----------------------
     // The sockaddr_in structure specifies the address family,
     // IP address, and port for the socket that is being bound.
     sockaddr_in service;
     service.sin_family = AF_INET;
     //service.sin_addr.s_addr = inet_addr("127.0.0.1");
     service.sin_addr.s_addr = INADDR_ANY;
     service.sin_port = htons(27015);
    
     if (bind(ListenSocket,
     	(SOCKADDR*)&service,
     	sizeof(service)) == SOCKET_ERROR) {
     	printf("bind() failed.绑定网络端口失败
    ");
     	closesocket(ListenSocket);
     	WSACleanup();
     	return 1;
     }
     else
     {
     	printf("绑定网络端口成功...
    ");
     }
    
     //----------------------
     // Listen for incoming connection requests.
     // on the created socket
     if (listen(ListenSocket, 5) == SOCKET_ERROR) {
     	printf("错误,监听网络端口失败...
    ");
     	closesocket(ListenSocket);
     	WSACleanup();
     	return 1;
     }
     else
     {
     	printf("监听网络端口成功...
    ");
     }
    
     printf("等待客户端连接...
    ");
    
     while (true)
     {
     	// Berkeley sockets
     	fd_set readfds;			// 描述符(socket)集合
     	fd_set writefds;
     	fd_set exceptfds;
    
     	// 清理集合
     	FD_ZERO(&readfds);
     	FD_ZERO(&writefds);
     	FD_ZERO(&exceptfds);
    
     	// 将描述符(socket)加入集合
     	FD_SET(ListenSocket, &readfds);
     	FD_SET(ListenSocket, &writefds);
     	FD_SET(ListenSocket, &exceptfds);
    
     	for (int n = (int)g_clientList.size() - 1;  n >= 0 ; n--)
     	{
     		FD_SET(g_clientList[n], &readfds);
     	}
    
     	// 设置超时时间 select 非阻塞
     	timeval timeout = { 1, 0 };
     	
     	// nfds是一个整数值,是指fd_set集合中所有描述符(socket)的范围,而不是数量
     	// 即是所有文件描述符最大值+1 在Windows中这个参数可以写0
     	//int ret = select(ListenSocket + 1, &readfds, &writefds, &exceptfds, NULL);
     	int ret = select(ListenSocket + 1, &readfds, &writefds, &exceptfds, &timeout);
     	if (ret < 0)
     	{
     		printf("select任务结束,called failed:%d!
    ", WSAGetLastError());
     		break;
     	}
    
     	// 是否有数据可读
     	// 判断描述符(socket)是否在集合中
     	if (FD_ISSET(ListenSocket, &readfds))
     	{
     		//FD_CLR(ListenSocket, &readfds);
    
     		// Create a SOCKET for accepting incoming requests.
     		// 4. accept 等待接受客户端连接
     		SOCKADDR_IN clientAddr = {};
     		int nAddrLen = sizeof(SOCKADDR_IN);
     		SOCKET ClientSocket = INVALID_SOCKET;
     		ClientSocket = accept(ListenSocket, (SOCKADDR*)&clientAddr, &nAddrLen);
     		if (INVALID_SOCKET == ClientSocket) {
     			printf("accept() failed: %d,接收到无效客户端Socket
    ", WSAGetLastError());
     			return 1;
     		}
     		else
     		{
     			// 有新的客户端加入,向之前的所有客户端群发消息
     			for (int n = (int)g_clientList.size() - 1; n >= 0; n--)
     			{
     				NewUserJoin userJoin;
     				send(g_clientList[n], (const char*)&userJoin, sizeof(NewUserJoin), 0);
     			}
    
     			g_clientList.push_back(ClientSocket);
     			// 客户端连接成功,则显示客户端连接的IP地址和端口号
     			printf("新客户端<Sokcet=%d>加入,Ip地址:%s,端口号:%d
    ", ClientSocket, inet_ntoa(clientAddr.sin_addr),
     				ntohs(clientAddr.sin_port));
     		}
     	}
    
     	for (int i = 0; i < (int)readfds.fd_count - 1; ++i)
     	{
     		if (-1 == processor(readfds.fd_array[i]))
     		{
     			auto iter = std::find(g_clientList.begin(), g_clientList.end(), 
     									readfds.fd_array[i]);
     			if (iter != g_clientList.end())
     			{
     				g_clientList.erase(iter);
     			}
     		}
     	}
    
     	//printf("空闲时间处理其他业务...
    ");
     }
     
     for (int n = (int)g_clientList.size() - 1; n >= 0; n--)
     {
     	closesocket(g_clientList[n]);
     }
    
     // 8.关闭套接字
     closesocket(ListenSocket);
     // 9.清除Windows Socket环境
     WSACleanup();
    
     printf("服务端已退出,任务结束
    ");
    
     getchar();
    
     return 0;
    }
    

    二、客户端程序代码如下:
    // Client.cpp

    #include <stdio.h>
    
    #define WIN32_LEAN_AND_MEAN
    #include <Windows.h>
    #include <WinSock2.h>
    
    #include <thread>
    
    #pragma comment(lib, "ws2_32.lib")
    
    // 要考虑字节对齐问题(32位和64位,平台和系统)
    enum CMDTYPE
    {
    	CMD_LOGIN,			// 登录
    	CMD_LOGIN_RESULT,	// 登录返回结果
    	CMD_LOGOUT,			// 登出
    	CMD_LOGOUT_RESULT,	// 登出返回结果
    	CMD_NEW_USER_JOIN,	// 新用户加入
    	CMD_ERROR			// 错误
    };
    
    // 消息头
    struct DataHeader
    {
    	int	cmd;			// 命令类型
    	int dataLength;		// 消息体的数据长度
    };
    
    // 消息体
    // DataPackage
    // 登录
    struct Login : public DataHeader
    {
    	Login()
    	{
    		dataLength = sizeof(Login);
    		cmd = CMD_LOGIN;
    	}
    	char userName[32];
    	char passWord[32];
    };
    
    // 登录结果
    struct LoginResult : public DataHeader
    {
    	LoginResult()
    	{
    		dataLength = sizeof(Login);
    		cmd = CMD_LOGIN_RESULT;
    		result = 0;
    	}
    	int result;
    };
    
    // 登出
    struct Logout : public DataHeader
    {
    	Logout()
    	{
    		dataLength = sizeof(Logout);
    		cmd = CMD_LOGOUT;
    	}
    	char userName[32];
    };
    
    // 登出结果
    struct LogoutResult : public DataHeader
    {
    	LogoutResult()
    	{
    		dataLength = sizeof(LogoutResult);
    		cmd = CMD_LOGOUT_RESULT;
    		result = 0;
    	}
    	int result;
    };
    
    // 新用户加入
    // 登出
    struct NewUserJoin : public DataHeader
    {
    	NewUserJoin()
    	{
    		dataLength = sizeof(NewUserJoin);
    		cmd = CMD_NEW_USER_JOIN;
    		sock = 0;
    	}
    	int sock;
    };
    
    
    int processor(SOCKET sock)
    {
    	// 缓冲区(4096字节)
    	char szRecv[4096] = {};
    	// 5、接收客户端的请求
    	// 先接收消息头
    	int recvLen = recv(sock, szRecv, sizeof(DataHeader), 0);
    	DataHeader *pHeader = (DataHeader*)szRecv;
    	if (recvLen <= 0)
    	{
    		printf("与服务器断开连接,任务结束...");
    		return -1;
    	}
    
    	// 6、处理请求
    	switch (pHeader->cmd)
    	{
    	case CMD_LOGIN_RESULT:
    	{
    		recv(sock, szRecv + sizeof(DataHeader), pHeader->dataLength - sizeof(DataHeader), 0);
    
    		LoginResult *loginRes = (LoginResult*)szRecv;
    
    		printf("收到服务器消息:CMD_LOGIN_RESULT, 数据长度:%d, result:%d
    ",
    			loginRes->dataLength, loginRes->result);
    	}
    	break;
    	case CMD_LOGOUT_RESULT:
    	{
    		recv(sock, szRecv + sizeof(DataHeader), pHeader->dataLength - sizeof(DataHeader), 0);
    
    		LogoutResult *logoutRes = (LogoutResult*)szRecv;
    
    		printf("收到服务器消息:CMD_LOGOUT_RESULT, 数据长度:%d, result:%d
    ",
    			logoutRes->dataLength, logoutRes->result);
    	}
    	case CMD_NEW_USER_JOIN:
    	{
    		recv(sock, szRecv + sizeof(DataHeader), pHeader->dataLength - sizeof(DataHeader), 0);
    
    		NewUserJoin *userJoin = (NewUserJoin*)szRecv;
    
    		printf("收到服务器消息:CMD_NEW_USER_JOIN, 数据长度:%d
    ",
    			userJoin->dataLength);
    	}
    	break;
    	}
    
    	return 0;
    }
    
    bool g_bRun = true;		// 是否退出程序
    
    // 命令输入 线程入口函数
    void cmdThread(SOCKET sock)
    {
    	while (true)
    	{
    		// 输入请求命令
    		char cmdBuf[128] = { 0 };
    		printf("请输入命令:[exit | login | logout | other]
    ");
    		scanf("%s", &cmdBuf);
    
    		// 处理请求
    		if (0 == strcmp(cmdBuf, "exit"))
    		{
    			g_bRun = false;
    			printf("退出cmdThread线程...
    ");
    			break;
    		}
    		else if (0 == strcmp(cmdBuf, "login"))
    		{
    			// 5.向服务器发送命令请求
    			Login login;
    			strcpy(login.userName, "ccf");
    			strcpy(login.passWord, "ccfPwd");
    
    			send(sock, (const char*)&login, sizeof(login), 0);
    		}
    		else if (0 == strcmp(cmdBuf, "logout"))
    		{
    			// 5.向服务器发送命令请求
    			Logout logout;
    			strcpy(logout.userName, "ccf");
    			send(sock, (const char*)&logout, sizeof(logout), 0);
    		}
    		else
    		{
    			printf("不支持的命令,请重新输入.
    ");
    		}
    	}
    }
    
    int main(int argc, char *argv[])
    {
    	//----------------------
    	// Initialize Winsock
    	WSADATA wsaData;
    	int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    	if (iResult != NO_ERROR)
    	{
    		printf("WSAStartup() 错误,创建套接字库失败!
    ");
    		return -1;
    	}
    
    	//----------------------
    	// Create a SOCKET for connecting to server
    	SOCKET ConnectSocket;
    	ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	if (ConnectSocket == INVALID_SOCKET) {
    		printf("创建套接字失败: %ld
    ", WSAGetLastError());
    		WSACleanup();
    		return -1;
    	}
    
    	//----------------------
    	// The sockaddr_in structure specifies the address family,
    	// IP address, and port of the server to be connected to.
    	sockaddr_in clientService;
    	clientService.sin_family = AF_INET;
    	clientService.sin_addr.s_addr = inet_addr("127.0.0.1");
    	clientService.sin_port = htons(27015);
    
    	//----------------------
    	// Connect to server.
    	if (connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService)) == SOCKET_ERROR) {
    		printf("连接服务器失败.
    ");
    		WSACleanup();
    		return -1;
    	}
    
    	printf("客户端成功连接到服务器.
    ");
    
    	// 循环输入命令
    	// 启动线程
    	std::thread thread_(cmdThread, ConnectSocket);		
    	thread_.detach();
    
    	while (g_bRun)
    	{
    		fd_set fdReads;
    		FD_ZERO(&fdReads);
    
    		FD_SET(ConnectSocket, &fdReads);
    
    		timeval timeout = { 0, 0 };
    		int ret = select(ConnectSocket, &fdReads, NULL, NULL, &timeout);
    		if (ret < 0)
    		{
    			printf("select任务结束1...
    ");
    			break;
    		}
    
    		if (FD_ISSET(ConnectSocket, &fdReads))
    		{
    			//FD_CLR(ConnectSocket, &fdReads);
    
    			// 有数据可读,可以处理了
    			if (-1 == processor(ConnectSocket))
    			{
    				printf("select任务结束2
    ");
    				break;
    			}
    		}
    
    		/*Login login;
    		strcpy(login.userName, "ccf");
    		strcpy(login.passWord, "ccfPwd");
    
    		send(ConnectSocket, (const char*)&login, sizeof(Login), 0);*/
    		//Sleep(1000);	// 发送登录数据后延时1s
    
    		//printf("空闲时间处理其他业务...
    ");
    	}
    	
    	WSACleanup();
    
    	printf("客户端已退出...
    ");
    
    	getchar();
    
    	return 0;
    }
    

    之前在CSDN上看到一篇博客,是基于UDP的Linux C++简单聊天室实现,我把源代码重新整理并放在个人的GitHub上面基于UDP的Linux C++简单聊天室,同样是使用select模型实现的,有兴趣可以看一下。

  • 相关阅读:
    sysctl.conf文件详解
    linux下高并发网络应用注意事项
    linux 异常
    myeclipse 上安装 Maven3
    windows 系统相关配置
    常用DOS命令总结
    豆瓣爬虫Scrapy“抄袭”改写
    vue基本配置和生命周期
    面试知识点
    ubuntu18.04国内软件源
  • 原文地址:https://www.cnblogs.com/ccf19881030/p/12004814.html
Copyright © 2020-2023  润新知