基于Qt的多线程TCP即时通讯软件的设计与实现

基于Qt的多线程TCP即时通讯软件的设计与实现

  • 前言
  • Demo演示
  • 设计与开发
    • 基本技术学习与验证
      • QTcpServer 与 QTcpSocket
        • QTcpServer
        • QTcpSocket
      • 多线程
        • 多线程基础
        • 信号槽与多线程
        • 多线程TCP
    • 通讯设计
    • 客户端设计
      • 界面设计
      • 网络设计
    • 服务器设计
      • 服务器架构设计
  • 打包与部署
  • 缺陷与改进
  • 工程源码与代码仓库
  • 参考源

前言

本文将从涉及到主要技术开始,讲解使用Qt来实现一个支持多客户端链接的多线程TCP服务器及其客户端的设计与实现的解决方案。
注:本文使用的开发环境为Qt5.15.2, 使用MSVC2019_64编译器, C++11及以上

Demo演示

基于Qt的多线程TCP即时通讯软件的设计与实现_第1张图片
基于Qt的多线程TCP即时通讯软件的设计与实现_第2张图片
基于Qt的多线程TCP即时通讯软件的设计与实现_第3张图片


设计与开发

接下来我将会详细讲解客户端和服务端的设计与实现的关键细节。完整的源代码在文章最后。

基本技术学习与验证

QTcpServer 与 QTcpSocket

注:如果你已经熟知Qt的TCP套接字的使用可以跳过这一部分。
Qt的TCP套接字编程相较于传统的套接字编程更加的简单方便。而且,其与Qt自身的信号槽机制和多线程相结合使得异步无阻塞的套接字编程实现更加的简单。如果你了解TCP协议的基本流程,以及传统的TCP套接字编程,那么Qt更加简单的语法将会使你欲罢不能。

首先来解释一下,“为什么有一个QTcpServer,传统的不是只有一个socket吗?”
其实QTcpServer和QTcpSocket都是对原生的socket的封装,只是QTcpServer对服务器接收新连接的socket做了更好的处理。
如果是原生的socket,你一定也会创建两个socket,一个负责监听并处理新的socket接入,另一个根据监听来创建与客户端的TCP连接,使得服务器可以与客户端通讯。如果只有一个socket那么一个客户端接入之后服务端变失去了监听并接受新的连接的效能。
所以,在Qt中直接将这样的两种职能的socket封装成了QTcpServer与QTcpSocket。

QTcpServer

见名知意其是处理TCP套接字的服务器端,与套接字本身并无太大的区别,Qt对其的封装使得你可以更好的实现客户端套接字的接入处理,而不同于传统的套接字编程都是直接使用。通过下面的语句即可创建一个QTcpServer

QTcpServer *tcpServer = new QTcpServer();

创建完成一个服务端的套接字,为了使其监听指定的IP和端口,使用 listen()方法即可

tcpServer->listen(QHostAddress::Any, quint16(8848));

这样便可以监听从 任何IP 发到 本机8848端口 的TCP套接字
不同于传统的socket编程需要先绑定bind()再启动监听listen(),Qt将其封装在一起.

当有新的TCP套接字接入时,即有一个QTcpSocket请求连接时,
相应的QTcpServer将会自动创建一个与客户端连接的QTcpSocket的SocketDescriptor,并发出newConnection()信号。描述符你可以理解成标识码,即每个SocketDescriptor表示了不同的socket连接的具体信息,其本质是一个指针指向了一个QTcpSocket的核心数据块。

使用如下方法,即可让一个空的QTcpSocket完成配置,使其与客户端的socket连接。如果连接成功该QTcpSocket会发出一个connected()信号,客户端相应的socket也会发出同样的信号。

QTcpSocket *tcpSocket = new QTcpSocket();
tcpSocket = tcpServer->nextPendingConnection();

相应的信号槽可以这么写

connect(tcpServer, &QTcpServer::newConnection(), [&](){
	tcpSocket = tcpServer->nextPendingConnection();
});

QTcpSocket

创建一个QTcpSocket,并发出连接请求

