必要介绍
在Qt中使用TCP通信功能,需要在qmake中加载网络模块:
QT+=network
QTcpServer和QTcpSocket的基类都继承自QObject,意味着我们在自定义组件时可以基于它们作为基类延申UI库的各种功能,也能将其封装至Item作为可视化的互动组件。
在QTcpServer中,有以下重要的成员函数:
void listen(const QHostAddress &address=QHostAddress::Any,quint16 port=0);
//开启监听功能,address用于定位主机,QHostAddress::Any表示监听连入的IPv4和IPv6网络,port指定一个端口。
QTcpSocket* QTcpServer::nextPendingConnection();
//用于接收一个与新连入的客户端通信的套接字。
bool QTcpServer::waitForNewConnection(int msec=0,bool * timedOut=Q_NULLPTR);
//阻塞式等待新连接接入,但一般推荐使用信号来检测新连接。
//msec是最大阻塞时长,timedOut为超时时的传出参数,当超时时改变为true。
void QTcpServer::newConnection();
//信号,当新客户端的socket接入时触发。
在QTcpSocket中,有以下重要的成员函数:
virtual void QAbstractSocket::connectToHost(const QString&hostName,quint16 port,OpenMod openMod=ReadWrite,NetworkLayerProtocol=AnyIPProtocol);
//hostName为目标服务器的地址或主机名,port为连入的端口,openMod为获取到的对应内存的权限,一般来说后两个默认即可。
void read(char*data,qint64 maxSize);
QByteArray read(qint64 maxSize);
void write(const char*data,qint64 maxSize);
void write(const QByteArray & byteArray);
//Qt框架维护了一块内存用于收发网络中的数据,进行的读写操作都是对这块内存的操作。
void connected();
//信号,表示成功连接上。
void disconnected();
//信号,表示断开了连接。
void readRead();
//信号,表示该socket的Read内存中已准备好读取的数据。(数据到达时触发)
通信流程如下:
-
服务器端:
创建套接字服务器QTcpServer对象。
通过QTcpServer对象设置监听,即listen();
基于newConnection信号检测是否有新的客户端连接。
当有新连接时,调用nextPandingConnection()得到新的套接字。
通过该socket与对应客户端进行通信。
-
客户端:
创建通信类套接字QTcpsocket对象。
使用服务器端绑定的IP和端口进行连接,即connectToHost();
使用QTcpSocket进行通信。
示例项目:
项目地址:https://github.com/AuraKaliya/QT_practice/tree/main
一个简易的双端通信示例
这是一个基于Qt的TCP通信模块的简易的双端通信的示例。
服务器端:
①利用UI设计师搭建UI界面
②在pro文件中添加网络模块,在必要头文件中添加<QTcpServer>
③设置逻辑-监听:获取端口号、开始监听。
④设置逻辑-信号:newConnection信号、readReady信号、disconnected信号。
//mainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QLabel>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
#pragma execution_character_set("utf-8")
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_listenStart_clicked();
void on_sendMessage_clicked();
private:
Ui::MainWindow *ui;
QTcpServer* m_server;
QTcpSocket* m_socket=nullptr;
QLabel * m_status;
};
#endif // MAINWINDOW_H
//mainWindow.coo
#include "mainwindow.h"
#include "ui_mainwindow.h"
#pragma execution_character_set("utf-8")
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->port->setText("9999");
setWindowTitle("服务器");
m_server=new QTcpServer(this);
m_status=new QLabel;
connect(m_server,&QTcpServer::newConnection,this,[=](){
m_socket=m_server->nextPendingConnection();
m_status->setPixmap(QPixmap(":/f2.png").scaled(QSize(20,20)));
//检测是否可接收数据
connect(m_socket,&QTcpSocket::readyRead,this,[=](){
QByteArray data=m_socket->readAll();
ui->recalled->append("客户端say:"+QString(data));
});
connect(m_socket,&QTcpSocket::disconnected,this,[=](){
m_socket->close();
m_socket->deleteLater();
m_socket=nullptr;
});
});
//状态栏添加
m_status->setPixmap(QPixmap(":/f1.png").scaled(QSize(20,20)));
ui->statusbar->addWidget(new QLabel("连接状态:"));
ui->statusbar->addWidget(m_status);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_listenStart_clicked()
{
unsigned short port=ui->port->text().toUShort();
m_server->listen(QHostAddress::Any,port);
ui->listenStart->setDisabled(true);
}
void MainWindow::on_sendMessage_clicked()
{
if(m_socket!=nullptr)
{
QString msg=ui->sendInfo->toPlainText();
m_socket->write(msg.toUtf8());
ui->recalled->append("服务器say:"+msg);
}
}
客户端:
①搭建UI界面。
②内置一个QTcpSocket作为通信套接字。
③绑定处理逻辑:readReady、disconnected、connected等
④进行初始化设置。
//mainWidow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpSocket>
#include <QLabel>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
#pragma execution_character_set("utf-8")
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_sendMessage_clicked();
void on_LinkBtn_clicked();
void on_disLinkBtn_clicked();
private:
Ui::MainWindow *ui;
QTcpSocket* m_socket;
QLabel * m_status;
};
#endif // MAINWINDOW_H
//mainWindw.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QHostAddress>
#pragma execution_character_set("utf-8")
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->port->setText("9999");
ui->IP->setText("127.0.0.1");
setWindowTitle("客户端");
m_socket=new QTcpSocket(this);
m_status=new QLabel;
connect(m_socket,&QTcpSocket::readyRead,this,[=](){
QByteArray data=m_socket->readAll();
ui->recalled->append("【服务器】"+data);
});
connect(m_socket,&QTcpSocket::disconnected,this,[=](){
m_status->setPixmap(QPixmap(":/f1.png").scaled(QSize(20,20)));
ui->LinkBtn->setDisabled(false);
ui->disLinkBtn->setDisabled(true);
ui->recalled->append("已断开连接...");
});
connect(m_socket,&QTcpSocket::connected,this,[=](){
m_status->setPixmap(QPixmap(":/f2.png").scaled(QSize(20,20)));
ui->LinkBtn->setDisabled(true);
ui->disLinkBtn->setDisabled(false);
ui->recalled->append("已成功连接...");
});
//状态栏添加
m_status->setPixmap(QPixmap(":/f1.png").scaled(QSize(20,20)));
ui->statusbar->addWidget(new QLabel("连接状态:"));
ui->statusbar->addWidget(m_status);
//默认断连状态
ui->disLinkBtn->setDisabled(true);
ui->LinkBtn->setDisabled(false);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_sendMessage_clicked()
{
QString msg=ui->sendInfo->toPlainText();
m_socket->write(msg.toUtf8());
ui->recalled->append("【我】:"+msg);
}
void MainWindow::on_LinkBtn_clicked()
{
QString ip=ui->IP->text();
unsigned short port=ui->port->text().toUShort();
m_socket->connectToHost(QHostAddress(ip),port);
}
void MainWindow::on_disLinkBtn_clicked()
{
m_socket->disconnectFromHost();
}
-
注意事项:
端口号为unsigned short 无符号短整型
IP为QString,用QHostAddress(ip)进行构造转换。
断连时使用disconnectedFromHost(),会发送一个disconnected信号,方便进行后续处理。
一个简易的文件传输示例
这是对TCP协议的一个应用。
额外增加的处理流程:服务器端接收文件,客户端发送文件,进行多线程处理。
- 客户端发送流程:
①QFileDialog获取文件位置,QFile打开文件描述符,QFileInfo获取文件信息。
②先发送格式化的文件必要信息,再发送格式化的文件大小。
③基于文件大小和当前发送数据包大小的积累进行数据传输。
//对客户端采用moveToThread方式进行多线程文件发送处理。
//sendFile:
void SendFile::sendFile(QString path)
{
QFile file(path);
QFileInfo info(path);
int fileSize=info.size();
QString fileName=info.fileName();
fileName.resize(100,'\0');
file.open(QFile::ReadOnly);
m_socket->write(fileName.toUtf8(),100);
m_socket->write((char*)&fileSize,4);
int num=0;
while(!file.atEnd())
{
QByteArray line= file.readLine();
num+=line.size();
int percent=(num*100/fileSize);
emit curPercent(percent);
m_socket->write(line);
}
}
- 服务器端接收流程:
①建立用于处理文件相关数据的临时变量。
②获取文件信息,进行格式化处理,如对获取的文件名的数据进行处理。
③获取文件大小。
④基于文件大小进行数据读取,并将数据写入文件。
//对服务器端采用继承自QThread方式进行文件接收处理
//run:
void RecvFile::run()
{
if(m_socket!=nullptr)
{
connect(m_socket,&QTcpSocket::readyRead,this,[=](){
static QFile *file=nullptr;
static int count=0;
static int total=0;
if(count == 0)
{
QString fileName(m_socket->read(100));
fileName.truncate(fileName.indexOf(QChar::Null));
m_socket->read((char*)&total,4);
file=new QFile(fileName);
file->open(QFile::WriteOnly);
}
QByteArray all=m_socket->readAll();
count+=all.size();
file->write(all);
if(count==total)
{
m_socket->close();
m_socket->deleteLater();
m_socket=nullptr;
file ->close();
file->deleteLater();
emit over();
}
});
}
//进入阻塞状态等待事件到达(进入事件循环)
exec();
}
-
解决QTcpServer不能在子线程中建立socket的问题:
①继承自QTcpServer,重写incommingConnection函数,该函数的作用时接收一个文件描述符。
②将接收的文件描述符通过信号转发出去,使用信号与槽机制中的队列连接方式。
③采用信号连接替换newConnection,创建新的socket,并将fd设置给socket,这部分需要在子线程中进行创建。
多客户端连接模型
使用容器容纳新连接的客户端指针建立的多客户端选择性通信的模型。
改变内容:新增容器、新连接接入时的逻辑处理。
服务器端:
connect(m_server,&QTcpServer::newConnection,this,[=](){
QTcpSocket* socket=m_server->nextPendingConnection();
m_socketList.append(socket);
//SocketWidget socketWidget(socket);
ui->socketList->addItem(QString::number(QRandomGenerator::global()->bounded(200)));
connect(ui->socketList,&QListWidget::currentRowChanged,this,[=](int row){
m_nowSocket=m_socketList[row];
ui->NowSocket->setText("NowSocket:"+ui->socketList->currentItem()->text());
});