基于TCP协议的聊天系统TCP-ChatSystem

基于TCP协议的点对点聊天系统

网络协议:TCP

聊天形式:点对点

所用技术:socket、多路转接、线程池、互斥锁、条件变量、MFC等。

功能点:注册、登录、添加好友、聊天等。

实现示意图

客户端登陆注册消息流转图:

 客户端添加好友消息流转图:

 客户端聊天消息流转图:

服务端处理请求消息流转图: 

服务端

Linux环境下的gcc升级

sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc

安装jsoncpp

yum install -y jsoncpp
yum install -y jsoncpp-devel

服务端模块划分

 数据库模块设计

数据库表设计

用户信息表:

Create Table: CREATE TABLE `user` (
 `userid` int(11) NOT NULL,
 `nickname` varchar(20) NOT NULL,
 `school` varchar(20) NOT NULL,
 `telnum` char(11) NOT NULL,
 `passwd` varchar(100) NOT NULL,
 `m_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTA
 PRIMARY KEY (`userid`),
 UNIQUE KEY `telnum` (`telnum`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

好友信息表:

Create Table: CREATE TABLE `friendinfo` (
 `userid` int(11) NOT NULL,
 `friend` int(11) NOT NULL,
 KEY `userid` (`userid`),
 KEY `friend` (`friend`),
 CONSTRAINT `friendinfo_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `user` (`userid
 CONSTRAINT `friendinfo_ibfk_2` FOREIGN KEY (`friend`) REFERENCES `user` (`userid
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

MySQL-C-API(MySQL的C语言接口)

初始化MySQL的操作句柄

//函数原型
MYSQL*
mysql_init(MYSQL *mysql);
  • 函数意义:分配或初始化与mysql_real_connect()相适应的MYSQL对象。如果mysql是NULL指针,该函数将分配、初始化、并返回新对象。否则,将初始化对象,并返回对象的地址。
  • 调用示例:MYSQL* mysql_ = mysql_init(NULL);

连接mysql服务端

//函数原型:
MYSQL*
mysql_real_connect(MYSQL *mysql,             //mysql操作句柄
                   const char *host,         //服务端IP地址
                   const char *user,         //⽤⼾名
                   const char *passwd,       //密码
                   const char *db,           //数据库
                   unsigned int port,        //端⼝
                   const char *unix_socket,  //是否使⽤本地域套接字
                   unsigned long client_flag //数据库标志位, 通常为0, 采⽤默认属性
                   );
  • 函数含义:连接MySQL服务端,如果连接成功,则返回的是MySQL操作句柄,失败返回NULL。
  • 调用示例:mysql_real_connect(&mysql,"host","user","passwd","database",0,null,0);

设置连接对应的字符集

//函数原型
int 
mysql_set_character_set(MYSQL *mysql,
                        const char *csname);
  • 函数含义:用于设置当前连接的默认字符集。
  • 调用示例:mysql_set_character_set(mysql_,"utf8");

执行sql语句

//函数原型
int
mysql_query(MYSQL *mysql, //mysql操作句柄
            const char *stmt_str //执⾏的sql语句
            );
  • 函数含义:执行sql语句。成功返沪0;失败返回非0。
  • 调用示例:mysql_query(mysql_,"select * from user;");

获取结果集

//函数原型
MYSQL_RES*
mysql_store_result(MYSQL *mysql); // mysql操作句柄
  • 函数含义:获取查询的结果,称之为结果集。成功,返回MYSQL_RES指针。失败返回NULL。
  • 注意事项:需要调用。
  • 调用示例:mysql_store_result(mysql_);

获取结果集行数

//函数原型
MYSQL_ROW
mysql_fetch_row(MYSQL_RES *result);
  • 函数含义:获取结果集的下一行内容。

释放结果集内存

//函数原型
void
mysql_close(MYSQL *mysql);

编译链接

//头文件包含
#include<mysql/mysql.h>
//链接时
-L /user/lib64/mysql -lmysqlclient

 数据库模块代码设计

class DBServer{
  public:
    DBServer(){};
    ~DBServer(){};
    
    //获取全部⽤⼾信息, ⽤于⽤⼾管理系统初始化阶段
    //参数为出参
    bool GetAllUesr(Json::Value* all_user){}
   
    //获取好友ID, ⽤于客⼾端获取好友列表
    //参数为出参
    bool GetFriend(int userid, std::vector<int>* f_id){}
    
    //插⼊⽤⼾, ⽤于⽤⼾注册
    bool InsertUser(int userid, const std::string& nickname,
    const std::string& school, const std::string& telnum,
    const std::string& passwd){};

    //插⼊好友, ⽤于⽤⼾添加好友
    //参数:userid : ⽤⼾id, friendid : 好友id
    bool InsertFriend(int userid, int friendid){};
  public:

    //初始化函数, 连接mysql服务端, 设置字符集
    bool MysqlInit(){};

    //执⾏sql语句
    bool MysqlQuery(const std::string& sql){};

  private:
    MYSQL* mysql_;
    std::mutex lock_;
};

用户管理模块 

描述用户信息类

class UserInfo{
  public:
    UserInfo(const std::string& nick_name, const std::string& school,
             const std::string& passwd,
             const std::string tel_num, int user_id){}
    UserInfo(){}
    ~UserInfo(){}
  public:
    std::string nick_name_; // ⽤⼾名称
    std::string school_; //学校
    std::string passwd_; //密码
    std::string tel_num_; //电话
    //⽤⼾id
    int user_id_; //⽤⼾ID
    int user_status_; //⽤⼾状态:ONLINE/OFFLINE
    int tcp_sockfd_; //客⼾端对应的sockfd
    std::vector<int> friend_id_; //⽤⼾的好友列表:存储好友ID
};

管理用户信息类

class UserManager {
  public:
    UserManager(){}
    ~UserManager(){}
    bool InitUserMana(){}
    int DealRegister(const std::string& nick_name,
    const std::string& school,
    const std::string& passwd,
    const std::string& tel_num,
    int* user_id){}
    int DealLogin(const std::string& tel_num,
                  const std::string& passwd,
                  int cli_sockfd){}
    int IsLogin(int user_id, UserInfo* ui){
    return 0;}
    int IsLogin(const std::string& tel_num, UserInfo* ui){}
    bool GetFriends(int user_id, std::vector<int>* fri){}
    bool GetUserInfo(int user_id, UserInfo* ui){}
    void SetFriend(int user_id1, int user_id2){}
  private:
    /*
     * std::string ==> id
     * UserInfo ==> 保存的具体⽤⼾的信息 
     */
    std::unordered_map<int, UserInfo> user_map_;
    pthread_mutex_t map_lock_;
    //预分配的⽤⼾id, 当⽤⼾管理模块接收到注册请求之后, 将prepare_id分配给注册的⽤⼾,
    int prepare_id_;
    DBServer* db_; //数据库服务指针
};

线程安全的队列

/*
 * 消息池当中使⽤vector来保存消息,vector这个容器并不是线程安全, STL当中的容器都是线程不安全
 * 保证线程安全的机制:
 * 互斥锁+条件变量
 */
#define CAPACITY 1024
template <class T>
class MsgPool{
  public:
    MsgPool(size_t capa = CAPACITY){capacity_ = capa;
                                    pthread_mutex_init(&lock_vec_, NULL);
                                    pthread_cond_init(&cond_con_, NULL);
                                    pthread_cond_init(&cond_pro_, NULL);}
    
    ~MsgPool(){pthread_mutex_destroy(&lock_vec_);
               pthread_cond_destroy(&cond_con_);
               pthread_cond_destroy(&cond_pro_);}
    
    void PushMsg(const T& msg){
    pthread_mutex_lock(&lock_vec_);
    while(vec_.size() >= capacity_){
        pthread_cond_wait(&cond_pro_, &lock_vec_);}
    vec_.push(msg);
    pthread_mutex_unlock(&lock_vec_);
    pthread_cond_signal(&cond_con_);}
    
    void PopMsg(T* msg){
    pthread_mutex_lock(&lock_vec_);
    while(vec_.empty()){
        pthread_cond_wait(&cond_con_, &lock_vec_);}
    *msg = vec_.front();
    vec_.pop();
    pthread_mutex_unlock(&lock_vec_);
    pthread_cond_signal(&cond_pro_);}

private:
/*
 * vec_ : 保存消息的容器
 * capacity_ : 定义的容器的容量
 * lock_vec_ : 保护消息容器的锁
 * cond_con_ : 消费者的条件变量
 * cond_pro_ : ⽣产者的条件变量
 */
    std::queue<T> vec_;
    size_t capacity_;
    pthread_mutex_t lock_vec_;
    pthread_cond_t cond_con_;
    pthread_cond_t cond_pro_;
};

网络通信模块

主线程接收链接&网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);
}

