项目---基于TCP的高并发聊天系统

目录

服务端

 服务端视角下的流程图

一、数据库管理模块

1.1 数据库表的创建

1.2 .对于数据库的操作

1.2.1首先得连接数据库

1.2.2执行数据库语句

 1.2.3 返回数据库中存放的所有用户的信息

 1.2.4返回数据库中存放的所有用户的好友信息

 二、用户管理模块

2.1、UserInfo类:描述单个用户的信息

 2.2UserMana类:组织用户的信息

2.2.1初始化

 剩下的就是各类业务的接口

三、自定义消息格式

3.1了解Json对象

 3.2、Json序列化和反序列化

3.3、模拟注册请求和发送请求

四、网络通信模块&业务模块

4.1、InitChatSvr函数

4.2 StartChatSvr函数

客户端

客户端视角下的流程图

1、注册

2、登录

3、发送消息

4、添加好友


服务端

 服务端视角下的流程图

一、数据库管理模块

数据库模块是项目的最底层,主要就是负责与数据库进行打交道,

具体功能有:连接数据库、将用户信息存入数据库中、从数据库中获取信息、操作数据库中的表

1.1 数据库表的创建

这里创建了两张表:user,friendinfo。分别用来存放用户信息,用户好友信息。

1.2 .对于数据库的操作

1.2.1首先得连接数据库

1.2.2执行数据库语句

 1.2.3 返回数据库中存放的所有用户的信息

用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息

 1.2.4返回数据库中存放的所有用户的好友信息

 二、用户管理模块

数据库模块是整个项目的最低层,而用户管理模块是倒数第二层,是建立在数据库模块之上的

2.1、UserInfo类:描述单个用户的信息

 2.2UserMana类:组织用户的信息

2.2.1初始化

用户管理模块刚初始化的时候就需要一个工作:从数据库当中获取所有用户的信息,还有每个用户的好友信息,并将这些信息放于user_map当中去,方便后续的查询

 剩下的就是各类业务的接口

三、自定义消息格式

3.1了解Json对象

只有当服务端和客户端发送的消息格式统一的时候,才能正常的进行通信,就像网络当中的各种协议一样,只有双方都遵守的时候,才能正常的进行数据交换。

我们这里采用的是Json数据格式

Json就是一个key  :value的东西,他可以包含多动类型

简单介绍:

json 数据类型:对象,数组,字符串,数字
对象:使⽤花括号 {} 括起来的表⽰⼀个对象。

示例:

{
        " 姓名" : " ⼩张 " ,
        " 年龄" : 20,
         "成绩 " : [ 99 , 89 , 66 ]
}

这就是一个Json对象,Json还支持嵌套,可以有无数种格式

数组:使⽤中括号 [] 括起来的表⽰⼀个数组。
这个就是一个数组的形式,一个元素的类型就是一个Json对象
[
        {
                " 姓名" : " ⼩张 " ,
                " 年龄" : 20,
                 "成绩 " : [ 99 , 89 , 66 ]
        },
        
        {
                " 姓名" : " ⼩王 " ,
                " 年龄" : 22,
                 "成绩 " : [ 77 , 88 , 66 ]
        },
]
字符串:使⽤常规双引号 "" 括起来的表⽰⼀个字符串
数字:包括整形和浮点型,直接使⽤
jsoncpp库,已经帮助我们封装了json对象,数组,以及序列化和反序列化的接口,我们在这里只需要会用就行了。

我们来测试一下:

 能够看到,json对象用起来是非常方便的。

 我们再来测试看一下Json的NB之处:

 我们来运行一下看能打印出什么:

 3.2、Json序列化和反序列化

为什么需要序列化呢?

在tcpsocket编程的时候,我们学习过,内存不连续的结构体是不能直接用send发送的,需要使用序列化,将内存组合起来,放到一块连续的内存当中去,然后再发送。反序列化就是反过来,具体可以去看我之前写的博客:网络基础2--HTTP协议详解_Flying clouds的博客-CSDN博客

序列化:

 反序列化:

3.3、模拟注册请求和发送请求

注册请求:

 注册响应:

四、网络通信模块&业务模块

1、该模块负责TCP的socket编程,来负责网络通信。用epoll来监控多个文件描述符,从而实现具有高并发的基础

2、该模块还负责处理各种业务如:注册、登录、添加好友、聊天的具体实现

4.1、InitChatSvr函数

