1 需求分析

1.1 编程实例背景

本实例旨在开发一个基于 TCP 协议的 Windows 套接字聊天室程序。该程序包含服务端和客户端两部分,服务端负责接收客户端的连接请求、管理用户信息、传递聊天消息等功能;客户端则负责向服务端发送连接请求、注册用户名、发送聊天消息等操作。

1.2 功能需求

  1. 用户注册与登录

    • 客户端在连接服务端后,需要发送自己的聊天用户名给服务端进行注册。
    • 服务端接收到用户名后,需检查当前用户列表中是否已存在该用户名。
    • 如果用户名不存在,服务端返回成功创建聊天用户的响应;如果用户名已存在,则返回报错信息。
  2. 点对点聊天

    • 客户端可以向服务端发送聊天消息,消息中包含目标聊天对象的用户名和具体的聊天内容。
    • 服务端接收到消息后,需根据目标用户名查找对应的客户端连接。
    • 如果找到目标客户端,服务端将聊天消息转发给目标客户端;如果找不到,则返回给发送方报错信息。
  3. 群发信息

    • 客户端可以向服务端发送群发信息,该信息将发送给所有在线的客户端(除了发送方本身)。
    • 服务端接收到群发信息后,需遍历所有在线客户端连接,并将消息发送给它们。

1.3 非功能需求

  1. 稳定性:程序应具有良好的稳定性,能够长时间运行而不出现崩溃或异常。
  2. 可扩展性:程序应具备一定的可扩展性,以便未来可以添加新的功能或优化现有功能。

2 服务端实现

2.1 技术实现方案

  • 使用 Windows 套接字(Winsock)API 进行网络通信。
  • 创建 TCP 监听套接字,绑定 IP 地址和端口号,并开始监听客户端连接请求。
  • 使用多线程或异步 IO 方式处理多个客户端连接,确保并发性能。
  • 维护一个用户列表,记录已注册的用户名和对应的客户端连接信息。
  • 实现消息转发逻辑,根据消息类型(点对点或群发)进行相应的处理。

2.2 代码实现

(1)引入相关头文件

#include <winsock2.h> 
#include <ws2tcpip.h>  
#include <iostream>  
#include <thread>  
#include <mutex>  
#include <string>  
#include <queue>  
#include <unordered_map>  
#include <vector>  
#include <memory>  
#include <sstream>  

#pragma comment(lib, "ws2_32.lib")  

(2)定义通用工具函数

std::vector<std::string> split(const std::string& str, char delimiter)
{
	std::vector<std::string> tokens;
	std::istringstream tokenStream(str);
	std::string token;

	while (std::getline(tokenStream, token, delimiter))
	{
		tokens.push_back(token);
	}

	return tokens;
}

该函数用于分隔客户端传来的字符串。本示例为了简单起见,使用逗号将字符串分隔,然后组成消息对象。

(3)定于消息对象以及消息缓冲队列

struct Message 
{
	enum MessageType
	{
		REGISTER,		// 注册
		P2P,			// 点对点
		SENDALL,		// 群发
	};
	int type = P2P;
	std::string from;
	std::string sendTo;
	std::string data;
};

std::queue<std::shared_ptr<Message>> g_revMsgs;

std::mutex g_revMsgMutex;
std::condition_variable g_revMsgCv;

(4)定于客户端连接后的通道对象

class Channel;
std::mutex g_channelMutex;
std::unordered_map<std::string, std::shared_ptr<Channel>> g_namedChannels;	// 已命名通道
std::vector<std::shared_ptr<Channel>> g_channels;	// 全部通道

class Channel : public std::enable_shared_from_this<Channel>
{
public :
	Channel(SOCKET sk) : m_socket(sk){};
	~Channel() {};

public:
	std::shared_ptr<Channel> getSharedPtr() {
		return shared_from_this();
	}