监控&接收客户端的数据

static void* epoll_wait_start(void* arg){
    pthread_detach(pthread_self());
    ChatServer* cs = (ChatServer*)arg;
    while(1){
        //1.epoll_wait 等待事件发⽣
        struct epoll_event arr[10];
        int ret = epoll_wait(cs->epoll_fd_, arr, sizeof(arr)/sizeof(arr[0]), -1);
        if(ret < 0){
            continue;}
        //2.获取到事件, 准备接收数据
        for(int i = 0; i < 10; i++){
            char buf[TCP_MAX_DATA_LEN] = {0};
            ssize_t recv_size = recv(arr[i].data.fd, buf, sizeof(buf) - 1, 0);
                if(recv_size < 0){
                //2.1 接收失败了
                continue;}
                else if(recv_size == 0){
                //2.2 对端关闭连接了
                epoll_ctl(cs->epoll_fd_, EPOLL_CTL_DEL, arr[i].data.fd, NULL);
                close(arr[i].data.fd);
                continue;}
            //2.3 正常接收回来, 放到消息池等待处理
            printf("epoll_wait_start buf is %s by client id [%d]\n", buf, arr[i].
            std::string msg;
            msg.assign(buf, strlen(buf));
            ChatMsg cm;
            cm.PraseChatMsg(arr[i].data.fd, msg);
            cs->msg_pool_->PushMsg(cm);
        }
    }
}

发送线程

static void* send_msg_start(void* arg){
    pthread_detach(pthread_self());
    ChatServer* cs = (ChatServer*)arg;
    while(1){
        ChatMsg cm;
        cs->send_msg_queue_->PopMsg(&cm);
        int cli_sockfd = cm.sockfd_;
        string msg ;
        cm.GetMsg(&msg);
        cout << "msg: " << msg << endl;
        ssize_t send_size = send(cli_sockfd, msg.c_str(), msg.size(), 0);
            if(send_size < 0){
            //放到缓存队列当中, 由缓存队列进⾏发送
            perror("send");
            continue;
        }
    }
}

自定义消息

Json Value对象的认识

char name = "⼩明";
int age = 18;
float score[3] = {88.5, 99, 58};
则json这种数据交换格式是将这多种数据对象组织成为⼀个字符串:
[
    {
        "姓名" : "⼩明",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
    },
    {
        "姓名" : "⼩⿊",
        "年龄" : 18,
        "成绩" : [88.5, 99, 58]
    }
]

json数据类型:对象,数组,字符串,数字

对象:使用花括号{}括起来的表示一个对象;

数组:使用中括号[]括起来的表示一个数组;

字符串:使用常规双引号“”括起来的表示一个字符串;

数字:包括整形和浮点型,直接使用。

/Json数据对象类
class Json::Value{
    Value& operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据
    Value& operator[](const std::string& key);//简单的⽅式完成 val["姓名"] = "⼩明";
    Value& operator[](const char* key);
    Value removeMember(const char* key);//移除元素
    const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
    Value& append(const Value& value);//添加数组元素val["成绩"].append(88);
    ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
    std::string asString() const;//转string string name = val["name"].asS
    const char* asCString() const;//转char* char *name = val["name"].asCString()
    Int asInt() const;//转int int age = val["age"]
    float asFloat() const;//转float
    bool asBool() const;//转 bool
};
//json序列化类,低版本⽤这个更简单
class JSON_API Writer {
    virtual std::string write(const Value& root) = 0;
}
class JSON_API FastWriter : public Writer {
    virtual std::string write(const Value& root);
}
class JSON_API StyledWriter : public Writer {
    virtual std::string write(const Value& root);
}
//json序列化类,⾼版本推荐,如果⽤低版本的接⼝可能会有警告
class JSON_API StreamWriter {
    virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
    virtual StreamWriter* newStreamWriter() const;
}
//json反序列化类,低版本⽤起来更简单
class JSON_API Reader {
    bool parse(const std::string& document, Value& root, bool collectComments
}
//json反序列化类,⾼版本更推荐
class JSON_API CharReader {
    virtual bool parse(char const* beginDoc, char const* endDoc,
    Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
    virtual CharReader* newCharReader() const;
}

Json序列化和反序列化

class JsonUtil {
  public:
    /*
    value : 待要序列化的json对象
    body : 序列化完毕产⽣的string对象 (出参)
    */
    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;
    }
    /*
    body : 待要反序列化的string对象
    value : 序列化完毕产⽣的json对象 (出参)
    */
    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, &er
        if (ret == false) {
            return false;
        }
        return true;
    }
};

使用Json数据格式封装自定义消息

class ChatMsg {
public:
    ChatMsg() {
        sockfd_ = -1;
        msg_type_ = -1;
        reply_status_ = -1;
        user_id_ = -1;
        json_msg_.clear();
    }
    ~ChatMsg() {};
    int PraseChatMsg(int sockfd, const std::string& msg) {
    Json::Value tmp;
    bool ret = JsonUtil::UnSerialize(msg, &tmp);
    if (ret == false) {
        return -1;
    }
    sockfd_ = sockfd;
    msg_type_ = tmp["msg_type"].asInt();
    user_id_ = tmp["user_id"].asInt();
    reply_status_ = tmp["reply_status"].asInt();
    json_msg_ = tmp["json_msg"];
    return 0;
}
void SetKeyValue(const std::string& key, const std::string& val) {
    json_msg_[key] = val;
}
std::string GetValue(const std::string& key) {
    if (!json_msg_.isMember(key)) {
        return "";
    }
    return json_msg_[key].asString();
}
/**/
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);
}
void Clear(int is_clear_jsonmsg = 1) {
    msg_type_ = -1;
    user_id_ = -1;
    reply_status_ = -1;
    if(is_clear_jsonmsg == 1){
        json_msg_.clear();
    }    
}
public:
    int sockfd_;
    /*
    Register = 0,
    Register_resp,
    Login,
    Login_resp,
    AddFriend,
    AddFriend_resp,
    SendMsg,
    PushMsg,
    PushAddFriendMsg,
    PushAddFriendMsgResp
    */
    int msg_type_;
    int user_id_;
    /*
    Register_Success : 0
    Register_Fail : 1
    Login_Success : 2
    Login_Fail : 3
    AddFriend_Success : 4
    AddFriend_Fail : 5
    SendMsg_Success : 6
    SendMsg_Fail : 7
    */
    int reply_status_;
    /*
    JsonлϢ
    */
    Json::Value json_msg_;
};