这个函数的作用是初始化资源,获取到需要的所有资源,比如用户管理模块的实例化指针、发送队列和接收队列、提供TCP服务的实例化指针、epoll操作句柄。

 bool InitChatSvr(int wtc = 4){
            /* 1. 初始化用户管理类的指针 */
            um_ = UserMana::GetInstance();
            if(um_ == NULL){
                std::cout << "初始化用户管理失败了, 请检查..." << std::endl;
                return false;
            }
            /* 2. 初始化队列资源 */
            recv_que_ = new MsgPool<ChatMsg>();
            if(recv_que_ == NULL){
                std::cout << "init MsgPool failed" << std::endl;
                return false;
            }

            send_que_ = new MsgPool<ChatMsg>();
            if(send_que_ == NULL){
                std::cout << "init MsgPool failed" << std::endl;
                return false;
            }

            work_thread_count_ = wtc;

            /* 3. 初始化tcp通信的内容 */
            sockfd_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            if(sockfd_ < 0){
                perror("socket");
                return false;
            }
            int opt = 1;
            // sockfd为需要端口复用的套接字
            setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));

            struct sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(port_);
            addr.sin_addr.s_addr = inet_addr("0.0.0.0");
            int ret = bind(sockfd_, (struct sockaddr*)&addr, sizeof(addr));
            if(ret < 0){
                perror("bind");
                return false;
            }

            ret = listen(sockfd_, 10);
            if(ret < 0){
                perror("listen");
                return false;
            }

            /* 4. 初始化epoll */
            ep_fd_ = epoll_create(5);
            if(ep_fd_ < 0){
                perror("epoll_create");
                return false;
            }

            return true;
        }

4.2 StartChatSvr函数

这个函数相当于是开始工作的函数,调用这个函数之后,主线程就会 启动接收线程、发送线程、工作线程。

启动完别的线程之后,我们给主线程也找了一个活干:一直在accept,并将接收到的新连接套接字放到epoll当中去,让epoll帮我们去监控这些个文件描述符的状态

