C++ 网络编程实践(第一篇)

以下内容来源于B站视频https://www.bilibili.com/video/BV1j4411S7jg?p=18学习资源
仅将学习笔记记录如下供以后查阅及大家分享,欢迎讨论与批评指正

网络通信基础

1. tcp通信的客户端和服务端

建立客户端的步骤

1. 建立通信套接字socket   -- 类似读写文件中的File*
2. 连接服务端        -- fopen 打开文件
3. 向服务端发送数据   --  fwrite
4. 接收服务端数据    -- fread
5. 关闭socket       -- fclose

建立服务端的步骤

1. 创建socket
2. 绑定端口
3. 监听网络端口
4. 等待客户端连接(则塞)
5. 接收客户端数据
6. 向客户端发送数据
7. 关闭socket

2. winsocket创建客户端和服务端

  • 头文件 <WinSock2.h> 放在<windows.h>之前

  • 宏重定义,winsock第一版和第二版的宏重定义了 #define WIN32_LEAN_AND_MEAN

  • 引入库文件 ws2_32.lib

  • 服务端代码(循环接收客户端连接,一旦连接上,向客户端发送一条消息)

    // 启动windows socket网络环境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);
    // 创建socket
    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET)
    {
    std::cout << "create sock failed" << std::endl;
    }
    // 绑定端口
    sockaddr_in sin = {};
    sin.sin_family = AF_INET; // IPV4
    sin.sin_port = htons(10086);
    sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// INADDR_ANY
    auto res = bind(sock, (sockaddr*)&sin, sizeof(sin));
    if (res == SOCKET_ERROR)
    {
    std::cout << "bind port error!" << std::endl;
    }
    // 监听端口
    res = listen(sock, 5);
    if (res == SOCKET_ERROR)
    {
    std::cout << "listen port error!" << std::endl;
    }
    std::cout << "wait for client in" << std::endl;
    // 等待客户端连接
    sockaddr_in clientAddr = {};
    int nAddrLen = sizeof(clientAddr);
    SOCKET clientSock = INVALID_SOCKET;
    char msgBuf[] = "hello";
    // 循环等待客户端连接
    while (1)
    {
    clientSock = accept(sock, (sockaddr*)&clientAddr, &nAddrLen);
    if (clientSock == INVALID_SOCKET)
    {
    std::cout << "receive client socket failed!" << std::endl;
    }
    std::cout << "client ip: " << inet_ntoa(clientAddr.sin_addr) << std::endl;
    // 向服务端发送数据
    send(clientSock, msgBuf, strlen(msgBuf) + 1, 0);
    }
    
    // 关闭套接字
    closesocket(sock);
    
    // 清除windows socket环境
    WSACleanup();
    
  • 客户端代码(连接服务器并接收服务端发送的一条消息)

    // 启动windows socket网络环境
    WORD ver = MAKEWORD(2, 2);
    WSADATA dat;
    WSAStartup(ver, &dat);
    
    // 创建socket
    SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET)
    {
    std::cout << "create socket failed" << std::endl;
    }
    // 连接服务器
    sockaddr_in sin = {};
    sin.sin_family = AF_INET;
    sin.sin_port = htons(10086);
    sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    auto res = connect(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
    if (res == SOCKET_ERROR)
    {
    std::cout << "connect server failed" << std::endl;
    }
    // 接收服务器发送的消息
    char buf[1024] = { 0 };
    int len = recv(sock, buf, 1024, 0);
    if (len > 0)
    {
    std::cout << "data from server is" << buf << std::endl;
    }
    
    // 关闭套接字
    closesocket(sock);
    WSACleanup();
    std::cin.get();
    
  • 服务端改造1(只等待一个客户端接入,循环等待客户端发送数据,处理数据并响应数据给客户端)

    	// 启动windows socket网络环境
    	WORD ver = MAKEWORD(2, 2);
    	WSADATA dat;
    	WSAStartup(ver, &dat);
    	// 创建socket
    	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	if (sock == INVALID_SOCKET)
    	{
    		std::cout << "create sock failed" << std::endl;
    	}
    	// 绑定端口
    	sockaddr_in sin = {};
    	sin.sin_family = AF_INET; // IPV4
    	sin.sin_port = htons(10086);
    	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// INADDR_ANY
    	auto res = bind(sock, (sockaddr*)&sin, sizeof(sin));
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "bind port error!" << std::endl;
    	}
    	// 监听端口
    	res = listen(sock, 5);
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "listen port error!" << std::endl;
    	}
    	std::cout << "wait for client in" << std::endl;
    	// 等待客户端连接
    	sockaddr_in clientAddr = {};
    	int nAddrLen = sizeof(clientAddr);
    	SOCKET clientSock = INVALID_SOCKET;
    	char msgBuf[] = "hello";
    	// 等待客户端连接
    	clientSock = accept(sock, (sockaddr*)&clientAddr, &nAddrLen);
    	if (clientSock == INVALID_SOCKET)
    	{
    		std::cout << "receive client socket failed!" << std::endl;
    	} 
    	std::cout << "client ip: " << inet_ntoa(clientAddr.sin_addr) << std::endl;
    	while (1)
    	{
    		int len = recv(clientSock, msgBuf, 1024, 0);
    		if (len <= 0)
    		{
    			std::cout << "receive from client failed" << std::endl;
    			break;
    		}
    		std::cout << "receive msg is : " << msgBuf << std::endl;
    		std::string resStr = "";
    		Handle(msgBuf, resStr);
    		// 向服务端发送数据
    		send(clientSock, resStr.c_str(), resStr.length() + 1, 0);
    	}
    
    	// 关闭套接字
    	closesocket(sock);
    
    	// 清除windows socket环境
    	WSACleanup();
    	std::cout << "session end" << std::endl;
    	std::cin.get();
    
  • 客户端改造1(连接成功后,循环输入命令发送给服务端,并接收服务端返回的消息并处理,即打印)

    	// 启动windows socket网络环境
    	WORD ver = MAKEWORD(2, 2);
    	WSADATA dat;
    	WSAStartup(ver, &dat);
    
    	// 创建socket
    	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (sock == INVALID_SOCKET)
    	{
    		std::cout << "create socket failed" << std::endl;
    	}
    	// 连接服务器
    	sockaddr_in sin = {};
    	sin.sin_family = AF_INET;
    	sin.sin_port = htons(10086);
    	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    	auto res = connect(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "connect server failed" << std::endl;
    	}
    	std::cout << "connect server succeed" << std::endl;
    
    	// 输入命令发送给服务器并接收返回的消息
    	char buf[1024] = { 0 };
    	while (true)
    	{
    		std::cout << "请输入命令:" << std::endl;
    		std::cin >> buf;
    		if (strcmp(buf, "exit") == 0)
    		{
    			std::cout << "receive end command" << std::endl;
    			break;
    		}
    		send(sock, buf, strlen(buf) + 1, 0);
    
    		// 接收响应
    		int len = recv(sock, buf, 1024, 0);
    		if (len > 0)
    		{
    			std::cout << "data from server is " << buf << std::endl;
    		}
    
    	}
    	// 关闭套接字
    	closesocket(sock);
    	WSACleanup();
    	std::cout << "session end" << std::endl;
    	std::cin.get();
    

    结构化消息数据

  • 结构体 struct(一次性将多个信息返回给客户端)

    typedef struct DataPackage_STRU
    {
    	int age;
    	char name[32];
    } DataPackage;
    
    // 单纯的结构作为报文时,客户端发送异常数据,服务器也会进行处理,未进行过滤
    
  • 网络数据报文

    1. 一个报文包含包头和包体
    2. 包头: 描述消息包的大小及数据的作用以及发送者,接收者等
    3. 包体: 实际使用的数据buf
    
    // 服务端接收两次消息,先接收消息头,判断消息是否有效,有效的话继续接收消息体
    // 服务端返回消息时,也是两次发送,先发送消息头,再发送消息体
    
    // 定义两种有效命令,每种命令对应相应的消息体
    enum CmdIndex
    {
    	LOGIN_IN,
    	LOGIN_OUT,
    	LOGIN_ERROR
    };
    
    // LOGIN_IN命令对应的消息体结构
    typedef struct LoginIn_STRU
    {
    	char userName[32];
    	char pwd[32];
    } LoginIn;
    
    typedef struct LoginInRes_STRU
    {
    	int res;
    } LoginInRes;
    
    // LOGIN_OUT命令对应的消息体结构
    typedef struct LoginOut_STRU
    {
    	char userName[32];
    } LoginOut;
    
    typedef struct LoginOutRes_STRU
    {
    	int res;
    } LoginOutRes;
    
    // 消息头
    typedef struct DataHeader_STRU
    {
    	unsigned short dataLen;
    	unsigned short cmdId;
    } DataHeader;
    
    • 服务端报文改造代码

      	// 启动windows socket网络环境
      	WORD ver = MAKEWORD(2, 2);
      	WSADATA dat;
      	WSAStartup(ver, &dat);
      	// 创建socket
      	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
      	if (sock == INVALID_SOCKET)
      	{
      		std::cout << "create sock failed" << std::endl;
      	}
      	// 绑定端口
      	sockaddr_in sin = {};
      	sin.sin_family = AF_INET; // IPV4
      	sin.sin_port = htons(10086);
      	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// INADDR_ANY
      	auto res = bind(sock, (sockaddr*)&sin, sizeof(sin));
      	if (res == SOCKET_ERROR)
      	{
      		std::cout << "bind port error!" << std::endl;
      	}
      	// 监听端口
      	res = listen(sock, 5);
      	if (res == SOCKET_ERROR)
      	{
      		std::cout << "listen port error!" << std::endl;
      	}
      	std::cout << "wait for client in" << std::endl;
      	// 等待客户端连接
      	sockaddr_in clientAddr = {};
      	int nAddrLen = sizeof(clientAddr);
      	SOCKET clientSock = INVALID_SOCKET;
      	char msgBuf[] = "hello";
      	// 等待客户端连接
      	clientSock = accept(sock, (sockaddr*)&clientAddr, &nAddrLen);
      	if (clientSock == INVALID_SOCKET)
      	{
      		std::cout << "receive client socket failed!" << std::endl;
      	} 
      	std::cout << "client ip: " << inet_ntoa(clientAddr.sin_addr) << std::endl;
      	DataPackage dataObj;
      	while (1)
      	{
      		// 先接收消息头
      		DataHeader header;
      		int len = recv(clientSock, (char*)&header, sizeof(header), 0);
      		if (len <= 0)
      		{
      			std::cout << "receive from client failed" << std::endl;
      			break;
      		}
      		std::cout << "receive msg header is : " << header.cmdId << std::endl;
      		switch (header.cmdId)
      		{
      		case LOGIN_IN:
      		{
      			// 接收消息体
      			LoginIn data;
      			recv(clientSock, (char*)&data, sizeof(data), 0);
      
      			// 返回消息头
      			send(clientSock, (char*)&header, sizeof(header), 0);
      			// 返回消息体
      			LoginInRes resData;
      			resData.res = 1;
      			send(clientSock, (char*)&resData, sizeof(resData), 0);
      		}
      			break;
      		case LOGIN_OUT:
      		{
      			// 接收消息体
      			LoginOut data;
      			recv(clientSock, (char*)&data, sizeof(data), 0);
      			// ... handle
      			// 返回消息头
      			send(clientSock, (char*)&header, sizeof(header), 0);
      			// 返回消息体
      			LoginOutRes resData;
      			resData.res = 1;
      			send(clientSock, (char*)&resData, sizeof(resData), 0);
      		}
      		default:
      			// 返回消息头
      			header.cmdId = LOGIN_ERROR;
      			header.dataLen = 0;
      			send(clientSock, (char*)&header, sizeof(header), 0);
      			break;
      		}
      	}
      
      	// 关闭套接字
      	closesocket(sock);
      
      	// 清除windows socket环境
      	WSACleanup();
      	std::cout << "session end" << std::endl;
      	std::cin.get();
      
    • 客户端报文改造代码(使用同一套报文定义)

      	// 启动windows socket网络环境
      	WORD ver = MAKEWORD(2, 2);
      	WSADATA dat;
      	WSAStartup(ver, &dat);
      
      	// 创建socket
      	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
      	if (sock == INVALID_SOCKET)
      	{
      		std::cout << "create socket failed" << std::endl;
      	}
      	// 连接服务器
      	sockaddr_in sin = {};
      	sin.sin_family = AF_INET;
      	sin.sin_port = htons(10086);
      	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
      	auto res = connect(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
      	if (res == SOCKET_ERROR)
      	{
      		std::cout << "connect server failed" << std::endl;
      	}
      	std::cout << "connect server succeed" << std::endl;
      
      	// 输入命令发送给服务器并接收返回的消息
      	char buf[1024] = { 0 };
      	DataPackage data;
      	while (true)
      	{
      		std::cout << "请输入命令:" << std::endl;
      		std::cin >> buf;
      		if (strcmp(buf, "exit") == 0)
      		{
      			std::cout << "receive end command" << std::endl;
      			break;
      		}
      		else if(strcmp(buf, "loginin") == 0)
      		{
      			LoginIn data = { "zhanghu", "mima" };
      			DataHeader header;
      			header.cmdId = LOGIN_IN;
      			header.dataLen = sizeof(LoginIn);
      			send(sock, (char*)&header, sizeof(DataHeader), 0);
      			send(sock, (char*)&data, sizeof(LoginIn), 0);
      
      			// 接收数据
      			DataHeader resHeader = {};
      			LoginInRes resData = {};
      			recv(sock, (char*)&resHeader, sizeof(DataHeader), 0);
      			recv(sock, (char*)&resData, sizeof(LoginInRes), 0);
      			std::cout << resData.res << std::endl;
      		}
      		else if(strcmp(buf, "loginout") == 0)
      		{
      			LoginOut data = { "zhanghu"};
      			DataHeader header;
      			header.cmdId = LOGIN_OUT;
      			header.dataLen = sizeof(LoginOut);
      			send(sock, (char*)&header, sizeof(DataHeader), 0);
      			send(sock, (char*)&data, sizeof(LoginOut), 0);
      
      			// 接收数据
      			DataHeader resHeader = {};
      			LoginOutRes resData = {};
      			recv(sock, (char*)&resHeader, sizeof(DataHeader), 0);
      			recv(sock, (char*)&resData, sizeof(LoginOutRes), 0);
      			std::cout << resData.res << std::endl;
      		}
      		else
      		{
      			std::cout << "not support cmd, please reentry:" << std::endl;
      		}
      	}
      	// 关闭套接字
      	closesocket(sock);
      	WSACleanup();
      	std::cout << "session end" << std::endl;
      	std::cin.get();
      
  • 合并包头和包体,一次发送和接收

    // 使用继承包头的方法合并
    // 客户端只发送一次,服务端需要分两次接收,先接收包头,再做偏移地址接收包体
    
    // 服务端
    	// 启动windows socket网络环境
    	WORD ver = MAKEWORD(2, 2);
    	WSADATA dat;
    	WSAStartup(ver, &dat);
    	// 创建socket
    	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    	if (sock == INVALID_SOCKET)
    	{
    		std::cout << "create sock failed" << std::endl;
    	}
    	// 绑定端口
    	sockaddr_in sin = {};
    	sin.sin_family = AF_INET; // IPV4
    	sin.sin_port = htons(10086);
    	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// INADDR_ANY
    	auto res = bind(sock, (sockaddr*)&sin, sizeof(sin));
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "bind port error!" << std::endl;
    	}
    	// 监听端口
    	res = listen(sock, 5);
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "listen port error!" << std::endl;
    	}
    	std::cout << "wait for client in" << std::endl;
    	// 等待客户端连接
    	sockaddr_in clientAddr = {};
    	int nAddrLen = sizeof(clientAddr);
    	SOCKET clientSock = INVALID_SOCKET;
    	char msgBuf[] = "hello";
    	// 等待客户端连接
    	clientSock = accept(sock, (sockaddr*)&clientAddr, &nAddrLen);
    	if (clientSock == INVALID_SOCKET)
    	{
    		std::cout << "receive client socket failed!" << std::endl;
    	} 
    	std::cout << "client ip: " << inet_ntoa(clientAddr.sin_addr) << std::endl;
    	while (1)
    	{
    		// 先接收消息头
    		DataHeader header;
    		int len = recv(clientSock, (char*)&header, sizeof(header), 0);
    		if (len <= 0)
    		{
    			std::cout << "receive from client failed" << std::endl;
    			break;
    		}
    		std::cout << "receive msg header is : " << header.cmdId << std::endl;
    		switch (header.cmdId)
    		{
    		case LOGIN_IN:
    		{
    			// 接收消息体
    			LoginIn data;
    			recv(clientSock, (char*)&data + sizeof(DataHeader), sizeof(data) - sizeof(DataHeader), 0);
    
    			// 返回消息
    			LoginInRes resData;
    			send(clientSock, (char*)&resData, sizeof(resData), 0);
    		}
    			break;
    		case LOGIN_OUT:
    		{
    			// 接收消息体
    			LoginOut data;
    			recv(clientSock, (char*)&data + sizeof(DataHeader), sizeof(data) - sizeof(DataHeader), 0);
    			// ... handle
    			// 返回消息
    			LoginOutRes resData;
    			send(clientSock, (char*)&resData, sizeof(resData), 0);
    		}
    		default:
    			// 返回消息头
    			header.cmdId = LOGIN_ERROR;
    			header.dataLen = 0;
    			send(clientSock, (char*)&header, sizeof(header), 0);
    			break;
    		}
    	}
    
    	// 关闭套接字
    	closesocket(sock);
    
    	// 清除windows socket环境
    	WSACleanup();
    	std::cout << "session end" << std::endl;
    	std::cin.get();
    
    // 客户端
    	// 启动windows socket网络环境
    	WORD ver = MAKEWORD(2, 2);
    	WSADATA dat;
    	WSAStartup(ver, &dat);
    
    	// 创建socket
    	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    	if (sock == INVALID_SOCKET)
    	{
    		std::cout << "create socket failed" << std::endl;
    	}
    	// 连接服务器
    	sockaddr_in sin = {};
    	sin.sin_family = AF_INET;
    	sin.sin_port = htons(10086);
    	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
    	auto res = connect(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
    	if (res == SOCKET_ERROR)
    	{
    		std::cout << "connect server failed" << std::endl;
    	}
    	std::cout << "connect server succeed" << std::endl;
    
    	// 输入命令发送给服务器并接收返回的消息
    	char buf[1024] = { 0 };
    	DataPackage data;
    	while (true)
    	{
    		std::cout << "请输入命令:" << std::endl;
    		std::cin >> buf;
    		if (strcmp(buf, "exit") == 0)
    		{
    			std::cout << "receive end command" << std::endl;
    			break;
    		}
    		else if(strcmp(buf, "loginin") == 0)
    		{
    			LoginIn data;
    			std::string userName = "zhangsan";
    			memcpy(data.userName, userName.c_str(), userName.length() + 1);
    			std::string pwd = "123456";
    			memcpy(data.pwd, pwd.c_str(), pwd.length() + 1);
    			send(sock, (char*)&data, sizeof(LoginIn), 0);
    
    			// 接收数据
    			LoginInRes resData = {};
    			recv(sock, (char*)&resData, sizeof(LoginInRes), 0);
    			std::cout << "res cmd is "<< resData.cmdId << " res is " << resData.res << std::endl;
    		}
    		else if(strcmp(buf, "loginout") == 0)
    		{
    			LoginOut data;
    			std::string userName = "zhangsan";
    			memcpy(data.userName, userName.c_str(), userName.length() + 1);
    			send(sock, (char*)&data, sizeof(LoginOut), 0);
    
    			// 接收数据
    			LoginOutRes resData = {};
    			recv(sock, (char*)&resData, sizeof(LoginOutRes), 0);
    			std::cout << "res cmd is " << resData.cmdId << " res is " << resData.res << std::endl;
    		}
    		else
    		{
    			std::cout << "not support cmd, please reentry:" << std::endl;
    		}
    	}
    	// 关闭套接字
    	closesocket(sock);
    	WSACleanup();
    	std::cout << "session end" << std::endl;
    	std::cin.get();
    	return 0;
    
  • 网络消息长度的问题

    1. 固长数据/变长数据
    2. 粘包/拆包
    3. 分包/组包
    

3. socket的select网络模型(处理多客户端场景)

// select的原理: 将多个套接字放在一个集合里,统一检查集合中套接字的状态(可读,可写,异常),调用、select后,会更新集合中套接字的状态,如果可读,即可执行fdRead中的socket,依次避免多个客户端时阻塞,当没有数据写入或连接时,select会阻塞,可设置等待时间timeout(s)

// 伯克利 socket
// 参数1 nfds: 在windows下无作用,整数值,fd_set中所有socket描述符的范围,而不是数量
// 即描述符最大值 + 1
// 参数2 readfds: 需要查询的套接字集合
// 参数3 writefds:
// 参数4 exceptfds: 
// 参数5 timeout:
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
FD_ZERO(&fdRead); // 清空集合
FD_ZERO(&fdWrite); // 清空集合
FD_ZERO(&fdExp); // 清空集合
FD_SET(sock, &fdRead);
FD_SET(sock, &fdWrite);
FD_SET(sock, &fdExp);
select(sock+1, &fdRead, &fdWrite, &fdExp, NULL)

报文结构定义

// 定义两种有效命令,每种命令对应相应的消息体
enum CmdIndex
{
	LOGIN_IN,
	LOGIN_OUT,
	LOGIN_IN_RES,
	LOGIN_OUT_RES,
	NEW_JOIN,
	LOGIN_ERROR
};

// 消息头
struct DataHeader
{
	unsigned short dataLen;
	unsigned short cmdId;
};

struct LoginNew : public DataHeader
{
	LoginNew()
	{
		dataLen = sizeof(LoginNew);
		cmdId = NEW_JOIN;
	}
	int socket;
};

// LOGIN_IN命令对应的消息体结构
struct LoginIn : public DataHeader
{
	LoginIn()
	{
		dataLen = sizeof(LoginIn);
		cmdId = LOGIN_IN;
	}
	char userName[32];
	char pwd[32];
};

struct LoginInRes : public DataHeader
{
	LoginInRes()
	{
		dataLen = sizeof(LoginInRes);
		cmdId = LOGIN_IN_RES;
	}
	int res{ 1 };
};

// LOGIN_OUT命令对应的消息体结构
struct LoginOut : public DataHeader
{
	LoginOut()
	{
		dataLen = sizeof(LoginOut);
		cmdId = LOGIN_OUT;
	}
	char userName[32];
};

struct LoginOutRes : public DataHeader
{
	LoginOutRes()
	{
		dataLen = sizeof(LoginOutRes);
		cmdId = LOGIN_OUT_RES;
	}
	int res{ 1 };
};

支持多客户端连接服务端改造代码(利用select模型实现多客户端连接,设置time_val,使select不阻塞,当有新客户端接入时,向已存在的客户端广播消息,消息处理过程:现获取消息头,再解析消息体,并返回消息)

	// 启动windows socket网络环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
	// 创建socket
	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET)
	{
		std::cout << "create sock failed" << std::endl;
	}
	// 绑定端口
	sockaddr_in sin = {};
	sin.sin_family = AF_INET; // IPV4
	sin.sin_port = htons(10086);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");// INADDR_ANY
	auto res = bind(sock, (sockaddr*)&sin, sizeof(sin));
	if (res == SOCKET_ERROR)
	{
		std::cout << "bind port error!" << std::endl;
	}
	// 监听端口
	res = listen(sock, 5);
	if (res == SOCKET_ERROR)
	{
		std::cout << "listen port error!" << std::endl;
	}


	while (1)
	{
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExp;
		FD_ZERO(&fdRead); // 清空集合
		FD_ZERO(&fdWrite); // 清空集合
		FD_ZERO(&fdExp); // 清空集合
		FD_SET(sock, &fdRead);
		FD_SET(sock, &fdWrite);
		FD_SET(sock, &fdExp);

		// 把新加入的客户端加入可读的集合
		for (int i = 0; i < vecSockets.size(); ++i)
		{
			FD_SET(vecSockets[i], &fdRead);
		}

		time_val 
		int ret = select(sock + 1, &fdRead, &fdWrite, &fdExp, NULL);
		if(ret < 0)
		{ 
			break;
		}

		// 为了accept不发生阻塞, 现在fd_set中判断sock的状态是否可读
		if (FD_ISSET(sock, &fdRead))
		{
			FD_CLR(sock, &fdRead);
			std::cout << "wait for client in" << std::endl;
			// 等待客户端连接
			sockaddr_in clientAddr = {};
			int nAddrLen = sizeof(clientAddr);
			SOCKET clientSock = INVALID_SOCKET;
			char msgBuf[] = "hello";
			// 等待客户端连接
			clientSock = accept(sock, (sockaddr*)&clientAddr, &nAddrLen);
			if (clientSock == INVALID_SOCKET)
			{
				std::cout << "receive client socket failed!" << std::endl;
			}
			std::cout << "client ip: " << inet_ntoa(clientAddr.sin_addr) << std::endl;
			vecSockets.push_back(clientSock);
		}

		for (int i = 0; i < fdRead.fd_count; i++)
		{
			if (-1 == HandleMessage(fdRead.fd_array[i]))
			{
				auto itr = std::find(vecSockets.begin(), vecSockets.end(), fdRead.fd_array[i]);
				if (itr != vecSockets.end())
				{
					vecSockets.erase(itr);
				}
			}
		}

	}

	// 关闭套接字
	for (int i = 0; i < vecSockets.size(); ++i)
	{
		closesocket(vecSockets[i]);
	}

	closesocket(sock);

	// 清除windows socket环境
	WSACleanup();
	std::cout << "session end" << std::endl;
	std::cin.get();