业务处理模块

根据消息类型处理不同类型的消息

static void* deal_start(void* arg){
    pthread_detach(pthread_self());
    ChatServer* cs = (ChatServer*)arg;
    while(1){
        //1.消息池当中获取消息
        ChatMsg cm;
        cs->msg_pool_->PopMsg(&cm);
        //3.分业务处理
        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 PushAddFriendMsgResp:{
                cs->DealAddFriendResp(cm);
                break;
            }
            case SendMsg:{
                cs->DealSendMsg(cm);
                break;
            }
            case GetFriendMsg:{
                cs->DealGetFriend(cm);
                break;
            }
            default:{
                break;
            }
        }
    }
}

客户端

MFC环境搭建

https://blog.csdn.net/qq_35392239/article/details/109190131

jsoncpp编译

https://blog.csdn.net/houxian1103/article/details/123343248

编译好的win版本的jsoncpp编译

32位debug版本

32位release版本

使用注意事项

  • 确保自己的创建的工程使用的同位数的jsoncpp库,例如32位程序九时用32位。
  • 确保自己的创建的工程和使用的jsoncpp同是debug版本或者release版本,否则会编译链接不通过。

windows下使用jsoncpp

win-tcp封装

准备工作

1. 要使⽤win socket,需要先引⼊ws2_32.lib库,该库提供了socket接⼝的实现具体可以在代码的最头部引⼊
//表⽰链接的时候,链接该库,当然你也可以在项⽬中进⾏设置,不过不推荐
#pragma comment(lib, "ws2_32.lib")

