Qt 是一個跨平臺C++圖形介面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以透過拖拽的方式將不同元件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹如何運用QTcpSocket
元件實現基於TCP的網路通訊功能。
QTcpSocket
和QTcpServer
是Qt中用於實現基於TCP(Transmission Control Protocol)通訊的兩個關鍵類。TCP是一種面向連線的協議,它提供可靠的、雙向的、面向位元組流的通訊。這兩個類允許Qt應用程式在網路上建立客戶端和伺服器之間的連線。
以下是QTcpSocket
類的一些常用函式:
函式 | 描述 |
---|---|
QTcpSocket() |
建構函式,建立一個新的QTcpSocket 物件。 |
~QTcpSocket() |
解構函式,釋放QTcpSocket 物件及其資源。 |
void connectToHost(const QString &hostName, quint16 port) |
嘗試與指定主機名和埠建立連線。 |
void disconnectFromHost() |
斷開與主機的連線。 |
QAbstractSocket::SocketState state() const |
返回套接字的當前狀態。 |
QHostAddress peerAddress() const |
返回與套接字連線的遠端主機的地址。 |
quint16 peerPort() const |
返回與套接字連線的遠端主機的埠。 |
QAbstractSocket::SocketError error() const |
返回套接字的當前錯誤程式碼。 |
qint64 write(const char *data, qint64 maxSize) |
將資料寫入套接字,返回實際寫入的位元組數。 |
qint64 read(char *data, qint64 maxSize) |
從套接字讀取資料,返回實際讀取的位元組數。 |
void readyRead() |
當套接字有可供讀取的新資料時發出訊號。 |
void bytesWritten(qint64 bytes) |
當套接字已經寫入指定位元組數的資料時發出訊號。 |
void error(QAbstractSocket::SocketError socketError) |
當套接字發生錯誤時發出訊號。 |
以下是QTcpServer
類的一些常用函式及其簡要解釋:
函式 | 描述 |
---|---|
QTcpServer() |
建構函式,建立一個新的QTcpServer 物件。 |
~QTcpServer() |
解構函式,釋放QTcpServer 物件及其資源。 |
bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0) |
開始監聽指定的地址和埠。 |
void close() |
停止監聽並關閉伺服器。 |
bool isListening() const |
返回伺服器是否正在監聽連線。 |
QList<QTcpSocket*> pendingConnections() |
返回等待處理的掛起連線的列表。 |
virtual void incomingConnection(qintptr socketDescriptor) |
當有新連線時呼叫,可以在子類中實現以處理新連線。 |
void maxPendingConnections() const |
返回允許的最大掛起連線數。 |
void setMaxPendingConnections(int numConnections) |
設定允許的最大掛起連線數。 |
QNetworkProxy proxy() const |
返回伺服器的代理設定。 |
void setProxy(const QNetworkProxy &networkProxy) |
設定伺服器的代理設定。 |
QAbstractSocket::SocketError serverError() const |
返回伺服器的當前錯誤程式碼。 |
QString errorString() const |
返回伺服器的錯誤訊息字串。 |
void pauseAccepting() |
暫停接受新連線,但保持現有連線。 |
void resumeAccepting() |
恢復接受新連線。 |
void close() |
關閉伺服器。 |
如上這些只是常用函式的簡要描述,詳細的函式說明和用法可以參考Qt官方文件或相關文件。
1.1 通訊的流程
1.1.1 服務端流程
在使用TCP通訊時同樣需要匯入Qt+=network
模組,並在標頭檔案中引入QTcpServer
和QTcpSocket
兩個模組,當有了模組的支援,接著就是偵聽套接字,此處可透過呼叫server.listen
來實現偵聽,此函式原型如下;
bool QTcpServer::listen(
const QHostAddress &address = QHostAddress::Any,
quint16 port = 0
);
這個函式用於開始在指定的地址和埠上監聽連線。它的引數包括:
address
:一個QHostAddress
物件,指定要監聽的主機地址。預設為QHostAddress::Any
,表示監聽所有可用的網路介面。port
:一個quint16
型別的埠號,指定要監聽的埠。如果設定為0,系統將選擇一個可用的未使用埠。
函式返回一個bool
值,表示是否成功開始監聽。如果成功返回true
,否則返回false
,並且可以透過呼叫errorString()
獲取錯誤訊息。
緊隨套接字偵聽其後,透過使用一個waitForNewConnection
等待新的連線到達。它的原型如下:
bool QTcpServer::waitForNewConnection(
int msec = 0,
bool *timedOut = nullptr
);
該函式在伺服器接受新連線之前會一直阻塞。引數包括:
msec
:等待連線的超時時間(以毫秒為單位)。如果設定為0(預設值),則表示無限期等待,直到有新連線到達。timedOut
:一個可選的布林指標,用於指示等待是否超時。如果傳遞了此引數,並且等待時間達到了指定的超時時間,*timedOut
將被設定為true
,否則為false
。如果不關心超時,可以將此引數設定為nullptr
。
函式返回一個布林值,表示是否成功等待新連線。如果在超時時間內有新連線到達,返回true
,否則返回false
。如果等待超時,可以透過檢查timedOut
引數來確定。如果函式返回false
,可以透過呼叫errorString()
獲取錯誤訊息。
套接字的接收會使用nextPendingConnection()
函式來實現,nextPendingConnection
是 QTcpServer
類的成員函式,用於獲取下一個已接受的連線的套接字(QTcpSocket
)。它的原型如下:
QTcpSocket *QTcpServer::nextPendingConnection();
函式返回一個指向新連線套接字的指標。如果沒有已接受的連線,則返回 nullptr
。
使用這個函式,你可以在伺服器接受連線之後獲取相應的套接字,以便進行資料傳輸和通訊。一般來說,在收到 newConnection
訊號後,你可以呼叫這個函式來獲取新連線的套接字。
當有了套接字以後,就可以透過QTcpServer
指標判斷對應的套接字狀態,一般套接字的狀態被定義在QAbstractSocket
類內。以下是QAbstractSocket
類中定義的一些狀態及其對應的標誌:
狀態標誌 | 描述 |
---|---|
UnconnectedState |
未連線狀態,套接字沒有連線到遠端主機。 |
HostLookupState |
正在查詢主機地址狀態,套接字正在解析主機名。 |
ConnectingState |
連線中狀態,套接字正在嘗試與遠端主機建立連線。 |
ConnectedState |
已連線狀態,套接字已經成功連線到遠端主機。 |
BoundState |
已繫結狀態,套接字已經與地址和埠繫結。 |
ClosingState |
關閉中狀態,套接字正在關閉連線。 |
ListeningState |
監聽中狀態,用於QTcpServer ,表示伺服器正在監聽連線。 |
這些狀態反映了套接字在不同階段的連線和通訊狀態。在實際使用中,可以透過呼叫state()
函式獲取當前套接字的狀態,並根據需要處理相應的狀態。例如,可以使用訊號和槽機制來捕獲狀態變化,以便在連線建立或斷開時執行相應的操作。
當套接字被連線後則可以透過socket->write()
方法向上線客戶端傳送一個字串,此處我們以傳送lyshark
為例,傳送時需要向write()
中傳入兩個引數。其原型如下:
qint64 QTcpSocket::write(const char *data, qint64 maxSize);
該函式接受兩個引數:
data
:指向要寫入套接字的資料的指標。maxSize
:要寫入的資料的最大位元組數。
函式返回實際寫入的位元組數,如果發生錯誤,則返回 -1。在寫入資料之後,可以使用 bytesWritten
訊號來獲取寫入的位元組數。此外,你也可以使用 waitForBytesWritten
函式來阻塞等待直到所有資料都被寫入。
至此服務端程式碼可總結為如下案例;
#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <iostream>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QTcpServer server;
server.listen(QHostAddress::Any,9000);
server.waitForNewConnection(100000);
QTcpSocket *socket;
socket = server.nextPendingConnection();
if(socket->state() && QAbstractSocket::ConnectedState)
{
QByteArray bytes = QString("lyshark").toUtf8();
socket->write(bytes.data(),bytes.length());
}
socket->close();
server.close();
return a.exec();
}
1.1.2 客戶端流程
客戶端的流程與服務端基本保持一致,唯一的區別在於將server.listen
更換為socket.connectToHost
連線到對應的主機,QTcpSocket
的 connectToHost
函式的原型如下:
void QTcpSocket::connectToHost(
const QString &hostName,
quint16 port,
OpenMode openMode = ReadWrite
);
hostName
:遠端主機的主機名或IP地址。port
:要連線的埠號。openMode
:套接字的開啟模式,預設為ReadWrite
。
函式用於初始化與指定遠端主機和埠的連線。在實際使用中,你可以透過呼叫這個函式來發起與目標主機的連線嘗試。
讀取資料時可以使用readAll
函式來實現,socket.readAll()
是 QTcpSocket
類的成員函式,用於讀取所有可用的資料並返回一個 QByteArray
物件。其函式函式原型如下:
QByteArray QTcpSocket::readAll();
該函式返回一個包含從套接字中讀取的所有資料的 QByteArray
物件。通常,你可以透過這個函式來獲取已經到達的所有資料,然後對這些資料進行進一步的處理。其客戶端功能如下所示;
#include <QCoreApplication>
#include <QTcpServer>
#include <QTcpSocket>
#include <iostream>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QTcpSocket socket;
socket.connectToHost(QHostAddress::LocalHost,9000);
if(socket.state() && QAbstractSocket::ConnectedState)
{
socket.waitForReadyRead(10000);
QByteArray ref = socket.readAll();
QString ref_string;
ref_string.prepend(ref);
std::cout << ref_string.toStdString() << std::endl;
}
socket.close();
return a.exec();
}
1.2 圖形化應用
1.2.1 服務端流程
與命令列版本的網路通訊不同,圖形化部分需要使用訊號與槽函式進行繫結,所有的通訊流程都是基於訊號的,對於服務端而言我們需要匯入QTcpServer
、QtNetwork
、QTcpSocket
模組,並新增四個槽函式分別對應四個訊號;
訊號 | 槽函式 | 描述 |
---|---|---|
connected() |
onClientConnected() |
當 tcpSocket 成功連線到遠端主機時觸發,執行 onClientConnected() 函式。 |
disconnected() |
onClientDisconnected() |
當 tcpSocket 斷開連線時觸發,執行 onClientDisconnected() 函式。 |
stateChanged(QAbstractSocket::SocketState) |
onSocketStateChange(QAbstractSocket::SocketState) |
當 tcpSocket 的狀態發生變化時觸發,執行 onSocketStateChange() 函式,傳遞新的狀態。 |
readyRead() |
onSocketReadyRead() |
當 tcpSocket 有可讀取的新資料時觸發,執行 onSocketReadyRead() 函式。 |
在程式入口處我們透過new QTcpServer(this)
新建TCP套接字類,並透過connect()
連線到初始化槽函式上,當程式執行後會首先觸發newConnection
訊號,執行onNewConnection
槽函式。
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 新建TCP套接字類
tcpServer=new QTcpServer(this);
// 連線訊號初始化其他訊號
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}
而在槽函式onNewConnection
中,透過nextPendingConnection
新建一個套接字,並繫結其他四個槽函式,這裡的槽函式功能各不相同,將其對應的訊號繫結到對應槽函式上即可;
// 初始化訊號槽函式
void MainWindow::onNewConnection()
{
// 建立新套接字
tcpSocket = tcpServer->nextPendingConnection();
// 連線觸發訊號
connect(tcpSocket, SIGNAL(connected()),this, SLOT(onClientConnected()));
onClientConnected();
// 關閉觸發訊號
connect(tcpSocket, SIGNAL(disconnected()),this, SLOT(onClientDisconnected()));
// 狀態改變觸發訊號
connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(tcpSocket->state());
// 讀入資料觸發訊號
connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}
當讀者點選偵聽時則直接呼叫tcpServer->listen
實現對本地IP的8888
埠的偵聽功能,停止偵聽則是呼叫tcpServer->close
函式實現,如下所示;
// 開始偵聽
void MainWindow::on_pushButton_2_clicked()
{
// 此處指定繫結本機的8888埠
tcpServer->listen(QHostAddress::LocalHost,8888);
ui->plainTextEdit->appendPlainText("[+] 開始監聽");
ui->plainTextEdit->appendPlainText(" 伺服器地址:" + tcpServer->serverAddress().toString() +
" 伺服器埠:"+QString::number(tcpServer->serverPort())
);
}
// 停止偵聽
void MainWindow::on_pushButton_3_clicked()
{
if (tcpServer->isListening())
{
tcpServer->close();
}
}
對於讀取資料可以透過canReadLine()
函式判斷行,並透過tcpClient->readLine()
逐行讀入資料,相對應的傳送資料可透過呼叫tcpSocket->write
函式實現,在傳送之前需要將其轉換為QByteArray
型別的字串格式,如下所示;
// 讀取資料
void MainWindow::onSocketReadyRead()
{
while(tcpSocket->canReadLine())
ui->plainTextEdit->appendPlainText("[接收] | " + tcpSocket->readLine());
}
// 傳送資料
void MainWindow::on_pushButton_clicked()
{
QString msg=ui->lineEdit->text();
ui->plainTextEdit->appendPlainText("[傳送] | " + msg);
QByteArray str=msg.toUtf8();
str.append('\n');
tcpSocket->write(str);
}
執行服務端程式,並點選偵聽按鈕此時將會在本地的8888
埠上啟用偵聽,如下圖所示;
1.2.2 客戶端流程
對於客戶端而言同樣需要繫結四個訊號並對應到特定的槽函式上,其初始化部分與服務端保持一致,唯一不同的是客戶端使用connectToHost
函式連結到服務端上,斷開連線時使用的是disconnectFromHost
函式,如下所示;
// 連線伺服器時觸發
void MainWindow::on_pushButton_2_clicked()
{
// 連線到8888埠
tcpClient->connectToHost(QHostAddress::LocalHost,8888);
}
// 斷開時觸發
void MainWindow::on_pushButton_3_clicked()
{
if (tcpClient->state()==QAbstractSocket::ConnectedState)
tcpClient->disconnectFromHost();
}
此處的讀取資料與服務端保持一致,傳送資料時則是透過tcpClient->write(str)
函式直接傳遞給客戶端,程式碼如下所示;
// 讀取資料時觸發
void MainWindow::onSocketReadyRead()
{
while(tcpClient->canReadLine())
{
ui->plainTextEdit->appendPlainText("[接收] | " + tcpClient->readLine());
}
}
// 傳送訊息時觸發
void MainWindow::on_pushButton_clicked()
{
QString msg=ui->lineEdit->text();
ui->plainTextEdit->appendPlainText("[傳送] | " + msg);
QByteArray str=msg.toUtf8();
str.append('\n');
tcpClient->write(str);
}
執行後,服務端啟用偵聽等待客戶端連線,客戶端連線後,雙方則可以實現資料的收發功能,由於採用了訊號機制,兩者的收發並不會阻斷可同時進行,如下圖所示;