QT5 TCP網路通訊 伺服器與客戶端建立連接listen() - connectToHost(); 觸發newPendingConnect信號 實時數據通訊write(); read(); 觸發readyRead信號 通訊主要使用的類: QTcpServer Class QTcpServer類提供 ...
QT5 TCP網路通訊
- 伺服器與客戶端建立連接listen() - connectToHost(); 觸發newPendingConnect信號
- 實時數據通訊write(); read(); 觸發readyRead信號
通訊主要使用的類:
QTcpServer Class
QTcpServer類提供了一個基於TCP的伺服器。
這個類可以接受傳入的TCP連接。您可以指定埠或讓QTcpServer自動選擇一個埠。您可以收聽特定地址或所有機器的地址。
調用listen()讓伺服器偵聽傳入的連接。每次客戶端連接到伺服器時,都會發出newConnection()信號。
QTcpSocket Class
QTcpSocket類提供了一個TCP套接字。
TCP(傳輸控制協議)是一種可靠的,面向流的,面向連接的傳輸協議。 它特別適合連續傳輸數據。
QTcpSocket是QAbstractSocket的一個方便的子類,它允許你建立一個TCP連接並傳輸數據流。
建立連接:
伺服器端以監聽的方式監聽客服端是否有連接請求
客戶端以調用connectToHost()函數主動連接伺服器端
tcp協議伺服器端實現流程
建立伺服器對象
listen伺服器, 通過建立的伺服器 監聽指定地址/埠的客服端;判斷是否有客戶連接有連接就觸發newConnection();
通過connect處理newConnection()信號;
server = new QTcpServer(this); //建立一個伺服器對象 server->listen(QHostAddress::Any, 8000);//通過建立的伺服器監聽指定ip地址及埠號的客服端,如不指定埠號,系統會隨機分配 connect(server, QTcpServer::newConnection, [=]() { qDebug() << "有連接進來"; } );
tcp協議客戶端實現流程
建立QTcpSocket套節字(ip,埠)
通過套節字connectToHost()函數主動連接伺服器;連接成功則觸發伺服器QTcpServer::newConnection信號;併發送套節字到伺服器端;
關閉連接;
QTcpSocket Sc(this); Sc.connectToHost("127.0.0.1", 8888);//實際代碼中參數要進行類型轉化 Sc.close();
實時通訊:
- 客戶端到伺服器端通訊
- 當客戶端與伺服器端建立連接後;
- 客戶端與伺服器端通訊在客戶端通過套節字對象調用write()函數發送上傳內容;
- 伺服器端有客戶端數據寫入時伺服器端會自動調用readyread信號
- 伺服器端在connect中處理readyread信號,並由nextPendingConnection()函數接收客戶端發送的套節字;
- 伺服器端對接收的套節字進行相應處理,即完成一次客戶端到伺服器端的通訊
- 伺服器端到客戶端的通訊
- 當客戶端與伺服器端建立連接後;
- 伺服器通過套節字對象調用write()函數發送上傳內容;客戶端會觸發readyread信號
- 客戶端在connect中處理readyread信號
客戶端到伺服器端實現代碼:
//伺服器端 connect(server, QTcpServer::newConnection, [=]() { QTcpSocket socket = server->nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, [=]() { tp = socket->readAll(); ui->textrc->append(tp); }); } );
//客戶端 void socket::on_buttonsend_clicked() { QString temp = ui->textrc->toPlainText(); if(!temp.isEmpty())sock->write(temp.toUtf8()); }
伺服器端到客戶端實現代碼:
//伺服器端 void Widget::on_buttonsend_clicked() { socket->write(ui->textEdit->toPlainText().toUtf8()); }
//客戶端 connect(sock, &QTcpSocket::readyRead, [=]() { ui->textdis->append(sock->readAll()); });
完整代碼:
伺服器ui設計:
伺服器端頭文件widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QTcpServer> #include <QTcpSocket> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private slots: void on_buttonsend_clicked(); private: Ui::Widget *ui; QTcpServer *server; //建立伺服器對象 QTcpSocket *socket; //套節字對象 QByteArray tp; // }; #endif // WIDGET_H
伺服器端cpp文件 widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); setWindowTitle("伺服器"); tp = nullptr; server = new QTcpServer(this); server->listen(QHostAddress::Any, 8000); connect(server, QTcpServer::newConnection, [=]() { socket = server->nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, [=]() { tp = socket->readAll(); ui->testdis->append(tp); }); } ); } Widget::~Widget() { delete ui; } void Widget::on_buttonsend_clicked() { socket->write(ui->textEdit->toPlainText().toUtf8()); }
客戶端ui:
客戶端頭文件socket.h:
#ifndef SOCKET_H #define SOCKET_H #include <QWidget> #include <QTcpSocket> #include <QHostAddress> namespace Ui { class socket; } class socket : public QWidget { Q_OBJECT public: explicit socket(QWidget *parent = 0); ~socket(); private slots: void on_buttonLink_clicked(); void on_buttonsend_clicked(); void on_serverclose_clicked(); private: Ui::socket *ui; QTcpSocket *sock; QHostAddress adrs; quint16 port; }; #endif // SOCKET_H
客戶端cpp文件socket.cpp
#include "socket.h" #include "ui_socket.h" socket::socket(QWidget *parent) : QWidget(parent), ui(new Ui::socket) { ui->setupUi(this); sock = new QTcpSocket(this); setWindowTitle("張三"); connect(sock, &QTcpSocket::readyRead, [=]() { ui->textdis->append(sock->readAll()); }); } socket::~socket() { delete ui; } void socket::on_buttonLink_clicked() { QString ip = ui->serverIP->text(); QString p = ui->serverPort->text(); sock->connectToHost(ip, p.toUShort()); } void socket::on_buttonsend_clicked() { QString temp = ui->textEdit->toPlainText(); if(!temp.isEmpty())sock->write(temp.toUtf8()); } void socket::on_serverclose_clicked() { sock->close(); }
main.cpp文件
#include "widget.h" #include <QApplication> #include "socket.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); socket w1; w1.show(); return a.exec(); }
最終運行效果:
當然在具體的實現過程中還有很多很多的細節需要優化;
QT5對tcp協議基本的通訊總結:
- QTcpServer *p = new QTcpServer(this);//建立伺服器對象 QTcpSocket *q = new QTcpSocket(this); //客戶機建立套節字對象
- p.listen(監聽的客戶ip , 監聽埠port);//監聽客戶機 q.conncetToHost(要連接的伺服器ip, 要連接的伺服器埠);
- connect(p, &QTcpServer::newConnection, )連接成功觸發信號 q.write();//發送數劇 到伺服器
- QTcpSocket skt = p.nextPendingConnection();//獲取客戶機套節字 connect(q, &QTcpSocket::readyRead, )//服務端發送數據客戶端觸發信號
- connect(skt, &QTcpSocket::readyRead, )//客戶發送數據觸發信號 q.readall();//讀取客戶端發送的數據;
- skt.readall();//讀取客戶端發送的數據; 客戶端處理數據
- 伺服器端處理數據
QT5 UDP網路通訊
UDP沒有伺服器與客戶端之分;單純通過writeDatagram發( 參數1, 參數2,參數3)送指定的內容(參數1)到指定的ip(參數2),埠(參數3)上;
當收取到網路中的數據發送,就會觸發自己的readyRead信號;readDatagram(參數1, 參數2,參數3),保存讀取的內容(參數1);保存對方ip(參數2);保存對方埠(參數3)
具體實現過程:
- 建立QUdpSocket套節字 QUdpSocket* p = new QUdpSocket(this);
- 綁定本程式埠號 bind() p.bind(8000);
- 通過writeDatagram()發送數據到指定目標 p.writeDatagram();
- 當有readyRead信號發生通過readDatagram()函數讀取保存數據 p.readDatagram();
實現代碼:
QUdpSocket *udp = new QUdpSocket(this); udp->bind(8000); connect(udp, &QUdpSocket::readyRead, [=]() { char temp[1024] = {0}; QHostAddress q; quint16 p; udp->readDatagram(temp, sizeof(temp), &q, &p); ui->textdis->append(temp); }); /......./ //按鍵確定發送 void Widget::on_buttonlink_clicked() { udp->writeDatagram(ui->textsend->toPlainText().toUtf8(), QHostAddress(ui->ip->text()), ui->port->text().toUShort()); }
整體代碼:
ui設計:
widget.h頭文件
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QUdpSocket> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private slots: void on_buttonlink_clicked(); private: Ui::Widget *ui; QUdpSocket *udp; }; #endif // WIDGET_H
widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QHostAddress> #include <QDialog> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); setWindowTitle("8000"); udp = new QUdpSocket(this); udp->bind(8000); udp->joinMulticastGroup(QHostAddress("")); connect(udp, &QUdpSocket::readyRead, [=]() { char temp[1024] = {0}; QHostAddress q; quint16 p; udp->readDatagram(temp, sizeof(temp), &q, &p); ui->textdis->append(temp); }); } Widget::~Widget() { delete ui; } void Widget::on_buttonlink_clicked() { udp->writeDatagram(ui->textsend->toPlainText().toUtf8(), QHostAddress(ui->ip->text()), ui->port->text().toUShort()); }
main.cpp文件
#include "widget.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); return a.exec(); }
以便測試:我們先編譯生成一份客戶端;再在源文件中改變bind埠號為8888再生成一份客戶端;最終就會有兩份客戶端以便相互通信測試
運行測試結果:
由於UDP不需要伺服器,所以,UDP發送的數據,只要能接收到你的ip及埠的客戶端就殾能收到信息;所以在區域網內,ip地址欄可輸入
255.255.255.255 即整個區域網內的客戶端都能收到信息;
UDP通訊組包
為了滿足,發送的信息指定ip段內的客戶收到信息可以用函數JoinMulticastGroup(IPAddress)加入到組;根據msdn記載,沒錯是同樣的功能;
目前個人理解也不深;詳細數據可查msdn;可用leaveMulticastGroup()函數離開組翻;
Tcp 與 Udp的比較:
Udp不需要伺服器,只管發送數據,不對數據進行檢查,也不對接收者檢測;速度快,易丟包,做即時數據傳送比較好;
Tcp方式總結:
伺服器端:QTcpServer
1】基本用法: 創建一個QTcpServer,然後調用listen函數監聽相應的地址和埠。當有客戶端鏈接到伺服器時,會有信號newConnection()產生。 調用nextPendingConnection()接受一個掛起的TcpSocket連接,該函數返回一個指向QTcpSocket的指針,同時進入到QAbstractSocket::ConnectedState狀態。這樣就可以和客戶端進行通信了。如果錯誤發生,可以用函數serverError()返回錯誤類型,用errorString()返回錯誤提示字元串。 調用close使得QTcpServer停止監聽連接請求。儘管QTcpServer使用了事件迴圈,但是可以不這麼使用。利用waitForNewConnection(),該函數阻塞直到有連接可用或者時間超時。 2】重要函數: void incomingConnection (int socketDescriptor); 當一個連接可以用時,QTcpServer調用該函數。其基本過程是現創建一個QTcpSocket,設置描述符和保存到列表,最後發送newConnection() 事件消息。 QTcpSocket*ss QTcpServer::nextPendingConnection(); 返回下一個將要連接的QTcpSocket對象,該返回對象是QTcpServer的子對象,意味著如果刪除了QTcpSServer,則刪除了該對象。也可以在你不需要該對象時,將他刪除掉,以免占用記憶體。ss->peerAddress();ss->peerName();ss->peerPort(); 當連接成功可能通過函數獲取客戶端ip,name,port;
客戶端:QTcpSocket,QAbstractSocket 1】基本用法: 在客戶端創建一個QTcpSocket,然後用connectToHost函數向對應的主機和埠建立連接。 任何時候,可以用state()查詢狀態,初始為UnconnectedState,然後調用連接函數之後,HostLookupState,如果連接成功進入ConnectedState,並且發送hostFound()信號。 當連接建立,發送connected(),在任何狀態下如果在錯誤發生error()信號發送。狀態改變發送stateChanged()信號。如果QTcpSocket準備好可讀可寫,則isValid() 函數範圍為真。 用read()和write()來讀寫,或者使用readLine()和readAll.當有數據到來的時候,系統會發送readyRead()信號。 bytesAvailable()返回包的位元組數,如果你不是一次性讀完數據,新的數據包到來的時候將會附加到內部讀緩存後面。setReadBufferSize()可以設置讀緩存的大小。 用disconnectFromHost()關閉連接,進入ClosingState。當所有數據寫入到socket,QAbstractSocket會關閉該台socket,同時發送disconnected()消息。 如果想立即終止一個連接,放棄數據發送,調用abort(). 如果遠程主機關閉連接,QAbstractSocket發送QAbstractSocket::RemoteHostClosedError錯誤,但是狀態還停留在ConnectedState,然後發送disconnected()信號。 QAbstractSocket提供幾個函數用來掛起調用線程,知道一定的信號發送,這些函數可以用來阻塞socket: waitForConnected() 阻塞知道一個連接建立。 waitForReadyRead() 阻塞知道有新的數據可以讀取。 waitForBytesWritten() 阻塞直到發送數據寫道socket中。 waitForDisconnected() 阻塞知道鏈接關閉。QT5 TCP網路通訊綜合案例之文件傳送
最終效果圖:
1.功能設計
伺服器端:
- 有客戶端連接進入就提示連接客戶ip埠號等基本信息
- [選擇文件]按鈕被激活可在系統選取任意文件,顯示選取的文件路徑
- [發送]按鈕被激活,點擊[發送]向客戶端發送文件請求;
- 文件發送完成後 顯示發送完成提示
客戶端:
- 連接伺服器ip,port編輯框
- 按鈕[連接伺服器]成功後顯示提示並激活斷開伺服器按鈕
- 可選文件接收保存路徑
- 當收到伺服器發送文件請求,顯示所要接收的文件名,文件大小等基本信息
- 僅當收到伺服器發送的文件請求與選擇了有效文件路徑前提下,激活[接收文件]按鈕
- 點擊[接收文件]開始從伺服器接收文件,文件接收進度條提示功能,並顯示文件傳送完成提示
實現原代碼:
//main主函數 #include "widget.h" #include "client.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); client w1; w1.show(); return a.exec(); }
//伺服器端頭文件 #ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QTcpServer> #include <QTcpSocket> #include <QTimer> #include <QFile> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); private slots: void on_buttoncheck_clicked(); void on_buttonsend_clicked(); private: Ui::Widget *ui; QTcpServer *sv; QTcpSocket *ss; QString sendFileName; qint64 sendFileSize; qint64 sendedSize; QFile sendFile; }; #endif // WIDGET_H
//伺服器cpp #include "widget.h" #include "ui_widget.h" #include <QFileDialog> #include <QFile> #include <QMessageBox> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); setWindowTitle("服務端-埠:9527"); sv = new QTcpServer(this); sv->listen(QHostAddress::Any, 9527); ui->buttonsend->setEnabled(false); ui->buttoncheck->setEnabled(false); sendedSize = 0; connect(sv, &QTcpServer::newConnection, [=]() { ss = sv->nextPendingConnection(); if(ss->isValid()) { ui->buttoncheck->setEnabled(true); QString pAd = ss->peerAddress().toString().remove(0, 7); QString pNa = ss->peerName(); quint16 pPo = ss->peerPort(); ui->textdis->setText(QString("已成功連接到用戶IP:%1:%2:Name:%3").arg(pAd).arg(pPo).arg(pNa)); }else { ui->textdis->setText("連接套節字無效"); } connect(ss, &QTcpSocket::readyRead,[=]() { if(ss->readAll() == "ok") { qint64 lenW = 0; if(sendFile.open(QIODevice::ReadOnly)) { do{ char tempdata[1024] = {0}; lenW = sendFile.read(tempdata, sizeof(tempdata)); ss->write(tempdata, lenW); sendedSize += lenW; }while(lenW > 0); if(sendedSize >= sendFileSize) { ui->textdis->append("發送完成"); ss->disconnectFromHost(); sendFile.close(); } }else { QMessageBox *abot = new QMessageBox(this); abot->setText("打開文件失敗"); } } }); }); } Widget::~Widget() { delete ui; } void Widget::on_buttoncheck_clicked() { QString File = QFileDialog::getOpenFileName(this, QString("選擇文件"), QString("../")); if(!File.isEmpty()) { ui->buttoncheck->setEnabled(false); ui->textdis->append(File); sendFile.setFileName(File); QFileInfo info(File); sendFileName = info.fileName(); sendFileSize = info.size(); ui->buttonsend->setEnabled(true); } } void Widget::on_buttonsend_clicked() { QString temp = QString("head@@%1@@%2").arg(sendFileName).arg(sendFileSize); ss->write(temp.toUtf8()); ui->buttonsend->setEnabled(false); }
//客戶端頭文件 #ifndef CLIENT_H #define CLIENT_H #include <QWidget> #include <QTcpSocket> #include <QFile> namespace Ui { class client; } class client : public QWidget { Q_OBJECT public: explicit client(QWidget *parent = 0); ~client(); private slots: void on_Buttonlink_clicked(); void on_Buttonclose_clicked(); void on_ButtonSelect_clicked(); void on_ButtonSave_clicked(); private: Ui::client *ui; QTcpSocket userSocket; QFile acpFile; QString headFile; QString savePath; bool head; bool openFile; QString _name; QString _size; qint64 nowsize; }; #endif // CLIENT_H
//客戶端cpp #include "client.h" #include "ui_client.h" #include <QFile> #include <QFileDialog> #include <QMessageBox> client::client(QWidget *parent) : QWidget(parent), ui(new Ui::client) { ui->setupUi(this); setWindowTitle("客戶端"); head = true; openFile =false; nowsize = 0; ui->progressBar->setMinimum(0); ui->progressBar->setValue(0); ui->ButtonSave->setEnabled(false); ui->Buttonclose->setEnabled(false); savePath = nullptr; connect(&userSocket, &QTcpSocket::connected, [=](){ ui->Buttonlink->setEnabled(false); ui->Buttonclose->setEnabled(true); ui->lineStatus->setText("連接成功");}); connect(&userSocket, &QTcpSocket::readyRead, [=](){ if(head) { headFile = QString(userSocket.readAll()); if(!headFile.isEmpty()) { head = false; _name = headFile.section("@@", 1, 1); _size = headFile.section("@@", 2, 2); ui->progressBar->setMaximum(_size.toLongLong()); ui->lineStatus->setText(QString("文件名:[%1]總大小:[%2]等待接收").arg(_name).arg(_size)); acpFile.setFileName(QString("%1/%2").arg(savePath).arg(_name)); if(!savePath.isEmpty()) { ui->ButtonSave->setEnabled(true); } } } else { if(openFile) { nowsize += acpFile.write(userSocket.readAll()); ui->progressBar->setValue(nowsize); } else { QMessageBox *abot = new QMessageBox(this); abot->setText("打開文件失敗"); } if(acpFile.size() >= _size.toLongLong()) { nowsize = 0; acpFile.close(); ui->lineStatus->setText("文件傳送完成"); head = true; } } }); } client::~client() { delete ui; } void client::on_Buttonlink_clicked() { QString linkIp = ui->lineIp->text(); quint16 linkPort = ui->linePort->text().toUShort(); userSocket.connectToHost(linkIp, linkPort); } void client::on_Buttonclose_clicked() { userSocket.disconnectFromHost(); userSocket.close(); ui->lineStatus->setText("斷開連接"); head = true; ui->Buttonlink->setEnabled(true); ui->Buttonclose->setEnabled(false); ui->progressBar->setValue(0); } void client::on_ButtonSelect_clicked() { savePath = QFileDialog::getExistingDirectory(this, QString("保存路徑"),QString("../")); if(!savePath.isEmpty()) { ui->linePath->setText(savePath); if(!head)ui->ButtonSave->setEnabled(true); } } void client::on_ButtonSave_clicked() { ui->ButtonSave->setEnabled(false); userSocket.write("ok"); openFile = acpFile.open(QIODevice::WriteOnly); }
主要運用函數:
-
QString QFileDialog::getExistingDirectory() //靜態函數 獲取文件路徑
-
QString QFileDialog::getOpenFileName()//靜態函數 獲取要打開文件的文件路徑文件名
-
QTcpSocket -> isValid(); //如果套節字有效返回true,否則返回false;
-
void setenable(); //設置控制項激活狀態
註意事項:
在伺服器端發送文件,採取的是分段讀取文件,邊讀邊發的方式發送,而在客戶端採取每次接收每次發送的全部內容方式保存文件
起初有採取過在伺服器端all = readAall();write(all);一次性讀取發送;但在測試中發現,實際發送大文件時還是會自動被分成多次(只是簡單測試,不排除受其他因素影響)