2. 选择socket库
使⽤win socket之前,需要先选择使⽤哪⼀个版本的socket库,并且和当前程序进⾏绑定,我们使⽤:
函数:
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData ); //具体⻅MSDN
参数:
wVersionRequested:标识了⽤⼾调⽤的Winsock的版本号。⾼字节指明辅版本编号,
低字节指明主版本编号。通常使⽤MAKEWORD[函数:WORD MAKEWORD( BYTE b
lpWSAData:指向WSADATA结构体的指针,lpWSAData返回了系统对Windows Sockets 的描述。
//可以在vs2019单击右键,查到到对应的版本相关结构体
typedef struct WSAData {
    WORD         wVersion;
    WORD         wHighVersion;
#ifdef _WIN64
    unsigned short iMaxSockets;
    unsigned     short iMaxUdpDg;
    char FAR     * lpVendorInfo;
    char         szDescription[WSADESCRIPTION_LEN+1];
    char         szSystemStatus[WSASYS_STATUS_LEN+1];
#else
    char         szDescription[WSADESCRIPTION_LEN+1];
    char         szSystemStatus[WSASYS_STATUS_LEN+1];
    unsigned     short iMaxSockets;
    unsigned     short iMaxUdpDg;
    char FAR     * lpVendorInfo;
#endif
} WSADATA;
typedef WSADATA FAR *LPWSADATA;

3. 释放对Winsock链接库的调⽤,以及释放相关资源。必须和WSAStartup成对出现
int WSACleanup (void);

socket接口(只关心客户端相关接口)

//除了上述特殊情况,其他接⼝基本与Linux⼤同⼩异,具体可以详⻅MSDN说明或者vs 2019查看接⼝细
1. 创建套接字
SOCKET socket( int af, int type, int protocol );
//返回值就是⼀个unsigned int值
typedef _W64 unsigned int UINT_PTR, *PUINT_PTR;
typedef UINT_PTR SOCKET;

样例:
SOCKET sk = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sk == INVALID_SOCKET) {
        std::cout << "Create Socket Error::" << GetLastError() <<
        return;
    }
其中:
IPPROTO_TCP = 6,

2. 发起链接请求
int connect(SOCKET s, const struct sockaddr FAR *name, int namelen);
样例:
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
memset(peer.sin_zero, 0, 8);
int ret = connect(sk, (struct sockaddr*)&peer, sizeof(peer));
if (ret == SOCKET_ERROR) {
    std::cout << "Connect Error" << std::endl;
    return -1;
}
其中:
struct sockaddr_in {
    short sin_family;
    u_short sin_port;
    struct in_addr sin_addr;
    char sin_zero[8];
};

3. 发送数据
int send(SOCKET s, const char FAR *buf, int len, int flags);

4. 接收数据
int recv(SOCKET s, char FAR *buf, int len, int flags);

设计tcp接口为单例模式

头文件:

#include <stdio.h>
#include <WinSock2.h>
#include <iostream>
#include <mutex>

#pragma comment(lib, "ws2_32.lib")

class TcpSvr {
public:
    static TcpSvr* getInstance();
    int Send(std::string& msg);
    int Recv(std::string* msg);
private:
    TcpSvr();
    TcpSvr(const TcpSvr&);
    static TcpSvr* instance_;
    SOCKET sockfd_;
private:
    int ConnectToSvr();
};

源文件:

#include "pch.h"
#include "tcp_svr.h"
class TcpSvr;
std::mutex mt;
TcpSvr* TcpSvr::instance_ = nullptr;
TcpSvr* TcpSvr::getInstance() {
    if (nullptr == instance_) {
        mt.lock();
        if (nullptr == instance_) {
            instance_ = new TcpSvr();
            if (instance_ != nullptr) {
                instance_->ConnectToSvr();
            }
        }
        mt.unlock();
    }
    return instance_;
}

TcpSvr::TcpSvr() {

}

int TcpSvr::ConnectToSvr() {
    /*1.加载套接字库*/
    WSADATA wsaData;
    int iRet = 0;
    iRet = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (0 != iRet) {
        std::cout << "WSAStartup(MAKEWORD(2, 2), &wsaData) execute failed!" << st
        return -1;
    }
    if (2 != LOBYTE(wsaData.wVersion) || 2 != HIBYTE(wsaData.wVersion)) {
        WSACleanup();
        return -1;
    }
    //创建套接字
    sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd_ == INVALID_SOCKET) {
        WSACleanup();
        return -1;
    }
    //初始化服务器端地址族变量
    SOCKADDR_IN srvAddr;
    srvAddr.sin_addr.S_un.S_addr = inet_addr("42.192.83.143");
    srvAddr.sin_family = AF_INET;
    srvAddr.sin_port = htons(19090);
    //连接服务器
    iRet = connect(sockfd_, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR));
    if (0 != iRet) {
        closesocket(sockfd_);
        WSACleanup();
        return -1;
    }
    return 0;
}