QTcpSocket *tcpSocket = new QTcpSocket();
tcpSocket->connectToHost(QHostAddress("127.0.0.1"), quint16(8848));

如果成功,那么tcpSocket将发出connected()信号。
发送数据使用write()方法即可

tcpSocket->write("Hello World!");

QString mesg = "Hello World!";
tcpSocket->write(mesg.toUtf8().data());

当服务器回送消息到达客户端后,客户端相应的QTcpSocket将会发出readyRead()信号,使用readAll()即可读取内容。

connect(tcpSocket, &QTcpSocket::readyRead, [&](){
	QString mesg = tcpSocket->readAll();
});

消息传送完毕后,使用disconnectedFromHost()方法来断开TCP连接,客户端和服务端对应的套接字都会发出disconnected()信号。

tcpSocket->disconnectFromHost();
connect(tcpSocket, &QTcpSocket::disconnected, [&](){
	qDebug() << "Disconnected!";
});

多线程

一个常见的问题,我看到(同时这让我畏缩)人们了解和使用Qt线程和如何对代码做一些他们认为正确的工作。人们展现自己的代码,或用自己的代码写例子,往往最终让我的思维定格在:

你这样做是错误的

—— Bradley T. Hughes:"You’re doing it wrong"

https://blog.csdn.net/hustyangju/article/details/9493485

如果你已经对多线程有了一定的了解请先看看上面这篇文章,然后看看下面这篇转载的文章(没有找到原文,但是这篇转载的质量很高)。如果你跟我一样是实现多线程时遇到了麻烦,那么这两篇文章应该可以解开你的所有疑惑,而不用看完本文余下的内容,就可以继续你自己的实现。
https://blog.csdn.net/lynfam/article/details/7081757
如果你还不甚熟悉Qt的多线程,那就忽略这些,继续看下去。

多线程基础

Qt中的多线程有两种写法,第一种是自定义一个类(例如ChatThread)继承于QThread,重写run()方法来实现。run()就是你希望线程干什么事就写在这里。
例如:

#include 
#include 

class ChatThread : public QThread
{
    Q_OBJECT
public:
    explicit ChatThread(QObject *parent = nullptr);
    void run() override;
signals:
	void showSignal();
};

ChatThread::ChatThread(QObject *parent) : QThread{parent}
{
}

void ChatThread::run()
{
	emit showSignal();
}

这样你就自定义了一个线程,接下来在主线程中,创建线程,并使用start()启动它就行了

#include 

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    ChatThread* chatThread = new ChatThread();
    QObject::connect(chatThread, &ChatThread::showSignal, [&](){
		qDebug() << "ChatThread running!";
	});
	chatThread->start();
    return a.exec();
}

但是,一般不建议使用这种方法。

推荐下面的方法
第二种方法:使用moveToThread()方法
我们认为QThread只是程序的一种控制转移。就是说我们应该将一些复杂的、耗时长的计算、业务处理与GUI界面分离,放入QThread中来正确的使用线程。而不是继承QThread,再在继承的类(例如ChatThread)中实现业务逻辑。

更加正确的写法应该是自定义继承QObject的业务类实现业务逻辑

class ChatBusiness : public QObject
{
    Q_OBJECT
public:
    explicit ChatBusiness(QObject *parent = nullptr);
    void mainBusiness();
signals:
	void startSignal();
};

ChatBusiness::ChatBusiness(QObject *parent) : QThread{parent}
{
	connect(this, &ChatBusiness::startSignal, this, &ChatBusiness::mainBusiness);
}

在需要的地方创建,移入线程中,然后发出信号启动线程

void someFunction()
{
	//some code
	//need that time cost process(ChatBusiness)
	ChatBusiness *chatBusiness = new chatBusiness();
	QThread *thread = new thread();
	chatBusiness->moveToThread(thread);
	emit chatBusiness->startSignal();//start the thread
}

信号槽与多线程

下面这篇文章很好的阐释了信号槽与多线程以及事件循环的相关内容。
Qt 的线程与事件循环:https://blog.csdn.net/lynfam/article/details/7081757
简单来说,

  1. connect()信号槽连接写在哪里不造成任何影响
  2. 信号发出者槽函数在同一个线程时, 采用Qt::DirectConnection,立即直接调用
  3. 信号发出者槽函数不在同一个线程时, 采用Qt::QueuedConnection,异步调用

