引言
TCP/IP通訊(即SOCKET通訊)是通過網線將伺服器Server端和客戶機Client端進行連線,在遵循ISO/OSI模型的四層層級構架的基礎上通過TCP/IP協議建立的通訊。控制器可以設定為伺服器端或客戶端。
關於TCP/IP協議可詳看:TCP/IP協議詳解 - 知乎 (zhihu.com)
總的來說,TCP/IP通訊有兩個部分:
-
客戶端和伺服器
-
QTcpServer(監聽套接字)和QTcpSocket(通訊套接字)
監聽套接字,顧名思義,監聽關於各種通訊的狀態,一旦進行通訊,監聽套接字會啟動通訊套接字,進行通訊
客戶端使用connectToHost函式主動連線伺服器後,伺服器會觸發newConnectio這個槽函式,並進行取出QTcpServer(監聽套接字),將相關內容取出並賦給QTcpSocket(通訊套接字)。
客戶端向伺服器傳送資料,觸發readyRead(),進行處理,彼此傳遞時,原理都是這樣的。
對雙方來說都起作用的部分:
- 一旦建立連線,就會觸發connected,伺服器特殊一點,觸發的是newConnectio
- 互傳資料也是一樣的,一旦接受到,就會觸發readyread
伺服器中,需要監聽套接字以及通訊套接字,監聽套接字用於監聽客戶端是否給伺服器傳送請求
本篇博文做了初步的學習與嘗試,編寫了一個客戶端和伺服器基於視窗通訊以及檔案傳輸的小例程。
一,客戶端
客戶端的程式碼比伺服器稍簡單,總的來說,使用QT中的QTcpSocket類與伺服器進行通訊只需要以下5步:
(1)建立QTcpSocket套接字物件
socket = new QTcpSocket(this);
(2)使用這個物件連線伺服器
QString ip = ui.lineEdit_ip->text();//獲取ip int port = ui.lineEdit_2->text().toInt();//獲取埠資料 socket->connectToHost(ip, port);
(3)使用write函式向伺服器傳送資料
QByteArray data = ui.lineEdit_3->text().toUtf8();//獲取lineEdit控制元件中的資料併傳送給伺服器 socket->write(data);
(4)當socket接收緩衝區有新資料到來時,會發出readRead()訊號,因此為該訊號新增槽函式以讀取資料
connect(socket, &QTcpSocket::readyRead, this, &QTcpClinet::ReadData); void QTcpClinet::ReadData() { QByteArray buf = socket->readAll(); ui.textEdit->append(buf); }
(5)斷開與伺服器的連線(關於close()和disconnectFromHost()的區別,可以按F1看幫助)
socket->disconnectFromHost();
客戶端例程:(新建一個qt專案QTcpClinet(客戶機))
- ui介面
本地迴路ip:127.0.0.1 可以連線到本地ip(電腦內部迴圈的ip)
如果要和區域網其他ip連線 -> 在執行(win+R)+cmd+ipconfig ->ipv4地址 檢視本機ip
- QTcpClinet.h
#include <QtWidgets/QWidget> #include "ui_QTcpClinet.h" #include"QTcpSocket.h" #pragma execution_character_set("utf-8") class QTcpClinet : public QWidget { Q_OBJECT public: QTcpClinet(QWidget *parent = Q_NULLPTR); ~QTcpClinet(); public slots: void on_btn_connect_clicked(); void ReadData(); void on_btn_push_clicked(); private: Ui::QTcpClinetClass ui; QTcpSocket* socket;//建立socket指標 };
- QTcpClinet.cpp
#include "QTcpClinet.h" QTcpClinet::QTcpClinet(QWidget *parent) : QWidget(parent) { ui.setupUi(this); socket = new QTcpSocket(this); } QTcpClinet::~QTcpClinet() { delete this->socket;//回收記憶體 } void QTcpClinet::on_btn_connect_clicked() { if (ui.btn_connect->text()==tr("連線伺服器")) { QString ip = ui.lineEdit_ip->text();//獲取ip int port = ui.lineEdit_2->text().toInt();//獲取埠資料 //取消已有的連線 socket->abort(); //連線伺服器 socket->connectToHost(ip, port); bool isconnect = socket->waitForConnected();//等待直到連線成功 //如果連線成功 if (isconnect) { ui.textEdit->append("The connection was successful!!"); ui.btn_push->setEnabled(true);//按鈕使能 //修改按鍵文字 ui.btn_connect->setText("斷開伺服器連線"); //接收緩衝區(伺服器)資訊 connect(socket, &QTcpSocket::readyRead, this, &QTcpClinet::ReadData); } else { ui.textEdit->append("The connection falied!!"); } } else { //斷開連線 socket->disconnectFromHost(); ui.btn_connect->setText("連線伺服器"); ui.btn_push->setEnabled(false);//關閉傳送按鈕使能 } } //接收緩衝區資訊函式 void QTcpClinet::ReadData() { QByteArray buf = socket->readAll(); ui.textEdit->append(buf); } //傳送按鈕事件 void QTcpClinet::on_btn_push_clicked() { QByteArray data = ui.lineEdit_3->text().toUtf8();//獲取lineEdit控制元件中的資料併傳送給伺服器 socket->write(data); //判斷是否寫入成功 bool iswrite = socket->waitForBytesWritten(); if (iswrite) { //寫入成功 } else { //沒有寫入成功 } }
二,伺服器(需要一直執行哦)
伺服器除了使用到了QTcpSocket類,還需要用到QTcpSever類。即便如此,也只是比客戶端複雜一點點,用到了6個步驟:
(1)建立QTcpSever物件
server = new QTcpServer(this);
(2)偵聽一個埠,使得客戶端可以使用這個埠訪問伺服器
server->listen(QHostAddress::Any, 6677);//監聽所有ip和6677埠
(3)當伺服器被客戶端訪問時,會發出newConnection()訊號,因此為該訊號新增槽函式,並用一個QTcpSocket物件接受客戶端訪問
connect(server, &QTcpServer::newConnection, this, &TcpServer::ClientConnect); void TcpServer::ClientConnect() { //解析所有客戶連線 while (server->hasPendingConnections()) { //連線上後通過socket(QTcpSocket物件)獲取連線資訊 socket = server->nextPendingConnection(); QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort());//監聽客戶端是否有訊息傳送 connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1); } }
(4)使用socket的write函式向客戶端傳送資料
socket->write(data);
(5)當socket接收緩衝區有新資料到來時,會發出readRead()訊號,因此為該訊號新增槽函式以讀取資料
//監聽客戶端是否有訊息傳送 connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1); //獲取客戶端向伺服器傳送的資訊 void TcpServer::ReadData1() { QByteArray buf = socket->readAll();//readAll最多接收65532的資料 QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort()); ui.textEdit_server->append(str +QString(buf)); //socket->write("ok");//伺服器接收到資訊後返回一個ok }
(6)取消偵聽
server->close();
伺服器例程:(新增一個新的qt專案TcpServer(伺服器))
- ui介面
- TcpServer.h
#include <QtWidgets/QWidget> #include"ui_TcpServer.h" #include"qtcpserver.h" #include"qtcpsocket.h" class TcpServer : public QWidget { Q_OBJECT public: TcpServer(QWidget *parent = Q_NULLPTR); ~TcpServer(); public slots: void on_btn_server_clicked(); void on_btn_listen_clicked(); private: Ui::TcpServerClass ui; QTcpServer* server; QTcpSocket* socket;//一個客戶端對應一個socket void ClientConnect(); void ReadData1(); };
- TcpServer.cpp
#include "TcpServer.h" #include"qstring.h" #include"qdebug.h" #pragma execution_character_set("utf-8") TcpServer::TcpServer(QWidget *parent) : QWidget(parent) { ui.setupUi(this); server = new QTcpServer(this); //客戶機連線訊號槽 connect(server, &QTcpServer::newConnection, this, &TcpServer::ClientConnect); } TcpServer::~TcpServer() { server->close(); server->deleteLater(); } void TcpServer::on_btn_listen_clicked() { if (ui.btn_listen->text()=="偵聽") { //從輸入框獲取埠號 int port = ui.lineEdit_port->text().toInt(); //偵聽指定埠的所有ip if (!server->listen(QHostAddress::Any, port)) { //若出錯,則輸出錯誤資訊 qDebug() << server->errorString(); return; } //修改按鍵文字 ui.btn_listen->setText("取消偵聽"); } else { socket->abort(); //取消偵聽 server->close(); //修改按鍵文字 ui.btn_listen->setText("偵聽"); } } void TcpServer::ClientConnect() { //解析所有客戶連線 while (server->hasPendingConnections()) { //連線上後通過socket獲取連線資訊 socket = server->nextPendingConnection(); QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort()); //提示連線成功 ui.textEdit_server->append(str+"Connect to the server"); //核取方塊選項為連線伺服器的ip ui.comboBox->addItem(str); //將socket地址放入combobox屬性內 //ui.comboBox->setItemData(ui.comboBox->count()-1, QVariant((int)socket)); //監聽客戶端是否有訊息傳送 connect(socket, &QTcpSocket::readyRead, this, &TcpServer::ReadData1); } } //獲取客戶端向伺服器傳送的資訊 void TcpServer::ReadData1() { QByteArray buf = socket->readAll();//readAll最多接收65532的資料 QString str = QString("[ip:%1,port:%2]").arg(socket->peerAddress().toString()).arg(socket->peerPort()); ui.textEdit_server->append(str +QString(buf)); } //伺服器向客戶端傳送資訊 void TcpServer::on_btn_server_clicked() { if(ui.comboBox->count()== 0)return; //QTcpSocket* skt= (QTcpSocket*)ui.comboBox->itemData(ui.comboBox->currentIndex()).value<int>(); socket->write(ui.lineEdit1->text().toUtf8()); }
注意:write中需要寫入char型別的元素或QByteArray型別的元素
效果展示:
三,TCP/IP檔案傳輸
上文實現了訊息的傳輸,由於socket->readAll();(readAll最多接收65532的資料),因此對於大檔案的傳輸用此方法是不可取的。
TCP/IP檔案傳輸的思路:
- 客戶端和伺服器連線
- 客戶端選擇檔案,併傳送檔案給伺服器(傳送的是檔案的幀頭,格式:檔名&大小)
- 伺服器觸發readyRead,然後解析檔案幀頭(獲取檔名和大小),並返回客戶端一個ok訊息
- 客戶端觸發readyRead,然後傳送檔案資料,通過progressBar顯示進度
- 伺服器再次觸發readyRead,接收檔案資料,並儲存(通過ishead判斷接收的是檔案幀頭還是檔案資料)
程式碼實現:
新建伺服器專案(TcpServer)
- TcpServer.h
#pragma once #include <QtWidgets/QWidget> #include "ui_TcpServer.h" #include"qtcpserver.h" #include"qtcpsocket.h" #pragma execution_character_set("utf-8") class TcpServer : public QWidget { Q_OBJECT public: TcpServer(QWidget *parent = Q_NULLPTR); void hasConnect(); private: Ui::TcpServerClass ui; QTcpServer* server; QTcpSocket* socket; bool ishead; QString fileName; int fileSize;//接收檔案的總大小 int recvSize;//當前接收檔案的大小 QByteArray filebuf;//當前接收的檔案資料 };
- TcpServer.cpp
#include "TcpServer.h" #include"qfile.h" TcpServer::TcpServer(QWidget *parent) : QWidget(parent) { ishead = true; ui.setupUi(this); server = new QTcpServer(this); //監聽1122埠的ip server->listen(QHostAddress::Any, 1122); //如果有使用者連線觸發槽函式 connect(server, &QTcpServer::newConnection, this, &TcpServer::hasConnect); } void TcpServer::hasConnect() { while (server->hasPendingConnections()>0)//判斷當前連線了多少人 { //用socket和我們的客戶端連線,一個客戶端對應一個套接字socket socket = server->nextPendingConnection(); //伺服器介面上輸出客戶端資訊 ui.textEdit->append(QString("%1:新使用者連線").arg(socket->peerPort())); //如果客戶端傳送資訊過來了,觸發匿名函式 connect(socket, &QTcpSocket::readyRead, [=]() { QByteArray buf = socket->readAll(); //用一個標誌位ishead判斷是頭還是資料位 if (ishead) { //如果是頭,解析頭(檔名,檔案大小) QString str = QString(buf); ui.textEdit->append(str); QStringList strlist = str.split("&"); fileName = strlist.at(0);//解析幀標頭檔案名 fileSize = strlist.at(1).toInt();//解析幀標頭檔案大小 ishead = false;//下次接收到的檔案就是我們的資料 recvSize = 0; filebuf.clear(); socket->write("ok"); } else { //根據檔名和檔案大小接收和儲存檔案 filebuf.append(buf); recvSize += buf.size();//每接收一次檔案,當前檔案大小+1 //當接收檔案大小等於總檔案大小,即檔案資料接收完畢 if (recvSize>=fileSize) { //儲存檔案 QFile file(ui.lineEdit->text() + fileName); file.open(QIODevice::WriteOnly); file.write(filebuf); file.close(); ishead = true; } } }); } }
新建客戶端專案(QTcpClient)
- QTcpClient.h
#include <QtWidgets/QWidget> #include"ui_QTcpClient.h" #include"qtcpsocket.h" #pragma execution_character_set("utf-8") class QTcpClient : public QWidget { Q_OBJECT public: QTcpClient(QWidget *parent = Q_NULLPTR); public slots: void on_btn_connect_clicked(); void on_btn_choose_clicked(); void on_btn_open_clicked(); private: Ui::QTcpClientClass ui; QTcpSocket* socket; };
- QTcpClient.cpp
#include "QTcpClient.h" #include"qfiledialog.h" #include"qfileinfo.h" QTcpClient::QTcpClient(QWidget *parent) : QWidget(parent) { ui.setupUi(this); socket = new QTcpSocket(this); } void QTcpClient::on_btn_connect_clicked() { QString ip = ui.lineEdit_ip->text();//獲取ip int port = ui.lineEdit_port->text().toInt();//獲取埠資料 socket->connectToHost(ip, port);//連線伺服器 //等待連線成功 if (socket->waitForConnected()) { ui.textEdit->append("<font color='green'>連線伺服器成功!</font>"); ui.btn_open->setEnabled(true); //如果伺服器傳送資訊到客戶端,觸發匿名函式 connect(socket, &QTcpSocket::readyRead, [=]() { //讀取伺服器傳送的資訊(即緩衝區資訊) QByteArray buf = socket->readAll(); if (buf=="ok") { QFile file = (ui.label_path->text()); if (!file.open(QIODevice::ReadWrite)) { //讀取檔案失敗 return; } qint64 currentlen = 0;//當前已經傳送的大小 qint64 allLength = file.size();//總檔案大小 do { char data[1024]; qint64 msize = file.read(data, 1024);//讀檔案放入打他陣列中,返回讀取到的大小 socket->write(data, msize);//把讀取到的data資料傳送給伺服器 currentlen += msize;//實時獲取當前傳送的檔案大小 ui.progressBar->setValue(currentlen *100 / allLength);//更新介面進度條 } while (currentlen < allLength);//當傳送檔案等於檔案大小時,傳送完畢,迴圈結束 } }); } else { ui.textEdit->append("<font color='red'>連線伺服器失敗!</font>"); } } //選擇檔案事件 void QTcpClient::on_btn_choose_clicked() { QString path = QFileDialog::getOpenFileName(this, "開啟檔案", "", "(*.*)"); ui.label_path->setText(path); } //傳送檔案事件 void QTcpClient::on_btn_open_clicked() { QFileInfo info(ui.label_path->text()); //用QFileInfo::fileName,size獲取檔名和大小 格式:檔名&大小 //伺服器用該格式解析檔名和大小 QString head = QString("%1&%2").arg(info.fileName()).arg(info.size()); //將該格式傳送給伺服器 toUtf8:QString轉QByteArray或char型別 socket->write(head.toUtf8()); }
效果展示: