目录
一、前言
QQ和微信是现代人们生活中必不可少的一部分,身边几乎很难找到人不使用QQ或者微信等聊天工具吧!那么你想要亲手打造一款属于自己的聊天系统吗?那么让我们一起打造出一款自己专属的应用吧。
二、产品的介绍
1.产品具有的功能
该聊天系统具有,登录,注册,添加好友,发送消息,四大功能。
2.产品的各个模块
3.使用的开发工具以及应用的技术
a.开发工具:VS2019,MFC
b.使用的技术:Socket编程,TCP网络通信,多线程,数据库,Json数据格式
三、产品的设计
1.服务端
1.1服务端总流程图
1.2数据库及其管理模块设计
数据库表:因为我们需要保存用户以及用户好友的信息,所以我们至少需要维护两个数据库表
friendInfo:保存用户好友信息的数据库表
user:保存用户信息的数据库表
管理数据库模块设计:
- a.数据库表的初始化:bool MysqlInit()
- b.获取所有用户的信息:bool GetAllUser(Json::Value* all_user)
- c.获取用户好友信息:bool GetFriend(int userid, std::vector<int>* f_id)
- d.用户注册时向数据库插入用户的信息:bool InsertUser(int userid, const std::string& nickname
- e.添加好友:bool InsertFriend(int userid1, int userid2)
class DataBaseSvr{ public: DataBaseSvr(){ mysql_ = NULL; } ~DataBaseSvr(){ if(mysql_ != NULL){ mysql_close(mysql_); } } /* * 初始化mysql操作句柄, 并且连接后台mysql服务端, 设置字符集 * */ bool MysqlInit(){ mysql_ = mysql_init(NULL); if(mysql_ == NULL){ std::cout << "mysql init failed" << std::endl; return false; } if(mysql_real_connect(mysql_, HOST, USER, PASSWD, DB, DBPORT,NULL, 0) == NULL){ std::cout << "msyql connect failed" << std::endl; mysql_close(mysql_); return false; } mysql_set_character_set(mysql_, "utf8"); return true; } /* * 获取 all user info, to usermanager model * 参数为Json对象, 是一个出参 * */ bool GetAllUser(Json::Value* all_user){ #define GETALLUSER "select * from user;" lock_.lock(); //1.数据库查询 if(MysqlQuery(GETALLUSER) == false){ lock_.unlock(); return false; } //2.获取结果集 MYSQL_RES* res = mysql_store_result(mysql_); if(res == NULL){ lock_.unlock(); return false; } lock_.unlock(); //3.获取单行数据 int row_nums = mysql_num_rows(res); for(int i = 0; i < row_nums; i++){ MYSQL_ROW row = mysql_fetch_row(res); //4.将单行数据按照格式, 组织起来。 传递给调用者 Json::Value tmp; tmp["userid"] = atoi(row[0]); tmp["nickname"] = row[1]; tmp["school"] = row[2]; tmp["telnum"] = row[3]; tmp["passwd"] = row[4]; all_user->append(tmp); } mysql_free_result(res); return true; } /* * 获取单个用户的好友信息, 在程序初始化阶段, 让用户管理模块维护起来 * userid : 用户的id * f_id : 该用户的所有好友id * */ bool GetFriend(int userid, std::vector<int>* f_id){ #define GETFRIEND "select friend from friendinfo where userid='%d';" //1.格式化sql语句 char sql[1204] = {0}; sprintf(sql, GETFRIEND, userid); lock_.lock(); //2.查询 if(MysqlQuery(sql) == false){ lock_.unlock(); return false; } //3.获取结果集 MYSQL_RES* res = mysql_store_result(mysql_); if(res == NULL){ lock_.unlock(); return false; } lock_.unlock(); //4.获取单行数据 int row_nums = mysql_num_rows(res); for(int i = 0; i < row_nums; i++){ MYSQL_ROW row = mysql_fetch_row(res); f_id->push_back(atoi(row[0])); } mysql_free_result(res); return true; } /* * 当用户注册的时候, 进行插入使用的函数 * */ bool InsertUser(int userid, const std::string& nickname , const std::string& school, const std::string& telnum , const std::string& passwd){ #define INSERTUSER "insert into user(userid, nickname, school, telnum, passwd) values('%d', '%s', '%s', '%s', '%s');" char sql[1024] = {0}; sprintf(sql, INSERTUSER, userid, nickname.c_str(), school.c_str(), telnum.c_str(), passwd.c_str()); std::cout << "Insert User: " << sql << std::endl; //2.查询 if(MysqlQuery(sql) == false){ return false; } return true; } /* * 添加好友 * */ bool InsertFriend(int userid1, int userid2){ #define INSERTFRIEND "insert into friendinfo values('%d', '%d');" char sql[1024] = {0}; sprintf(sql, INSERTFRIEND, userid1, userid2); //2.查询 if(MysqlQuery(sql) == false){ return false; } return true; } private: bool MysqlQuery(const std::string& sql){ if(mysql_query(mysql_, sql.c_str()) != 0){ std::cout << "exec failed sql: " << sql << std::endl; return false; } return true; } private: MYSQL* mysql_; std::mutex lock_; };
1.3用户管理模块设计
用户信息类
- 1.用户注册时的相关信息,nickname_,school_,telnum_,passwd_,userid_
- 2.用户状态:user_status_
- 3.登录的客户端对应的套接字描述符
enum UserStatus{ OFFLINE, //0 ONLINE //1 }; /* * 用户信息类 * */ class UserInfo{ public: //注册的时候, UserInfo(const std::string& nickname, const std::string& school, const std::string& telnum, const std::string& passwd, int userid){ nickname_ = nickname; school_ = school; telnum_ = telnum; passwd_ = passwd; userid_ = userid; user_status_ = OFFLINE; tcp_socket_ = -1; friend_id_.clear(); } UserInfo(){ } ~UserInfo(){ } public: std::string nickname_; std::string school_; std::string telnum_; std::string passwd_; int userid_; //用户状态 //OFFLINE ONLINE int user_status_; //登录的客户端对应的套接字描述符 int tcp_socket_; std::vector<int> friend_id_; };
用户信息管理模块
- a.初始化管理模块:bool InitUserMana(),调用数据库模块GetAllUser函数获取数据库中用户信息,并使用unordered_map管理起来
- b.处理注册请求:int DealRegister(const std::string& nickname, const std::string& school, const std::string& tel, const std::string& passwd, int* userid),组织用户信息,并插入到user_map_ 和数据库中。
- c.处理用户登录请求:int DealLogin(const std::string& tel, const std::string& passwd, int sockfd)
- d.判断当前用户的在线情况:int IsLogin(int userid)/int IsLogin(const std::string& telnum, UserInfo* ui)发送消息的时候需要判断对方在线与否
- e.获取用户信息:bool GetUserInfo(int userid, UserInfo* ui),通过出参带出
- f.获取用户好友信息:bool GetFriends(int userid, std::vector<int>* fri),通过出参带出
- g.添加好友:void SetFriend(int userid1, int userid2)
- h.转变用户状态为下线: void SetUserOffLine(int sockfd),客户端下线时使用
class UserManager{ public: UserManager(){ user_map_.clear(); pthread_mutex_init(&map_lock_, NULL); //如果一开始就从0进行分配, 一定是不对的 // 因为用户管理类还会从数据库当中将已经存在的用户信息读回来 prepare_id_ = -1; db_ = NULL; } ~UserManager(){ pthread_mutex_destroy(&map_lock_); if(db_){ delete db_; db_ = NULL; } } bool InitUserMana(){ //1.连接数据库 db_ = new DataBaseSvr(); if(db_ == NULL){ printf("create db case failed\n"); return false; } if(db_->MysqlInit() == false){ return false; } //2.查询所有用户信息, 维护起来 Json::Value all_user; if(db_->GetAllUser(&all_user) == false){ return false; } for(int i = 0; i < (int)all_user.size(); i++){ //个人信息 UserInfo ui; ui.nickname_ = all_user[i]["nickname"].asString(); ui.school_ = all_user[i]["school"].asString(); ui.telnum_ = all_user[i]["telnum"].asString(); ui.passwd_ = all_user[i]["passwd"].asString(); ui.userid_ = all_user[i]["userid"].asInt(); ui.user_status_ = OFFLINE; //个人好友信息 db_->GetFriend(ui.userid_, &ui.friend_id_); pthread_mutex_lock(&map_lock_); user_map_[ui.userid_] = ui; if(ui.userid_ > prepare_id_){ prepare_id_ = ui.userid_ + 1; } pthread_mutex_unlock(&map_lock_); } return true; } /* * 处理用户注册 * userid : 如果注册成功, 通过userid,告诉注册的客户端,他的id是什么 * */ int DealRegister(const std::string& nickname, const std::string& school, const std::string& tel, const std::string& passwd, int* userid){ //1.判断注册信息是否为空 if(nickname.size() == 0 || school.size() == 0 || tel.size() == 0 || passwd.size() == 0){ *userid = -2; return -1; } //2.判断用户是否已经注册过了 pthread_mutex_lock(&map_lock_); auto iter = user_map_.begin(); while(iter != user_map_.end()){ if(iter->second.telnum_ == tel){ *userid = -2; pthread_mutex_unlock(&map_lock_); return -1; } iter++; } //3.创建UserInfo, 分配userid, 保存用户信息 UserInfo ui(nickname, school, tel, passwd, prepare_id_); *userid = prepare_id_; user_map_[prepare_id_] = ui; prepare_id_++; pthread_mutex_unlock(&map_lock_); //4.插入到数据库当中 db_->InsertUser(ui.userid_, nickname, school, tel, passwd); return 0; } /* * 处理登录请求 * sockfd 是 服务端为登录客户端创建的新连接套接字 * */ int DealLogin(const std::string& tel, const std::string& passwd, int sockfd){ //1.判断字段是否为空 if(tel.size() == 0 || passwd.size() == 0){ return -1; } //2.判断用户是否合法 pthread_mutex_lock(&map_lock_); auto iter = user_map_.begin(); while(iter != user_map_.end()){ if(iter->second.telnum_ == tel){ break; } iter++; } if(iter == user_map_.end()){ pthread_mutex_unlock(&map_lock_); return -1; } //3.校验密码是否正确 if(iter->second.passwd_ != passwd){ pthread_mutex_unlock(&map_lock_); return -1; } //4.更改用户的状态信息为ONLINE iter->second.user_status_ = ONLINE; int userid = iter->second.userid_; iter->second.tcp_socket_ = sockfd; pthread_mutex_unlock(&map_lock_); return userid; } int IsLogin(int userid){ pthread_mutex_lock(&map_lock_); auto iter = user_map_.find(userid); if(iter == user_map_.end()){ //这个用户都不存在 pthread_mutex_unlock(&map_lock_); return -1; } if(iter->second.user_status_ == OFFLINE){ pthread_mutex_unlock(&map_lock_); return OFFLINE; } pthread_mutex_unlock(&map_lock_); return ONLINE; } int IsLogin(const std::string& telnum, UserInfo* ui){ pthread_mutex_lock(&map_lock_); auto iter = user_map_.begin(); while(iter != user_map_.end()){ if(iter->second.telnum_ == telnum){ break; } iter++; } if(iter == user_map_.end()){ pthread_mutex_unlock(&map_lock_); return -1; } *ui = iter->second; if(iter->second.user_status_ == OFFLINE){ pthread_mutex_unlock(&map_lock_); return OFFLINE; } pthread_mutex_unlock(&map_lock_); return ONLINE; } bool GetUserInfo(int userid, UserInfo* ui){ pthread_mutex_lock(&map_lock_); auto iter = user_map_.find(userid); if(iter == user_map_.end()){ //这个用户都不存在 pthread_mutex_unlock(&map_lock_); return false; } *ui = iter->second; pthread_mutex_unlock(&map_lock_); return true; } bool GetFriends(int userid, std::vector<int>* fri){ pthread_mutex_lock(&map_lock_); auto iter = user_map_.find(userid); if(iter == user_map_.end()){ //这个用户都不存在 pthread_mutex_unlock(&map_lock_); return false; } *fri = iter->second.friend_id_; pthread_mutex_unlock(&map_lock_); return true; } void SetFriend(int userid1, int userid2){ //1.找userid1, 将userid2放到userid1的好友列表当中 pthread_mutex_lock(&map_lock_); auto iter = user_map_.find(userid1); if(iter == user_map_.end()){ //这个用户都不存在 pthread_mutex_unlock(&map_lock_); return; } iter->second.friend_id_.push_back(userid2); //2.找userid2, 将userid1放到userid2的好友列表当中 iter = user_map_.find(userid2); if(iter == user_map_.end()){ //这个用户都不存在 pthread_mutex_unlock(&map_lock_); return; } iter->second.friend_id_.push_back(userid1); pthread_mutex_unlock(&map_lock_); //3.插入到数据库当中 db_->InsertFriend(userid1, userid2); db_->InsertFriend(userid2, userid1); } void SetUserOffLine(int sockfd){ pthread_mutex_lock(&map_lock_); auto iter = user_map_.begin(); while(iter != user_map_.end()){ if(iter->second.tcp_socket_ == sockfd){ iter->second.user_status_ = OFFLINE; } iter++; } pthread_mutex_unlock(&map_lock_); } private: std::unordered_map<int, UserInfo> user_map_; pthread_mutex_t map_lock_; //针对注册用户分配的ID int prepare_id_; //数据库管理模块的实例化指针 DataBaseSvr* db_; };
1.4业务模块设计
- a.初始化资源:int InitChatServer(uint16_t tcp_port = TCP_PORT, int thread_count=THREAD_COUNT),tcpSOCK初始化,客户端进行监听
- b.启动各类线程函数:StartChatServer(),epoll等待线程,接收线程,发送线程,工作线程的创建
- epoll等待线程:主线程循环的接收, 将接收回来的数据放到接收线程的队列当中, 等到工作线程从队列当中获取消息, 进而进行处理
- 发送线程:从消息队列中拿消息进行发送
- 工作线程:从接收队列中拿消息,并根据消息的类型进行处理(注册,登录,添加好友,添加好友应答,获取好友信息)
struct Msg{ Msg(){ sockfd_ = -1; memset(buf, '\0', 1024); } int sockfd_; char buf[1024]; }; class ChatServer{ public: ChatServer(){ tcp_sock_ = -1; tcp_port_ = TCP_PORT; user_mana_ = NULL; epoll_fd_ = -1; thread_count_ = THREAD_COUNT; send_que_ = NULL; ready_sockfd_que_ = NULL; recv_que_ = NULL; } ~ChatServer(){ } //初始化资源的函数 int InitChatServer(uint16_t tcp_port = TCP_PORT, int thread_count=THREAD_COUNT){ tcp_port_ = tcp_port; thread_count_ = thread_count; //tcp初始化 tcp_sock_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(tcp_sock_ < 0){ perror("socket"); return -1; } //端口重用 int opt = 1; setsockopt(tcp_sock_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(tcp_port_); addr.sin_addr.s_addr = inet_addr("0.0.0.0"); int ret = bind(tcp_sock_, (struct sockaddr*)&addr, sizeof(addr)); if(ret < 0){ perror("bind"); return -1; } ret = listen(tcp_sock_, 1024); if(ret < 0){ perror("listen"); return -1; } //epoll 初始化 epoll_fd_ = epoll_create(5); if(epoll_fd_ < 0){ return -1; } //用户管理模块 user_mana_ = new UserManager(); if(user_mana_ == NULL){ return -1; } if(user_mana_->InitUserMana() == false){ return -1; } recv_que_ = new MsgQueue<ChatMsg>(); if(recv_que_ == NULL){ return -1; } send_que_ = new MsgQueue<ChatMsg>(); if(send_que_ == NULL){ return -1; } ready_sockfd_que_ = new MsgQueue<int>(); if(ready_sockfd_que_ == NULL){ return -1; } return 0; } //启动各类线程的函数 - 主线程调用的 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(1){ 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; } static void* epoll_wait_start(void* arg){ pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(1){ 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){ //对端关闭连接了 epoll_ctl(cs->epoll_fd_, EPOLL_CTL_DEL,arr[i].data.fd, NULL); close(arr[i].data.fd); //组织一个更改用户状态的消息 , 鸡贼做法(客户端退出的时候, 发送下线通知) cs->user_mana_->SetUserOffLine(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; } static void* recv_msg_start(void* arg){ pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(1){ } return NULL; } static void* send_msg_start(void* arg){ pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(1){ //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; } static void* deal_start(void* arg){ pthread_detach(pthread_self()); ChatServer* cs = (ChatServer*)arg; while(1){ //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:{ cs->DealAddFriend(cm); break; } case PushAddFriendMsg_Resp:{ 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.json_msg_ = 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){ std::cout << be_add_ui.nickname_ + " status is OFFLINE" << std::endl; //将消息先缓存下来, 择机发送 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 int addr_user_id = atoi(cm.GetValue("userid").c_str()); UserInfo ui; user_mana_->GetUserInfo(addr_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_ + " failed"; 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(addr_user_id, be_add_user); } //TODO if(ui.user_status_ == OFFLINE){ //消息就放到缓存队列当中, 择机发送 } //3.给添加方回复响应 send_que_->Push(cm); } void GetAllFriendInfo(ChatMsg& cm){ //1. 好友信息的数据从用户管理模块当中获取到 int user_id = cm.user_id_; cm.Clear(); std::vector<int> fri; bool ret = user_mana_->GetFriends(user_id, &fri); if(ret == false){ cm.reply_status_ = GETFRIEND_FAILED; }else{ cm.reply_status_ = GETFRIEND_SUCCESS; } cm.msg_type_ = GetFriendMsg_Resp; for(size_t i = 0; i < fri.size(); i++){ UserInfo tmp; user_mana_->GetUserInfo(fri[i], &tmp); Json::Value val; val["nickname"] = tmp.nickname_; val["school"] = tmp.school_; val["userid"] = tmp.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); } private: //侦听套接字 int tcp_sock_; int tcp_port_; //用户管理模块的实例化指针 UserManager* user_mana_; //epoll操作句柄 int epoll_fd_; //工作线程的数量 int thread_count_; //就绪的文件描述符队列 MsgQueue<int>* ready_sockfd_que_; //接收线程的队列 MsgQueue<ChatMsg>* recv_que_; //发送线程的队列 MsgQueue<ChatMsg>* send_que_; };
1.5消息的操作
消息类型和响应类型
enum chat_msg_type{
Register = 0, //0, 注册请求
Register_Resp, //1, 注册应答
Login, //2. 登录请求
Login_Resp, //3, 登录应答
AddFriend, //4, 添加好友请求
AddFriend_Resp, //5, 添加好友请求应答
SendMsg, //6, 发送消息
SendMsg_Resp, //7, 发送消息应答
PushMsg, //8, 推送消息
PushMsg_Resp, //9, 推送消息应答
PushAddFriendMsg, //10, 推送添加好友请求
PushAddFriendMsg_Resp, //11, 推送添加好友请求的应答
GetFriendMsg, //12, 获取全部好友信息
GetFriendMsg_Resp, //13, 获取全部好友信息应答
SetUserOffLine //14
//后续如果要增加业务, 可以在后面增加其他的消息类型
};
enum reply_status{
REGISTER_SUCCESS = 0, //0, 注册成功
REGISTER_FAILED, //1,注册失败
LOGIN_SUCESSS, //2, 登录成功
LOGIN_FAILED, //3, 登陆失败
ADDFRIEND_SUCCESS, //4, 添加好友成功
ADDFRIEND_FAILED, //5, 添加好友失败
SENDMSG_SUCCESS, //6, 发送消息成功
SENDMSG_FAILED, //7, 发送给消息失败
GETFRIEND_SUCCESS, //8,获取好友列表成功
GETFRIEND_FAILED //9, 获取好友列表失败
};
消息类型的格式
/*
* 注册请求的消息格式
* sockfd_ (消息达到服务端之后, 由服务端接收之后, 打上sockfd_)
* msg_type_ : Register
* json_msg: {
* nickname : 'xxx'
* school : "xxx"
* telnum : "xxxx"
* passwd : "xxxx"
* }
*
* 注册的应答:
* msg_type_ : Register_Resp
* reply_status_ = REGISTER_SUCCESS / REGISTER_FAILED
* 如果是REGISTER_SUCCESS : [user_id_]
*
*
*
* 登录的请求消息格式
* sockfd_ (消息达到服务端之后, 由服务端接收之后, 打上sockfd_)
* msg_type_ : Login
* json_msg_ : {
* telnum : xxx
* passwd : xxx
* }
*
* 登录的应答:
* msg_type : Login_Resp;
* reply_status_ : LOGIN_SUCCESS/LOGIN_FAILED
* 如果是LOGIN_SUCCESS : [user_id_]
*
*
*
* 添加好友请求:
* msg_type_ : AddFriend
* json_msg_ :{
* fri_tel_num : xxxx
* }
*
*
* 推送添加好友的请求
* msg_type : PushAddFriendMsg
* sockfd_ : 被添加方的套接字描述符
* json_msg_: {
* adder_nickname : xxx
* adder_school : xxx
* adder_userid : xxx
* }
*
* 推送添加好友的应答(被添加方发送给服务端的)
* msg_type : PushAddFriendMsg_Resp
* user_id : 被添加方的id
* reply_status : ADDFRIEND_SUCCESS / ADDFRIEND_FAILED
* 如果说是ADDFRIEND_SUCCESS
* json_msg_ : 添加方的id
*
* 添加好友的应答:
* msg_type: AddFriend_Resp
* reply_status : ADDFRIEND_FAILED / ADDFRIEND_SUCCESS
* 如果是成功:ADDFRIEND_SUCCESS
* json_msg_ :
* BeAdd_nickname : 被添加方的名字
* BeAdd_school : 被添加方的学校
* BeAdd_userid : 被添加方的id
* */
消息的序列化和反序列化
原因:面向对象语言设计的程序是通过各种类的使用实现的,而在信息传输的过程中我们传输的是二进制的文件,因此我们不能直接来进行传输,需要先对消息进行序列化,同样的拿到消息后也需要先做反序列化处理
class JsonUtil{ public: static bool Serialize(const Json::Value& value, std::string* body) { Json::StreamWriterBuilder swb; std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); std::stringstream ss; int ret = sw->write(value, &ss); if (ret != 0) { return false; } *body = ss.str(); return true; } static bool UnSerialize(const std::string& body, Json::Value* value) { Json::CharReaderBuilder crb; std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); std::string err; bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), value, &err); if (ret == false) { return false; } return true; } };
Json消息的操作
- a.获取json_msg_当中的value值,string GetValue(const std::string& key)
- b. 设置json_msg_当中的kv键值对,string GetValue(const std::string& key)/SetValue(const std::string& key, const std::string& value)
/* * 提供序列化的接口 - 回复应答的时候使用 * msg : 出参, 用于获取序列化完毕的字符串 * */ bool GetMsg(std::string* msg){ Json::Value tmp; tmp["msg_type"] = msg_type_; tmp["user_id"] = user_id_; tmp["reply_status"] = reply_status_; tmp["json_msg"] = json_msg_; return JsonUtil::Serialize(tmp, msg); } /* * 获取json_msg_当中的value值 * */ std::string GetValue(const std::string& key){ if(!json_msg_.isMember(key)){ return ""; } return json_msg_[key].asString(); } /* * 设置json_msg_当中的kv键值对 * */ void SetValue(const std::string& key, const std::string& value){ json_msg_[key] = value; } void SetValue(const std::string& key, int value){ json_msg_[key] = value; } void Clear(){ msg_type_ = -1; user_id_ = -1; reply_status_ = -1; json_msg_.clear(); } public: //存放的客户端文件名描述符, 方便发送线程, 通过该字段将数据发送给对应的客户端 int sockfd_; int msg_type_; //用户id int user_id_; //应答的状态 int reply_status_; /* * Json消息 * json消息的内容会随着消息类型的不同, 字段不一样 * */ Json::Value json_msg_; };
1.6消息队列
- a.向队列中放消息:void Push(const T& msg)
- b.从队列中拿消息:void Pop(T* msg)
#define CAPACITY 10000 template <class T> class MsgQueue{ public: MsgQueue(){ capacity_ = CAPACITY; pthread_mutex_init(&lock_vec_, NULL); pthread_cond_init(&cons_cond_, NULL); pthread_cond_init(&prod_cond_, NULL); } ~MsgQueue(){ pthread_mutex_destroy(&lock_vec_); pthread_cond_destroy(&cons_cond_); pthread_cond_destroy(&prod_cond_); } void Push(const T& msg){ pthread_mutex_lock(&lock_vec_); while(vec_.size() >= capacity_){ pthread_cond_wait(&prod_cond_, &lock_vec_); } vec_.push(msg); pthread_mutex_unlock(&lock_vec_); pthread_cond_signal(&cons_cond_); } void Pop(T* msg){ pthread_mutex_lock(&lock_vec_); while(vec_.empty()){ pthread_cond_wait(&cons_cond_, &lock_vec_); } *msg = vec_.front(); vec_.pop(); pthread_mutex_unlock(&lock_vec_); pthread_cond_signal(&prod_cond_); } private: std::queue<T> vec_; size_t capacity_; pthread_mutex_t lock_vec_; pthread_cond_t cons_cond_; pthread_cond_t prod_cond_; };
2.客户端
客户端使用VS2019的MFC功能创建的
2.1登录及注册消息流转图
代码实现:
// ChatSystemLd.cpp: 定义应用程序的类行为。 // #include "pch.h" #include "framework.h" #include "ChatSystemLd.h" #include "ChatSystemLdDlg.h" #include "TcpSvr.h" #include "MsgQueue.h" #include <thread> #ifdef _DEBUG #define new DEBUG_NEW #endif void RecvMsgStart() { TcpSvr* ts = TcpSvr::getInstance(); if (ts == NULL) { return; } MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { return; } while (1) { std::string msg; int ret = ts->Recv(&msg); if (ret <= 0) { continue; } ChatMsg cm; cm.PraseChatMsg(-1, msg); mq->Push(cm.msg_type_, msg); } } // CChatSystemLdApp BEGIN_MESSAGE_MAP(CChatSystemLdApp, CWinApp) ON_COMMAND(ID_HELP, &CWinApp::OnHelp) END_MESSAGE_MAP() // CChatSystemLdApp 构造 CChatSystemLdApp::CChatSystemLdApp() { // 支持重新启动管理器 m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART; // TODO: 在此处添加构造代码, // 将所有重要的初始化放置在 InitInstance 中 } // 唯一的 CChatSystemLdApp 对象 CChatSystemLdApp theApp; // CChatSystemLdApp 初始化 BOOL CChatSystemLdApp::InitInstance() { // 如果一个运行在 Windows XP 上的应用程序清单指定要 // 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式, //则需要 InitCommonControlsEx()。 否则,将无法创建窗口。 INITCOMMONCONTROLSEX InitCtrls; InitCtrls.dwSize = sizeof(InitCtrls); // 将它设置为包括所有要在应用程序中使用的 // 公共控件类。 InitCtrls.dwICC = ICC_WIN95_CLASSES; InitCommonControlsEx(&InitCtrls); /* 创建接收线程, 让接收线程, 去接收应答 */ std::thread recv_thread(RecvMsgStart); recv_thread.detach(); CWinApp::InitInstance(); AfxEnableControlContainer(); // 创建 shell 管理器,以防对话框包含 // 任何 shell 树视图控件或 shell 列表视图控件。 CShellManager *pShellManager = new CShellManager; // 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题 CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows)); // 标准初始化 // 如果未使用这些功能并希望减小 // 最终可执行文件的大小,则应移除下列 // 不需要的特定初始化例程 // 更改用于存储设置的注册表项 // TODO: 应适当修改该字符串, // 例如修改为公司或组织名 SetRegistryKey(_T("应用程序向导生成的本地应用程序")); CChatSystemLdDlg dlg; m_pMainWnd = &dlg; INT_PTR nResponse = dlg.DoModal(); if (nResponse == IDOK) { // TODO: 在此放置处理何时用 // “确定”来关闭对话框的代码 } else if (nResponse == IDCANCEL) { // TODO: 在此放置处理何时用 // “取消”来关闭对话框的代码 } else if (nResponse == -1) { TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n"); TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n"); } // 删除上面创建的 shell 管理器。 if (pShellManager != nullptr) { delete pShellManager; } #if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS) ControlBarCleanUp(); #endif // 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序, // 而不是启动应用程序的消息泵。 return FALSE; }
2.2注册界面
代码实现:
// CDialogRegister.cpp: 实现文件 // #include "pch.h" #include "ChatSystemLd.h" #include "CDialogRegister.h" #include "afxdialogex.h" #include "ChatMsg.h" #include "TcpSvr.h" #include "MsgQueue.h" // CDialogRegister 对话框 IMPLEMENT_DYNAMIC(CDialogRegister, CDialogEx) CDialogRegister::CDialogRegister(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOGREGISTER, pParent) , m_nickname_(_T("")) , m_school_(_T("")) , m_telnum_(_T("")) , m_passwd_(_T("")) { } CDialogRegister::~CDialogRegister() { } void CDialogRegister::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Text(pDX, IDC_EDIT1, m_nickname_); DDX_Text(pDX, IDC_EDIT2, m_school_); DDX_Text(pDX, IDC_EDIT3, m_telnum_); DDX_Text(pDX, IDC_EDIT4, m_passwd_); } BEGIN_MESSAGE_MAP(CDialogRegister, CDialogEx) ON_BN_CLICKED(IDC_BUTTONCOMMIT, &CDialogRegister::OnBnClickedButtoncommit) END_MESSAGE_MAP() // CDialogRegister 消息处理程序 void CDialogRegister::OnBnClickedButtoncommit() { // TODO: 在此添加控件通知处理程序代码 // TODO: 在此添加控件通知处理程序代码 //1.获取用户的输入 //获取输入控件当中最新的值 UpdateData(TRUE); if (m_nickname_.IsEmpty() || m_school_.IsEmpty() || m_telnum_.IsEmpty() || m_passwd_.IsEmpty()) { MessageBox(TEXT("输入内容不能为空")); return; } //2.组织登录消息(ChatMsg) ChatMsg cm; cm.msg_type_ = Register; cm.json_msg_["nickname"] = m_nickname_.GetString(); cm.json_msg_["school"] = m_school_.GetString(); cm.json_msg_["telnum"] = m_telnum_.GetString(); cm.json_msg_["passwd"] = m_passwd_.GetString(); //cm.SetValue("telnum", m_telnum_.GetString()); //cm.SetValue("passwd", m_passwd_.GetString()); std::string msg; cm.GetMsg(&msg); //3.获取TCP服务实例化指针 TcpSvr* ts = TcpSvr::getInstance(); if (ts == NULL) { MessageBox("获取tcp服务失败, 请重试.."); return; } //4.发送登录消息 ts->Send(msg); //5.获取消息队列的实例化指针 MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { MessageBox("获取消息队列失败, 请联系开发人员..."); return; } msg.clear(); mq->Pop(Register_Resp, &msg); //6.获取登录应答 cm.Clear(); cm.PraseChatMsg(-1, msg); //7.判断登录应答当中的应答状态(LOGIN_SUCCESS/LOGIN_FAILED) if (cm.reply_status_ == REGISTER_SUCCESS) { MessageBox("register success"); //退出当前的注册界面, 相当于回到了登录界面 CDialog::OnCancel(); } else { MessageBox("register failed, please retry..."); } }
2.3聊天界面及消息流转图
代码实现
// CDialogChatWin.cpp: 实现文件 // #include "pch.h" #include "ChatSystemLd.h" #include "CDialogChatWin.h" #include "afxdialogex.h" #include "CDialogAddFriend.h" #include "TcpSvr.h" #include "MsgQueue.h" #include "ChatMsg.h" // CDialogChatWin 对话框 IMPLEMENT_DYNAMIC(CDialogChatWin, CDialogEx) CDialogChatWin::CDialogChatWin(int userid,CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOGCHATWIN, pParent) , m_sendmsg_(_T("")) , user_id_(userid) { } CDialogChatWin::~CDialogChatWin() { } void CDialogChatWin::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); //DDX_Text(pDX, IDC_EDIT1, m_sendmsg_); DDX_Text(pDX, IDC_EDITSENDMSG, m_sendmsg_); DDX_Control(pDX, IDC_LISTFRIENDLIST, m_userlist_); DDX_Control(pDX, IDC_LISTHISTORYMSG, m_output_); DDX_Control(pDX, IDC_EDITSENDMSG, m_sendmsg_edit_); //DDX_Control(pDX, IDC_BUTTONSENDMSG, user_id_); } BEGIN_MESSAGE_MAP(CDialogChatWin, CDialogEx) //ON_BN_CLICKED(IDC_BUTTON2, &CDialogChatWin::OnBnClickedButton2) ON_BN_CLICKED(IDC_BUTTONADDFRIEND, &CDialogChatWin::OnBnClickedButtonaddfriend) ON_BN_CLICKED(IDC_BUTTONSENDMSG, &CDialogChatWin::OnBnClickedButtonsendmsg) ON_LBN_SELCHANGE(IDC_LISTFRIENDLIST, &CDialogChatWin::OnLbnSelchangeListfriendlist) END_MESSAGE_MAP() // CDialogChatWin 消息处理程序 void DealPushMsg(CDialogChatWin* cc) { MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { return; } while (1) { std::string msg; mq->Pop(PushMsg, &msg); ChatMsg cm; cm.PraseChatMsg(-1, msg); std::string peer_nickname = cm.json_msg_["peer_nickname"].asString(); std::string peer_school = cm.json_msg_["peer_school"].asString(); std::string peer_msg = cm.json_msg_["peer_msg"].asString(); int peer_id = cm.json_msg_["peer_userid"].asInt(); for (size_t i = 0; i < cc->fri_vec_.size(); i++) { if (peer_id == cc->fri_vec_[i].user_id_) { std::string tmp = peer_nickname + "-" + peer_school + ": " + peer_msg; cc->fri_vec_[i].history_msg_.push_back(tmp); if (peer_id == cc->send_user_id_) { //cc->m_output_.AddString(tmp.c_str()); cc->m_output_.InsertString(cc->m_output_.GetCount(), tmp.c_str()); } else { cc->fri_vec_[i].msg_cnt_++; } } } cc->RefreshUserList(); } } //能够调用到这个线程函数, 说明当前客户端作为被添加方 void DealPushAddFriendMsg(CDialogChatWin* cc) { //1.获取消息队列实例化指针 & tcp的实例化指针 MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { MessageBox(cc->m_hWnd, "获取消息队列失败, 请联系开发人员...", "error", MB_YESNO); return; } //2.循环获取 PushAddFriendMsg 消息类型的消息 while (1) { std::string msg; mq->Pop(PushAddFriendMsg, &msg); ChatMsg cm; cm.PraseChatMsg(-1, msg); std::string adder_nickname = cm.json_msg_["adder_nickname"].asString(); std::string adder_school = cm.json_msg_["adder_school"].asString(); int adder_userid = cm.json_msg_["adder_userid"].asInt(); //3.通过获取的消息内容, 展示是那个用户想要添加自己 std::string show_msg = adder_nickname + ":" + adder_school + " want add you as friend."; cm.Clear(); int i = MessageBox(cc->m_hWnd, show_msg.c_str(), "添加好友", MB_YESNO); if (i == IDYES) { //同意添加 //a. 将新好友信息维护起来 struct UserInfo ui; ui.nickname_ = adder_nickname; ui.school_ = adder_school; ui.user_id_ = adder_userid; ui.msg_cnt_ = 0; cc->fri_vec_.push_back(ui); //b. 刷新用户列表 cc->RefreshUserList(); //c. 组织应答 cm.msg_type_ = PushAddFriendMsg_Resp; cm.reply_status_ = ADDFRIEND_SUCCESS; cm.user_id_ = cc->user_id_; // 被添加方的id、 cm.json_msg_["userid"] = adder_userid; } else { //不同意添加 //a. 组织应答 cm.msg_type_ = PushAddFriendMsg_Resp; cm.reply_status_ = ADDFRIEND_FAILED; cm.user_id_ = cc->user_id_; // 被添加方的id、 cm.json_msg_["userid"] = adder_userid; } msg.clear(); cm.GetMsg(&msg); //4.根据不同结果返回应答 TcpSvr* ts = TcpSvr::getInstance(); if (ts == NULL) { continue; } ts->Send(msg); } } //能够调用到这个线程函数, 说明当前客户端作为添加方 void DealAddFriendResp(CDialogChatWin* cc) { //1.获取消息队列实例化指针 & tcp的实例化指针 MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { MessageBox(cc->m_hWnd, "获取消息队列失败, 请联系开发人员...", "error", MB_YESNO); return; } //2.循环获取 PushAddFriendMsg 消息类型的消息 while (1) { std::string msg; mq->Pop(AddFriend_Resp, &msg); ChatMsg cm; cm.PraseChatMsg(-1, msg); std::string content = cm.GetValue("content"); MessageBox(cc->m_hWnd, content.c_str(), "添加好友应答", MB_OK); if (cm.reply_status_ == ADDFRIEND_FAILED) { continue; } std::string be_adder_nickname = cm.json_msg_["peer_nick_name"].asString(); std::string be_adder_school = cm.json_msg_["peer_school"].asString(); int be_adder_userid = cm.json_msg_["peer_userid"].asInt(); //a. 将新好友信息维护起来 struct UserInfo ui; ui.nickname_ = be_adder_nickname; ui.school_ = be_adder_school; ui.user_id_ = be_adder_userid; ui.msg_cnt_ = 0; cc->fri_vec_.push_back(ui); //b. 刷新用户列表 cc->RefreshUserList(); } } BOOL CDialogChatWin::OnInitDialog() { CDialogEx::OnInitDialog(); // TODO: 在此添加额外的初始化 std::thread recv_msg(DealPushMsg, this); recv_msg.detach(); std::thread recv_addfriendmsg(DealPushAddFriendMsg, this); recv_addfriendmsg.detach(); std::thread recv_addfriendrespmsg(DealAddFriendResp, this); recv_addfriendrespmsg.detach(); // TODO: 在此添加额外的初始化 //1. 获取TCP实例化指针 TcpSvr* ts = TcpSvr::getInstance(); if (ts == NULL) { MessageBox("获取tcp服务失败, 请重试.."); return false; } //2. 组织获取好友信息的数据 ChatMsg cm; cm.msg_type_ = GetFriendMsg; cm.user_id_ = user_id_; std::string msg; cm.GetMsg(&msg); ts->Send(msg); //3. 解析应答 MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { MessageBox("获取消息队列失败, 请联系开发人员..."); return false; } msg.clear(); mq->Pop(GetFriendMsg_Resp, &msg); //4. 展示好友信息到userlist(展示? 要不要保存呢?) cm.Clear(); cm.PraseChatMsg(-1, msg); for (int i = 0; i < (int)cm.json_msg_.size(); i++) { struct UserInfo ui; ui.nickname_ = cm.json_msg_[i]["nickname"].asString(); ui.school_ = cm.json_msg_[i]["school"].asString(); ui.user_id_ = cm.json_msg_[i]["userid"].asInt(); ui.msg_cnt_ = 0; if (i == 0) { send_user_id_ = ui.user_id_; } fri_vec_.push_back(ui); } //5.刷新userlist RefreshUserList(); return TRUE; // return TRUE unless you set the focus to a control // 异常: OCX 属性页应返回 FALSE } BOOL CDialogChatWin::PreTranslateMessage(MSG* pMsg) { // TODO: 在此添加专用代码和/或调用基类 if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { OnBnClickedButtonsendmsg(); return TRUE; } if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE) { return TRUE; } return CDialogEx::PreTranslateMessage(pMsg); } void CDialogChatWin::OnBnClickedButtonsendmsg() { // TODO: 在此添加控件通知处理程序代码 //1. 获取输入框的内容 UpdateData(TRUE); if (m_sendmsg_.IsEmpty()) { MessageBox(TEXT("输入内容不能为空")); return; } //2. 组织发送消息 ChatMsg cm; cm.msg_type_ = SendMsg; //消息是谁发的 cm.user_id_ = user_id_; //消息发给谁 cm.json_msg_["recvmsgid"] = send_user_id_; //消息内容 cm.json_msg_["msg"] = m_sendmsg_.GetString(); std::string msg; cm.GetMsg(&msg); TcpSvr* ts = TcpSvr::getInstance(); if (ts == NULL) { MessageBox("获取tcp服务失败, 请重试.."); return; } //3. 将这个消息发送出去 ts->Send(msg); //4. 接收应答, 判断消息是否发送成功了 MsgQueue* mq = MsgQueue::GetInstance(); if (mq == NULL) { MessageBox("获取消息队列失败, 请联系开发人员..."); return; } msg.clear(); mq->Pop(SendMsg_Resp, &msg); cm.Clear(); cm.PraseChatMsg(-1, msg); //5. 添加到该好友的历史消息当中, 并且展示到输出框 for (size_t i = 0; i < fri_vec_.size(); i++) { if (send_user_id_ == fri_vec_[i].user_id_) { std::string tmp = "我: "; tmp += m_sendmsg_.GetString(); if (cm.reply_status_ == SENDMSG_SUCCESS) { tmp += " (send success)"; } else { tmp += " (send failed)"; } fri_vec_[i].history_msg_.push_back(tmp); m_output_.InsertString(m_output_.GetCount(), tmp.c_str()); //m_output_.AddString(tmp.c_str()); } } //6. 清空编辑框 m_sendmsg_.Empty(); m_sendmsg_edit_.SetWindowTextA(0); } void CDialogChatWin::OnLbnSelchangeListfriendlist() { // TODO: 在此添加控件通知处理程序代码 //1. 获取点击的文本内容 CString strText; int cur = m_userlist_.GetCurSel(); m_userlist_.GetText(cur, strText); //2. 判断当前点击的是哪一个用户, 更改发送id for (size_t i = 0; i < fri_vec_.size(); i++) { if (strstr(strText, fri_vec_[i].nickname_.c_str()) != NULL) { send_user_id_ = fri_vec_[i].user_id_; } } //3. 清空output区域 for (int i = m_output_.GetCount(); i >= 0; i--) { m_output_.DeleteString(i); } //4. 展示该用户的历史消息 for (size_t i = 0; i < fri_vec_.size(); i++) { if (send_user_id_ == fri_vec_[i].user_id_) { //把历史消息展示再output区域 for (size_t j = 0; j < fri_vec_[i].history_msg_.size(); j++) { m_output_.AddString(fri_vec_[i].history_msg_[j].c_str()); } fri_vec_[i].msg_cnt_ = 0; } } //5. 刷新userlist RefreshUserList(); } void CDialogChatWin::RefreshUserList() { int Count = m_userlist_.GetCount(); //先清空 for (int i = Count; i >= 0; i--) { m_userlist_.DeleteString(i); } //再展示 for (int i = 0; i < (int)fri_vec_.size(); i++) { std::string tmp = fri_vec_[i].nickname_; //当该好友的未读消息个数是大于0 的时候, 展示未读消息个数 if (fri_vec_[i].msg_cnt_ > 0) { tmp += " : "; tmp += std::to_string(fri_vec_[i].msg_cnt_); } m_userlist_.AddString(tmp.c_str()); } } void CDialogChatWin::OnBnClickedButtonaddfriend() { // TODO: 在此添加控件通知处理程序代码 //1.获取消息队列实例化指针 & tcp的实例化指针 CDialogAddFriend caf(user_id_); caf.DoModal(); }
2.4添加好友界面及消息流转
// CDialogAddFriend.cpp: 实现文件 // #include "pch.h" #include "ChatSystemLd.h" #include "CDialogAddFriend.h" #include "afxdialogex.h" #include <string> #include "ChatMsg.h" #include "MsgQueue.h" #include "TcpSvr.h" // CDialogAddFriend 对话框 IMPLEMENT_DYNAMIC(CDialogAddFriend, CDialogEx) CDialogAddFriend::CDialogAddFriend(int user_id,CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_DIALOGADDFRIEND, pParent) , m_fri_telnum_(_T("")) ,user_id_(user_id) { } CDialogAddFriend::~CDialogAddFriend() { } void CDialogAddFriend::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Text(pDX, IDC_EDITTELNUM, m_fri_telnum_); } BEGIN_MESSAGE_MAP(CDialogAddFriend, CDialogEx) ON_BN_CLICKED(IDC_BUTTONADD, &CDialogAddFriend::OnBnClickedButtonadd) END_MESSAGE_MAP() // CDialogAddFriend 消息处理程序 void CDialogAddFriend::OnBnClickedButtonadd() { // TODO: 在此添加控件通知处理程序代码 //1.获取用户的输入 UpdateData(TRUE);//获取输入控件当中最新的值 if (m_fri_telnum_.IsEmpty()) { MessageBox("输入内容不能为空"); return; } //2.组织登录消息ChatMsg ChatMsg cm; cm.msg_type_ = AddFriend; //消息是谁发的 cm.user_id_ = user_id_; //消息内容 cm.json_msg_["telnum"] = m_fri_telnum_.GetString(); std::string msg; cm.GetMsg(&msg); //3.获取TCP服务的指针 TcpSvr* ts = TcpSvr::getInstance(); if (ts == nullptr) { MessageBox("获取tcp服务失败"); return; } //4.发送登录消息 ts->Send(msg); CDialog::OnCancel();//取消添加好友界面 //线程等待接收应答,因为添加好友的应答不会立即到 }
四、产品的测试(视频演示)
TCP聊天系统测试
今天的文章基于tcp的网络聊天系统有哪些_TCP的报文分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/71442.html