1 需求分析
1.1 编程实例背景
本实例旨在开发一个基于 TCP 协议的 Windows 套接字聊天室程序。该程序包含服务端和客户端两部分,服务端负责接收客户端的连接请求、管理用户信息、传递聊天消息等功能;客户端则负责向服务端发送连接请求、注册用户名、发送聊天消息等操作。
1.2 功能需求
-
用户注册与登录
- 客户端在连接服务端后,需要发送自己的聊天用户名给服务端进行注册。
- 服务端接收到用户名后,需检查当前用户列表中是否已存在该用户名。
- 如果用户名不存在,服务端返回成功创建聊天用户的响应;如果用户名已存在,则返回报错信息。
-
点对点聊天
- 客户端可以向服务端发送聊天消息,消息中包含目标聊天对象的用户名和具体的聊天内容。
- 服务端接收到消息后,需根据目标用户名查找对应的客户端连接。
- 如果找到目标客户端,服务端将聊天消息转发给目标客户端;如果找不到,则返回给发送方报错信息。
-
群发信息
- 客户端可以向服务端发送群发信息,该信息将发送给所有在线的客户端(除了发送方本身)。
- 服务端接收到群发信息后,需遍历所有在线客户端连接,并将消息发送给它们。
1.3 非功能需求
- 稳定性:程序应具有良好的稳定性,能够长时间运行而不出现崩溃或异常。
- 可扩展性:程序应具备一定的可扩展性,以便未来可以添加新的功能或优化现有功能。
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 粘包问题))”。