目前的客户端只能输入命令后,发送一条消息,并接收一条响应消息,无法接收服务器主动推送的消息,

利用select模型实现客户端接收服务器推送消息

	// 启动windows socket网络环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);

	// 创建socket
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == INVALID_SOCKET)
	{
		std::cout << "create socket failed" << std::endl;
	}
	// 连接服务器
	sockaddr_in sin = {};
	sin.sin_family = AF_INET;
	sin.sin_port = htons(10086);
	sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	auto res = connect(sock, (sockaddr*)&sin, sizeof(sockaddr_in));
	if (res == SOCKET_ERROR)
	{
		std::cout << "connect server failed" << std::endl;
	}
	std::cout << "connect server succeed" << std::endl;

	while (true)
	{
		fd_set fdRead;
		FD_ZERO(&fdRead);
		FD_SET(sock, &fdRead);
		timeval t = { 1, 0 };
		int ret = select(sock + 1, &fdRead, 0, 0, &t);
		if (ret < 0)
		{
			std::cout << "select 任务结束";
			break;
		}
		if (FD_ISSET(sock, &fdRead))
		{
			FD_CLR(sock, &fdRead);
			if (-1 == HandleMessage(sock))
			{
				std::cout << "ending" << std::endl;
				break; 
			}
		}
		std::cout << "spare time,you can do something" << std::endl;
	}
	// 关闭套接字
	closesocket(sock);
	WSACleanup();
	std::cout << "session end" << std::endl;
	std::cin.get();

