目录
服务端
服务端视角下的流程图
一、数据库管理模块
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 数据类型:对象,数组,字符串,数字 对象:使⽤花括号 {} 括起来的表⽰⼀个对象。
示例:
{ “姓名” : “⼩张“, “年龄” : 20, “成绩” : [99, 89, 66] }
这就是一个Json对象,Json还支持嵌套,可以有无数种格式
数组:使⽤中括号 [] 括起来的表⽰⼀个数组。 这个就是一个数组的形式,一个元素的类型就是一个Json对象 [ { “ 姓名” : “ ⼩张 “ , “ 年龄” : 20, “成绩 ” : [ 99 , 89 , 66 ] },{ “姓名”: “⼩王“, “年龄”: 22, “成绩” : [77, 88, 66] }, ]字符串:使⽤常规双引号 “” 括起来的表⽰⼀个字符串 数字:包括整形和浮点型,直接使⽤jsoncpp库,已经帮助我们封装了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();if(recv_que_ == NULL){std::cout << "init MsgPool failed" << std::endl;return false;}send_que_ = new MsgPool();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-bitezs-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 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();