int TcpSvr::Send(std::string& msg) {
    return send(sockfd_, msg.c_str(), msg.size(), 0);
}

int TcpSvr::Recv(std::string* msg) {
    char buf[1024] = { 0 };
    int r_s = recv(sockfd_, buf, sizeof(buf) - 1, 0);
    if (r_s < 0) {
        return r_s;
    }
    else if(r_s == 0){
        //重新连
        exit(1);
    }    
    else {
    *msg = buf;
    }
    return 0;
}

消息队列

客户端的接收线程从网络当中将消息收回来之后,按照消息的类型丢入到消息队列当中,不同类的线程可以按照消息类型去获取消息。例如:注册线程只需关系注册消息,登陆线程只需要关心登陆消息。

所以:客户端的消息队列需要能够支持按照消息类型获取消息;设计成为单例模式。

头文件:

#pragma once
#include <vector>
#include <queue>
#include <string>
#include <mutex>
#include <iostream>
using namespace std;
class MsgQueue
{
public:
    static MsgQueue* getInstance();
    void Push(int msg_type, const string& msg);
    void Pop(int msg_type, string* msg);
private:
    MsgQueue();
    MsgQueue(const MsgQueue&);
    static MsgQueue* instance_;
    vector<queue<string>> v_msg_;
};

