基于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环境搭建
jsoncpp编译
编译好的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());
}
}