学习目标: TCP网络通信编程
前置环境
运行环境:qt creator 4.12
学习内容
一、TCP 协议基础知识:
- TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议。
- TCP 拥塞控制算法包括慢启动、拥塞避免、快速重传和快速恢复。
- TCP 通信需要建立连接,Qt 提供 QTcpSocket 和 QTcpServer 类用于 TCP 客户端和服务器编程。
- QTcpServer 类用于建立网络监听和 socket 连接,主要接口函数如 listen()、newConnection() 等。
QTcpServer* tcpServer; 它负责监听指定的 IP 地址和端口,并在有新的客户端连接时发出 newConnection() 信号。
QTcpSocket* tcpSocket; 它代表一个与服务端建立的 TCP 连接。
QTcpServer类
QTcpServer 是 Qt 框架提供的 TCP 服务器类,它提供了一系列常用的成员函数和信号,用于实现 TCP 服务器的基本功能。以下是 QTcpServer 的一些常用成员函数:
-
构造函数和析构函数:
QTcpServer(QObject *parent = nullptr)
:创建一个 QTcpServer 对象。~QTcpServer()
:析构函数,用于释放资源。
-
服务器状态控制:
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
:开始监听指定的地址和端口。void close()
:停止监听并关闭服务器。-
abort()是强行立即关闭连接,相当于调用disconnectFromHost()。关闭后会释放所有相关资源并触发 QAbstractSocket::abort信号。对端可能不知道连接已经关闭。
-
close()是通过标准关闭序列完成关闭,等待对端确认,保证数据完整性。它会优雅地关闭socket连接,先发送关闭请求给对端,并等待对端确认。然后会进入关闭状态,触发QAbstractSocket::stateChanged(QAbstractSocket::ClosingState)和QAbstractSocket::disconnected信号。
-
bool isListening() const
:检查服务器是否正在监听。
-
获取服务器信息:
QHostAddress serverAddress() const
:返回服务器绑定的地址。quint16 serverPort() const
:返回服务器绑定的端口。QAbstractSocket::SocketError socketError() const
:返回最近一次发生的套接字错误。QString errorString() const
:返回最近一次发生的错误的描述字符串。
-
新连接管理:
QTcpSocket *nextPendingConnection()
:返回下一个待处理的连接。int hasPendingConnections() const
:返回待处理连接的数量。
-
信号处理:
void newConnection()
:当有新的连接到达时发出此信号。void acceptError(QAbstractSocket::SocketError socketError)
:当接受新连接时发生错误时发出此信号。
-
其他功能:
void setMaxPendingConnections(int numConnections)
:设置服务器可以同时处理的最大待处理连接数。int maxPendingConnections() const
:返回服务器的最大待处理连接数。void setSocketOption(QAbstractSocket::SocketOption option, const QVariant &value)
:设置套接字选项。QVariant socketOption(QAbstractSocket::SocketOption option) const
:获取套接字选项的当前值。
-
地址和端口设置:
void setAddress(const QHostAddress &address)
:设置服务器监听的地址。QHostAddress address() const
:返回服务器监听的地址。void setPort(quint16 port)
:设置服务器监听的端口。quint16 port() const
:返回服务器监听的端口。
-
SSL/TLS 支持:
void setSecureMode(QSsl::SslMode mode)
:设置服务器的 SSL/TLS 模式。QSsl::SslMode secureMode() const
:返回服务器的 SSL/TLS 模式。void setLocalCertificate(const QSslCertificate &certificate)
:设置服务器的本地证书。QSslCertificate localCertificate() const
:返回服务器的本地证书。void setPrivateKey(const QSslKey &key)
:设置服务器的私钥。QSslKey privateKey() const
:返回服务器的私钥。
-
线程安全:
void setThreadPool(QThreadPool *threadPool)
:设置用于处理新连接的线程池。QThreadPool *threadPool() const
:返回用于处理新连接的线程池。
-
日志记录:
void setProxy(const QNetworkProxy &proxy)
:设置代理服务器。QNetworkProxy proxy() const
:返回当前使用的代理服务器。
这些成员函数提供了更多的灵活性和控制能力,使开发者能够根据具体需求配置和管理 TCP 服务器。例如,可以设置 SSL/TLS 模式以提供安全的通信,使用线程池来提高并发处理能力,以及设置代理服务器以实现更复杂的网络拓扑。
QTcpServer
类的使用:
QTcpServer
是从QObject
继承的类,用于服务器建立网络监听和创建网络 socket 连接。- 主要接口函数包括
listen()
、nextPendingConnection()
、serverAddress()
和serverPort()
等。
服务器Server提供的回调函数
在 Qt TCP 编程中,主要提供了以下几种重要的回调接口:
-
QTcpServer
相关的回调:QTcpServer::newConnection()
: 当有新的客户端连接到达时触发该信号。QTcpServer::acceptError(QAbstractSocket::SocketError socketError)
: 当服务器无法接受新的连接时触发该信号,可以获取错误信息。
-
QTcpSocket
相关的回调:QTcpSocket::connected()
: 当套接字成功连接到远程主机时触发该信号。QTcpSocket::disconnected()
: 当套接字断开连接fin 位 为1时触发该信号。QTcpSocket::readyRead()
: 当套接字有新数据可读时触发该信号。QTcpSocket::bytesWritten(qint64 bytes)
: 当成功写入数据到套接字时触发该信号,并返回写入的字节数。QTcpSocket::error(QAbstractSocket::SocketError socketError)
: 当套接字发生错误时触发该信号,可以获取错误信息。QTcpSocket::stateChanged(QAbstractSocket::SocketState socketState)
: 当套接字的连接状态发生变化时触发该信号。-
bytesWritten(qint64 bytes)
信号:- 当成功写入数据到
QTcpSocket
时会触发此信号。 bytes
参数表示成功写入的字节数。- 可以用来监控数据发送的进度。
- 当成功写入数据到
-
aboutToClose()
信号:- 当
QTcpSocket
将要关闭时会触发此信号。 - 可以在此信号的槽函数中执行一些数据刷新或保存操作。
- 当
-
hostFound()
信号:- 当
QTcpSocket
成功解析了主机地址时会触发此信号。 - 可以用来监控 DNS 解析的进度。
- 当
-
QSslSocket
相关的回调(用于 SSL/TLS 连接):QSslSocket::sslErrors(const QList<QSslError> &errors)
: 当 SSL/TLS 连接发生错误时触发该信号。QSslSocket::encrypted()
: 当 SSL/TLS 连接成功加密时触发该信号。
-
QNetworkProxy
相关的回调:QTcpSocket::proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator)
: 当需要代理服务器认证时触发该信号。
这些回调接口涵盖了 TCP 连接的整个生命周期,包括连接建立、数据交互、连接断开以及各种错误情况。通过监听和处理这些信号,我们可以更好地控制和管理 TCP 连接,提高应用程序的可靠性和健壮性。
QT TCP网络通信编程项目
TCP服务端代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle("TCP通讯服务端");
//获取主机名和ip地址
QHostInfo info =QHostInfo::fromName(QHostInfo::localHostName());
QList<QHostAddress> addrs=info.addresses();
if(!addrs.empty()){
foreach(const QHostAddress & addr,addrs){
if(addr.protocol()==QAbstractSocket::IPv4Protocol){
ui->serverip->addItem(addr.toString());
}
}
}
tcpServer=new QTcpServer(this);
//注册新连接回调 表示有新的客户端连接到达服务器。
connect(tcpServer,&QTcpServer::newConnection,this,&MainWindow::ConnectCallback);
}
void MainWindow::ConnectCallback(){ //连接到达
tcpSocket = tcpServer->nextPendingConnection();
//连接成功后回调 表示当前 TCP 连接已经成功建立。
auto clientconnect=[this](){
// 客户端连接
ui->plainTextEdit->appendPlainText("**********客户端socket连接成功**********");
ui->plainTextEdit->appendPlainText("**********peer address:"+tcpSocket->peerAddress().toString());
ui->plainTextEdit->appendPlainText("**********peer port:"+QString::number(tcpSocket->peerPort()));
};
connect(tcpSocket,&QTcpSocket::connected,this,clientconnect);
clientconnect(); //因为当前连接已经到达了 所有手动调用一次
//读前回调 当 tcpSocket 有数据可读时,该信号会被触发
connect(tcpSocket,&QTcpSocket::readyRead,this,[this](){
while(tcpSocket->canReadLine()){
QString result = "ip:%1 prot:%2 in:%3 ";
result = result.arg(tcpSocket->peerAddress().toString())
.arg(QString::number(tcpSocket->peerPort()))
.arg(QString(tcpSocket->readLine()));
ui->plainTextEdit->appendPlainText(result);
}
});
//错误回调 当 tcpSocket 发生错误时,该信号会被触发
connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),this, [this](QAbstractSocket::SocketError error) {
// ui->plainTextEdit->appendPlainText("Socket error:" + error + tcpSocket->errorString());
});
//当套接字的连接状态发生变化时,该信号会被触发,并且会传递一个 QAbstractSocket::SocketState 类型的参数,表示当前的连接状态。
connect(tcpSocket,QOverload<QAbstractSocket::SocketState>::of(&QTcpSocket::stateChanged),this,
[this](QTcpSocket::SocketState state){
// ui->plainTextEdit->appendPlainText("套接字状态变更:" + state);
});
//断开连接 fin位确认 回调
connect(tcpSocket,&QTcpSocket::disconnected,this,[this](){
// 客户端断开连接
QString result = "**********客户端socket断开连接[ip:%1,prot:%2,]";
result = result.arg(tcpSocket->peerAddress().toString())
.arg(QString::number(tcpSocket->peerPort()));
ui->plainTextEdit->appendPlainText(result);
tcpSocket->deleteLater();
});
}
MainWindow::~MainWindow()
{
delete ui;
if(tcpServer->isListening()){
// 关闭TCP服务器
tcpServer->close();
tcpServer->deleteLater(); //最终关闭
qDebug() << "TCP server MainWindow.";
}
}
void MainWindow::closeEvent(QCloseEvent *e){ //关闭窗口
//关闭close
MainWindow::on_stopServer_clicked();
e->accept(); //允许窗口完成关闭操作。
}
void MainWindow::on_startServer_clicked()
{
QString ip(ui->serverip->currentText());
uint16_t port =ui->serverport->value();
tcpServer->listen(QHostAddress(ip),port);
ui->plainTextEdit->appendPlainText("$$$$$$$$$$开始监听$$$$$$$$$$");
ui->plainTextEdit->appendPlainText("服务器地址:"+tcpServer->serverAddress().toString());
ui->plainTextEdit->appendPlainText("服务器端口:"+QString::number(tcpServer->serverPort()));
ui->startServer->setEnabled(false);
ui->stopServer->setEnabled(true);
}
void MainWindow::on_stopServer_clicked() //关闭tcpserver
{
//先关闭所有socket
if(!tcpSocket){
tcpSocket->disconnect(); //用于断开 QTcpSocket 对象的所有信号与槽的连接。
tcpSocket->close(); //它会向对端发送 FIN 数据包,并等待对端的确认,完成 TCP 连接的正常关闭过程。
//fin回调 已调用 tcpSocket->deleteLater(); //它不会立即删除对象,而是将其标记为待删除状态,等到当前事件循环结束后再执行删除操作。
}
if(tcpServer->isListening())
{
tcpServer->close();
//不调用 deleteLater 为了下次再次开启
ui->startServer->setEnabled(true);
ui->stopServer->setEnabled(false);
ui->plainTextEdit->clear();
}
}
void MainWindow::on_sendmsg_clicked()
{
QString msg =ui->lineEdit->text();
ui->lineEdit->clear();
ui->plainTextEdit->appendPlainText("[out]:"+msg);
tcpSocket->write(msg.toUtf8()+'\n');
}
TCP客户端代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
setWindowTitle("tcp通信客户端");
QHostInfo info =QHostInfo::fromName(QHostInfo::localHostName());
QList<QHostAddress> addrs =info.addresses();
if(!addrs.empty())
{
foreach(const QHostAddress& addr ,addrs){
if(addr.protocol() == QAbstractSocket::IPv4Protocol){
ui->serverip->addItem(addr.toString());
}
}
}
client = new QTcpSocket();
//当 socket 成功连接到服务器时,会发射 connected() 信号。
connect(client,&QTcpSocket::connected,this,[this](){
ui->plainTextEdit->appendPlainText("**********已经连接到服务器端**********");
ui->plainTextEdit->appendPlainText("服务器端ip:"+client->peerAddress().toString());
ui->plainTextEdit->appendPlainText("服务器端port:"+QString::number(client->peerPort()));
ui->tcpconnect->setEnabled(false);
ui->tcpclose->setEnabled(true);
});
//当 socket 与服务器断开连接时,会发射 disconnected() 信号。
connect(client,&QTcpSocket::disconnected,this,[this](){
ui->plainTextEdit->appendPlainText("**********已断开与服务器端的连接**********");
client->close();
ui->tcpconnect->setEnabled(true);
ui->tcpclose->setEnabled(false);
});
//当 socket 有新的数据可读时,会发射 readyRead() 信号。
connect(client,&QTcpSocket::readyRead,this,[this](){
while(client->canReadLine()){
ui->plainTextEdit->appendPlainText("[in]:"+client->readLine());
}
});
}
MainWindow::~MainWindow()
{
delete ui;
}
\
void MainWindow::on_send_clicked()
{
QString msg=ui->lineEdit->text();
ui->plainTextEdit->appendPlainText("[out]:"+msg);
ui->lineEdit->clear();
client->write(msg.toUtf8()+'\n');
}
void MainWindow::on_tcpconnect_clicked()
{
QString ip=ui->serverip->currentText();
quint16 port =ui->serverport->value();
client->connectToHost(ip,port);
if (!client->waitForConnected(3000)) {// 5s 连接超时处理逻辑
ui->plainTextEdit->appendPlainText("连接超时,请检查服务器ip和port是否正确.");
}
}
void MainWindow::on_tcpclose_clicked()
{
if(client->state() ==QAbstractSocket::ConnectedState) client->disconnectFromHost(); //这里首先检查 tcpclient 对象的当前连接状态。
ui->tcpconnect->setEnabled(true);
ui->tcpclose->setEnabled(false);
}
// 在应用程序退出或客户端断开连接时 点关闭窗口
void MainWindow::closeEvent(QCloseEvent* event) {
// 1. 断开与服务器的连接
if (client->state() == QAbstractSocket::ConnectedState) {
client->disconnectFromHost();
}
qDebug()<<"closeEvent";
// 2. 释放 QTcpSocket 对象
client->deleteLater();
// 允许窗口关闭
event->accept();
}
总结
server端
-
TCP 服务器的创建和启停:
- 在构造函数中创建
QTcpServer
对象,并连接newConnection()
信号到ConnectCallback
函数。 on_startServer_clicked()
函数中,监听指定的 IP 和端口,启动 TCP 服务器。on_stopServer_clicked()
函数中,关闭 TCP 服务器,断开所有客户端连接。- 在主窗口关闭时,调用
on_stopServer_clicked()
函数关闭服务器。
- 在构造函数中创建
-
客户端连接的处理:
- 在
ConnectCallback
函数中,获取新连接的QTcpSocket
对象。 - 连接客户端连接成功、数据可读、错误、状态变更、断开连接等信号。
- 在信号处理函数中,输出连接信息并处理数据收发。
- 在
-
数据收发和协议处理:
- 连接
readyRead()
信号,处理客户端发送的数据。 - 使用
tcpSocket->readLine()
读取并解析数据,输出到 UI 界面。 on_sendmsg_clicked()
函数中,通过tcpSocket->write()
将消息发送给客户端。
- 连接
-
异常处理:
- 连接
error()
信号,处理套接字错误。 - 连接
stateChanged()
信号,监控连接状态变更。
- 连接
-
生命周期管理:
- 在主窗口析构函数中,关闭 TCP 服务器并释放资源。
- 在客户端断开连接时,释放
QTcpSocket
对象。-
如果需要立即关闭连接而不管数据完整性,使用abort()。
-
如果需要负责任关闭保证数据完整,则使用close()。
-
通过学习这段代码,我们可以掌握以下 Qt TCP 编程的关键点:
- 如何创建 TCP 服务器并监听端口。
- 如何处理新的客户端连接,并与之进行数据通信。
- 如何处理连接错误和状态变更。
- 如何优雅地关闭服务器并释放资源。
客户端
-
TCP 客户端的创建和连接:
- 在构造函数中创建
QTcpSocket
对象client
。 - 连接
connected()
信号,处理与服务器成功连接的情况。 on_tcpconnect_clicked()
函数中,调用connectToHost()
连接到服务器。- 使用
waitForConnected()
处理连接超时的情况。
- 在构造函数中创建
-
数据收发和协议处理:
- 连接
readyRead()
信号,处理服务器发送的数据。 - 使用
client->readLine()
读取并解析数据,输出到 UI 界面。 on_send_clicked()
函数中,通过client->write()
将消息发送给服务器。
- 连接
-
连接状态管理:
- 连接
disconnected()
信号,处理与服务器的断开连接。 - 在
closeEvent()
中,检查连接状态并断开连接。 - 更新 UI 按钮的状态,反映当前的连接状态。
- 连接
-
异常处理:
- 在连接超时的情况下,输出错误提示信息。
- 在
closeEvent()
中,释放QTcpSocket
对象。
通过学习这段代码,我们可以掌握以下 Qt TCP 客户端编程的关键点:
- 如何创建 TCP 客户端并连接到服务器。
- 如何处理数据的收发,并按照约定的协议进行解析。
- 如何管理连接状态,处理连接成功、断开等情况。
- 如何优雅地关闭连接并释放资源。
最后附上源代码链接
对您有帮助的话,帮忙点个star
35-tcpSocket-client · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)
35-tcpSocket-server · jbjnb/Qt demo - 码云 - 开源中国 (gitee.com)