客户端封装

将客户端封装成库

message

#ifndef COMMON_MESSAGE_DEF_
#define COMMON_MESSAGE_DEF_

typedef struct DataPackage_STRU
{
    
    
	int age;
	char name[32];
} DataPackage;


// 定义两种有效命令,每种命令对应相应的消息体
enum CmdIndex
{
    
    
	LOGIN_IN,
	LOGIN_OUT,
	LOGIN_IN_RES,
	LOGIN_OUT_RES,
	NEW_JOIN,
	LOGIN_ERROR
};

// 消息头
struct DataHeader
{
    
    
	unsigned short dataLen;
	unsigned short cmdId;
};

// LOGIN_IN命令对应的消息体结构
struct LoginIn : public DataHeader
{
    
    
	LoginIn()
	{
    
    
		dataLen = sizeof(LoginIn);
		cmdId = LOGIN_IN;
	}
	char userName[32];
	char pwd[32];
};

struct LoginInRes : public DataHeader
{
    
    
	LoginInRes()
	{
    
    
		dataLen = sizeof(LoginInRes);
		cmdId = LOGIN_IN_RES;
	}
	int res{
    
     1 };
};

// LOGIN_OUT命令对应的消息体结构
struct LoginOut : public DataHeader
{
    
    
	LoginOut()
	{
    
    
		dataLen = sizeof(LoginOut);
		cmdId = LOGIN_OUT;
	}
	char userName[32];
};

