项目---基于TCP的高并发聊天系统
目录
服务端
服务端视角下的流程图
一、数据库管理模块
1.1 数据库表的创建
1.2 .对于数据库的操作
1.2.1首先得连接数据库
1.2.2执行数据库语句
1.2.3 返回数据库中存放的所有用户的信息
1.2.4返回数据库中存放的所有用户的好友信息
二、用户管理模块
2.1、UserInfo类:描述单个用户的信息
2.2UserMana类:组织用户的信息
2.2.1初始化
剩下的就是各类业务的接口
三、自定义消息格式
3.1了解Json对象
3.2、Json序列化和反序列化
3.3、模拟注册请求和发送请求
四、网络通信模块&业务模块
4.1、InitChatSvr函数
4.2 StartChatSvr函数
客户端
客户端视角下的流程图
1、注册
2、登录
3、发送消息
4、添加好友
服务端
服务端视角下的流程图

一、数据库管理模块
数据库模块是项目的最底层,主要就是负责与数据库进行打交道,
具体功能有:连接数据库、将用户信息存入数据库中、从数据库中获取信息、操作数据库中的表
1.1 数据库表的创建
这里创建了两张表:user,friendinfo。分别用来存放用户信息,用户好友信息。

1.2 .对于数据库的操作
1.2.1首先得连接数据库

1.2.2执行数据库语句

1.2.3 返回数据库中存放的所有用户的信息
用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息

1.2.4返回数据库中存放的所有用户的好友信息

二、用户管理模块
数据库模块是整个项目的最低层,而用户管理模块是倒数第二层,是建立在数据库模块之上的
2.1、UserInfo类:描述单个用户的信息

2.2UserMana类:组织用户的信息
2.2.1初始化
用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息,并将这些信息放于user_map当中去,方便后续的查询

剩下的就是各类业务的接口
三、自定义消息格式
3.1了解Json对象
只有当服务端和客户端发送的消息格式统一的时候,才能正常的进行通信,就像网络当中的各种协议一样,只有双方都遵守的时候,才能正常的进行数据交换。
我们这里采用的是Json数据格式
Json就是一个key :value的东西,他可以包含多动类型
简单介绍:
示例:
这就是一个Json对象,Json还支持嵌套,可以有无数种格式
我们来测试一下:

能够看到,json对象用起来是非常方便的。

我们再来测试看一下Json的NB之处:

我们来运行一下看能打印出什么:

3.2、Json序列化和反序列化
为什么需要序列化呢?
在tcpsocket编程的时候,我们学习过,内存不连续的结构体是不能直接用send发送的,需要使用序列化,将内存组合起来,放到一块连续的内存当中去,然后再发送。反序列化就是反过来,具体可以去看我之前写的博客:网络基础2--HTTP协议详解_Flying clouds的博客-CSDN博客
序列化:
反序列化:

3.3、模拟注册请求和发送请求
注册请求:

注册响应:

