C++ 代码实现局域网即时通信功能 (windows 系统 客户端)
本项目使用C++实现具备多个客户端和服务器端即时通信聊天功能软件
一:项目内容
使用C++实现一个具备多客户端和一个服务器端即时通信功能的聊天软件。
本项目的目的是
学习在windows平台下,进行C++网络开发的基本概念:TCP/IP socket通信,多线程编程,文件配置读写和通信协议制定等;
二:需求分析
这个聊天室主要有两个程序:
1.服务端:能够接受新的客户连接,并将每个客户端发来的信息,转发给对应的目标客户端。
2.客户端:能够连接服务器,并向服务器发送消息,同时可以接收服务器发来的消息。
属于C/S模型。
三:抽象与细化
服务端类需要支持:
1.支持多个客户端接入,实现聊天室基本功能。
2.启动服务,建立监听端口等待客户端连接。
3.使用epoll机制实现并发,增加效率。
4.客户端连接时,发送欢迎消息,并存储连接记录。
5.客户端发送消息时,根据消息类型,广播给所有用户(群聊)或者指定用户(私聊)。
6.客户端请求退出时,对相应连接信息进行清理。
客户端类需要支持:
1.连接服务器。
2.支持用户输入消息,发送给服务端。
3.接受并显示服务端发来的消息。
4.退出连接。
四:C/S模型