而我们需要实现的多线程TCP就是属于第二类。

多线程TCP

我们这里只是实现简单的多线程TCP服务器。就是说新的链接接入,就为其创建一个线程单独处理。所以我们需要自定义新链接接入的处理逻辑。
这一点可以通过继承QTcpServer,实现自己的MyTcpServer并重写incomingConnection(qintptr handle)来实现

void MyTcpServer::incomingConnection(qintptr handle)
{
	//create subThreaad
    QThread *thread = new QThread(this);
    ChatBusiness *chatBusiness = new ChatBusiness();
    chatBusiness->moveToThread(thread);
    //def handle of start
    connect(chatBusiness, &ChatBusiness::start, chatBusiness, &ChatBusiness::mainBusiness);
    thread->start();
    //send the SocketDescriptor
    emit chatBusiness->start(handle);
}

void ChatBusiness::mainBusiness(qintptr handle)
{
    QTcpSocket *tcpSocket = new QTcpSocket(this);
    tcpSocket->setSocketDescriptor(handle);
}

上面代码中,在子线程中业务处理类的处理开始是mainBusiness(qintptr handle)方法,其将传递的SocketDescriptor赋给创建的socket

至此你便了解所有的基本核心技术


通讯设计

从功能演示中我们看到,用户通过客户端发送自己的昵称,服务器进行认证注册;用户输入发送的目标,点击发送,服务器转发消息到相应的用户。其通讯流程如下图设计:
基于Qt的多线程TCP即时通讯软件的设计与实现_第4张图片
由于是直接使用的TCP通讯,而不是应用层协议,需要自己根据需要设计通讯格式。
在本程序中通讯格式设计成:

服务器回送系统消息:
[username,15]@[Server,15] [ServerMesgType,20]

ServerMesgType 解释
RegisterSuccessful 名称注册成功
RegisterFailed 名称注册失败,重名
TargetOffline 接收方不在线

服务器转发用户消息:
[targetName,15]@[username,15] [Mesg]

用户发送系统消息(注册名称):
[username,15]@[Server,15] [ServerMesgType,20]

ServerMesgType 解释
Register 请求注册名称

客户端设计

界面设计

登录窗口
基于Qt的多线程TCP即时通讯软件的设计与实现_第5张图片
聊天主窗口
基于Qt的多线程TCP即时通讯软件的设计与实现_第6张图片
此外为了保证UI显示在不同分辨率的显示器、不同的缩放比例下有着较好的适配,我们需要如下设置

添加新的qrc基于Qt的多线程TCP即时通讯软件的设计与实现_第7张图片
基于Qt的多线程TCP即时通讯软件的设计与实现_第8张图片
然后在文件夹中创建 /etc/qt.conf 文件
基于Qt的多线程TCP即时通讯软件的设计与实现_第9张图片
qt.conf 文件中添加如下内容

[Platforms]
WindowsArguments = dpiawareness=0
WindowsArguments = fontengine=freetype

第一行是让程序的缩放程度跟随系统控制
第二行是使用freetype字体,使得缩放时的字体不会出现明显的锯齿

同时在main.cpp文件中添加,来使得缩放时图像不会有锯齿

QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);

基于Qt的多线程TCP即时通讯软件的设计与实现_第10张图片


网络设计

客户端的网络实现比较简单,只需要使用QTcpSocket。实现其readyRead(),connected(),disconnected()信号的处理即可。
这里不再赘述。


服务器设计

无论从功能需要还是性能要求,服务端做成控制台应用即可,无需使用Qt的界面。

服务器架构设计

基于Qt的多线程TCP即时通讯软件的设计与实现_第11张图片
即:

  • MyTcpServer作为服务器主要的调度,接受新的链接、注册名称、转发消息
  • 重写M有TCPServer的incomingConnection(qintptr handle)方法,创建ChatBusiness业务对象,并移入子线程启动
  • ChatBusiness创建对应的MyTcpSocket套接字与客户端连接
  • 实现自己的MyTcpSocket使其之间内部就可以处理消息的发送,转发系统请求到ChatBusiness


