网络知识 娱乐 你还不了解QQ聊天是如何实现的吗?手把手教你实现网络聊天室

你还不了解QQ聊天是如何实现的吗?手把手教你实现网络聊天室

目录

一、前言

二、聊天协议         

1、自定义聊天协议

三、登录、聊天业务

1、登录业务

1)客户端登录核心代码

2)服务器登录核心代码

3)登录效果展示

2、聊天业务

1)客户端聊天核心代码

2)服务器聊天核心代码

3)聊天效果展示

四、服务器全部代码


一、前言

💕

        都2022年了,大家和心中的那个他👦(她👧)是怎么保持联系的呢?我想绝大部分人都是通过手机的聊天软件进行聊天或者打语音、视频电话来保持联系。

💕

        那么网络聊天离我们这么近,我们发送的信息怎么通过网络传输到他👦(她👧)的手机上呢?本文将通过一个在线聊天的小案例来详细介绍网络聊天的实现过程,带你了解网络信息传递的奥秘!!!

👀下面是博主实现的效果

虽然还不是很完善,但是基本网络聊天业务是可以做到了!

  • 开发环境

        🔎windows linux

  • 开发软件

        🔎Visual Studio 2019  +  linux_Qt 5.9.8

话不多说!下面跟着博主一起来实现👇

二、聊天协议         

       网络传输都是有一个协议的,像TCP、UDP协议都是网络通信常用的通信协议,那么可不可以自己定义一个聊天协议,来进行网络通信(聊天)呢?

        所以在聊天前我们先定义一个聊天协议。

1、自定义聊天协议

  • 假设我们的聊天协议是如下一系列结构体
#ifndef PROTOCOL_H
#define PROTOCOL_H

#define LOGIN 1
#define CHAT 2


/*----------------------------请求头------------------------------*/
typedef struct head_t
{
    int businessType; //业务类型
    int businessLength; //业务长度
}HEAD_T;


/*----------------------------请求体------------------------------*/

//聊天信息
typedef struct chatMsg_t
{
    char sengName[20]; //发送者
    char receiveName[20]; //接收者
    char message[100]; //发送的信息
}CHATMSG_T;


//用户信息
typedef struct userinfo_t
{
    char user_name[20];
    char user_pwd[20];
}USERINFO_T;


/*----------------------------反馈头------------------------------*/
typedef struct feedback_t
{
    int businessType; //业务类型
    int businessLength; //业务长度
    int flag; //业务成功与否的状态
}FEEDBACK_T;


#endif // PROTOCOL_H
  • 业务类型有两种(即登录和聊天)
  • 业务长度是请求体的大小如sizeof(CHATMSG_T)以及sizeof(USERINFO_T)
  • 业务成功的标志假设1为成功,-1为失败

三、登录、聊天业务

1、登录业务

  • 要聊天首先需要登录APP,登录的流程如下图所示

  •  在服务器定义一个map容器

        🌿一旦客户端账号密码正确,就将用户名和用户连接服务器时的文件描述符存入map容器中,用于双方聊天时在服务器能够找到那个他👦(她👧),并将信息转发给那个他👦(她👧)。

        🌿假设账号密码正确,服务器则将登录成功的标志通过反馈包发回给客户端。此时客户端收到登录成功的反馈包,则进行界面跳转进入聊天界面。

1)客户端登录核心代码

  • 博主的客户端是在linux下使用Qt进行ui设计

🍒合并协议(请求头+请求体)

//定义请求头  请求头包含业务类型+用户信息结构体大小
this->protocolHead.businessType = LOGIN;
this->protocolHead.businessLength = sizeof(USERINFO_T);
memcpy(buf, &protocolHead, sizeof(HEAD_T));

