-
目录
聊天系统功能
- 实现普遍情况下用户的注册,登录,添加好友,相互聊天
- 处理的特殊情况:服务端缓存未发出的消息,择机发送
-
开发环境:
- 服务端在Linux下开发,客户端在vs2019下开发
-
服务端各个模块的简单介绍
- 基础模块
- 数据库模块-->用于管理用户信息,包括获取全部的用户信息,获取某个用户的好友信息,用户注册时插入数据和给某个用户设置好友
- 用户管理模块-->把每个用户的基本信息(包括昵称,学校,电话,密码,服务端分配给用户的id,用户的状态(登录还是未登录)和用户的好友信息)从数据库中读出来并管理起来.
- 消息队列模块-->通用模块,当需要队列存储数据时,都可以调用这个模块
- 消息类型数据格式模块-->为了能正确解析消息内容,约定客户端和服务端的消息格式,客户端不同的请求携带的内容不同,所以在消息中有固定信息也有非固定信息,非固定信息采用json数据格式,通过对整个消息采用json的序列化和反序列化进行数据的传输
- 主干模块()
- 处理业务模块-->首先进行各种资源的初始化,然后创建多个线程循环处理不同的请求,
- 服务端运行的过程:1.初始化资源,2.主线程创建多个线程,3.线程之间相互协作循环处理业务
- 主干模块的功能
- 1.能处理客户端的登录请求
- 2.能处理客户端的注册请求
- 3.能处理客户端A给B的添加好友请求
- 4.能处理客户端B对于添加好友这个请求的回应
- 5.在客户端A给客户端B发送消息时,能将这个消息转发给B
- 6.能在客户端A登录上来时处理客户端A获取他全部好友信息的请求
- 处理业务模块-->首先进行各种资源的初始化,然后创建多个线程循环处理不同的请求,
- 基础模块
-
服务端处理任务流程和流程图
- 1.监控线程监控就绪事件,将就绪的套接字对应的数据放到接收队列,
- 2.工作线程通过数据中的数据类型字段进行解析并处理
- 3.工作线程组织应答并判断应答接收方受否在线
- 如果在线,处理的结果放在发送队列里
- 如果不在线,将消息放到缓存队列里,由缓存监控线程轮询监控接收方状态,如果接收方上线,把消息从缓存队列中拿出来放到发送线程里
- 4.发送线程从发送队列中拿数据根据数据中的套接字描述符发给对应的客户
-
服务端各模块功能实现
-
1.数据库模块
- 数据库模块中管理的表:维护用户信息和用户好友信息和服务端未处理的消息
- 主要功能:将程序产生的数据插入数据库或者程序需要的数据从数据库中读出来,总的来说就是用户应用程序和数据库的交互
- 成员变量:数据库模块为最底层的模块,所以只需要数据库的操作句柄即可
- 成员函数:
- 初始化接口:作用是连接数据库,将程序和数据库连接起来
- 获取全部用户信息接口:作用是将所有用户信息都读回来,并管理起来,方便以后的查询,而不是每次查询都要进入数据库查询
/* 54 * 获取 all user info 55 * 参数为Json对象, 是一个出参 56 * */ 57 bool GetAllUser(Json::Value* all_user)
- 获取单个用户的好友信息接口:作用是当用户登录上来时,需要将好友信息从数据库中读回来并发给客户端,显示在客户端界面上.
/* 93 * 获取单个用户的好友信息, 在程序初始化阶段, 让用户管理模块维护起来 94 * userid : 用户的id 95 * f_id : 该用户的所有好友id 96 * */ 97 bool GetFriend(int userid, std::vector<int>* f_id)
- 注册信息接口:当用户进行注册的时候,将用户注册的信息插入数据库中
/* 130 * 当用户注册的时候, 进行插入使用的函数 131 * */ 132 bool InsertUser(int userid, const std::string& nickname 133 , const std::string& school, const std::string& telnum 134 , const std::string& passwd)
- 添加好友接口:当用户添加了好友后,在数据库中给该用户添加好友
/* 151 * 添加好友 152 * */ 153 bool InsertFriend(int userid1, int userid2)
当服务端非正常结束时,把未发出的消息插入数据库,服务端再启动时读回未发出的消息
-
void HoldNoReadInfo(std::vector<ChatMsg>*v)//服务端启动之后从数据库中拿未读数据 void ClearTable()//拿出未读数据后删除表中的数据,以防重复发送 void InsertNoReadInfo(std::vector<ChatMsg>v)//服务端非正常退出时,保存未发送的消息
-
2.用户管理模块
- 作用:用于管理用户数据,即数据库模块中读回来的数据,将用户数据保存在内存中
- 成员变量:
- unordered_map用于建立用户id和用户信息的映射关系,方便查找
- 锁用来保护unordered_map,多线程时容器是不安全的
- prepare_id_用来保存下一次分配的用户id
- db_与数据管理模块进行交互,即调用数据库模块的接口查询插入
- UserInfo:单个用户的所有信息
- 成员函数
- 初始化接口:调用数据管理模块的初始化连接数据库,再将数据库中的数据通过数据库模块中的接口拿出来,维护在unordered_map中
- 注册接口:当服务端接收到用户注册请求时用户管理模块给用户分配id,并将用户的数据维护起来,并插入数据库
/* 135 * 处理用户注册 136 * userid : 如果注册成功, 通过userid,告诉注册的客户端,他的id是什么 137 * */ 138 int DealRegister(const std::string& nickname, 139 const std::string& school, 140 const std::string& tel, 141 const std::string& passwd, 142 int* userid)
- 登录接口:当服务端接收到用户登录请求时用户管理模块查询自己管理的数据,判断是否是已经注册用户,判断用户输入信息的正确性
* 处理登录请求 * sockfd 是 服务端为登录客户端创建的新连接套接字 * */ int DealLogin(const std::string& tel, const std::string& passwd, int sockfd)
- 判断用户状态接口:用来判断当前用户信息中的user_status_是否是登录状态
int IsLogin(int userid) int IsLogin(const std::string& telnum, UserInfo* ui)
- 获取用户信息接口:用来获取单个用户的所有信息
//以出参的形式返回用户信息 bool GetUserInfo(int userid, UserInfo* ui)
- 获取用户的好友信息接口
//以出参的形式返回userid对应的好友信息 bool GetFriends(int userid, std::vector<int>* fri)
- 设置好友接口:给用户管理模块中的对应的用户设置好友信息,并将好友信息调用数据库的指针插入数据库中
//把userid2设置为userid1的好友,并将1设置为2的好友 void SetFriend(int userid1, int userid2)
对数据库模块中解决未读消息的接口简单封装
void InsertNoRead(std::vector<ChatMsg>v) { db_->InsertNoReadInfo(v); } void HoldNoReadInfo(std::vector<ChatMsg>*v) { db_->HoldNoReadInfo(v); } void ClearTable() { db_->ClearTable(); }
-
3.消息队列模块
- 消息队列模块是一个通用模块:用于在需要队列时创建消息队列
- 成员变量:
- 成员函数
- Push:往队列中添加元素
void Push(const T& msg) 29 { 30 pthread_mutex_lock(&lock_vec_); 31 while(vec_.size() >= capacity_) 32 { 33 pthread_cond_wait(&prod_cond_, &lock_vec_); 34 } 35 vec_.push(msg); 36 pthread_mutex_unlock(&lock_vec_); 37 38 pthread_cond_signal(&cons_cond_); 39 }
- Pop:从对列中拿出元素
41 void Pop(T* msg) 42 { 43 pthread_mutex_lock(&lock_vec_); 44 while(vec_.empty()) 45 { 46 pthread_cond_wait(&cons_cond_, &lock_vec_); 47 } 48 *msg = vec_.front(); 49 vec_.pop(); 50 pthread_mutex_unlock(&lock_vec_); 51 52 pthread_cond_signal(&prod_cond_); 53 }
- Push:往队列中添加元素
-
4.消息类型数据格式模块
- 消息类型:客户端发送的请求内有消息类型,服务端通过对消息类型的判断,进行不同的处理
- 消息状态:用来标记消息应答是成功状态还是失败状态
- 消息格式
- 成员变量
- 成员函数
- 为什么采用json数据格式
- 1.因为客户端不同的请求携带的消息项是不同的,所以需要根据不同的请求创建对象,对象包含的消息项不同,如果是定长的数据结构(struct里全是char的数组),需要将所有消息项全部罗列出来并为其开辟好空间,例如姓名,电话,密码等多个信息,但是大部分消息只需要其中的几个信息就够了,所以会造成空间的浪费(要不浪费空间,考虑struct里方string,但是不同的string对象的空间也是不连续的,也要序列化,json提供了序列化接口),而且后续如果还要增加消息类型需要不断的往定长的数据结构中插入,而在json对象中,可以只将需要的消息通过K-V结构插进来,不需要的消息不用插入对象中,即使要增加其他消息项,只需要客户端和服务端同步解析就可以
- 既然能用K-V键值对为什么map不行呢,因为map中的K-V键值对的类型实例化的时候就已经确认好了,是不能改变的,不够灵活.
- 如果采用json,就需要将数据序列化,因为json中的数据再内存中是不连续的
- 序列化接口:用于将整个类按照不同的字段组织成json数据,再进行序列化string对象进行传输
188 bool GetMsg(std::string* msg) 189 { 190 Json::Value tmp; 191 tmp["msg_type"] = msg_type_; 192 tmp["user_id"] = user_id_; 193 tmp["reply_status"] = reply_status_; 194 tmp["json_msg"] = json_msg_; 195 196 return JsonUtil::Serialize(tmp, msg); 197 } static bool Serialize(const Json::Value& value, std::string* body) 114 { 115 Json::StreamWriterBuilder swb; 116 std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); 117 118 std::stringstream ss; 119 int ret = sw->write(value, &ss); 120 if (ret != 0) 121 { 122 return false; 123 } 124 *body = ss.str(); 125 return true; 126 }
- 反序列化接口:用于将从网络中接收到的string转化成类对象,方便解析
/* 163 * 提供反序列化的接口, 接收完毕请求之后进行反序列化 164 * */ 165 int PraseChatMsg(int sockfd, const std::string& msg) 166 { 167 //1.调用jsoncpp的反序列化接口 168 Json::Value tmp; 169 bool ret = JsonUtil::UnSerialize(msg, &tmp); 170 if(ret == false) 171 { 172 return -1; 173 } 174 //2.赋值给成员变量 175 sockfd_ = sockfd; 176 msg_type_ = tmp["msg_type"].asInt(); 177 user_id_ = tmp["user_id"].asInt(); 178 reply_status_ = tmp["reply_status"].asInt(); 179 json_msg_ = tmp["json_msg"]; 180 return 0; 181 } 128 static bool UnSerialize(const std::string& body, Json::Value* value) 129 { 130 Json::CharReaderBuilder crb; 131 std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); 132 133 std::string err; 134 bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), value, &err); 135 if (ret == false) 136 { 137 return false; 138 } 139 return true; 140 }
- 通过key获取json中的value的接口
/* 200 * 获取json_msg_当中的value值 201 * */ 202 std::string GetValue(const std::string& key) 203 { 204 if(!json_msg_.isMember(key)) 205 { 206 return ""; 207 } 208 return json_msg_[key].asString(); 209 }
- 使用<key,value>给json中添加值
215 void SetValue(const std::string& key, const std::string& value) 216 { 217 json_msg_[key] = value; 218 } 219 220 void SetValue(const std::string& key, int value) 221 { 222 json_msg_[key] = value; 223 }
- 清理对象接口:为了让对象可以重复使用,将创建好的对象清理之后可以继续
使用224 void Clear() 225 { 226 msg_type_ = -1; 227 user_id_ = -1; 228 reply_status_ = -1; 229 json_msg_.clear(); 230 }
- 为什么采用json数据格式
- 成员变量
- 消息类型:客户端发送的请求内有消息类型,服务端通过对消息类型的判断,进行不同的处理
-
5.处理业务模块
- 成员变量
- 成员函数
- 初始化接口:初始化各个成员变量
- 服务端处理业务接口->主线程循环监听侦听套接字,并将将新连接加入epoll中
//启动各类线程的函数 - 主线程调用的 int StartChatServer() { //1.创建epoll等待线程 pthread_t tid; int ret = pthread_create(&tid, NULL, epoll_wait_start, (void*)this); if(ret < 0) { perror("pthread_create"); return -1; } //2.创建接收线程 ret = pthread_create(&tid, NULL, recv_msg_start, (void*)this); if(ret < 0) { perror("pthread_create"); return -1; } //3.创建发送线程 ret = pthread_create(&tid, NULL, send_msg_start, (void*)this); if(ret < 0) { perror("pthread_create"); return -1; } //4.创建工作线程 for(int i = 0; i < thread_count_; i++) { ret = pthread_create(&tid, NULL, deal_start, (void*)this); if(ret < 0) { thread_count_--; } } if(thread_count_ <= 0) { return -1; } //5.主线程循环接收新连接 & 将新连接的套接字放到epoll当中 struct sockaddr_in cli_addr; socklen_t cli_addr_len = sizeof(cli_addr); while(main_flag_) { int newsockfd = accept(tcp_sock_,(struct sockaddr*)&cli_addr, &cli_addr_len); if(newsockfd < 0) { continue; } //接收上了, 添加到epoll当中进行监控 struct epoll_event ee; ee.events = EPOLLIN; ee.data.fd = newsockfd; epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, newsockfd, &ee); } return 0; }
- 线程1:监听线程:将epoll监控到的就绪事件放在接收队列里
//监控线程线控到了就绪事件之后放在接收队列里,工作线程从接收队列里拿,然后处理处理得结果(应答)放在发送队列里.发送线程进行发送 static void* epoll_wait_start(void* arg) { pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(epoll_flag_) { struct epoll_event arr[10]; int ret = epoll_wait(cs->epoll_fd_, arr, sizeof(arr)/sizeof(arr[0]), -1); if(ret < 0) { continue; } //正常获取了就绪的事件结构, 一定全部都是新连接套接字 for(int i = 0; i < ret; i++) { char buf[TCP_DATA_MAX_LEN] = {0}; //隐藏的问题: TCP粘包 ssize_t recv_size = recv(arr[i].data.fd, buf, sizeof(buf) - 1, 0); if(recv_size < 0) { //接收失败了 std::cout << "recv failed : sockfd is " << arr[i].data.fd << std::endl; continue; }else if(recv_size == 0) { //对端关闭连接了->设置用户状态为OFFLINE? epoll_ctl(cs->epoll_fd_, EPOLL_CTL_DEL,arr[i].data.fd, NULL); close(arr[i].data.fd); continue; } printf("epoll_wait_start recv msg : %s from sockfd is %d\n", buf, arr[i].data.fd); //正常接收回来了, 将接收回来的数据放到接收线程的队列当中, 等到工作线程从队列当中获取消息, 进而进行处理 //3.将接收到的数据放到接收队列当当中 std::string msg; msg.assign(buf, strlen(buf)); ChatMsg cm; cm.PraseChatMsg(arr[i].data.fd, msg); cs->recv_que_->Push(cm); } } return NULL; }
- 线程2:工作线程:从接收队列里拿出消息,根据消息类型执行不同的处理函数,并把处理结果放在发送队列里
static void* deal_start(void* arg) { pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(work_flag_) { //1. 从接收队列当中获取消息 ChatMsg cm; cs->recv_que_->Pop(&cm); //2. 通过消息类型分业务处理 int msg_type = cm.msg_type_; switch(msg_type) { case Register: //服务端收到是一个注册请求 { cs->DealRegister(cm); break; } case Login://是一个登录请求 { cs->DealLogin(cm); break; } case AddFriend://是一个添加好友请求A发到服务端 { cs->DealAddFriend(cm); break; } case PushAddFriendMsg://添加好友的结果B发到服务端 { cs->DealAddFriendResp(cm); break; } case SendMsg: //推送消息 { cs->DealSendMsg(cm); break; } case GetFriendMsg: { cs->GetAllFriendInfo(cm); break; } default: { break; } } //3. 组织应答 } return NULL; }
void DealRegister(ChatMsg& cm) { //1.获取注册信息 std::string nickname = cm.GetValue("nickname"); std::string school = cm.GetValue("school"); std::string telnum = cm.GetValue("telnum"); std::string passwd = cm.GetValue("passwd"); //2.调用用户管理系统当中的注册接口 int userid = -1; int ret = user_mana_->DealRegister(nickname, school, telnum, passwd, &userid); //3.回复应答 cm.Clear(); cm.msg_type_ = Register_Resp; if(ret < 0) { cm.reply_status_ = REGISTER_FAILED; }else { cm.reply_status_ = REGISTER_SUCCESS; } cm.user_id_ = userid; send_que_->Push(cm); } void DealLogin(ChatMsg& cm) { //1.获取数据 std::string telnum = cm.GetValue("telnum"); std::string passwd = cm.GetValue("passwd"); //2.调用用户管理模块的代码 int ret = user_mana_->DealLogin(telnum, passwd, cm.sockfd_); //3.回复应答 cm.Clear(); cm.msg_type_ = Login_Resp; if(ret < 0) { cm.reply_status_ = LOGIN_FAILED; }else { cm.reply_status_ = LOGIN_SUCESSS; } cm.user_id_ = ret; send_que_->Push(cm); } void DealAddFriend(ChatMsg& cm) { //1.获取被添加方的电话号码 std::string tel = cm.GetValue("telnum"); //添加方的userid int add_userid = cm.user_id_; cm.Clear(); //2.查询被添加方是否是登录状态 UserInfo be_add_ui; int ret = user_mana_->IsLogin(tel, &be_add_ui); if(ret == -1) { //用户不存在 cm.msg_type_= AddFriend_Resp; cm.reply_status_ = ADDFRIEND_FAILED; cm.SetValue("content", "user not exist, please check friend tel num."); send_que_->Push(cm); return; }else if(ret == OFFLINE) { //将消息先缓存下来, 择机发送 return; } //ONLINE状态的 //3.给被添加方推送添加好友请求 UserInfo add_ui; user_mana_->GetUserInfo(add_userid, &add_ui); cm.sockfd_ = be_add_ui.tcp_socket_; cm.msg_type_ = PushAddFriendMsg; cm.SetValue("adder_nickname", add_ui.nickname_); cm.SetValue("adder_school", add_ui.school_); cm.SetValue("adder_userid", add_ui.userid_); send_que_->Push(cm); } void DealAddFriendResp(ChatMsg& cm) { //1.获取双方的用户信息 int reply_status=cm.reply_status_; //获取被添加方的用户信息 int be_add_user=cm.user_id_; UserInfo be_userinfo; user_mana_->GetUserInfo(be_add_user,&be_userinfo); //获取添加方的用户信息,通过应答获取添加方的userid,所以被添加方在发送消息的时候要把添加方的userid带上 int add_user_id=atoi(cm.GetValue("userid").c_str()); UserInfo ui; user_mana_->GetUserInfo(add_user_id,&ui); //2.判断响应状态 cm.Clear(); cm.sockfd_=ui.tcp_socket_; cm.msg_type_=AddFriend_Resp; if(reply_status==ADDFRIEND_FAILED) { cm.reply_status_=ADDFRIEND_FAILED; std::string content="add_user"+be_userinfo.nickname_+"faild"; cm.SetValue("content",content); } else if(reply_status==ADDFRIEND_SUCCESS) { cm.reply_status_=ADDFRIEND_SUCCESS; std::string content="add_user"+be_userinfo.nickname_+"success"; cm.SetValue("content",content); cm.SetValue("peer_nick_name",be_userinfo.nickname_); cm.SetValue("peer_school",be_userinfo.school_); cm.SetValue("peer_userid",be_userinfo.userid_); //给用户管理模块设置好友信息 user_mana_->SetFriend(add_user_id,be_add_user); } //3.给添加方回复响应 if(ui.user_status_==OFFLINE) { //用户下线怎么设置 //存下来,择机发送 return; } send_que_->Push(cm); } void GetAllFriendInfo(ChatMsg&cm) { //好友信息从用户管理模块中来 int user_id=cm.user_id_; cm.Clear(); std::vector<int>f; bool ret=user_mana_->GetFriends(user_id,&f); if(ret==false) { cm.reply_status_=GETFRIEND_FAILED; } else { cm.reply_status_=GETFRIEND_SUCCESS; } cm.msg_type_=GetFriendMsg_Resp; size_t i=0; for(;i<f.size();++i) { UserInfo ui; user_mana_->GetUserInfo(f[i],&ui); Json::Value val; val["nickname"]=ui.nickname_; val["school"]=ui.school_; val["telnum"]=ui.telnum_; val["userid"]=ui.userid_; cm.json_msg_.append(val); } send_que_->Push(cm); } //客户端发出消息,服务端在这处理 void DealSendMsg(ChatMsg &cm) { int send_id=cm.user_id_;//消息是谁发的 int recv_id=cm.json_msg_["recvmsgid"].asInt();//发给谁 std::string send_msg=cm.json_msg_["msg"].asString(); cm.Clear(); UserInfo recv_ui; bool ret=user_mana_->GetUserInfo(recv_id,&recv_ui); if(ret==false||recv_ui.user_status_==OFFLINE)//如果没找到人或者他不在线,直接给发送消息的人回复应答 { cm.msg_type_=SendMsg_Resp;//这是发送消息人收到的应答,和推送消息区分开 cm.reply_status_=SENDMSG_FAILED; send_que_->Push(cm); return; } //人找到了,给发送消息的人回复成功应答,给接收消息的人推送消息 cm.Clear(); cm.msg_type_=SendMsg_Resp; cm.reply_status_=SENDMSG_SUCCESS; send_que_->Push(cm); UserInfo send_ui; user_mana_->GetUserInfo(send_id,&send_ui); cm.Clear(); cm.msg_type_=PushMsg; cm.sockfd_=recv_ui.tcp_socket_; cm.SetValue("peer_nickname",send_ui.nickname_); cm.SetValue("peer_school",send_ui.school_); cm.json_msg_["peer_userid"]=send_ui.userid_; cm.SetValue("peer_msg",send_msg); send_que_->Push(cm); }
- 线程3:发送线程:从发送队列里拿出数据进行发送
static void* send_msg_start(void* arg) { pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(send_flag_) { //1.从队列拿出数据 ChatMsg cm; cs->send_que_->Pop(&cm); std::string msg; cm.GetMsg(&msg); std::cout << "send thread: " << msg << std::endl; //2.发送数据 send(cm.sockfd_, msg.c_str(), msg.size(), 0); } return NULL; }
线程4:当消息接收方不在线时,服务端负责把消息放在缓存队列中,然后由线程4轮询遍历缓存队列,当接收方在线时,再把消息放到发送队列中
static void* cyc_look(void *arg) { pthread_detach(pthread_self()); ChatServer*cs=(ChatServer*)arg; while(cyc_flag_) { if(cs->cache_que_.size()>0) { auto it =cs->cache_que_.begin(); while(it!=cs->cache_que_.end()) { //std::cout<<"缓存队列里有"<<it->json_msg_<<std::endl; //std::cout<<it->sockfd_<<std::endl; //sleep(1); int id=it->user_id_; //std::cout<<"接收方的id"<<id<<std::endl; UserInfo ui; int a=cs->user_mana_->IsLogin(id,&ui); //std::cout<<a<<std::endl; if(a==ONLINE) { it->sockfd_=ui.tcp_socket_; //std::cout<<ui.tcp_socket_<<std::endl; cs->send_que_->Push(*it); it=cs->cache_que_.erase(it); continue; } it++; } } } return NULL; }
保存缓存队列接口,调用用户管理模块的接口实现将未读消息插入数据库
static void HandleSig(void *arg) { ChatServer* cs = (ChatServer*)arg; cs->user_mana_->InsertNoRead(cs->cache_que_); }
- 线程1:监听线程:将epoll监控到的就绪事件放在接收队列里
- 成员变量
-
客户端框架基本介绍
- 客户端采用MFC,MFC是微软基础类库的简称,是微软公司实现的一个c++类库,主要封装了大部分的windows API函数,所以在MFC中,你可以直接调用 windows API,同时需要引用对应的头文件或库文件;另外,MFC除了是一个类库以外,还是一个框架,在vc++里新建一个MFC的工程,开发环境会自动帮你产生许多文件,同时它使用了mfcxx.dll。xx是版本,它封装了mfc内核,所以你在你的代码看不到原本的SDK编程中的消息循环等等东西,因为MFC框架帮你封装好了,这样你就可以专心的考虑你程序的逻辑,而不是这些每次编程都要重复的东西。但是由于是通用框架,没有最好的针对性,当然也就丧失了一些灵活性和效率,但是MFC的封装很浅,所以在灵活性以及效率上损失不大,可以忽略不计。
- MFC框架建立起来之后会默认生成这个三个类
- 分别是CAboutDlg:关于界面,对应生成的版本信息对话框
- C+MFC工程的名字+App:应用程序类,也叫主类,封装了初始化,运行,终止程序的代码,由MFC框架调用这个类中的InitInstance()让程序跑起来
- C+MFC工程的名字+Dlg:对话框类,从CdialogEx中继承过来,在程序中看到的对话框就是像这样的一个类,这个是MFC默认创建出来的一个对话框.以后自己创建出来的和对话框和这个类属性类似..
-
客户端消息流转图
-
客户端模块
-
基础模块介绍
- 首先是tcp模块,目的是能够通过套接字实现网络通信
- 消息类型和数据格式模块,为了和服务端能达成同步解析,消息类型和数据格式和服务端保持一致
- 消息队列模块:
- 消息队列和服务端不同,1.服务端关心的是使用消息中包含的套接字将消息发送到对应的客户端,而客户端关心服务端发来的消息得类型,不同的消息类型执行不用的代码,为了区分不同的消息,所以消息队列采用vector<queue<string>>的方式定义,vector的下标代表消息类型,实现将不同的消息归类,2.服务端要接收连接,监听,拿到消息后要进行不同的处理,判断用户状态等,所以服务端需要不同的队列,但是客户端功能单一,只需要接收消息放到对应下标的队列里,然会不同的线程或函数直接在队列里拿,所以客户端的消息队列采用单例模式创建
- 消息队列的成员
-
主干模块
-
(因为每个界面都有一个与之对应的类,所以也可称为主干类)
-
主类:
- 主类不显示任何对话框,是整个程序跑起来的入口,整个MFC从主类的InitInstance函数开始执行
- 主类内的InitInstance函数中需要创建一个线程,用于在整个程序刚跑起来分时候接收来自网络中的数据
-
登录界面
- 对应登录类,也是MFC程序的第一个界面
- 登录界面有注册和登录两个button,点击之后分别跳转到对应的类中执行对应的函数,点击点击注册之后会进入注册界面,输入电话和密码点击登录之后会进入聊天界面
- 输入内容后点击登录后执行流程:
- 1.获取输入框的内容并判断输入框是否为空
- 2.组织携带消息类型获取的电话,密码消息通过tcp发送
- 3.从消息队列里拿登录应答类型的数据
- 4.如果应答是成功的,跳转到聊天界面,如果失败保持界面
- 点击注册之后,跳转到注册界面
-
注册界面
- 输入对应内容后点击提交后执行流程
- 1.获取输入内容并判空
- 2.组织消息,通过tcp发送
- 3.从消息队列中获取对应的应答
- 4.判断应答状态,如果成功退出注册界面,回到登录界面,如果失败保持这个界面
- 注册与登录关键信息流转
- 输入对应内容后点击提交后执行流程
-
聊天界面
- 在创建聊天类的对象过程中,也就是跳转到登录界面之前应该做什么
- 因为聊天界面需要展示好友信息列表,所以在初始化函数中应该组织获取全部好友信息列表的请求,并展示在聊天界面
- 因为例如发消息或者添加好友等信息都是需要第二个客户端参与的,都是随时进行的,所以当用户登录成功后到跳转到聊天界面的过程中需要创建一些用于一直循环接收消息的线程,以便用户能及时收到消息,又因为消息类型不同和需要用户端实时处理,所以选择每一种消息对应一个线程,能够及时拿到消息队列中对应类型的消息并及时处理
- 接收消息线程:用于一直接收好友发来的消息
- 1.获取消息队列
- 2.死循环从队列里拿PushMsg类型的数据
- 3.将数据解析出来
- 4.将消息加入到对应好友的历史消息中
- 5.判断当前聊天框是否是发送消息人的聊天框
- 接收推送添加好友的应答的线程
- 1.获取PushAddFriendMsg类型的消息
- 2.解析消息,并将消息源头信息展示出来供用户选择是否
- 3.如果同意,把好友维护在好友信息中,刷新好友信息列表
- 4.回复响应
- 接收添加好友请求应答的线程
- 1.从消息队列中拿AddFriend_Resp类型的消息
- 2.解析数据
- 3.如果添加好友失败重新拿消息,如果成功,维护好友信息,刷新好友列表
- 接收消息线程:用于一直接收好友发来的消息
- 点击发送之后触发的函数
- 1.获取输入内容并判空
- 2.组织消息,通过tcp发送
- 3.接收应答
- 4.添加消息内容到对应好友的历史消息中,并将消息展示到聊天界面
- 5.清空编辑框
- 发送聊天消息关键信息流转
- 点击好友名字触发的函数
- 1.获取点击好友列表里好友的下标
- 2.用下标获取好友的nickname
- 3.用nickname对比找到好友,send_user_id改成这个好友(send_user_id标记当前在哪个好友的聊天界面)
- 4.清空聊天框
- 5.把send_user_id对应好友的历史消息展示出来
- 6.刷新好友信息列表
- 点击添加好友触发的函数
- 跳转到添加好友的的聊天框,也就是需要跳转到另一个类中
- 在创建聊天类的对象过程中,也就是跳转到登录界面之前应该做什么
-
添加好友界面
- 输入后点击添加执行的函数流程
- 1.获取输入框中对应好友的电话
- 2.组织消息发送
- 3.取消添加好友的界面
- (对端处理推送添加好友的应答类型消息的线程接收并处理)
- 添加好友关键信息流转
- 输入后点击添加执行的函数流程
-
-
- 项目源码:yf1228/Linux - Gitee.com
https://gitee.com/hello--sg/linux/tree/master/ChatSystem
- 数据库模块中管理的表:维护用户信息和用户好友信息和服务端未处理的消息
基于TCP的聊天系统
于 2022-08-05 19:33:55 首次发布