	void startRevMsg() {
		revThread = std::thread([&] {
			while (true)
			{
				int result = recv(m_socket, buffer, sizeof(buffer), 0);
				if (result > 0) {
					if (result < 1024) {
						buffer[result] = '\0';
					}
					std::vector<std::string> strs = split(buffer,',');
					if (strs.size() < 4) {
						sendMsg("Error: wrong message format, the correct one should be: type(register, p2p or sendall) , from , sendTo , message");
					}
					else {
						if ("register" == strs[0]) {
							std::shared_ptr<Message> msg = std::make_shared<Message>();
							msg->type = Message::REGISTER;
							m_name = strs[1];
							msg->from = m_name;
							std::unique_lock<std::mutex> lock(g_revMsgMutex);
							g_revMsgs.push(msg);
							g_revMsgCv.notify_all();
						}
						else if("p2p" == strs[0]){
							std::shared_ptr<Message> msg = std::make_shared<Message>();
							msg->type = Message::P2P;
							msg->from = m_name;
							msg->sendTo = strs[2];
							msg->data = strs[3];
							std::unique_lock<std::mutex> lock(g_revMsgMutex);
							g_revMsgs.push(msg);
							g_revMsgCv.notify_all();
						}
						else if ("sendall" == strs[0]) {
							std::shared_ptr<Message> msg = std::make_shared<Message>();
							msg->type = Message::SENDALL;
							msg->from = m_name;
							msg->data = strs[3];
							std::unique_lock<std::mutex> lock(g_revMsgMutex);
							g_revMsgs.push(msg);
							g_revMsgCv.notify_all();
						} else {
							sendMsg("Error: wrong message type, the correct type should be: register, p2p or sendall");
						}

					}
				}
				else {
					printf("recv failed with error: %d\n", WSAGetLastError());
					break;
				}
			}
		});
		revThread.detach();
	}
	
	void sendMsg(const std::string& str) {
		std::unique_lock<std::mutex> lock(m_sendMutex);
		send(m_socket, str.c_str(), (int)strlen(str.c_str()), 0);
	}

	void setName(std::string name) { m_name = name; }
	std::string getName() { return m_name; }

private:
	std::string m_name;
	SOCKET m_socket;
	std::thread revThread;
	char buffer[1024] = { 0 };
	std::mutex m_sendMutex;
};

(5)实现业务主逻辑

int main() {
	WSADATA wsaData;
	SOCKET serverSocket;
	struct sockaddr_in serverAddr, clientAddr;
	int addrSize = sizeof(clientAddr);

	// 初始化Winsock库  
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		printf("WSAStartup failed: %d\n", WSAGetLastError());
		return 1;
	}

	// 创建套接字  
	serverSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (serverSocket == INVALID_SOCKET) {
		printf("Could not create socket: %d\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// 设置服务器地址信息  
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = INADDR_ANY;
	serverAddr.sin_port = htons(12345);

	// 绑定套接字到服务器地址  
	if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		printf("Bind failed with error: %d\n", WSAGetLastError());
		closesocket(serverSocket);
		WSACleanup();
		return 1;
	}

	// 开始监听连接请求  
	if (listen(serverSocket, 5) == SOCKET_ERROR) {
		printf("Listen failed with error: %d\n", WSAGetLastError());
		closesocket(serverSocket);
		WSACleanup();
		return 1;
	}

	// 接受客户端连接  
	std::thread acceptChannelThread = std::thread([&] {
		while (true) {
			SOCKET clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &addrSize);
			if (clientSocket == INVALID_SOCKET) {
				printf("Accept failed with error: %d\n", WSAGetLastError());
				closesocket(serverSocket);
				WSACleanup();
				return 1;
			}
			else {
				struct sockaddr_in peerAddr;
				int addrlen = sizeof(peerAddr);
				if (getpeername(clientSocket, (struct sockaddr*)&peerAddr, &addrlen) == SOCKET_ERROR) {
					int error = WSAGetLastError();
					// 处理错误  
				}
				else {
					char peerIP[INET_ADDRSTRLEN];
					inet_ntop(AF_INET, &peerAddr.sin_addr, peerIP, sizeof(peerIP));
					printf("Revice one channel, IP address: %s, port: %d\n", peerIP, ntohs(peerAddr.sin_port));
				}

				std::shared_ptr<Channel> channel = std::make_shared<Channel>(clientSocket);
				g_channels.push_back(channel);
				channel->startRevMsg();
			}
		}
	});
	
	acceptChannelThread.detach();

	// 处理接收到的数据
	std::thread revMsgThread = std::thread([&] {
		while (true) {
			std::unique_lock<std::mutex> lock(g_revMsgMutex);
			g_revMsgCv.wait(lock, [] {
				return !g_revMsgs.empty();
			});

			while (!g_revMsgs.empty()) {
				auto msg = g_revMsgs.front();
				g_revMsgs.pop();
				switch (msg->type) {
				case Message::REGISTER: {
					for (auto channel : g_channels) {
						if (msg->from == channel->getName()) {
							auto res = g_namedChannels.insert(std::pair<std::string, std::shared_ptr<Channel>>(channel->getName(), channel));
							std::string strMsg;
							if (res.second) {
								strMsg = "Successfully registered";
							} else {
								strMsg = "Registration failed, duplicate channel name";
							}
							channel->sendMsg(strMsg);
							break;
						}
					}
					break;
				}
				case Message::P2P: {
					auto it = g_namedChannels.find(msg->sendTo);
					if (g_namedChannels.end() != it) {
						std::string strMsg = std::string("[P2P] ") + msg->from + ": " + msg->data;;
						it->second->sendMsg(strMsg);
					}
					break;
				}
				case Message::SENDALL: {
					auto it = g_namedChannels.begin();
					while (it != g_namedChannels.end()) {
						if (it->second->getName() != msg->from) {
							std::string strMsg = std::string("[SENDALL] ") + msg->from + ": " + msg->data;;
							it->second->sendMsg(strMsg);
						}
						++it;
					}
					break;
				}
				default:
					break;
				}
			}
		}
	});


	std::string inputMsg;
	while (std::cin >> inputMsg){
		if ("exit" == inputMsg) {// 关闭套接字和Winsock库  
			closesocket(serverSocket);
			WSACleanup();
			break;
		}
	}
	
	return 0;
}