这样客户端和服务器的设计和实现细节便全部描述完毕。你可以一次尝试自己开发,或者在文章的最后部分下载工程源码

打包与部署

在完成编码后我们需要测试,或者给朋友玩玩之类,就需要对程序打包部署。
首先我们以Release模式重新构建项目,然后将构建好的对应的.exe文件拷贝到一个单独的文件夹
例如.../Client/client-Demo.exe

然后在Qt的 安装目录 找到项目使用的对应的 编译器文件夹
例如:
基于Qt的多线程TCP即时通讯软件的设计与实现_第12张图片
确认有该工具
基于Qt的多线程TCP即时通讯软件的设计与实现_第13张图片
在该目录打开命令行,输入.\windeployqt.exe Client.exe的完整路径,例如

 .\windeployqt.exe C:\Client\Client-Demo2.exe

之后使用Enigma Virtual Box打包即可,如果在其他电脑上不能运行。。。那就根据提示再打包。例如提示找不到xxxx.dll,就把该dll放到Client.exe同级文件夹下,再使用Enigma Virtual Box打包。
嗯,是的,感觉很是愚蠢。但是这是相对来说比较省事的方法了。

如果涉及Qt之外的库,那就需要用depends.exe来查找依赖了。
可以参考这篇文章:QT+Opencv 程序打包发布:https://blog.csdn.net/qq_43599883/article/details/106251915


缺陷与改进

  1. 客户端交互体验较差。没有Enter快捷键;无法切换聊天窗口等。
  2. 客户端界面不够美观。没有使用QSS等进行界面设计。
  3. 无法传输图像等多媒体数据。
  4. 服务器性能设计不足。一个socket一个线程有点过于奢侈。
  5. 服务器没有存储消息等数据存储能力,应该与数据库结合实现。
  6. 明文传输数据。应当考虑使用https传输,或者自己实现AES对称加密消息,RSA加密密钥来传输。
  7. TCP连接链路拥塞。应当使用QTimer来发送“心跳”包确认链路通畅,以避免连接中断而disconnected()信号没有发送的情况。
  8. 开发中没有使用Github等工具合理的管理程序版本。
  9. 实现过程不够规范。缺少对类和类之间关系的建模、文档模型化,导致编写时常常忘记链接信号槽、实现相应的槽函数等。(身为软件工程出身这太不应该,欲速则不达〒▽〒)
  10. 客户端连接接服务器的IP不可变化。
  11. 软件部署方式难以更新。
  12. 额,欢迎提出建议…

部分内容已经在下一个版本的开发日程中,之后会在本博客中更新新的工程源码,欢迎持续关注。ヾ(≧▽≦*)o


工程源码与代码仓库

版本1:https://github.com/SWULWJ/AirChat
版本2:待定


参考源

QT 使用全局缩放进行全分辨率适配(QT_SCALE_FACTOR):https://blog.csdn.net/u014410266/article/details/107488789
Qt Windows高清DPI自适应分辨率缩放:https://blog.csdn.net/startl/article/details/105862817
Qt 的线程与事件循环:https://blog.csdn.net/lynfam/article/details/7081757
Qt多线程中的信号与槽:https://blog.csdn.net/qq_29344757/article/details/78136829
TCP_UDP_Assistant.zip (一个很厉害的例子):https://download.csdn.net/download/yxy244/12030948
QT+Opencv 程序打包发布:https://blog.csdn.net/qq_43599883/article/details/106251915
Qt应用程序在windows和Linux操作系统下的打包发布:https://blog.csdn.net/qq_41488943/article/details/104963916

RSA算法原理:https://zhuanlan.zhihu.com/p/48249182
AES加密算法的详细介绍与实现:https://blog.csdn.net/qq_28205153/article/details/55798628


低劣的转载真的太多了。。。


感谢阅读!

有疑问或者认为有错误请留言,谢谢!

你可能感兴趣的:(项目作品,qt,tcp/ip,网络)