//拷贝请求体  请求体包含用户名+密码
memcpy(info.user_name, ui->lineEdit->text().toStdString().c_str(), sizeof(info.user_name));
memcpy(info.user_pwd, ui->lineEdit_2->text().toStdString().c_str(), sizeof(info.user_pwd));
memcpy(buf + sizeof(HEAD_T), &info, sizeof(USERINFO_T));

🍒发送协议(整个 buf write给服务器)

void writeThread::run()
{
    if(strlen(this->buf) > 0)
    {
        //this->socketFd代表服务器的文件描述符
       int res = write(this->socketFd, this->buf, this->size);
       qDebug()<<"send res = "<buf, 0, sizeof(this->buf));
    }
}

        上面两段代码的含义是将协议头和协议体统一合在一个字符数组buf里,然后将整个buf发给服务器。

🍒客户端读取服务器发来的反馈包

bzero(&back, sizeof(FEEDBACK_T));

read(this->socketFd, &back, sizeof(FEEDBACK_T));

if(back.businessType == LOGIN)//业务类型为登录
{
    userdata::loginstate = back.flag;
    emit sendloginflag(back.flag);//将登录是否成功的标志发送给登录界面
}

        客户端的设计是将read和write的业务用两条线程来完成,并且read线程是死循环一直读取服务器发来的信息

        因为是登录业务,所以反馈包只包含了反馈头,所以只需要read一次反馈头即可。通过Qt中emit发送信号的形式将登录成功与否的标志发送给登录界面。


2)服务器登录核心代码

int loginstate = 0;       //登录状态
HEAD_T head = { 0 };      //协议头
USERINFO_T user = { 0 };  //协议体
char name[20] = { 0 };    //存用户名
FEEDBACK_T back = { 0 };  //反馈头

//先读请求头 判断是什么业务  fd为客户端连接上服务器时的文件描述符
read(fd, &head, sizeof(HEAD_T));

if (head.businessType == LOGIN) //登录业务
{
        
	int res = read(fd, &user, head.businessLength); //再读请求体 拿到账号和密码

	if (res == head.businessLength) //如果读到的res和业务长度一致,说明数据没有丢失
	{
            
		for (int i = 0; i < 6; i++) //遍历用户信息结构体数组
		{
			if (strcmp(userInfo[i].user_name, user.user_name) == 0 && strcmp(userInfo[i].user_pwd, user.user_pwd) == 0)//用户名密码正确
			{
				back.flag = 1;//账号密码一致 标志为1
				strcpy(name, user.user_name);
				onlineMap.insert(make_pair(name, fd));//容器插入数据
				cout << "在线人数:" << onlineMap.size() << endl;
				break;
			}
			back.flag = -1;
		}
            
		back.businessType = LOGIN;  //设置反馈包的业务类型和业务长度
		back.businessLength = sizeof(USERINFO_T);
            
		write(fd, &back, sizeof(FEEDBACK_T));  //将反馈包发给客户端
	}
}

        不论聊天业务还是登录业务,客户端发来的协议都是 请求头+请求体,所以服务器先read请求头,判断是登录业务之后再read请求体。

        请求体里面包含用户的账号密码,博主服务器没有与数据库对接,为了方便是将所有用户信息存放到一个结构体数组中,通过遍历结构体数组的方式查找登录的用户是否存在。

        然后将查找的结果通过反馈包的形式write给客户端


3)登录效果展示

  • 全部用户信息如下
USERINFO_T userInfo[6] = { {"1001","123456" },{"1002", "123456" } ,{"似末", "123456" } ,
	{"1003", "123456" },{"玛卡巴卡", "123456" } ,{"花开富贵", "123456" } };
  • 假设博主的账号为似末,聊天对象账号为玛卡巴卡,来进行两个客户端的登录

  • 账号密码输入正确,点击登录按钮,跳转到聊天界面

        🙋博主(似末)看到的聊天界面如下

        👧聊天对象(玛卡巴卡)看到的聊天界面如下

 ✅ok,此时双方都已经在线啦!!

        登录成功跳转到聊天界面,此时登录业务已经做完了,接下来我们来分析一下核心业务(聊天业务)。