3 客户端实现

3.1 技术实现方案

  • 使用 Winsock API 与服务端进行通信。
  • 提供用户注册命令规则。
  • 实现聊天命令规则,允许用户输入聊天对象用户名和聊天内容,并发送消息给服务端。
  • 提供群发命令规则,允许用户选择发送群发信息。
  • 显示接收到的聊天消息和报错信息。。

3.2 代码实现

#include <winsock2.h>  
#include <iostream>  
#include <thread>  
#include <string>  
#include <memory>  
#include <sstream>  

#pragma comment(lib, "ws2_32.lib")  

int main() {
	WSADATA wsaData;
	SOCKET clientSocket;
	struct sockaddr_in serverAddr;
	char buffer[1024];

	// 初始化Winsock库  
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
		printf("WSAStartup failed: %d\n", WSAGetLastError());
		return 1;
	}

	// 创建套接字  
	clientSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (clientSocket == INVALID_SOCKET) {
		printf("Could not create socket: %d\n", WSAGetLastError());
		WSACleanup();
		return 1;
	}

	// 设置服务器地址信息  
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 假设服务器运行在本地  
	serverAddr.sin_port = htons(12345);

	// 连接到服务器  
	if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		printf("Connect failed with error: %d\n", WSAGetLastError());
		closesocket(clientSocket);
		WSACleanup();
		return 1;
	}

	// 接收响应  
	std::thread revThread = std::thread([&] {
		while (true) {
			int result = recv(clientSocket, buffer, sizeof(buffer), 0);
			if (result > 0) {
				buffer[result] = '\0';
				printf("%s\n", buffer);
			}
			else {
				printf("recv failed with error: %d\n", WSAGetLastError());
			}
		}
	});

	revThread.detach();

	std::string inputMsg;
	while (std::cin >> inputMsg) {
		if ("exit" == inputMsg) {// 关闭套接字和Winsock库  
			closesocket(clientSocket);
			WSACleanup();
			break;
		} else {
			// 注册样例:"register,client1,,,"
			// 点对点发送样例:"p2p,,client2,hello,"
			// 群发样例:"sendall,,,hello,"
			send(clientSocket, inputMsg.c_str(), (int)strlen(inputMsg.c_str()), 0);
		}
	}

	return 0;
}

4 编译运行

分别编译服务端以及客户端,运行服务端,并且运行 3 个客户端。

(1)在第 1 个客户端中输入:

register,client1,,,

点击回车后,得到消息返回:

Successfully registered

(2)在第 2 个客户端中输入:

register,client2,,,

点击回车后,得到消息返回:

Successfully registered

(3)在第 3 个客户端中输入:

register,client3,,,

点击回车后,得到消息返回:

Successfully registered

(4)此时服务端显示:

Revice one channel, IP address: 127.0.0.1, port: 53718
Revice one channel, IP address: 127.0.0.1, port: 53720
Revice one channel, IP address: 127.0.0.1, port: 53721

(5)在第 1 个客户端中输入:

p2p,,client2,hello,
sendall,,,hello,

(6)此时第 2 个客户端显示:

[P2P] client1: hello
[SENDALL] client1: hello

(6)第 3 个客户端显示:

[SENDALL] client1: hello

5 注意点

本示例代码没有处理 TCP 粘包问题,在实际开发中,这个是一定要做处理的。具体方法可以查看教程 “突破编程_C++_网络编程(Windows 套接字(处理 TCP 粘包问题))”。

04-15 20:14