struct LoginOutRes : public DataHeader
{
    
    
	LoginOutRes()
	{
    
    
		dataLen = sizeof(LoginOutRes);
		cmdId = LOGIN_OUT_RES;
	}
	int res{
    
     1 };
};

struct LoginNew : public DataHeader
{
    
    
	LoginNew()
	{
    
    
		dataLen = sizeof(LoginNew);
		cmdId = NEW_JOIN;
	}
	int socket;
};


#endif // !COMMON_MESSAGE_DEF_


头文件

#ifndef EASY_CLIENT_H_
#define EASY_CLIENT_H_

#ifdef  _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(-0)
#define SOCKET_ERROR           (-1)
#endif //  _WIN32

#define ExportDll  __declspec(dllexport)

class ExportDll EasyClient
{
    
    
public:
	EasyClient();
	virtual ~EasyClient();

	// 初始化环境及socket
	void InitSocket();

	// 连接服务器
	int Connect(const char* ip, const unsigned short port);

	// 关闭socket
	void CloseSocket();
	int SendData(DataHeader* header);
	int RecvData();

	bool IsRunning();

	// 其他
	// 发送消息
	// 接收消息
	// 处理消息
	bool HandleMessage();

private:
	
	void ProcessMessage(DataHeader* header);
private:
	SOCKET m_sock;
};

