目录
服务端
服务端视角下的流程图
一、数据库管理模块
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、添加好友
数据库模块是项目的最底层,主要就是负责与数据库进行打交道,
具体功能有:连接数据库、将用户信息存入数据库中、从数据库中获取信息、操作数据库中的表
这里创建了两张表:user,friendinfo。分别用来存放用户信息,用户好友信息。
用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息
数据库模块是整个项目的最低层,而用户管理模块是倒数第二层,是建立在数据库模块之上的
用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息,并将这些信息放于user_map当中去,方便后续的查询
只有当服务端和客户端发送的消息格式统一的时候,才能正常的进行通信,就像网络当中的各种协议一样,只有双方都遵守的时候,才能正常的进行数据交换。
我们这里采用的是Json数据格式
Json就是一个key :value的东西,他可以包含多动类型
简单介绍:
示例:
这就是一个Json对象,Json还支持嵌套,可以有无数种格式
我们来测试一下:
能够看到,json对象用起来是非常方便的。
我们再来测试看一下Json的NB之处:
我们来运行一下看能打印出什么:
为什么需要序列化呢?
在tcpsocket编程的时候,我们学习过,内存不连续的结构体是不能直接用send发送的,需要使用序列化,将内存组合起来,放到一块连续的内存当中去,然后再发送。反序列化就是反过来,具体可以去看我之前写的博客:网络基础2--HTTP协议详解_Flying clouds的博客-CSDN博客
反序列化:
注册请求:
注册响应:
1、该模块负责TCP的socket编程,来负责网络通信。用epoll来监控多个文件描述符,从而实现具有高并发的基础
2、该模块还负责处理各种业务如:注册、登录、添加好友、聊天的具体实现
这个函数的作用是初始化资源,获取到需要的所有资源,比如用户管理模块的实例化指针、发送队列和接收队列、提供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;
}
这个函数相当于是开始工作的函数,调用这个函数之后,主线程就会 启动接收线程、发送线程、工作线程。
启动完别的线程之后,我们给主线程也找了一个活干:一直在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站上面的视频,上面的讲解是非常清晰的,我们这里只是简单来用。
我们在客户端主要是实现了四个基本的业务:注册,登录,添加好友,给好友发送消息
我们先来看看四个窗口:
我们首先要插入一个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("注册成功!"));
}
}
登录的时候,需要做到是点击登录按钮之后能够跳转到我们的聊天界面,需要注意的是,在跳转到聊天界面之前,我们需要提前吧聊天界面的内容给加载好,这样一条转到界面我们就能进行操作的,跳转界面我们已经知道了是调用domodel,用模态的方式打开界面,但是怎么加载数据呢?
我们这里可以找到方法:在调用domodel函数的时候,会调用一个函数,这个函数是重写父类的虚函数
BOOL CChatDlg::OnInitDialog()
在这个函数里面,我们可以能够实现获取聊天界面内容的功能,我们要获取的内容就两个:好友列表和好友的消息记录。
知道了这些之后,我就只剩下开工了:
如何展示:
保存好友信息到本地
因为好友信息是动态变化的,我们如果只是在获取好友信息的时候就在列表当中展示一次的话,那么当添加好友或者删除好友的时候,我们只能获取到新的好友信息,原来的好友信息就不知道从哪里再去获取了,方便起见,我们就直接保存在客户端,用vector来进行保存。
我们想要发送消息,第一步肯定是要知道消息到底要发送给谁,就像微信一样,我们在好友列表当中点击谁,就代表要给哪个好友发送消息了,我们为了能实现这个功能:需要做到的就是能够时刻识别出来我们点击了好友列表,并且要知道到底点击了谁。
这个功能我们用一个MFC的函数来实现 : GetText,这个函数能够知道我们点击了哪个好友
/*
当用户点击了 frilist,
1. 要及时的更新 recv_userid
2. 更新对话框
*/
/* 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 ls
std::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();
我们想要做到的是输入一个好友的电话号码,然后给服务端发送一个添加好友的请求,在服务端这里进行判断,如果有这个电话号码是一个注册用户的话,我们就给这个用户推送一个添加好友的请求,如果不是的话服务端就忽略这条消息。
当用户收到添加好友请求的时候,可以选择同意或者拒绝,如果拒绝,就当作无事发生就行;如果点击了同意,就需要给服务端回一个同意添加好友的请求,然后服务端进行处理,将两个人的关系设置为好友,并再次给客户端推送应答,让客户端的好友列表进行更新,将新添加的好友展示上去。
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();