五:涉及数据读写、转发等操作所以需要使用Windows 下IOCP模型:
IOCP 全称I/O Completion Port,中文译为I/O完成端口。IOCP是一个异步I/O的Windows API,它可以高效地将I/O事件通知给应用程序,类似于Linux中的Epoll,详细信息请参考linux之epoll。
I/O 完成端口可以充分利用 Windows 内核来进行 I/O 调度,相较于传统的Winsock模型,IOCP的优势主要体现在两方面:独特的异步I/O方式和优秀的线程调度机制。
IOCP模型通信机制,主要过程为:1、socket关联iocp;2、在socket上投递I/O请求;3、事件完成返回完成通知封包;4、工作线程在iocp上处理事件。IOCP的这种工作模式:程序只需要把事件投递出去,事件交给操作系统完成后,工作线程在完成端口上轮询处理。该模式充分利用了异步模式高速率输入输出的优势,能够有效提高程序的工作效率。完成端口可以抽象为一个公共消息队列,当用户请求到达时,完成端口把这些请求加入其抽象出的公共消息队列。这一过程与多个工作线程轮询消息队列并从中取出消息加以处理是并发操作。这种方式很好地实现了异步通信和负载均衡,因为它使几个线程“公平地”处理多客户端的I/O,并且线程空闲时会被挂起,不会占用CPU周期。
IOCP模型充分利用Windows系统内核,可以实现仅用少量的几个线程来处理和多个client之间的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。
软件运行效果如下图:
客户端详细代码如下:
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 PublicDefine.h*@描述 公共数据结构定义**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#pragma once
#include<stdio.h>
#include <iostream>
#include"winerror.h"
#define WIN32_LEAN_AND_MEAN
#include"Winsock2.h"#define OutErr(a) std::cout << "error :" << (a) << std::endl \<< "出错代码:"<< WSAGetLastError() << std::endl \<< "出错文件:"<< __FILE__ << std::endl \<< "出错行数:"<< __LINE__ << std::endl \
#define OutMsg(a) std::cout << (a) << std::endl;#define PORT 5050 // 监听端口
#define LOCAL_HOST "127.0.0.1" // 本地回路地址
#define DATA_BUFSIZE 8192#define MAX_LISTEN_QUEUE 200#define MAX_CONNECT 3000#define MAX_DATA_LEN 2048 // 数据包长度#define SEND_DATA_LEN 4096 // 发送数据包长度/// 结构体定义
/**@brief 用于IOCP的特定函数*@author GhY*@date 2024/07/24*/
typedef struct {OVERLAPPED Overlapped;WSABUF DataBuf;CHAR Buffer[DATA_BUFSIZE];
} PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;/**@brief 用于IOCP的特定结构*@author GhY*@date 2024/07/24*/
typedef struct _PER_HANDLE_DATA {SOCKET _socket;CHAR _ip[32];int _port;_PER_HANDLE_DATA(){_socket = NULL;memset(_ip, 0, 32);_port = -1;}
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;#pragma pack(1)
/**@brief 数据包头*@author GhY*@date 2024/07/24*/
typedef struct _DataHead {unsigned short _type; // 0=上传数据, 1=转发数据,2=请求数据unsigned int _node; // 客户端IDunsigned long _time;_DataHead(){memset(this, 0, sizeof(_DataHead));}} TcpHead, Udp_Head;/**@brief 数据包体*@author GhY*@date 2024/07/24*/
typedef struct _DataBody {char _srcName[32];int _length;char _data[MAX_DATA_LEN];_DataBody(){memset(this, 0, sizeof(_DataBody));}} TcpBody, UdpBody;/**@brief 发送数据*@author GhY*@date 2024/07/24*/
typedef struct _SendData {TcpHead _head;TcpBody _body;
} Tcp_SendData, Udp_SendData;#pragma pack()/**@brief socket连接管理*@author GhY*@date 2024/07/24*/
struct ClientManager {unsigned int _id;char _name[32];SOCKET _socket;char _addr[16];int _port;ClientManager(){memset(this, 0, sizeof(ClientManager));}};/**@brief 通信消息*@author GhY*@date 2024/07/24*/
struct Message {unsigned int _sendId;char _send_name[32];unsigned int _receiverId;char _receiverName[32];char _data[MAX_DATA_LEN];Message(){memset(this, 0, sizeof(Message));}
};
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 application.h*@描述 app基类**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#ifndef __APPLICATION_H__
#define __APPLICATION_H__class application
{
public:application();virtual ~application();/**@brief Do some initialize before application lanuch*/virtual bool initinstance();/**@brief Run application*/virtual int run();/**@brief Exit application*/virtual bool exitinstance();};#endif // !__APPLICATION_H__
#include "application.h"application::application()
{
}application::~application()
{
}bool application::initinstance()
{return true;
}int application::run()
{if (initinstance()) {run();}return exitinstance();
}bool application::exitinstance()
{return true;
}
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 client.h*@描述 客户端类声明**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#ifndef __CLIENT_H__
#define __CLIENT_H__#include<stdio.h>
#include<iostream>
#include "MySocket.h"
#include "MyReceive.h"
#include "application.h"
#include <string>
#include <vector>
#include <list>class CCinData;/**@描述: 客户端类*@作者: GhY*@日期: 2024/07/24*@历史:*/
class CAppClient : public application, public sigslot::has_slots<>
{
public:CAppClient();~CAppClient();/**@brief 关联信号槽*@author GhY*@date 2024/07/24*/void InitSigslot();/**@brief 初始化*@author GhY*@date 2024/07/24*/bool initinstance();/**@brief 退出*@author GhY*@date 2024/07/24*/bool exitinstance();int run();/**@desc 发送数据*@param: sdata 待传输数据*@return void*@author GhY*@date 2024/07/24*@version v1.0.0*@history:*/void SendData(std::string* sdata);public:MySocket* m_mysocket;std::list<std::string*> m_sendBufs;CCinData* m_sendData;bool m_exitFlag; // 退出标志protected:private:MyReceive* m_myrev;
};/**@描述: 获取输入数据类*@作者: GhY*@日期: 2024/07/24*@历史:*/
class CCinData : public sigslot::has_slots<>
{
public:typedef sigslot::signal1<std::string* > SendDataEvent;SendDataEvent OnSendEvent;public:CCinData(CAppClient* app);~CCinData();void Run();private:CAppClient* m_appClient;bool m_exitFlag;
};#endif //!__CLIENT_H__
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 client.cpp*@描述 客户端类实现**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#include "client.h"DWORD WINAPI ClientCinProcess(LPVOID lpParam)
{CAppClient* appclient = (CAppClient*)lpParam;if (!appclient) {return 1;}if (appclient->m_sendData) {appclient->m_sendData->Run();}return 0;
}DWORD WINAPI RunSendBufProcess(LPVOID lpParam)
{CAppClient* appclient = (CAppClient*)lpParam;if (!appclient) {return 1;}while (true) {if (appclient->m_exitFlag) {break;}if (appclient->m_sendBufs.empty()) {Sleep(300);continue;}if (appclient->m_sendBufs.size() > 0) {std::string* strBuf = appclient->m_sendBufs.front();appclient->m_sendBufs.pop_front();if (appclient->m_mysocket && !strBuf->empty()) {appclient->m_mysocket->SendData(*strBuf);delete strBuf;}}}return 0;
}CAppClient::CAppClient()
{m_exitFlag = false;m_mysocket = new MySocket();std::string sIp = g_ConfigPtr.getConfigValueWithKey("net", "ip");std::string sPort = g_ConfigPtr.getConfigValueWithKey("net", "port");int iPort = sPort.empty() ? PORT : atoi(sPort.c_str());m_mysocket->InitData(sIp, iPort);m_mysocket->ClientConnect();m_myrev = new MyReceive(m_mysocket);m_sendBufs.clear();m_sendData = new CCinData(this);InitSigslot();
}CAppClient::~CAppClient()
{this->disconnect_all();if (m_myrev) {delete m_myrev;m_myrev = nullptr;}if (m_mysocket) {m_mysocket->Close();delete m_mysocket;m_mysocket = nullptr;}if (m_sendData) {delete m_sendData;m_sendData = nullptr;}
}bool CAppClient::initinstance()
{return true;
}bool CAppClient::exitinstance()
{return true;
}int CAppClient::run()
{std::cout << "使用说明:输入 quit 退出程序" << std::endl;std::string currentName = g_ConfigPtr.getConfigValueWithKey("base", "name");if (currentName.empty()) {char nameT[64] = { 0 };std::cout << "请输入名字:";std::cin >> nameT;g_ConfigPtr.SetConfigValue("base", "name", nameT);} else {std::cout << "当前用户名:" << currentName.c_str() << std::endl;}static int nCnt = 0;char sendBuf[2000] = { 0 };int recvdata = 0;HANDLE hProcessIO = CreateThread(NULL, 0, ClientCinProcess, this, 0, NULL);if (hProcessIO) {CloseHandle(hProcessIO);}HANDLE hProcessIO2 = CreateThread(NULL, 0, RunSendBufProcess, this, 0, NULL);if (hProcessIO2) {CloseHandle(hProcessIO2);}while (true) {if (m_exitFlag) {break;}recvdata = m_mysocket->ReceiveData();if (recvdata == 0) {Sleep(500);} else {recvdata = 0;}}return 0;
}void CAppClient::SendData(std::string* sdata)
{std::string* tmpdata = sdata;if (tmpdata->empty()) {return;}if (tmpdata->compare("quit") == 0) {m_exitFlag = true;m_mysocket->Close();delete tmpdata;return;}m_sendBufs.push_back(tmpdata);
}void CAppClient::InitSigslot()
{if (m_sendData) {m_sendData->OnSendEvent.connect(this, &CAppClient::SendData);}
}CCinData::CCinData(CAppClient* app): m_appClient(app), m_exitFlag(false)
{}CCinData::~CCinData()
{m_exitFlag = true;
}void CCinData::Run()
{std::cout << "please cin message: " << std::endl;std::string* sendTest = new std::string("上线");OnSendEvent.emit(sendTest);while (true) {if (m_exitFlag) {break;}std::string* sendBuf = new std::string();//std::cin >> sendBuf;getline(std::cin, *sendBuf);if (!sendBuf->empty() && sendBuf->compare("quit") == 0) {m_exitFlag = true;}if (sendBuf->size() > 0) {OnSendEvent.emit(sendBuf);} else {Sleep(500);}}
}
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 MyReceive.h*@描述 处理数据类声明**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#ifndef __MYRECEIVE_H__
#define __MYRECEIVE_H__
#include "MySocket.h"/**@描述: 接收数据处理类*@作者: GhY*@日期: 2024/07/24*@历史:*/
class MyReceive : public sigslot::has_slots<>
{
public:MyReceive(MySocket* s);~MyReceive();/**@brief 关联信号槽*@author GhY*@date 2024/07/24*/void InitSigslot();/**@desc 接收数据*@param: sdata 待接收数据*@return void*@author GhY*@date 2024/07/24*@version v1.0.0*@history:*/void ReceiveData(Tcp_SendData* sdata);private:MySocket* m_mysocket;};#endif //!__MYRECEIVE_H__
/******************************************************@Copyright (c) 2024, GhY, All rights reserved.*@文件 MyReceive.cpp*@描述 处理数据类实现**@作者 GhY*@日期 2024年7月24日*@版本 v1.0.0*****************************************************/
#include "MyReceive.h"MyReceive::MyReceive(MySocket* s): m_mysocket(s)
{InitSigslot();
}MyReceive::~MyReceive()
{if (m_mysocket) {m_mysocket->disconnect_all();}
}void MyReceive::InitSigslot()
{if (m_mysocket) {m_mysocket->OnSelectEvent.connect(this, &MyReceive::ReceiveData);}
}void MyReceive::ReceiveData(Tcp_SendData* sdata)
{if (!sdata) {return;}do {if (sdata->_head._type == 2) {std::string tmp = sdata->_body._data;g_ConfigPtr.SetConfigValue("base", "id", tmp);} else {std::string tmpName = sdata->_body._srcName;std::string tmp = sdata->_body._data;std::cout << "send: " << tmpName.c_str() << " -- message: " << tmp.c_str() << std::endl;}} while (0);}
注意:
文章中依赖的文件(.h,.cpp)请参见本专栏其他文章。
源代码下载地址:源代码
相关文章:
C++ 代码实现局域网即时通信功能 (windows 系统 客户端)
本项目使用C实现具备多个客户端和服务器端即时通信聊天功能软件 一:项目内容 使用C实现一个具备多客户端和一个服务器端即时通信功能的聊天软件。 本项目的目的是 学习在windows平台下,进行C网络开发的基本概念:TCP/IP socket通信࿰…...
机器人阻抗控制实现方法及其存在的科学问题
一、机器人阻抗控制的实现方法 机器人阻抗控制主要分为两种方法:基于位置的阻抗控制和基于力的阻抗控制。 基于位置的阻抗控制: 工作原理:让机器人电机在位置模式下工作,通过发送目标位置和速度实现阻抗特性。主要目的:控制机器人的位置精度和运动轨迹。特点:该方法侧重…...
解决:xxx.xxx/res/modules/.ds_store: error: the file name must end with .xml 问题
解决:xxx.xxx/res/modules/.ds_store: error: the file name must end with .xml 问题 该问题是由于Android Studio校验到布局文件中存在不以.xml后缀名结尾的文件,这个文件就是.DS_store,它是Mac上系统自动创造的隐藏文件,把该文…...
EEtrade:区块链技术的五大应用场景
区块链技术,作为近年来备受瞩目的颠覆性技术,其去中心化、透明化、安全性和可追溯性等特性,为各行各业带来了前所未有的机遇。从数字货币到金融资产交易结算,从数字政务到存证防伪,再到数据服务,区块链正逐…...
DAO、DPO、DTO、POJO、VO、BO、EBO
目录 1. DAO (Data Access Object) 2. DPO (Data Persistence Object) 3. DTO (Data Transfer Object) 4. POJO (Plain Old Java Object) 5. VO (Value Object) 6. BO (Business Object) 7. EBO (Entity Bean Object) 在Java开发中,尤其是与数据访问和对象映…...
数据库期末复习
数据库期末复习 分析题 1 (1)使用数据库系统可以大大提高应用开发的效率,方便用户的使用减轻数据库系统管理人员维护的负担,请回答数据库系统有哪些部分组成?什么是数据库管理系统,其主要功能包括哪些方而&…...
pyinstaller带浏览器一起打包playwright 独立运行exe
前置条件 没有安装自带环境,则 playwright install 安装了自带的浏览器 查看playwright的浏览器的位置 playwright install --dry-run 打开此文件夹可以看到 新建一个多层级目录playwright\driver\package.local-browsers 然后复制chromium-1124到playwright\dr…...
docker添加容器服务所需字体
1、在宿主机新建chinese目录 [rootHS-AP-application ~]#mkdir /usr/share/fonts/chinese 2、上传字体 把windows c盘下的Windows/Fonts下的所有字段上传至/usr/shared/fonts/chinese 3、授权chinese目录 chmod -R 755 /usr/share/fonts/chinese 4、生成fonts.scale文件 …...
Java面试八股之Spring AOP 和 AspectJ AOP 的区别
Spring AOP 和 AspectJ AOP 的区别 Spring AOP 和 AspectJ AOP 是两种不同的面向切面编程(Aspect-Oriented Programming, AOP)实现。它们各有特点,适用于不同的场景。下面是一些主要的区别: 1. 实现机制 Spring AOP: 基于代理…...
Java人力资源招聘社会校招类型招聘系统PC端
🔍【揭秘】人力资源新利器!社会校招一站式PC端招聘系统全攻略🚀 🌈 开篇引言:招聘新纪元,效率为王! Hey小伙伴们,你是否还在为繁琐的招聘流程头疼不已?🤯 面…...
C# 知识点总结
入门 C#程序在.NET上运行,.NET framework包含两个部分: ①:.NET framework类库 ②:公共语言运行库CLR(.NET虚拟机) CLS(公共语言规范) CTS(通用类型系统) .N…...
【ffmpeg命令入门】视频的旋转与翻转
文章目录 前言什么时候需要使用旋转与翻转1. 视频拍摄方向不正确2. 视频编辑特效使用什么参数1. 旋转视频 - transpose2. 水平翻转视频 - hflip3. 垂直翻转视频 - vflip 总结 前言 在视频编辑的过程中,我们经常会遇到需要旋转或翻转视频的情况。无论是因为拍摄时相…...
学懂C语言(二十五):深入理解 C语言结构体 位域 的概念
目录 一、位域的基本概念 二、位域的定义 三、位域的内存分配和大小计算 示例1:简单位域 示例2:跨越多个存储单元 注意事项 结构体对齐控制 总结 C语言中的位域(Bit-Field)是一种特殊的数据结构,允许在结构体中…...
LLM推理优化——KV Cache篇(百倍提速)
LLM推理优化——KV Cache篇(百倍提速) 注意:KV Cache本质上是空间换时间的技术。与计算机组成原理中的cache不同,它不涉及访存优化。 不知道大家在用LLM的时候,有没有注意到一个问题:我们在输入我们的问题…...
Linux进程--system
...
[Office] Word 特殊字符
0 打开“特殊字符集” 依次选择:Insert -> Symbol -> More Symbol 1 带圈编号 字体Font选择Wingdings...
联想电脑怎么重装系统_联想电脑U盘重装win10详细图文教程
联想电脑怎么重装系统?在当今科技发展迅猛的时代,联想电脑已经成为了人们生活中不可或缺的一部分。然而,随着时间的推移,我们可能会遇到一些问题,例如系统崩溃或者需要更换操作系统。这时,使用U盘来重新安装…...
前端开发者必备:揭秘谷歌F12调试的隐藏技巧!
前言 使用断点(breakpoint)是调试 JavaScript 代码的一种非常有效的方式。通过在代码的关键位置设置断点,可以阻止页面的状态变化,从而方便地检查和修改页面的当前状态。 1. 使用 setTimeout 配合 debugger 和 console.log setTi…...
vivado IP_REPO_PATHS
此属性允许您创建自定义IP目录,以与Vivado Design Suite一起使用。 IP_REPO_PATHS属性定义了一个或多个目录的路径,这些目录包含 第三方或用户定义的IP。指定的目录和任何子目录是 搜索要添加到Vivado Design Suite IP目录以用于设计的IP定义 进入或与IP…...
前端代码混淆加密(使用Terser、WebpackObfuscator)
零、相关技术及版本号 "vue": "2.6.12", "vue/cli-service": "4.4.6", "javascript-obfuscator": "^4.1.1", "terser-webpack-plugin": "^4.2.3", "vue-template-compiler": &quo…...
国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
设计模式和设计原则回顾
设计模式和设计原则回顾 23种设计模式是设计原则的完美体现,设计原则设计原则是设计模式的理论基石, 设计模式 在经典的设计模式分类中(如《设计模式:可复用面向对象软件的基础》一书中),总共有23种设计模式,分为三大类: 一、创建型模式(5种) 1. 单例模式(Sing…...
突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
盘古信息PCB行业解决方案:以全域场景重构,激活智造新未来
一、破局:PCB行业的时代之问 在数字经济蓬勃发展的浪潮中,PCB(印制电路板)作为 “电子产品之母”,其重要性愈发凸显。随着 5G、人工智能等新兴技术的加速渗透,PCB行业面临着前所未有的挑战与机遇。产品迭代…...
解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
Golang——7、包与接口详解
包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