#endif // !EASY_CLIENT_H_

源文件

#include <iostream>
#include "common_message_def.h"
#include "easy_client.h"

EasyClient::EasyClient()
{
    
    
	m_sock = INVALID_SOCKET;
}

EasyClient::~EasyClient()
{
    
    
	CloseSocket();
}

void EasyClient::InitSocket()
{
    
    
#ifdef _WIN32
	// 启动windows socket环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif // _WIN32
	// 若已经初始化过
	if (INVALID_SOCKET != m_sock)
	{
    
    
		std::cout << "m_sock已有连接,先关闭" << std::endl;
		CloseSocket();
	}

	// 建立socket
	m_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == m_sock)
	{
    
    
		std::cout << "error, create socket failed..." << std::endl;
	}
	else
	{
    
    
		std::cout << "create socket success..." << std::endl;
	}
}

int EasyClient::Connect(const char* ip, const unsigned short port)
{
    
    
	if (INVALID_SOCKET == m_sock)
	{
    
    
		std::cout << "m_sock无效,是否忘记初始化,这里重新初始化" << std::endl;
		InitSocket();
	}
	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);
#ifdef _WIN32
	sin.sin_addr.S_un.S_addr = inet_addr(ip);
#else
	sin.sin_addr.s_addr = inet_addr(ip);
