自学内容网 自学内容网

突破编程_C++_网络编程(Windows 套接字(编程实例 - 使用 TCP 开发聊天室))

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 粘包问题))”。


原文地址:https://blog.csdn.net/h8062651/article/details/137681097

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!