四、网络通信模块&业务模块
1、该模块负责TCP的socket编程,来负责网络通信。用epoll来监控多个文件描述符,从而实现具有高并发的基础
2、该模块还负责处理各种业务如:注册、登录、添加好友、聊天的具体实现
4.1、InitChatSvr函数
这个函数的作用是初始化资源,获取到需要的所有资源,比如用户管理模块的实例化指针、发送队列和接收队列、提供TCP服务的实例化指针、epoll操作句柄。
 bool InitChatSvr(int wtc = 4){/* 1. 初始化用户管理类的指针 */um_ = UserMana::GetInstance();if(um_ == NULL){std::cout << "初始化用户管理失败了, 请检查..." << std::endl;return false;}/* 2. 初始化队列资源 */recv_que_ = new MsgPool<ChatMsg>();if(recv_que_ == NULL){std::cout << "init MsgPool failed" << std::endl;return false;}send_que_ = new MsgPool<ChatMsg>();if(send_que_ == NULL){std::cout << "init MsgPool failed" << std::endl;return false;}work_thread_count_ = wtc;/* 3. 初始化tcp通信的内容 */sockfd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(sockfd_ < 0){perror("socket");return false;}int opt = 1;// sockfd为需要端口复用的套接字setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(port_);addr.sin_addr.s_addr = inet_addr("0.0.0.0");int ret = bind(sockfd_, (struct sockaddr*)&addr, sizeof(addr));if(ret < 0){perror("bind");return false;}ret = listen(sockfd_, 10);if(ret < 0){perror("listen");return false;}/* 4. 初始化epoll */ep_fd_ = epoll_create(5);if(ep_fd_ < 0){perror("epoll_create");return false;}return true;}4.2 StartChatSvr函数
这个函数相当于是开始工作的函数,调用这个函数之后,主线程就会 启动接收线程、发送线程、工作线程。
启动完别的线程之后,我们给主线程也找了一个活干:一直在accept,并将接收到的新连接套接字放到epoll当中去,让epoll帮我们去监控这些个文件描述符的状态
bool StartChatSvr(){/* 1. 启动接收线程 */pthread_t tid;int ret = pthread_create(&tid, NULL, RecvStart, (void*)this);if(ret < 0){std::cout << "create thread failed\n";return false;}/* 2. 启动发送线程 */ret = pthread_create(&tid, NULL, SendStart, (void*)this);if(ret < 0){std::cout << "create thread failed\n";return false;}/* 3. 启动工作线程 */for(int i = 0; i < work_thread_count_; i++){ret = pthread_create(&tid, NULL, WorkerStart, (void*)this);if(ret < 0){std::cout << "create thread failed\n";return false;}}/* 4. 主线程accept *//* 5. 将新连接套接字放到epoll当中 */while(1){int new_sockfd = accept(sockfd_, NULL, NULL);if(new_sockfd < 0){continue;}/* 添加到epoll */struct epoll_event ee;ee.events = EPOLLIN;ee.data.fd = new_sockfd;epoll_ctl(ep_fd_, EPOLL_CTL_ADD, new_sockfd, &ee);}客户端
客户端视角下的流程图

客户端我们这里选取了MFC框架来进行,我们这里只是用到了MFC最基本的使用方法,想要熟练掌握MFC的同学可以去看b站上面的视频,上面的讲解是非常清晰的,我们这里只是简单来用。
我们在客户端主要是实现了四个基本的业务:注册,登录,添加好友,给好友发送消息
我们先来看看四个窗口:

1、注册
我们首先要插入一个Dialog,然后去对其进行操作,具体做法就是:插入一个Dialog,然后点击这个Dialog,我们就能看到一个窗口,然后再对窗口添加button。。。啥的各种内容。
在窗口当中对 : 姓名、学校、电话、密码添加变量,用来保存之后输入的值。
我们右键去对其添加变量、控件什么的,双击就是会给我们自动生成代码。
我们这个注册窗口是需要点击提交之后,成功的话就直接跳转到登录的界面,我们这里选择的是双击提交,生成代码,然后在这个函数里面实现我们想要的功能

void CRegisterdlg::OnBnClickedCommit()
{// TODO: 在此添加控件通知处理程序代码/*1.获取输入框的内容*/UpdateData(true);if (m_nickname_.IsEmpty()||m_school_.IsEmpty()||m_tel_.IsEmpty()||m_passwd_.IsEmpty()) {MessageBox(TEXT("请输入完整的注册内容!"));return;}std::string nickname = CT2A(m_nickname_.GetString());std::string school = CT2A(m_school_.GetString());std::string telnum = CT2A(m_tel_.GetString());std::string passwd = CT2A(m_passwd_.GetString());/*2.组织ChatMSg数据*/ChatMsg cm;cm.msg_type_ = Register;cm.json_msg_["nickname"] = nickname.c_str();cm.json_msg_["school"] = school.c_str();cm.json_msg_["telnum"] = telnum.c_str();cm.json_msg_["passwd"] = passwd.c_str();/*3.获取TCP实例化指针*/TcpSvr* ts = TcpSvr::GetInstance();/*4.发送消息*/std::string msg;cm.GetMsg(&msg);ts->Send(msg);/*5.获取消息队列的实例化指针*/MsgQueue* mq = MsgQueue::GetInstance();if (mq == NULL) {exit(1);}/*6.按照消息类型获取注册应答*/msg.clear();mq->Pop(Register_Resp,&msg);/*7.解析应答,判断应答结果注册成功 ==》跳转到登 录界面注册失败 ==》情空注册框,提示失败了(电话号码重复了)*/cm.clear();cm.PraseMsg(-1,msg);if (cm.reply_status_ == REGISTER_FAILED) {MessageBox(TEXT("电话号码重复了,请检查输入。。。"));}else {MessageBox(TEXT("注册成功!"));}}2、登录
登录的时候,需要做到是点击登录按钮之后能够跳转到我们的聊天界面,需要注意的是,在跳转到聊天界面之前,我们需要提前吧聊天界面的内容给加载好,这样一条转到界面我们就能进行操作的,跳转界面我们已经知道了是调用domodel,用模态的方式打开界面,但是怎么加载数据呢?
我们这里可以找到方法:在调用domodel函数的时候,会调用一个函数,这个函数是重写父类的虚函数
BOOL CChatDlg::OnInitDialog()在这个函数里面,我们可以能够实现获取聊天界面内容的功能,我们要获取的内容就两个:好友列表和好友的消息记录。
知道了这些之后,我就只剩下开工了:
如何展示:

保存好友信息到本地
因为好友信息是动态变化的,我们如果只是在获取好友信息的时候就在列表当中展示一次的话,那么当添加好友或者删除好友的时候,我们只能获取到新的好友信息,原来的好友信息就不知道从哪里再去获取了,方便起见,我们就直接保存在客户端,用vector来进行保存。
3、发送消息
我们想要发送消息,第一步肯定是要知道消息到底要发送给谁,就像微信一样,我们在好友列表当中点击谁,就代表要给哪个好友发送消息了,我们为了能实现这个功能:需要做到的就是能够时刻识别出来我们点击了好友列表,并且要知道到底点击了谁。
这个功能我们用一个MFC的函数来实现 : GetText,这个函数能够知道我们点击了哪个好友
/*当用户点击了 frilist,1. 要及时的更新 recv_userid2. 更新对话框*//* 1. 获取点击的内容 */CString str;m_frilist_.GetText(m_frilist_.GetCurSel(), str);/* 2. 根据点击的内容进行比对*//* 3. 更新 recv_userid_ */int i = 0;for (; i < fris_info_.size(); i++) {// eg: zs-bite    zs-bite:10// zs lsstd::string tmp = CT2A(str.GetString());if (strstr((tmp.c_str()), fris_info_[i].nickname_.c_str()) != NULL) {recv_userid_ = fris_info_[i].user_id_;break;}}当我们切换了好友之后,我们也要同步更新聊天框的内容
我们这里的做法也很简单粗暴:将现在所有的消息全部清空,然后再将当前好友的历史记录依次展示上去
/* 4. 刷新聊天界面 */for (int i = m_output_.GetCount(); i >= 0; i--) {m_output_.DeleteString(i);}std::vector<std::string> h_m = fris_info_[i].history_msg_;for (int i = 0; i < h_m.size(); i++) {m_output_.InsertString(m_output_.GetCount(), h_m[i].c_str());}fris_info_[i].unread_msg_ = 0;/* 5. 清空输入框 */m_input_.Empty();m_input_edit_.SetWindowTextA(0);/* 6. 刷新UserList */ReferFriList();4、添加好友
我们想要做到的是输入一个好友的电话号码,然后给服务端发送一个添加好友的请求,在服务端这里进行判断,如果有这个电话号码是一个注册用户的话,我们就给这个用户推送一个添加好友的请求,如果不是的话服务端就忽略这条消息。
当用户收到添加好友请求的时候,可以选择同意或者拒绝,如果拒绝,就当作无事发生就行;如果点击了同意,就需要给服务端回一个同意添加好友的请求,然后服务端进行处理,将两个人的关系设置为好友,并再次给客户端推送应答,让客户端的好友列表进行更新,将新添加的好友展示上去。
UpdateData(true);if (m_fritel.IsEmpty()) {MessageBox(TEXT("输入内容不能为空..."));return;}std::string input = CT2A(m_fritel.GetString());ChatMsg cm;cm.msg_type_ = AddFriend;cm.user_id_ = userid_;cm.json_msg_["fri_telnum"] = input.c_str();/* 3. 获取TCP的实例化指针 */TcpSvr* ts = TcpSvr::GetInstance();/* 4. 发送消息 */std::string msg;cm.GetMsg(&msg);ts->Send(msg);CDialog::OnCancel();相关文章:
 
项目---基于TCP的高并发聊天系统
目录 服务端 服务端视角下的流程图 一、数据库管理模块 1.1 数据库表的创建 1.2 .对于数据库的操作 1.2.1首先得连接数据库 1.2.2执行数据库语句 1.2.3 返回数据库中存放的所有用户的信息 1.2.4返回数据库中存放的所有用户的好友信息 二、用户管理模块 2.1、UserInfo类&…...
iOS热更新-8种实现方式
一、JSPatch 热更新时,从服务器拉去js脚本。理论上可以修改和新建所有的模块,但是不建议这样做。 建议 用来做紧急的小需求和 修复严重的线上bug。 二、lua脚本 比如: wax。热更新时,从服务器拉去lua脚本。游戏开发经常用到。…...
 
R语言 | 编写自己的函数
目录 一、正式编写程序 二、设计第一个函数 三、函数也是一个对象 四、程序代码的简化 五、return()函数的功能 六、省略函数的大括号 七、传递多个参数函数的应用 7.1 设计可传递2个参数的函数 7.2 函数参数的默认值 7.3 3点参数“…”的使用 八、函数也可以作为参数 …...
 
【Java校招面试】基础知识(七)——数据库
目录 前言一、数据库索引二、数据库锁三、数据库事务四、数据库连接池后记 前言 本篇主要介绍数据库的相关内容。 “基础知识”是本专栏的第一个部分,本篇博文是第六篇博文,如有需要,可: 点击这里,返回本专栏的索引文…...
 
MySQL高级--锁
一、锁 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题…...
 
Maven(六):Maven的使用——继承与聚合
Maven(六):Maven的使用——继承与聚合 前言一、实验九:继承1、概念2、作用3、举例4、操作4.1 创建父工程4.2 创建模块工程4.3 查看被添加新内容的父工程 pom.xml4.4 解读子工程的pom.xml4.5 在父工程中配置依赖的统一管理4.6 子工…...
 
Java ---System类
System 类位于 java.lang 包,代表当前 Java 程序的运行平台,系统级的很多属性和控制方法都放置在该类的内部。由于该类的构造方法是 private 的,所以无法创建该类的对象,也就是无法实例化该类。 System 类提供了一些类变量和类方…...
代码随想录_贪心_leetcode 406 452
leetcode 406. 根据身高重建队列 406. 根据身高重建队列 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高…...
C++类的静态成员详解:成员函数非静态成员函数的非法调用
在C中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存。 静态成员的定义或声明要…...
 
Qt之滑动条和进度条(QSlider、QProgressBar)
文章目录 前言一、QSliderQSlider的常用API信号与槽 二、QProgressBar滑动条和滚动条的常用API 总结 前言 在用户界面设计中,滑动条和进度条是常见的控件。Qt中提供了QProgressBar和QSlider两个类来实现滚动条和滑动条。 一、QSlider 在Qt中,QSlider是…...
 
Flutter之插件开发plugin
目的:适用于独立业务模块,或者与原生页面交互频繁的地方。 基于flutter3.x , IDE :androidStudio demo:https://download.csdn.net/download/SHTLoveXX/87751845 步骤: 1.新建flutter project 【New flutter project】. 2. 在新建工程面板记得切换 …...
 
asp.net基于web的音乐管理网站dzkf17A9程序
本系统主要包含了等系统用户管理、公告信息管理、音乐资讯管理、音乐类型管理多个功能模块。下面分别简单阐述一下这几个功能模块需求。 管理员的登录模块:管理员登录系统对本系统其他管理模块进行管理。 用户的登录模块:用户登录本系统,对个…...
 
itop-3568开发板驱动学习笔记(25)设备树(四)GPIO 实例分析
《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记 文章目录 GPIO 控制器必要属性其他属性 指定 GPIO 引脚 和时钟类似,GPIO 在设备树中也存在两层定义,首先是 GPIO 控制器,这部分由芯片原厂工程师编写,相当于 GPIO 底层…...
函数(定义、返回值、调用、参数)
目录 ❤ 无参函数 ❤ 有参函数 ❤ 空函数 ❤ 什么是返回值? ❤ 为什么要有返回值? ❤ 什么是函数调用? ❤ 为何用调用函数? ❤ 函数调用的三种形式 ❤ 形参和实参 形参 实参 ❤ 位置参数 位置形参 位置实…...
28. Kubernetes 核心组件讲解——API Server
本章讲解知识点 Kubernetes API Server 概述etcd 简介API Server 架构解析API Server 的 List-Watch 机制独特的 Kubernetes Proxy API 接口集群功能模板之间的通信1. Kubernetes API Server 概述 1.1 基本概念 Kubernetes API Server(API Server)是 Kubernetes 的核心组件…...
 
springboot框架开发医院云HIS 住院医生站、住院护士站功能实现
住院医生站主模块:包括医嘱管理、病案首页、分配入科、住院清单、我的质控等子模块 (1)医嘱管理功能简介 ①住院患者开立医嘱、支持医嘱复制、停止、作废等操作; ②医嘱类型含药品、项目、材料、嘱托; ③支持住院各…...
高性能定时器介绍及代码逐行解析--时间堆
简介 在《Linux高性能服务器编程》中,介绍了三种定时方法: socket选项SO_RCVTIMEO和SO_SNDTIMEOSIGALRM信号I/O复用系统调用的超时参数 基础知识 非活跃,是指客户端(这里是浏览器)与服务器端建立连接后,…...
汇编语言学习笔记五
div指令 除法, 被除数:默认是放在ax或者dx中,其位数为16位,则在ax中,如位数为32位,则高位在dx中,低位在ax中 除数:放在寄存器或者内存单元中,有8位和16位两种。 结果&am…...
Linux下的epf 是什么?
EPF (Extended Page Frame) 是 Linux 内核中的一个功能,它用于管理大内存系统中的物理页框。具体来说,当系统中的物理内存超过 1TB 时,传统的页表结构会变得非常庞大和复杂,给内存管理带来很大的困难。 EPF 架构通过将物理地址分…...
如何在广告形式选择上化解用户厌恶和变现瓶颈?
用户讨厌广告,这似乎是一个共识。在日复一日的使用中,用户会遇到各种各样的广告形式,从搜索结果中的广告链接,到视频中不间断的广告,再到流行应用中的推广内容。 无处不在的广告已经让用户不胜其烦,这也…...
 
SpringBoot-17-MyBatis动态SQL标签之常用标签
文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
 
处理vxe-table 表尾数据是单独一个接口,表格tableData数据更新后,需要点击两下,表尾才是正确的
修改bug思路: 分别把 tabledata 和 表尾相关数据 console.log() 发现 更新数据先后顺序不对 settimeout延迟查询表格接口 ——测试可行 升级↑:async await 等接口返回后再开始下一个接口查询 ________________________________________________________…...
 
day36-多路IO复用
一、基本概念 (服务器多客户端模型) 定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标…...
Caliper 配置文件解析:fisco-bcos.json
config.yaml 文件 config.yaml 是 Caliper 的主配置文件,通常包含以下内容: test:name: fisco-bcos-test # 测试名称description: Performance test of FISCO-BCOS # 测试描述workers:type: local # 工作进程类型number: 5 # 工作进程数量monitor:type: - docker- pro…...
MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用
文章目录 一、背景知识:什么是 B-Tree 和 BTree? B-Tree(平衡多路查找树) BTree(B-Tree 的变种) 二、结构对比:一张图看懂 三、为什么 MySQL InnoDB 选择 BTree? 1. 范围查询更快 2…...
适应性Java用于现代 API:REST、GraphQL 和事件驱动
在快速发展的软件开发领域,REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名,不断适应这些现代范式的需求。随着不断发展的生态系统,Java 在现代 API 方…...