#endif // _WIN32
	int ret = connect(m_sock, (sockaddr*)&sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == ret)
	{
    
    
		std::cout << "error, connect server failed..." << std::endl;
	}
	else
	{
    
    
		std::cout << "connect server succeed..." << std::endl;
	}
	return ret;
}

void EasyClient::CloseSocket()
{
    
    
	if (INVALID_SOCKET != m_sock)
	{
    
    
#ifdef _WIN32
		closesocket(m_sock);
		WSACleanup();
#else
		close(m_sock);
#endif // _WIN32
	}
	m_sock = INVALID_SOCKET;
}

bool EasyClient::HandleMessage()
{
    
    
	if (IsRunning())
	{
    
    
		fd_set fdReads;
		FD_ZERO(&fdReads);
		FD_SET(m_sock, &fdReads);
		timeval t = {
    
     1, 0 };
		int ret = select(m_sock + 1, &fdReads, 0, 0, &t);// select模型 判断socket范围内是否有可读的数据
		if (ret < 0)
		{
    
    
			std::cout << "[socket=" << ret << "]select任务结束.." << std::endl;
			return false;
		}

		if (FD_ISSET(m_sock, &fdReads))
		{
    
    
			FD_CLR(m_sock, &fdReads);
			if (-1 == RecvData())
			{
    
    
				std::cout << "[socket=" << ret << "]select任务结束.." << std::endl;
				return false;
			}

		}
		return true;
	}
	return false;
}

bool EasyClient::IsRunning()
{
    
    
	return m_sock != INVALID_SOCKET;
}

int EasyClient::RecvData()
{
    
    
	char szRecv[4096] = {
    
     0 };
	int len = recv(m_sock, szRecv, sizeof(DataHeader), 0);
	if (len <= 0)
	{
    
    
		std::cout << "receive from client failed" << std::endl;
		return -1;
	}
	DataHeader* header = (DataHeader*)szRecv;
	std::cout << "client receive msg header is : " << header->cmdId << std::endl;
	
	// 接收消息体
	recv(m_sock, szRecv + sizeof(DataHeader), header->dataLen - sizeof(DataHeader), 0);
	ProcessMessage(header);
	
	return 0;
}

void EasyClient::ProcessMessage(DataHeader* header)
{
    
    
	switch (header->cmdId)
	{
    
    
	case LOGIN_IN_RES:
	{
    
    
		// 接收消息体
		LoginInRes* data = (LoginInRes*)header;
		std::cout << "receive message from server : LOGIN_IN_RES, data length is: " << data->dataLen << std::endl;

	}
	break;
	case LOGIN_OUT_RES:
	{
    
    
		// 接收消息体
		LoginOutRes* data = (LoginOutRes*)header;
		std::cout << "receive message from server : LOGIN_OUT_RES, data length is: " << data->dataLen << std::endl;
	}
	break;
	case NEW_JOIN:
	{
    
    
		// 接收消息体
		LoginNew* data = (LoginNew*)header;
		std::cout << "receive message from server : NEW_JOIN, data length is: " << data->dataLen << std::endl;
	}
	break;
	default:
		std::cout << "invalid message" << std::endl;
		break;
	}
}

int EasyClient::SendData(DataHeader* header)
{
    
    
	if (INVALID_SOCKET == m_sock)
	{
    
    
		return SOCKET_ERROR;
	}
	return send(m_sock, (char*)header, header->dataLen, 0);
}

