在上一篇博文Qt學習之路_4(Qt UDP的初步使用) 中,初步了解了Qt下UDP的使用,這一節就學習下TCP的使用。2者其實流程都差不多。當然了,本文還是參考的《Qt及Qt Quick開發實戰精解》一書中的第5個例子,即局域網聊天工具中的UDP聊天和TCP文件傳送部分。另外http://www.yafeilinux.com/ 上有其源碼和相關教程下載。
其發送端界面如下:
接收端界面如下:
發送端,也即承擔服務器角色的操作:
在主界面程序右側選擇一個需要發送文件的用戶,彈出發送端界面后,點擊打開按鈕,在本地計算機中選擇需要發送的文件,點擊發送按鈕,則進度條上會顯示當前文件傳送的信息,有已傳送文件大小信息,傳送速度等信息。如果想關閉發送過程,則單擊關閉按鈕。
其流程圖如下:
接收端,也即承擔客戶端角色的操作:
當在主界面中突然彈出一個對話框,問是否接自某個用戶名和IP地址的文件傳送信息,如果接受則單擊yes按鈕,否則就單擊no按鈕。當接收文件時,選擇好接收文件所存目錄和文件名后就開始接收文件了,其過程也會顯示已接收文件的大小,接收速度和剩余時間的大小等信息。
其流程圖如下:
TCP部分程序代碼和注釋如下:
Widget.h:
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> class QUdpSocket; class TcpServer;//可以這樣定義類?不用保護頭文件的? namespace Ui { class Widget; } // 枚舉變量標志信息的類型,分別為消息,新用戶加入,用戶退出,文件名,拒絕接受文件 enum MessageType{Message, NewParticipant, ParticipantLeft, FileName, Refuse}; class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = 0); ~Widget(); protected: void newParticipant(QString userName, QString localHostName, QString ipAddress); void participantLeft(QString userName, QString localHostName, QString time); void sendMessage(MessageType type, QString serverAddress=""); QString getIP(); QString getUserName(); QString getMessage(); void hasPendingFile(QString userName, QString serverAddress, QString clientAddress, QString fileName); private: Ui::Widget *ui; QUdpSocket *udpSocket; qint16 port; QString fileName; TcpServer *server; private slots: void processPendingDatagrams(); void on_sendButton_clicked(); void getFileName(QString); void on_sendToolBtn_clicked(); }; #endif // WIDGET_H
Widget.cpp:
#include "widget.h" #include "ui_widget.h" #include <QUdpSocket> #include <QHostInfo> #include <QMessageBox> #include <QScrollBar> #include <QDateTime> #include <QNetworkInterface> #include <QProcess> #include "tcpserver.h" #include "tcpclient.h" #include <QFileDialog> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); udpSocket = new QUdpSocket(this); port = 45454; udpSocket->bind(port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint); connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams())); sendMessage(NewParticipant); //TcpServer是tcpserver.ui對應的類,上面直接用QUdpSocket是因為沒有單獨的udpserver.ui類 server = new TcpServer(this); //sendFileName()函數一發送,則觸發槽函數getFileName() connect(server, SIGNAL(sendFileName(QString)), this, SLOT(getFileName(QString))); } Widget::~Widget() { delete ui; } // 使用UDP廣播發送信息 void Widget::sendMessage(MessageType type, QString serverAddress) { QByteArray data; QDataStream out(&data, QIODevice::WriteOnly); QString localHostName = QHostInfo::localHostName(); QString address = getIP(); out << type << getUserName() << localHostName; switch(type) { case Message : if (ui->messageTextEdit->toPlainText() == "") { QMessageBox::warning(0,tr("警告"),tr("發送內容不能為空"),QMessageBox::Ok); return; } out << address << getMessage(); ui->messageBrowser->verticalScrollBar() ->setValue(ui->messageBrowser->verticalScrollBar()->maximum()); break; case NewParticipant : out << address; break; case ParticipantLeft : break; case FileName : { int row = ui->userTableWidget->currentRow();//必須選中需要發送的給誰才可以發送 QString clientAddress = ui->userTableWidget->item(row, 2)->text();//(row,,2)為ip地址 out << address << clientAddress << fileName;//發送本地ip,對方ip,所發送的文件名 break; } case Refuse : out << serverAddress; break; } udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast, port); } // 接收UDP信息 void Widget::processPendingDatagrams() { while(udpSocket->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(udpSocket->pendingDatagramSize()); udpSocket->readDatagram(datagram.data(), datagram.size()); QDataStream in(&datagram, QIODevice::ReadOnly); int messageType; in >> messageType; QString userName,localHostName,ipAddress,message; QString time = QDateTime::currentDateTime() .toString("yyyy-MM-dd hh:mm:ss"); switch(messageType) { case Message: in >> userName >> localHostName >> ipAddress >> message; ui->messageBrowser->setTextColor(Qt::blue); ui->messageBrowser->setCurrentFont(QFont("Times New Roman",12)); ui->messageBrowser->append("[ " +userName+" ] "+ time); ui->messageBrowser->append(message); break; case NewParticipant: in >>userName >>localHostName >>ipAddress; newParticipant(userName,localHostName,ipAddress); break; case ParticipantLeft: in >>userName >>localHostName; participantLeft(userName,localHostName,time); break; case FileName: { in >> userName >> localHostName >> ipAddress; QString clientAddress, fileName; in >> clientAddress >> fileName; hasPendingFile(userName, ipAddress, clientAddress, fileName); break; } case Refuse: { in >> userName >> localHostName; QString serverAddress; in >> serverAddress; QString ipAddress = getIP(); if(ipAddress == serverAddress) { server->refused(); } break; } } } } // 處理新用戶加入 void Widget::newParticipant(QString userName, QString localHostName, QString ipAddress) { bool isEmpty = ui->userTableWidget->findItems(localHostName, Qt::MatchExactly).isEmpty(); if (isEmpty) { QTableWidgetItem *user = new QTableWidgetItem(userName); QTableWidgetItem *host = new QTableWidgetItem(localHostName); QTableWidgetItem *ip = new QTableWidgetItem(ipAddress); ui->userTableWidget->insertRow(0); ui->userTableWidget->setItem(0,0,user); ui->userTableWidget->setItem(0,1,host); ui->userTableWidget->setItem(0,2,ip); ui->messageBrowser->setTextColor(Qt::gray); ui->messageBrowser->setCurrentFont(QFont("Times New Roman",10)); ui->messageBrowser->append(tr("%1 在線!").arg(userName)); ui->userNumLabel->setText(tr("在線人數:%1").arg(ui->userTableWidget->rowCount())); sendMessage(NewParticipant); } } // 處理用戶離開 void Widget::participantLeft(QString userName, QString localHostName, QString time) { int rowNum = ui->userTableWidget->findItems(localHostName, Qt::MatchExactly).first()->row(); ui->userTableWidget->removeRow(rowNum); ui->messageBrowser->setTextColor(Qt::gray); ui->messageBrowser->setCurrentFont(QFont("Times New Roman", 10)); ui->messageBrowser->append(tr("%1 於 %2 離開!").arg(userName).arg(time)); ui->userNumLabel->setText(tr("在線人數:%1").arg(ui->userTableWidget->rowCount())); } // 獲取ip地址 QString Widget::getIP() { QList<QHostAddress> list = QNetworkInterface::allAddresses(); foreach (QHostAddress address, list) { if(address.protocol() == QAbstractSocket::IPv4Protocol) return address.toString(); } return 0; } // 獲取用戶名 QString Widget::getUserName() { QStringList envVariables; envVariables << "USERNAME.*" << "USER.*" << "USERDOMAIN.*" << "HOSTNAME.*" << "DOMAINNAME.*"; QStringList environment = QProcess::systemEnvironment(); foreach (QString string, envVariables) { int index = environment.indexOf(QRegExp(string)); if (index != -1) { QStringList stringList = environment.at(index).split('='); if (stringList.size() == 2) { return stringList.at(1); break; } } } return "unknown"; } // 獲得要發送的消息 QString Widget::getMessage() { QString msg = ui->messageTextEdit->toHtml(); ui->messageTextEdit->clear(); ui->messageTextEdit->setFocus(); return msg; } // 發送消息 void Widget::on_sendButton_clicked() { sendMessage(Message); } // 獲取要發送的文件名 void Widget::getFileName(QString name) { fileName = name; sendMessage(FileName); } // 傳輸文件按鈕 void Widget::on_sendToolBtn_clicked() { if(ui->userTableWidget->selectedItems().isEmpty())//傳送文件前需選擇用戶 { QMessageBox::warning(0, tr("選擇用戶"), tr("請先從用戶列表選擇要傳送的用戶!"), QMessageBox::Ok); return; } server->show(); server->initServer(); } // 是否接收文件,客戶端的顯示 void Widget::hasPendingFile(QString userName, QString serverAddress, QString clientAddress, QString fileName) { QString ipAddress = getIP(); if(ipAddress == clientAddress) { int btn = QMessageBox::information(this,tr("接受文件"), tr("來自%1(%2)的文件:%3,是否接收?") .arg(userName).arg(serverAddress).arg(fileName), QMessageBox::Yes,QMessageBox::No);//彈出一個窗口 if (btn == QMessageBox::Yes) { QString name = QFileDialog::getSaveFileName(0,tr("保存文件"),fileName);//name為另存為的文件名 if(!name.isEmpty()) { TcpClient *client = new TcpClient(this); client->setFileName(name); //客戶端設置文件名 client->setHostAddress(QHostAddress(serverAddress)); //客戶端設置服務器地址 client->show(); } } else { sendMessage(Refuse, serverAddress); } } }
Tcpserver.h:
#ifndef TCPSERVER_H #define TCPSERVER_H #include <QDialog> #include <QTime> class QFile; class QTcpServer; class QTcpSocket; namespace Ui { class TcpServer; } class TcpServer : public QDialog { Q_OBJECT public: explicit TcpServer(QWidget *parent = 0); ~TcpServer(); void initServer(); void refused(); protected: void closeEvent(QCloseEvent *); private: Ui::TcpServer *ui; qint16 tcpPort; QTcpServer *tcpServer; QString fileName; QString theFileName; QFile *localFile; qint64 TotalBytes; qint64 bytesWritten; qint64 bytesToWrite; qint64 payloadSize; QByteArray outBlock; QTcpSocket *clientConnection; QTime time; private slots: void sendMessage(); void updateClientProgress(qint64 numBytes); void on_serverOpenBtn_clicked(); void on_serverSendBtn_clicked(); void on_serverCloseBtn_clicked(); signals: void sendFileName(QString fileName); }; #endif // TCPSERVER_H
Tcpserver.cpp:
#include "tcpserver.h" #include "ui_tcpserver.h" #include <QFile> #include <QTcpServer> #include <QTcpSocket> #include <QMessageBox> #include <QFileDialog> #include <QDebug> TcpServer::TcpServer(QWidget *parent) : QDialog(parent), ui(new Ui::TcpServer) { ui->setupUi(this); //每一個新類都有一個自己的ui setFixedSize(350,180); //初始化時窗口顯示固定大小 tcpPort = 6666; //tcp通信端口 tcpServer = new QTcpServer(this); //newConnection表示當tcp有新連接時就發送信號 connect(tcpServer, SIGNAL(newConnection()), this, SLOT(sendMessage())); initServer(); } TcpServer::~TcpServer() { delete ui; } // 初始化 void TcpServer::initServer() { payloadSize = 64*1024; TotalBytes = 0; bytesWritten = 0; bytesToWrite = 0; ui->serverStatusLabel->setText(tr("請選擇要傳送的文件")); ui->progressBar->reset();//進度條復位 ui->serverOpenBtn->setEnabled(true);//open按鈕可用 ui->serverSendBtn->setEnabled(false);//發送按鈕不可用 tcpServer->close();//tcp傳送文件窗口不顯示 } // 開始發送數據 void TcpServer::sendMessage() //是connect中的槽函數 { ui->serverSendBtn->setEnabled(false); //當在傳送文件的過程中,發送按鈕不可用 clientConnection = tcpServer->nextPendingConnection(); //用來獲取一個已連接的TcpSocket //bytesWritten為qint64類型,即長整型 connect(clientConnection, SIGNAL(bytesWritten(qint64)), //? this, SLOT(updateClientProgress(qint64))); ui->serverStatusLabel->setText(tr("開始傳送文件 %1 !").arg(theFileName)); localFile = new QFile(fileName); //localFile代表的是文件內容本身 if(!localFile->open((QFile::ReadOnly))){ QMessageBox::warning(this, tr("應用程序"), tr("無法讀取文件 %1:\n%2") .arg(fileName).arg(localFile->errorString()));//errorString是系統自帶的信息 return; } TotalBytes = localFile->size();//文件總大小 //頭文件中的定義QByteArray outBlock; QDataStream sendOut(&outBlock, QIODevice::WriteOnly);//設置輸出流屬性 sendOut.setVersion(QDataStream::Qt_4_7);//設置Qt版本,不同版本的數據流格式不同 time.start(); // 開始計時 QString currentFile = fileName.right(fileName.size() //currentFile代表所選文件的文件名 - fileName.lastIndexOf('/')-1); //qint64(0)表示將0轉換成qint64類型,與(qint64)0等價 //如果是,則此處為依次寫入總大小信息空間,文件名大小信息空間,文件名 sendOut << qint64(0) << qint64(0) << currentFile; TotalBytes += outBlock.size();//文件名大小等信息+實際文件大小 //sendOut.device()為返回io設備的當前設置,seek(0)表示設置當前pos為0 sendOut.device()->seek(0);//返回到outBlock的開始,執行覆蓋操作 //發送總大小空間和文件名大小空間 sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64)*2)); //qint64 bytesWritten;bytesToWrite表示還剩下的沒發送完的數據 //clientConnection->write(outBlock)為套接字將內容發送出去,返回實際發送出去的字節數 bytesToWrite = TotalBytes - clientConnection->write(outBlock); outBlock.resize(0);//why?? } // 更新進度條,有數據發送時觸發 void TcpServer::updateClientProgress(qint64 numBytes) { //qApp為指向一個應用對象的全局指針 qApp->processEvents();//processEvents為處理所有的事件? bytesWritten += (int)numBytes; if (bytesToWrite > 0) { //沒發送完畢 //初始化時payloadSize = 64*1024;qMin為返回參數中較小的值,每次最多發送64K的大小 outBlock = localFile->read(qMin(bytesToWrite, payloadSize)); bytesToWrite -= (int)clientConnection->write(outBlock); outBlock.resize(0);//清空發送緩沖區 } else { localFile->close(); } ui->progressBar->setMaximum(TotalBytes);//進度條的最大值為所發送信息的所有長度(包括附加信息) ui->progressBar->setValue(bytesWritten);//進度條顯示的進度長度為bytesWritten實時的長度 float useTime = time.elapsed();//從time.start()還是到當前所用的時間記錄在useTime中 double speed = bytesWritten / useTime; ui->serverStatusLabel->setText(tr("已發送 %1MB (%2MB/s) " "\n共%3MB 已用時:%4秒\n估計剩余時間:%5秒") .arg(bytesWritten / (1024*1024)) //轉化成MB .arg(speed*1000 / (1024*1024), 0, 'f', 2) .arg(TotalBytes / (1024 * 1024)) .arg(useTime/1000, 0, 'f', 0) //0,‘f’,0是什么意思啊? .arg(TotalBytes/speed/1000 - useTime/1000, 0, 'f', 0)); if(bytesWritten == TotalBytes) { //當需發送文件的總長度等於已發送長度時,表示發送完畢! localFile->close(); tcpServer->close(); ui->serverStatusLabel->setText(tr("傳送文件 %1 成功").arg(theFileName)); } } // 打開按鈕 void TcpServer::on_serverOpenBtn_clicked() { //QString fileName;QFileDialog是一個提供給用戶選擇文件或目錄的對話框 fileName = QFileDialog::getOpenFileName(this); //filename為所選擇的文件名(包含了路徑名) if(!fileName.isEmpty()) { //fileName.right為返回filename最右邊參數大小個字文件名,theFileName為所選真正的文件名 theFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1); ui->serverStatusLabel->setText(tr("要傳送的文件為:%1 ").arg(theFileName)); ui->serverSendBtn->setEnabled(true);//發送按鈕可用 ui->serverOpenBtn->setEnabled(false);//open按鈕禁用 } } // 發送按鈕 void TcpServer::on_serverSendBtn_clicked() { //tcpServer->listen函數如果監聽到有連接,則返回1,否則返回0 if(!tcpServer->listen(QHostAddress::Any,tcpPort))//開始監聽6666端口 { qDebug() << tcpServer->errorString();//此處的errorString是指? close(); return; } ui->serverStatusLabel->setText(tr("等待對方接收... ...")); emit sendFileName(theFileName);//發送已傳送文件的信號,在widget.cpp構造函數中的connect()觸發槽函數 } // 關閉按鈕,服務器端的關閉按鈕 void TcpServer::on_serverCloseBtn_clicked() { if(tcpServer->isListening()) { //當tcp正在監聽時,關閉tcp服務器端應用,即按下close鍵時就不監聽tcp請求了 tcpServer->close(); if (localFile->isOpen())//如果所選擇的文件已經打開,則關閉掉 localFile->close(); clientConnection->abort();//clientConnection為下一個連接?怎么理解 } close();//關閉本ui,即本對話框 } // 被對方拒絕 void TcpServer::refused() { tcpServer->close(); ui->serverStatusLabel->setText(tr("對方拒絕接收!!!")); } // 關閉事件 void TcpServer::closeEvent(QCloseEvent *) { on_serverCloseBtn_clicked(); }
Tcpclient.h:
#ifndef TCPCLIENT_H #define TCPCLIENT_H #include <QDialog> #include <QHostAddress> #include <QFile> #include <QTime> class QTcpSocket; namespace Ui { class TcpClient; } class TcpClient : public QDialog { Q_OBJECT public: explicit TcpClient(QWidget *parent = 0); ~TcpClient(); void setHostAddress(QHostAddress address); void setFileName(QString fileName); protected: void closeEvent(QCloseEvent *); private: Ui::TcpClient *ui; QTcpSocket *tcpClient; quint16 blockSize; QHostAddress hostAddress; qint16 tcpPort; qint64 TotalBytes; qint64 bytesReceived; qint64 bytesToReceive; qint64 fileNameSize; QString fileName; QFile *localFile; QByteArray inBlock; QTime time; private slots: void on_tcpClientCancleBtn_clicked(); void on_tcpClientCloseBtn_clicked(); void newConnect(); void readMessage(); void displayError(QAbstractSocket::SocketError); }; #endif // TCPCLIENT_H
Tcpclient.cpp:
#include "tcpclient.h" #include "ui_tcpclient.h" #include <QTcpSocket> #include <QDebug> #include <QMessageBox> TcpClient::TcpClient(QWidget *parent) : QDialog(parent), ui(new Ui::TcpClient) { ui->setupUi(this); setFixedSize(350,180); TotalBytes = 0; bytesReceived = 0; fileNameSize = 0; tcpClient = new QTcpSocket(this); tcpPort = 6666; connect(tcpClient, SIGNAL(readyRead()), this, SLOT(readMessage())); connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError))); } TcpClient::~TcpClient() { delete ui; } // 設置文件名 void TcpClient::setFileName(QString fileName) { localFile = new QFile(fileName); } // 設置地址 void TcpClient::setHostAddress(QHostAddress address) { hostAddress = address; newConnect(); } // 創建新連接 void TcpClient::newConnect() { blockSize = 0; tcpClient->abort(); //取消已有的連接 tcpClient->connectToHost(hostAddress, tcpPort);//連接到指定ip地址和端口的主機 time.start(); } // 讀取數據 void TcpClient::readMessage() { QDataStream in(tcpClient); //這里的QDataStream可以直接用QTcpSocket對象做參數 in.setVersion(QDataStream::Qt_4_7); float useTime = time.elapsed(); if (bytesReceived <= sizeof(qint64)*2) { //說明剛開始接受數據 if ((tcpClient->bytesAvailable() //bytesAvailable為返回將要被讀取的字節數 >= sizeof(qint64)*2) && (fileNameSize == 0)) { //接受數據總大小信息和文件名大小信息 in>>TotalBytes>>fileNameSize; bytesReceived += sizeof(qint64)*2; } if((tcpClient->bytesAvailable() >= fileNameSize) && (fileNameSize != 0)){ //開始接受文件,並建立文件 in>>fileName; bytesReceived +=fileNameSize; if(!localFile->open(QFile::WriteOnly)){ QMessageBox::warning(this,tr("應用程序"),tr("無法讀取文件 %1:\n%2.") .arg(fileName).arg(localFile->errorString())); return; } } else { return; } } if (bytesReceived < TotalBytes) { bytesReceived += tcpClient->bytesAvailable();//返回tcpClient中字節的總數 inBlock = tcpClient->readAll(); //返回讀到的所有數據 localFile->write(inBlock); inBlock.resize(0); } ui->progressBar->setMaximum(TotalBytes); ui->progressBar->setValue(bytesReceived); double speed = bytesReceived / useTime; ui->tcpClientStatusLabel->setText(tr("已接收 %1MB (%2MB/s) " "\n共%3MB 已用時:%4秒\n估計剩余時間:%5秒") .arg(bytesReceived / (1024*1024)) .arg(speed*1000/(1024*1024),0,'f',2) .arg(TotalBytes / (1024 * 1024)) .arg(useTime/1000,0,'f',0) .arg(TotalBytes/speed/1000 - useTime/1000,0,'f',0)); if(bytesReceived == TotalBytes) { localFile->close(); tcpClient->close(); ui->tcpClientStatusLabel->setText(tr("接收文件 %1 完畢") .arg(fileName)); } } // 錯誤處理 //QAbstractSocket類提供了所有scoket的通用功能,socketError為枚舉型 void TcpClient::displayError(QAbstractSocket::SocketError socketError) { switch(socketError) { //RemoteHostClosedError為遠處主機關閉了連接時發出的錯誤信號 case QAbstractSocket::RemoteHostClosedError : break; default : qDebug() << tcpClient->errorString(); } } // 取消按鈕 void TcpClient::on_tcpClientCancleBtn_clicked() { tcpClient->abort(); if (localFile->isOpen()) localFile->close(); } // 關閉按鈕 void TcpClient::on_tcpClientCloseBtn_clicked() { tcpClient->abort(); if (localFile->isOpen()) localFile->close(); close(); } // 關閉事件 void TcpClient::closeEvent(QCloseEvent *) { on_tcpClientCloseBtn_clicked(); }
Main.cpp:
#include <QtGui/QApplication> #include "widget.h" #include <QTextCodec> int main(int argc, char *argv[]) { QApplication a(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForLocale()); Widget w; w.show(); return a.exec(); }