源文件:

#include "pch.h"
#include "MsgQueue.h"
static mutex g_lock;
MsgQueue* MsgQueue::instance_ = nullptr;
MsgQueue::MsgQueue() {
    v_msg_.resize(20);
}

MsgQueue* MsgQueue::getInstance() {
    if (nullptr == instance_) {
        g_lock.lock();
        if (nullptr == instance_) {
            instance_ = new MsgQueue();
        }
        g_lock.unlock();
    }
    return instance_;
}

void MsgQueue::Push(int msg_type, const string& msg) {
    g_lock.lock();
    v_msg_[msg_type].push(msg);
    g_lock.unlock();
}

void MsgQueue::Pop(int msg_type, string* msg) {
    while (1) {
        if (v_msg_[msg_type].empty()) {
            //单位毫秒
            Sleep(1);
            continue;
        }
        g_lock.lock();
        *msg = v_msg_[msg_type].front();
        v_msg_[msg_type].pop();
        g_lock.unlock();
        break;
    }
}

MFC基于对话框编程(vs)

创建基于对话框的MFC应用程序框架

 

 

对话框应用程序框架介绍

资源视图

在MFC中,与用户进行交互的对话框界面被认为是一种资源。

 

展开:“Dialog”,可以看到有一个ID为IDD_CHATSYSTEMCLIENT _DLALOG(中间部分(CHATSYSTEMCLIENT)与项目名称相同)的资源,对应中间的对话框设计界面。不管在何时,只要双击对话框资源的ID,对话框设计界面就会显示在中间。

 