客户端应用

#include <iostream>
#include <thread>
#include "../EasyClient//common_message_def.h"
#include "../EasyClient/easy_client.h"

#define _CRT_SECURE_NO_WARNINGS

void HandleSendProc(EasyClient* client)
{
    
    
	while (true)
	{
    
    
		char cmdBuf[256] = {
    
     0 };
		std::cout << "please enter the command" << std:: endl;
		std::cin >> cmdBuf;
		if (0 == strcmp(cmdBuf, "exit"))
		{
    
    
			client->CloseSocket();
			std::cout << "exit send data process thread" << std::endl;
			break;
		}
		else if (0 == strcmp(cmdBuf, "loginin"))
		{
    
    
			LoginIn data;
			strcpy(data.userName, "zhangsan");
			strcmp(data.pwd, "123456");
			client->SendData(&data);
		}
		else if (0 == strcmp(cmdBuf, "loginout"))
		{
    
    
			LoginOut data;
			strcpy(data.userName, "zhangsan");
			client->SendData(&data);
		}
		else
		{
    
    
			std::cout << "not support cmd" << std::endl;
		}
	}
}


int main()
{
    
    
	EasyClient client;
	client.InitSocket();
	client.Connect("127.0.0.1", 10087);

	// 创建发送命令线程
	std::thread t(HandleSendProc, &client);
	t.detach();

	// 循环接收消息并处理消息
	while (client.IsRunning())
	{
    
    
		client.HandleMessage();
	}

	client.CloseSocket();

	std::cout << "client exit" << std::endl;
	std::cin.get();
	return 0;

}

服务端封装
头文件

#ifndef EASY_SERVER_H_
#define EASY_SERVER_H_

#ifdef  _WIN32
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <windows.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#define SOCKET int
#define INVALID_SOCKET (SOCKET)(-0)
#define SOCKET_ERROR           (-1)
#endif //  _WIN32

#include <vector>
#include "common_message_def.h"
#define ExportDll_Server  __declspec(dllexport)

class ExportDll_Server EasyServer
{
    
    
public:
	EasyServer();
	virtual ~EasyServer();

	// 初始化Socket
	void InitSocket();

	// 绑定端口
	int BindPort(const char* ip, const unsigned short port);
	// 监听端口 num -- 最多可接受的连接的个数
	int ListenPort(const int num);
	// 接收客户端连接
	SOCKET AcceptClient();
	// 关闭Socket
	void CloseSocket();
	// 处理网络消息
	bool HandleMessage();
	// 是否工作

	// 接收数据 丢包,粘包
	int RecvData(SOCKET sock);

	// 响应网络消息
	virtual void RespondMessage(SOCKET sock, DataHeader* header);

	// 发送数据
	int SendData(SOCKET sock, DataHeader* header);

	// 群发消息
	void SendDataToAll(DataHeader* header);

	bool IsRuning();

private:
	SOCKET m_listenSocket;
	std::vector<SOCKET> m_vecClientSockets;
};

#endif // !EASY_SERVER_H_


源文件

#include <iostream>
#include "easy_server.h"

EasyServer::EasyServer()
{
    
    
	m_listenSocket = INVALID_SOCKET;
}

EasyServer::~EasyServer()
{
    
    
	CloseSocket();
}

void EasyServer::InitSocket()
{
    
    
#ifdef _WIN32
	// 启动windows socket环境
	WORD ver = MAKEWORD(2, 2);
	WSADATA dat;
	WSAStartup(ver, &dat);
#endif // _WIN32
	// 若已经初始化过
	if (INVALID_SOCKET != m_listenSocket)
	{
    
    
		std::cout << "m_sock已有连接,先关闭" << std::endl;
		CloseSocket();
	}

	// 建立socket
	m_listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (INVALID_SOCKET == m_listenSocket)
	{
    
    
		std::cout << "error, create listening socket failed..." << std::endl;
	}
	else
	{
    
    
		std::cout << "create listening socket success..." << std::endl;
	}
}

int EasyServer::BindPort(const char* ip, const unsigned short port)
{
    
    
	if (INVALID_SOCKET == m_listenSocket)
	{
    
    
		InitSocket();
	}

	sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);
	

#ifdef _WIN32
	if (ip != nullptr)
	{
    
    
		sin.sin_addr.S_un.S_addr = inet_addr(ip);
	}
	else
	{
    
    
		sin.sin_addr.S_un.S_addr = INADDR_ANY;
	}
	
#else
	if (ip != nullptr)
	{
    
    
		sin.sin_addr.s_addr = inet_addr(ip);
	}
	else
	{
    
    
		sin.sin_addr.s_addr = INADDR_ANY;
	}
#endif // _WIN32
	int ret = bind(m_listenSocket, (sockaddr*)&sin, sizeof(sockaddr_in));
	if (SOCKET_ERROR == ret)
	{
    
    
		std::cout << "error, bind port"<<port<<" failed." << std::endl;
	}
	else
	{
    
    
		std::cout << "bind port" << port << " succeed." << std::endl;
	}
	return ret;
}

int EasyServer::ListenPort(const int num)
{
    
    
	int ret = listen(m_listenSocket, num);
	if (SOCKET_ERROR == ret)
	{
    
    
		std::cout << "error, listen port failed, scok = " << m_listenSocket << std::endl;
	}
	else
	{
    
    
		std::cout << "listen port succeed, scok = " << m_listenSocket << std::endl;
	}
	return ret;
}

int EasyServer::RecvData(SOCKET sock)
{
    
    
	char szRecv[4096] = {
    
     0 };
	int nLen = (int)recv(sock, szRecv, sizeof(DataHeader), 0);
	DataHeader* header = (DataHeader*)szRecv;
	if (nLen <= 0)
	{
    
    
		std::cout << "client: " << (int)sock << " has exit, task ending" << std:: endl;
		return -1;
	}

	// 接收消息体
	recv(sock, szRecv + sizeof(DataHeader), header->dataLen - sizeof(DataHeader), 0);

	RespondMessage(sock, header);
	return 0;
}