2、聊天业务

  • 聊天的流程如下图所示

        上面登录业务提到的map容器在聊天业务这边就用到了,因为容器里面存放的是已上线的用户和对应的文件描述符,只要根据客户端发来的请求体来找到接收者并进行消息的转发即可。

🙌注意:服务器发回来的反馈包包含反馈头+反馈体(聊天体),而上面的登录业务服务器只发回反馈头。

  •  📝

        由此我们可以知道双方聊天并不是直接将信息发送给对方,而是信息先发给服务器,然后服务器进行转发给你要发送的用户。

  • 从聊天流程来看服务器又把你发送的信息发回给你,这是为什么呢

👀看博主的“直男3连问

  • 📝

        其实也不难理解,我们发送出去的信息我们自己肯定要看得到,而我们看到的聊天信息都是服务器发来的,所以服务器要把你发的信息再发回给你,即让你确认你的信息已经成功发送出去了。

1)客户端聊天核心代码

  • 客户端聊天的核心代码和登录的差不多

🍒合并协议(请求头+请求体)

//定义请求头  请求头包含 业务类型+聊天信息结构体大小
this->protocolHead.businessType = CHAT;
this->protocolHead.businessLength = sizeof(CHATMSG_T);
memcpy(buf, &protocolHead, sizeof(HEAD_T));

//拷贝请求体  请求体包含 聊天信息+发送者+接收者
memcpy(msg.message, sendmsg.toStdString().c_str(), sizeof(msg.message));
memcpy(msg.sengName, userdata::currentUser.toStdString().c_str(), sizeof(msg.sengName));
memcpy(msg.receiveName, userdata::receiveUser.toStdString().c_str(), sizeof(msg.receiveName));

memcpy(buf + sizeof(HEAD_T), &msg, sizeof(CHATMSG_T));

🍒发送协议(整个 buf write给服务器)

void writeThread::run()
{
    if(strlen(this->buf) > 0)
    {
        //this->socketFd代表服务器的文件描述符
       int res = write(this->socketFd, this->buf, this->size);
       qDebug()<<"send res = "<buf, 0, sizeof(this->buf));
    }
}

🍒客户端读取服务器发来的反馈包

//receive from sever
void readThread::run()
{

    while(1)
    {
        bzero(&back, sizeof(FEEDBACK_T));
        bzero(&chatmsg, sizeof(CHATMSG_T));
        read(this->socketFd, &back, sizeof(FEEDBACK_T)); //先读头

        if(back.businessType == LOGIN) //登录业务
        {
            userdata::loginstate = back.flag;
            emit sendloginflag(back.flag); //发送给登录界面
        }
        else if(back.businessType == CHAT) //聊天业务
        {
            read(this->socketFd, &chatmsg, back.businessLength); //再读体

            //将发送者和聊天内容拼接起来 并发送给聊天界面
            QString msg = QString("%1.%2").arg(chatmsg.sengName).arg(chatmsg.message); 
            emit receivemsg(msg); 
        }
    }
}

        因为是聊天业务,反馈包包含反馈头+反馈体(聊天信息),所以需要read两次,第一次读头,第二次读体。通过Qt中emit发送信号的形式将发送者和聊天内容发送给聊天界面。

2)服务器聊天核心代码

HEAD_T head = { 0 };   //协议头
CHATMSG_T buf = { 0 }; //协议体
map::iterator it;//迭代器
FEEDBACK_T back = { 0 }; //反馈头

//先读请求头 判断是什么业务  fd为客户端连接上服务器时的文件描述符
read(fd, &head, sizeof(HEAD_T));

