项目---基于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 架构通过将物理地址分…...
如何在广告形式选择上化解用户厌恶和变现瓶颈?
用户讨厌广告,这似乎是一个共识。在日复一日的使用中,用户会遇到各种各样的广告形式,从搜索结果中的广告链接,到视频中不间断的广告,再到流行应用中的推广内容。 无处不在的广告已经让用户不胜其烦,这也…...

Chapter03-Authentication vulnerabilities
文章目录 1. 身份验证简介1.1 What is authentication1.2 difference between authentication and authorization1.3 身份验证机制失效的原因1.4 身份验证机制失效的影响 2. 基于登录功能的漏洞2.1 密码爆破2.2 用户名枚举2.3 有缺陷的暴力破解防护2.3.1 如果用户登录尝试失败次…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Java数值运算常见陷阱与规避方法
整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...

wpf在image控件上快速显示内存图像
wpf在image控件上快速显示内存图像https://www.cnblogs.com/haodafeng/p/10431387.html 如果你在寻找能够快速在image控件刷新大图像(比如分辨率3000*3000的图像)的办法,尤其是想把内存中的裸数据(只有图像的数据,不包…...

通过 Ansible 在 Windows 2022 上安装 IIS Web 服务器
拓扑结构 这是一个用于通过 Ansible 部署 IIS Web 服务器的实验室拓扑。 前提条件: 在被管理的节点上安装WinRm 准备一张自签名的证书 开放防火墙入站tcp 5985 5986端口 准备自签名证书 PS C:\Users\azureuser> $cert New-SelfSignedCertificate -DnsName &…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...