bool StartChatSvr(){

            /* 1. 启动接收线程 */
            pthread_t tid;
            int ret = pthread_create(&tid, NULL, RecvStart, (void*)this);
            if(ret < 0){
                std::cout << "create thread failed\n";
                return false;
            }

            /* 2. 启动发送线程 */
            ret = pthread_create(&tid, NULL, SendStart, (void*)this);
            if(ret < 0){
                std::cout << "create thread failed\n";
                return false;
            }
            /* 3. 启动工作线程 */
            for(int i = 0; i < work_thread_count_; i++){
                ret = pthread_create(&tid, NULL, WorkerStart, (void*)this);
                if(ret < 0){
                    std::cout << "create thread failed\n";
                    return false;
                }
            }
            /* 4. 主线程accept */
            /* 5. 将新连接套接字放到epoll当中 */

            while(1){
                int new_sockfd = accept(sockfd_, NULL, NULL);
                if(new_sockfd < 0){
                    continue;
                }
                /* 添加到epoll */
                struct epoll_event ee;
                ee.events = EPOLLIN;
                ee.data.fd = new_sockfd;
                epoll_ctl(ep_fd_, EPOLL_CTL_ADD, new_sockfd, &ee);
            }
        

客户端

客户端视角下的流程图

客户端我们这里选取了MFC框架来进行,我们这里只是用到了MFC最基本的使用方法,想要熟练掌握MFC的同学可以去看b站上面的视频,上面的讲解是非常清晰的,我们这里只是简单来用。

我们在客户端主要是实现了四个基本的业务:注册,登录,添加好友,给好友发送消息

我们先来看看四个窗口:

1、注册

我们首先要插入一个Dialog,然后去对其进行操作,具体做法就是:插入一个Dialog,然后点击这个Dialog,我们就能看到一个窗口,然后再对窗口添加button。。。啥的各种内容。

在窗口当中对 : 姓名、学校、电话、密码添加变量,用来保存之后输入的值。

我们右键去对其添加变量、控件什么的,双击就是会给我们自动生成代码。

我们这个注册窗口是需要点击提交之后,成功的话就直接跳转到登录的界面,我们这里选择的是双击提交,生成代码,然后在这个函数里面实现我们想要的功能

void CRegisterdlg::OnBnClickedCommit()
{
	// TODO: 在此添加控件通知处理程序代码
	/*1.获取输入框的内容*/
	UpdateData(true);
	if (m_nickname_.IsEmpty()||m_school_.IsEmpty()||
		m_tel_.IsEmpty()||m_passwd_.IsEmpty()) {
		MessageBox(TEXT("请输入完整的注册内容!"));
		return;
	}

	std::string nickname = CT2A(m_nickname_.GetString());
	std::string school = CT2A(m_school_.GetString());
	std::string telnum = CT2A(m_tel_.GetString());
	std::string passwd = CT2A(m_passwd_.GetString());
	/*2.组织ChatMSg数据*/
	ChatMsg cm;
	cm.msg_type_ = Register;
	cm.json_msg_["nickname"] = nickname.c_str();
	cm.json_msg_["school"] = school.c_str();
	cm.json_msg_["telnum"] = telnum.c_str();
	cm.json_msg_["passwd"] = passwd.c_str();
	/*3.获取TCP实例化指针*/
	TcpSvr* ts = TcpSvr::GetInstance();
	/*4.发送消息*/
	std::string msg;
	cm.GetMsg(&msg);
	ts->Send(msg);
	/*5.获取消息队列的实例化指针*/
	MsgQueue* mq = MsgQueue::GetInstance();
	if (mq == NULL) {
		exit(1);
	}

	/*6.按照消息类型获取注册应答*/
	msg.clear();
	mq->Pop(Register_Resp,&msg);
	/*
		7.解析应答,判断应答结果
		   注册成功 ==》跳转到登 录界面
		   注册失败 ==》情空注册框,提示失败了(电话号码重复了)
		*/
	cm.clear();
	cm.PraseMsg(-1,msg);
	if (cm.reply_status_ == REGISTER_FAILED) {
		MessageBox(TEXT("电话号码重复了,请检查输入。。。"));
	}
	else {
		MessageBox(TEXT("注册成功!"));
	}
	
}

2、登录

登录的时候,需要做到是点击登录按钮之后能够跳转到我们的聊天界面,需要注意的是,在跳转到聊天界面之前,我们需要提前吧聊天界面的内容给加载好,这样一条转到界面我们就能进行操作的,跳转界面我们已经知道了是调用domodel,用模态的方式打开界面,但是怎么加载数据呢?

我们这里可以找到方法:在调用domodel函数的时候,会调用一个函数,这个函数是重写父类的虚函数

BOOL CChatDlg::OnInitDialog()

在这个函数里面,我们可以能够实现获取聊天界面内容的功能,我们要获取的内容就两个:好友列表好友的消息记录。

知道了这些之后,我就只剩下开工了:

如何展示:

 保存好友信息到本地

因为好友信息是动态变化的,我们如果只是在获取好友信息的时候就在列表当中展示一次的话,那么当添加好友或者删除好友的时候,我们只能获取到新的好友信息,原来的好友信息就不知道从哪里再去获取了,方便起见,我们就直接保存在客户端,用vector来进行保存。

3、发送消息

我们想要发送消息,第一步肯定是要知道消息到底要发送给谁,就像微信一样,我们在好友列表当中点击谁,就代表要给哪个好友发送消息了,我们为了能实现这个功能:需要做到的就是能够时刻识别出来我们点击了好友列表,并且要知道到底点击了谁。

这个功能我们用一个MFC的函数来实现 : GetText,这个函数能够知道我们点击了哪个好友

/*
	当用户点击了 frilist,
		1. 要及时的更新 recv_userid
		2. 更新对话框
	*/
	/* 1. 获取点击的内容 */

	CString str;
	m_frilist_.GetText(m_frilist_.GetCurSel(), str);

	/* 2. 根据点击的内容进行比对*/
	/* 3. 更新 recv_userid_ */
	int i = 0;
	for (; i < fris_info_.size(); i++) {
		// eg: zs-bite    zs-bite:10
		// zs ls
		std::string tmp = CT2A(str.GetString());
		if (strstr((tmp.c_str()), fris_info_[i].nickname_.c_str()) != NULL) {
			recv_userid_ = fris_info_[i].user_id_;
			break;
		}
	}

当我们切换了好友之后,我们也要同步更新聊天框的内容

我们这里的做法也很简单粗暴:将现在所有的消息全部清空,然后再将当前好友的历史记录依次展示上去

/* 4. 刷新聊天界面 */
	for (int i = m_output_.GetCount(); i >= 0; i--) {
		m_output_.DeleteString(i);
	}

	std::vector<std::string> h_m = fris_info_[i].history_msg_;
	for (int i = 0; i < h_m.size(); i++) {
		m_output_.InsertString(m_output_.GetCount(), h_m[i].c_str());
	}
	fris_info_[i].unread_msg_ = 0;

	/* 5. 清空输入框 */
	m_input_.Empty();
	m_input_edit_.SetWindowTextA(0);

	/* 6. 刷新UserList */
	ReferFriList();

4、添加好友

我们想要做到的是输入一个好友的电话号码,然后给服务端发送一个添加好友的请求,在服务端这里进行判断,如果有这个电话号码是一个注册用户的话,我们就给这个用户推送一个添加好友的请求,如果不是的话服务端就忽略这条消息。

当用户收到添加好友请求的时候,可以选择同意或者拒绝,如果拒绝,就当作无事发生就行;如果点击了同意,就需要给服务端回一个同意添加好友的请求,然后服务端进行处理,将两个人的关系设置为好友,并再次给客户端推送应答,让客户端的好友列表进行更新,将新添加的好友展示上去。

UpdateData(true);
	if (m_fritel.IsEmpty()) {
		MessageBox(TEXT("输入内容不能为空..."));
		return;
	}
	std::string input = CT2A(m_fritel.GetString());

	ChatMsg cm;
	cm.msg_type_ = AddFriend;
	cm.user_id_ = userid_;
	cm.json_msg_["fri_telnum"] = input.c_str();


	/* 3. 获取TCP的实例化指针 */
	TcpSvr* ts = TcpSvr::GetInstance();

	/* 4. 发送消息 */
	std::string msg;
	cm.GetMsg(&msg);
	ts->Send(msg);

	CDialog::OnCancel();

猜你喜欢

转载自blog.csdn.net/flyingcloud6/article/details/129667256