if(head.businessType == CHAT) //聊天业务
{
	//cout << "开始聊天" < 1) //在线人数大于1
	{
		read(fd, &buf, sizeof(CHATMSG_T));  //再读请求体

        //设置反馈头
		back.flag = 1;
		back.businessType = CHAT;
		back.businessLength = sizeof(CHATMSG_T);
        
        //将反馈头和反馈体合起来
		char backbuf[sizeof(FEEDBACK_T) + sizeof(CHATMSG_T)] = { 0 };
		memcpy(backbuf, &back, sizeof(FEEDBACK_T));
		memcpy(backbuf + sizeof(FEEDBACK_T), &buf, sizeof(CHATMSG_T));

		write(fd, &backbuf, sizeof(backbuf));//发给发送者

		int sendFd = 0;
		for (it = onlineMap.begin(); it != onlineMap.end(); it++)//寻找需发送的对象的文件描述符
		{
			if (strcmp(buf.receiveName, (*it).first) == 0)
			{
				sendFd = (*it).second;
				write(sendFd, &backbuf, sizeof(backbuf));//发给接收者
				cout << "收到用户 " << buf.sengName << " 发来的消息,并转发给用户 " << buf.receiveName << endl;
				break;
			}
		}
	}
}

        不论聊天业务还是登录业务,客户端发来的协议都是 请求头+请求体,所以服务器先read请求头,判断是聊天业务之后再read请求体。

        请求体里面包含(发送者、接收者、聊天内容),使用迭代器通过遍历map容器的方式查找在线的接收者。

        然后合并反馈头+聊天体,发给发送者,再发给接收者


3)聊天效果展示

  • 👧聊天对象(玛卡巴卡)发一条信息给🙋博主(似末)

  • 🙋博主(似末)低情商回复

  • 🙋博主(似末)高情商这样回复

  • 🙋博主(似末)后续

原来阻断我和她的聊天是红色感叹号啊!!! 

  ✅到这里我们的所有业务都做完啦!!大家对网络聊天是不是更加了解了呢!

  • 下面附服务器全部代码

四、服务器全部代码

#include 
#include          
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "protocol.h"


using namespace std;
map onlineMap; //在线用户容器

typedef struct protocol_t
{
	char head[255];
	char body[255];
}PROTOCOL_T;

USERINFO_T userInfo[6] = { {"1001","123456" },{"1002", "123456" } ,{"似末", "123456" } ,
	{"1003", "123456" },{"玛卡巴卡", "123456" } ,{"花开富贵", "123456" } };

void* pthread_function(void* p);

int main()
{
	int socketfd = 0;
	int acceptfd = 0;
	pthread_t pthreadid;
	int len = 0;
	int pid = 0;
	int opt_val = 1;
	char buf[255] = { 0 };//存放客户端发过来的信息
	//初始化网络  参数一:使用ipv4  参数二:流式传输
	socketfd = socket(AF_INET, SOCK_STREAM, 0);
	if (socketfd == -1)
	{
		perror("socket error");
	}
	else
	{
		//原本使用struct sockaddr,通常使用sockaddr_in更为方便,两个数据类型是等效的,可以相互转换
		struct sockaddr_in s_addr;
		//确定使用哪个协议族  ipv4
		s_addr.sin_family = AF_INET;

		//系统自动获取本机ip地址 也可以是本地回环地址:127.0.0.1
		s_addr.sin_addr.s_addr = INADDR_ANY;

		//端口一个计算机有65535个  10000以下是操作系统自己使用的,  自己定义的端口号为10000以后
		s_addr.sin_port = htons(12345);  //自定义端口号为12345

		len = sizeof(s_addr);

		//端口复用 解决 address already user 问题
		setsockopt(socketfd, SOL_SOCKET, SO_REUSEADDR, (const void*)opt_val, sizeof(opt_val));

		//绑定ip地址和端口号
		int res = bind(socketfd, (struct sockaddr *)&s_addr, len);
		if (res == -1)
		{
			perror("bind error");
		}
		else
		{
			//监听这个地址和端口有没有客户端来连接  第二个参数现在没有用  只要大于0就行
			if (listen(socketfd, 2) == -1)
			{
				perror("listen error");
			}
			//死循环保证服务器一直在线
			while (1)
			{
				cout << "等待客户端上线" << endl;
				//等待客户端上线,阻塞式函数  acceptfd为连上来的客户端fd
				acceptfd = accept(socketfd, NULL, NULL);
				cout << "客户端上线 fd = " << acceptfd << endl;
				
				int tempFd = 0;
				tempFd = acceptfd;
				int* fd = NULL;
				fd = &tempFd;
				pthreadid = pthread_create(&pthreadid, NULL, pthread_function, fd);
				if (pthreadid != 0)
				{
					perror("pthread_create error");
				}
			}
		}
	}
}

