基于TCP的网络聊天系统

目录

一、前言

二、产品的介绍

1.产品具有的功能

2.产品的各个模块

 3.使用的开发工具以及应用的技术

三、产品的设计

1.服务端

1.1服务端总流程图

1.2数据库及其管理模块设计

1.3用户管理模块设计

1.4业务模块设计

1.5消息的操作

1.6消息队列

2.客户端

2.1登录及注册消息流转图

2.2注册界面

2.3聊天界面及消息流转图

2.4添加好友界面及消息流转

四、产品的测试(视频演示)


一、前言

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 failedn");
                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, '', 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 %dn", 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聊天系统测试

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>