类视图

在类视图中,可以看到生成了3个类:CAboutDlg、CDialogApp和CDialogDlg。

 

  • CAboutDlg:对应生成版本本信息对话框;
  • CDialogApp:应用程序类,从CWinApp继承过来,封装了初始化、运行、终止该程序的代码;
virtual BOOL InitInstance(); //这个函数就是初始化程序的函数
  • CDialogDlg:对话框类,从CdialogEx继承过来的,在程序运行时看到的对话框就是他的一个具体对象。(DoDataExchange函数:该函数完成对话框数据的交换和校验;OnlineDialog:相当于对对话框进行初始化处理。)

设计界面和工具箱

 

对话框模拟

当模拟对话框显示时。程序(之前窗口)会暂停执行,直到关闭这个模拟对话框之后,才能执行程序中的其他任务。

void CDialogDlg::OnBnClickedButton1()
{
    CDlgExec dlg;
    dlg.DoModal(); //以模态⽅式运⾏
}

聊天界面

好友信息维护

struct UserInfo {
public:
    std::string nickname_;
    std::string school_;
    int user_id_;
    std::vector<std::string> history_msg_;
    int msg_cnt_;
};

初始化:
 

BOOL CChatWin::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    std::thread recv_msg(DealPushMsg, this);
    recv_msg.detach();
    std::thread recv_addfriendmsg(DealPushAddFriMsg, this);
    recv_addfriendmsg.detach();
    std::thread recv_AddFriendResp(DealAddFriendResp, this);
    recv_AddFriendResp.detach();
    //1.获取tcp连接
    TcpSvr* ts = TcpSvr::getInstance();
    if (ts == nullptr) {
        MessageBox(TEXT("连接后台失败, 请联系管理员"));
        return false;
    }

    //2.组织json数据, 发送获取好友的请求
    ChatMsg cm;
    cm.msg_type_ = GetFriendMsg;
    cm.user_id_ = user_id_;
    string msg;
    cm.GetMsg(&msg);
    ts->Send(msg);
    //3.获取结果进⾏分析
    MsgQueue* mq = MsgQueue::getInstance();
    if (mq == nullptr) {
        return false;
    }
    msg.clear();
    cm.Clear();
    mq->Pop(GetFriendMsgResp, &msg);
    cm.PraseChatMsg(-1, msg);
    for (int i = 0; i < (int)cm.json_msg_.size(); i++) {
        //给列表框中添加数据
        struct UserInfo ui;
        ui.user_id_ = cm.json_msg_[i]["userid"].asInt();
        if (ui.user_id_ == user_id_) {
            ui.nickname_ = "我";
        }else {
            ui.nickname_ = cm.json_msg_[i]["nickname"].asString();
        }
        ui.school_ = cm.json_msg_[i]["school"].asString();
        ui.msg_cnt_ = 0;
        fri_vec_.push_back(ui);
    }
    RefreshUserList();
    return TRUE; // return TRUE unless you set the focus to a control
    // 异常: OCX 属性⻚应返回 FALSE
}

创建接收推送消息的线程:

static void DealPushMsg(CChatWin* cc) {
    MsgQueue* mq = MsgQueue::getInstance();
    if (mq == nullptr) {
        return;
    }
    while (1) {
        //2.获取推送的消息
        std::string msg;
        mq->Pop(PushMsg, &msg);
        //3.解析消息
        ChatMsg cm;
        cm.PraseChatMsg(-1, msg);
        string peer_nick_name = cm.GetValue("peer_nick_name");
        string school = cm.GetValue("peer_school");
        int peer_user_id = atoi(cm.GetValue("peer_user_id").c_str());
        string peer_msg = cm.GetValue("msg");
        //4.将消息放到对应好友的消息池当中
        for (int i = 0; i < (int)cc->fri_vec_.size(); i++) {
            if (cc->fri_vec_[i].user_id_ == peer_user_id) {
                string tmp = peer_nick_name + "-" + school + ": "
                cc->fri_vec_[i].history_msg_.push_back(tmp);
                if (peer_user_id == cc->send_friend_id_) {
                    cc->m_output.AddString(tmp.c_str());
                }
                else {
                    cc->fri_vec_[i].msg_cnt_++;
                }
            }
        }
        cc->RefreshUserList();
    }
}

创建处理推送添加好友的线程:

static void DealPushAddFriMsg(CChatWin* cc) {
    MsgQueue* mq = MsgQueue::getInstance();
    if (mq == nullptr) {
        return;
    }
    while (1) {
        std::string msg;
        mq->Pop(PushAddFriendMsg, &msg);
        //解析消息
        ChatMsg cm;
        cm.PraseChatMsg(-1, msg);
        string peer_name = cm.GetValue("peer_nick_name");
        string peer_school = cm.GetValue("peer_school_name");
        int peer_user_id = atoi(cm.GetValue("peer_user_id").c_str());
        string show_msg = peer_name + "-" + peer_school + ": want add you"
        cm.Clear();
        UINT i = MessageBox(cc->m_hWnd, _T(show_msg.c_str()), _T("提⽰"),
        if (i == IDYES){
            //添加到⽤⼾信息当中
            struct UserInfo ui;
            ui.user_id_ = peer_user_id;
            ui.nickname_ = peer_name;
            ui.school_ = peer_school;
            ui.msg_cnt_ = 0;
            cc->fri_vec_.push_back(ui);
            cm.msg_type_ = PushAddFriendMsgResp;
            cm.reply_status_ = ADDFRIEND_SUCCESS;
            cm.user_id_ = cc->user_id_;
            cm.SetKeyValue("userid", std::to_string(peer_user_id));
            cc->RefreshUserList();
        }else {
            cm.msg_type_ = PushAddFriendMsgResp;
            cm.reply_status_ = ADDFRIEND_FAILED;
            cm.user_id_ = cc->user_id_;
            cm.SetKeyValue("userid", std::to_string(peer_user_id));
        }
        msg.clear();
        cm.GetMsg(&msg);
        TcpSvr* ts = TcpSvr::getInstance();
        if (ts == nullptr) {
            continue;
        }
        ts->Send(msg);
    }
}

添加处理添加好友应答的线程:

static void DealAddFriendResp(CChatWin* cc) {
    MsgQueue* mq = MsgQueue::getInstance();
    if (mq == nullptr) {
        return;
    }
    while (1) {
        //2.获取推送的消息
        std::string msg;
        mq->Pop(AddFriend_resp, &msg);
        //3.解析消息
        ChatMsg cm;
        cm.PraseChatMsg(-1, msg);
        string content = cm.GetValue("content");
        MessageBox(cc->m_hWnd, _T(content.c_str()), _T("add friend resp")
        if (cm.reply_status_ == ADDFRIEND_FAILED) {
            return;
        }
        string peer_nick_name = cm.GetValue("peer_nick_name");
        string peer_school = cm.GetValue("peer_school");
        int peer_user_id = atoi(cm.GetValue("peer_user_id").c_str());
        //4.将消息放到对应好友的消息池当中
        struct UserInfo ui;
        ui.user_id_ = peer_user_id;
        ui.nickname_ = peer_nick_name;
        ui.school_ = peer_school;
        ui.msg_cnt_ = 0;
        cc->fri_vec_.push_back(ui);
        cc->RefreshUserList();
    }
}

刷新好友列表:

void CChatWin::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++) {
        string tmp = fri_vec_[i].nickname_;
        if (fri_vec_[i].msg_cnt_ > 0) {
            tmp += " : ";
            tmp += std::to_string(fri_vec_[i].msg_cnt_);
        }
        m_userlist.AddString(tmp.c_str());
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值