void* pthread_function(void* p)
{
	int fd = 0;
	fd = *((int*)p);//客户端连接服务器后的文件描述符
	int loginstate = 0;
	
	HEAD_T head = { 0 };//协议头
	USERINFO_T user = { 0 };//协议体
	CHATMSG_T buf = { 0 };//协议体
	char name[20] = { 0 };
	map::iterator it;//迭代器
	FEEDBACK_T back = { 0 };
	
	while (1)
	{
		bzero(&head, sizeof(HEAD_T));
		bzero(&buf, sizeof(CHATMSG_T));
		bzero(&user, sizeof(USERINFO_T));
		bzero(&back, sizeof(FEEDBACK_T));
		//先读请求头
		read(fd, &head, sizeof(HEAD_T));

		if (head.businessType == LOGIN) //登录业务
		{
			int res = read(fd, &user, head.businessLength);
			//如果读到的res和业务长度一致,说明数据没有丢失
			if (res == head.businessLength)
			{
				for (int i = 0; i < 6; i++)
				{
					if (strcmp(userInfo[i].user_name, user.user_name) == 0 && strcmp(userInfo[i].user_pwd, user.user_pwd) == 0)//用户名密码正确
					{
						back.flag = 1;
						strcpy(name, user.user_name);
						onlineMap.insert(make_pair(name, fd));//容器插入数据
						cout << "在线人数:" << onlineMap.size() << endl;
						break;
					}
					back.flag = -1;
				}
				back.businessType = LOGIN;
				back.businessLength = sizeof(USERINFO_T);
				write(fd, &back, sizeof(FEEDBACK_T));
			}
			else
			{
				cout << "数据丢包" << endl;
			}
		}
		else if(head.businessType == CHAT) //聊天业务
		{
			//cout << "开始聊天" < 1)
			{
				read(fd, &buf, sizeof(CHATMSG_T));
				back.flag = 1;
				back.businessType = CHAT;
				back.businessLength = sizeof(CHATMSG_T);

				char backbuf[sizeof(FEEDBACK_T) + sizeof(CHATMSG_T)] = { 0 };
				memcpy(backbuf, &back, sizeof(FEEDBACK_T));
				memcpy(backbuf + sizeof(FEEDBACK_T), &buf, sizeof(CHATMSG_T));

				write(fd, &backbuf, sizeof(backbuf));//发给发送者

				int sendFd = 0;
				for (it = onlineMap.begin(); it != onlineMap.end(); it++)//寻找需发送的对象的文件描述符
				{
					if (strcmp(buf.receiveName, (*it).first) == 0)
					{
						sendFd = (*it).second;
						write(sendFd, &backbuf, sizeof(backbuf));//发给接收者
						cout << "收到用户 " << buf.sengName << " 发来的消息,并转发给用户 " << buf.receiveName << endl;
						break;
					}
				}
			}
		}
	}
	
}

🙌注意:#include "protocol.h" 这个头文件为文章开头的自定义聊天协议

 😘The end ……🔚

原创不易,转载请标明出处。

对您有帮助的话可以一键三连,会持续更新的(嘻嘻)。