int EasyServer::SendData(SOCKET sock, DataHeader* header)
{
    
    
	if (IsRuning() && header != nullptr)
	{
    
    
		return send(sock, (char*)header, header->dataLen, 0);
	}
	return SOCKET_ERROR;
}

void EasyServer::SendDataToAll(DataHeader* header)
{
    
    

	// 向已有的客户端发送加入的消息
	for (int i = m_vecClientSockets.size() - 1; i >= 0; --i)
	{
    
    
		SendData(m_vecClientSockets[i], header);
	}

}

void EasyServer::RespondMessage(SOCKET sock, DataHeader* header)
{
    
    
	switch (header->cmdId)
	{
    
    
	case LOGIN_IN:
	{
    
    
		LoginIn* data = (LoginIn*)header;
		std::cout << "recv client:" << (int)sock << " request: LOGIN_IN, data length is " << data->dataLen << "username is " << data->userName << "pwd is " << data->pwd << std::endl;
		LoginInRes ret;
		send(sock, (char*)&ret, sizeof(LoginInRes), 0);
	}
	break;
	case LOGIN_OUT:
	{
    
    
		LoginOut* data = (LoginOut*)header;
		std::cout << "recv client:" << (int)sock << " request: LOGIN_OUT, data length is " << data->dataLen << "username is " << data->userName << std::endl;
		LoginOutRes ret;
		send(sock, (char*)&ret, sizeof(LoginOutRes), 0);
	}
	break;
	default:
	{
    
    
		DataHeader ret = {
    
     0, LOGIN_ERROR };
		send(sock, (char*)&ret, sizeof(DataHeader), 0);
	
	}
		break;
	}
}

bool EasyServer::HandleMessage()
{
    
    
	if (IsRuning())
	{
    
    
		fd_set fdRead;
		fd_set fdWrite;
		fd_set fdExp;
		FD_ZERO(&fdRead);
		FD_ZERO(&fdWrite);
		FD_ZERO(&fdExp);
		FD_SET(m_listenSocket, &fdRead);
		FD_SET(m_listenSocket, &fdWrite);
		FD_SET(m_listenSocket, &fdExp);
		SOCKET maxSock = m_listenSocket;
		for (int i = m_vecClientSockets.size() - 1; i >= 0; --i)
		{
    
    
			FD_SET(m_vecClientSockets[i], &fdRead);
			if (maxSock < m_vecClientSockets[i])
			{
    
    
				maxSock = m_vecClientSockets[i];
			}
		}

		timeval t = {
    
     1, 0 };
		int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
		if (ret < 0)
		{
    
    
			std::cout << "select task ending." << std::endl;
			CloseSocket();
			return false;
		}

		if (FD_ISSET(m_listenSocket, &fdRead))
		{
    
    
			FD_CLR(m_listenSocket, &fdRead);
			AcceptClient();
		}

		for (int i = m_vecClientSockets.size() - 1; i >= 0; --i)
		{
    
    
			if (FD_ISSET(m_vecClientSockets[i], &fdRead))
			{
    
    
				if (-1 == RecvData(m_vecClientSockets[i]))
				{
    
    
					auto itr = m_vecClientSockets.begin() + i;
					if (itr != m_vecClientSockets.end())
					{
    
    
						m_vecClientSockets.erase(itr);
					}
				}
			}
		}
	
		return true;
	}
	return false;
	
}

SOCKET EasyServer::AcceptClient()
{
    
    
	// 用于保存接收的客户端的信息
	sockaddr_in clientAddr;
	int nAddrLen = sizeof(sockaddr_in);
	SOCKET clientSock = INVALID_SOCKET;
#ifdef _WIN32
	clientSock = accept(m_listenSocket, (sockaddr*)&clientAddr, &nAddrLen);
#else
	clientSock = accept(m_listenSocket, (sockaddr*)&clientAddr, (socklen_t)&nAddrLen);
#endif // _WIN32
	if (INVALID_SOCKET == clientSock)
	{
    
    
		std::cout << "error, accept a invalid client, sock = " << m_listenSocket << std::endl;
	}
	else
	{
    
    
		// 向已有的客户端发送加入的消息
		LoginNew data;
		SendDataToAll(&data);
		m_vecClientSockets.push_back(clientSock);
		std::cout << "a new client join, socket = " << (int)clientSock << "ip = " << inet_ntoa(clientAddr.sin_addr) << std::endl;
	}
	return clientSock;
}
void EasyServer::CloseSocket()
{
    
    
	if (INVALID_SOCKET != m_listenSocket)
	{
    
    
#ifdef _WIN32
		for (int i = m_vecClientSockets.size() - 1; i >= 0; --i)
		{
    
    
			closesocket(m_vecClientSockets[i]);
		}
		closesocket(m_listenSocket);
		WSACleanup();
#else
		for (int i = m_vecClientSockets.size() - 1; i >= 0; --i)
		{
    
    
			close(m_vecClientSockets[i]);
		}
		close(m_sock);
#endif // _WIN32
	}
	m_listenSocket = INVALID_SOCKET;
}

bool EasyServer::IsRuning()
{
    
    
	return true;
}

服务端应用

#include <iostream>
#include "../EasyServer/easy_server.h"
#include "../EasyServer/common_message_def.h"

int main()
{
    
    
	EasyServer server;
	server.InitSocket();
	server.BindPort(nullptr, 10087);
	server.ListenPort(5);

	while (server.IsRuning())
	{
    
    
		server.HandleMessage();
	}

	server.CloseSocket();
	std::cout << "server exit " << std::endl;
	std::cin.get();
	return 0;
}

猜你喜欢

转载自blog.csdn.net/tianzhiyi1989sq/article/details/112755062