个人博客传送门:grey66.cn
1、网络聊天程序的设计与实现 :
服务端:服务端收到客户端的消息,对消息进行特征判断来决定是广播以实现群聊效果,还是仅转发给目的客户端以实现私聊效果,客户端之间并不直接相互通信,服务端利用一个map来实现进程与用户名的映射,引用线程但是未进行加锁操作。
运行结果:
服务端代码如下:
#include <winsock2.h>
#include <iostream>
#include <list>
#include <map>
#include <string>
#include <cstring>
/*
creat by grey
2205010216
20241225
*/
#pragma comment(lib, "ws2_32.lib")
using namespace std;
// 线程处理函数
DWORD WINAPI ThreadFun(LPVOID lpThreadParameter);
// 初始化函数
void init_app();
// 发送消息到所有客户端
void Send_All(list<SOCKET> Client_List_Now, const char* msg, SOCKET This_Socket);
// 私聊功能:通过名字发送消息
void Send_Private(SOCKET clientSocket, const char* targetName, const char* msg);
// 获取客户端套接字,通过名字
SOCKET Get_Client_By_Name(const char* name);
// 获取客户端名称
const char* GetName(SOCKET clientSocket);
// 客户端名字和套接字的映射
map<SOCKET, string> ClientNames;
WSADATA wd;
SOCKET Socket;
list<SOCKET> Client_List;
sockaddr_in addrClient;
int len = sizeof(sockaddr_in);
// 默认服务器地址为 127.0.0.1
char serverIP[100] = "127.0.0.1";
int main() {
init_app();
// 主线程循环接收客户端的连接
while (true) {
SOCKET Client = accept(Socket, (SOCKADDR*)&addrClient, &len);
if (Client != INVALID_SOCKET) {
// 创建线程并传入与客户端通讯的套接字
HANDLE hThread = CreateThread(NULL, 0, ThreadFun, (LPVOID)Client, 0, NULL);
CloseHandle(hThread); // 关闭对线程的引用
}
}
// 关闭监听套接字
closesocket(Socket);
// 清理winsock2的环境
WSACleanup();
return 0;
}
DWORD WINAPI ThreadFun(LPVOID lpThreadParameter) {
SOCKET This_Socket = (SOCKET)lpThreadParameter;
// 获取客户端名称
char name[100] = { 0 };
int ret = recv(This_Socket, name, sizeof(name), 0);
if (ret > 0) {
ClientNames[This_Socket] = string(name); // 记录客户端名称
cout << "客户端 " << name << " 进入聊天室" << endl;
}
// 发送欢迎消息
char buf[100] = { 0 };
sprintf(buf, "********欢迎 %s 进入聊天室*******\n", ClientNames[This_Socket].c_str());
Send_All(Client_List, buf, This_Socket);
Client_List.push_back(This_Socket);
// 循环接收客户端数据
do {
char buf2[100] = { 0 };
ret = recv(This_Socket, buf2, 100, 0);
if (ret > 0) {
cout << ClientNames[This_Socket] << " : " << buf2 << endl;
// 检查是否为私聊消息 (/private <用户名> <消息内容>)
if (strncmp(buf2, "/private", 8) == 0) {
char targetName[20] = { 0 };
char privateMsg[80] = { 0 };
sscanf(buf2, "/private %s %[^\n]", targetName, privateMsg);
// 发送私聊消息
Send_Private(This_Socket, targetName, privateMsg);
}
else {
// 广播消息
Send_All(Client_List, buf2, This_Socket);
}
}
} while (ret != SOCKET_ERROR && ret != 0);
// 客户端离开,移除列表
Client_List.remove(This_Socket);
ClientNames.erase(This_Socket); // 移除客户端名字
return 0;
}
void Send_All(list<SOCKET> Client_List_Now, const char* msg, SOCKET This_Socket) {
// 获取发送者的名字
const char* senderName = GetName(This_Socket);
// 创建一个缓冲区来存储格式化后的消息
char formattedMsg[200];
// 将发送者的名字和消息内容格式化到一起
snprintf(formattedMsg, sizeof(formattedMsg), "%s: %s", senderName, msg);
// 移除自己,避免发送给自己
Client_List_Now.remove(This_Socket);
// 向其他客户端发送消息
for (auto iter : Client_List_Now) {
send(iter, formattedMsg, strlen(formattedMsg), 0);
}
}
void Send_Private(SOCKET clientSocket, const char* targetName, const char* msg) {
SOCKET targetSocket = Get_Client_By_Name(targetName);
if (targetSocket != INVALID_SOCKET) {
char privateMsg[150];
snprintf(privateMsg, sizeof(privateMsg), "%s 说: %s(私聊)", ClientNames[clientSocket].c_str(), msg); // 发送者名字
send(targetSocket, privateMsg, strlen(privateMsg), 0);
cout << "私聊给 " << targetName << ": " << msg << endl;
}
else {
const char* errorMsg = "目标用户不存在。\n";
send(clientSocket, errorMsg, strlen(errorMsg), 0);
}
}
SOCKET Get_Client_By_Name(const char* name) {
for (auto& entry : ClientNames) {
if (entry.second == name) {
return entry.first; // 返回匹配的客户端套接字
}
}
return INVALID_SOCKET; // 找不到目标客户端
}
const char* GetName(SOCKET clientSocket) {
auto it = ClientNames.find(clientSocket);
if (it != ClientNames.end()) {
return it->second.c_str(); // 返回客户端名称
}
return "Unknown"; // 如果找不到客户端,返回 "Unknown"
}
void init_app() {
cout << "===========服务端============" << endl;
char falg;
char* ip;
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0) {
cout << "WSAStartup 错误:" << WSAGetLastError() << endl;
return;
}
// 创建流式套接字
Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (Socket == INVALID_SOCKET) {
cout << "socket 错误:" << WSAGetLastError() << endl;
return;
}
cout << "是否自定义当前服务器IP(y/n):";
cin >> falg;
switch (falg) {
case 'n': {
ip = "127.0.0.1";
break;
}
case 'y': {
cout << "请输入IP:";
cin.ignore();
cin.getline(serverIP, sizeof(serverIP));
ip = serverIP;
break;
}
}
// 绑定端口和IP
sockaddr_in addr;
memset(&addr, 0, sizeof(sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
addr.sin_addr.s_addr = inet_addr(ip);
cout << "服务端IP:" << ip << "\n";
// 服务端bind绑定
if (bind(Socket, (SOCKADDR*)&addr, len) == SOCKET_ERROR) {
cout << "bind 错误:" << WSAGetLastError() << endl;
return;
}
// 监听20个以下设备
listen(Socket, 20);
}
客户端:仅和服务端进行连接,然后发送和接受来自客户端的消息
#include <winsock2.h>
#include <iostream>
#include <cstring>
#include <thread>
#include <ws2tcpip.h>
/*
creat by grey
2205010216
20241225
*/
#pragma comment(lib, "ws2_32.lib")
using namespace std;
void init_app();
void* GetMsg(void* socket);
void SendMsg();
void SendMsgWithName(SOCKET sock, const char* msg);
#define BUFFSIZE 1024
WSADATA wd;
SOCKET Socket;
char clientName[100];
char serverIP[100] = "127.0.0.1";
int main() {
init_app();
thread recvThread(GetMsg, &Socket);
thread sendThread(SendMsg);
// 等待两个线程结束
recvThread.join();
sendThread.join();
closesocket(Socket);
WSACleanup();
return 0;
}
// 向服务器发送消息
void SendMsg() {
int ret = 0;
do {
char buf[BUFFSIZE] = { 0 };
cin.getline(buf, sizeof(buf));
if (strlen(buf) == 0) continue;
cout << "你发送了:" << buf << endl;
SendMsgWithName(Socket, buf); // 发送带名称的消息
} while (true);
}
// 接收服务器的消息
void* GetMsg(void* socket) {
SOCKET sock = *(SOCKET*)socket;
do {
int ret = 0;
char buf[BUFFSIZE] = { 0 };
ret = recv(sock, buf, sizeof(buf), 0);
if (ret > 0) {
cout << buf << endl;
}
} while (true);
}
// 发送带名称的消息
void SendMsgWithName(SOCKET sock, const char* msg) {
char buf[BUFFSIZE] = { 0 };
snprintf(buf, sizeof(buf), "%s", msg); // 在消息前添加名称
send(sock, buf, strlen(buf), 0);
}
// 初始化客户端套接字
void init_app() {
cout << "=========== 客户端 ============" << endl;
// 让用户输入自定义名称
cout << "请输入您的名称: ";
cin.getline(clientName, sizeof(clientName));
// 让用户选择是否自定义服务器IP
char falg;
cout << "是否自定义当前服务器IP(y/n): ";
cin >> falg;
cin.ignore(); // 忽略换行符
if (falg == 'y' || falg == 'Y') {
cout << "请输入服务器IP: ";
cin.getline(serverIP, sizeof(serverIP));
}
// 初始化 Winsock
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0) {
cout << "WSAStartup 错误:" << WSAGetLastError() << endl;
return;
}
// 创建套接字
Socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (Socket == INVALID_SOCKET) {
cout << "socket 错误:" << WSAGetLastError() << endl;
return;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
// 使用 inet_pton 转换 IP 地址
if (inet_pton(AF_INET, serverIP, &addr.sin_addr) <= 0) {
cerr << "Invalid IP address format!" << endl;
return;
}
cout << "连接到服务器 IP: " << serverIP << " 端口: 8000\n";
// 连接到服务器
if (connect(Socket, (SOCKADDR*)&addr, sizeof(addr)) == SOCKET_ERROR) {
cout << "连接失败:" << WSAGetLastError() << endl;
return;
}
// 发送客户端名称给服务器
send(Socket, clientName, strlen(clientName), 0);
}
2.滑动窗口协议仿真
接收端:接受来自发送端的帧并将接收到帧的情况发送反馈给发送端(期望序号的更新存在bug 思路:可完成接收窗口以完成正确更新)
演示效果:
随机丢失帧2:
发送窗口满且未ack则重发:
接收端代码如下:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <list>
#include <thread>
#include <windows.h>
#include <string.h>
/*