基於Qt的P2P局域網聊天及文件傳送軟件設計編程
zouxy09@qq.comwindows
http://blog.csdn.net/zouxy09服務器
這是個人《通訊網絡》的課程設計做業,以前沒怎麼學過Qt,但Qt實在太好用了,它提供的網絡通訊的接口使用起來很方便,因此搞這東西才花了幾天時間,如今擺上來和你們分享下。呵呵。另外,下面不會解釋具體的代碼實現過程(須要有一點點的Qt基礎和Qt網絡編程基礎),在工程的代碼文件中會有清晰的註釋。網絡
工程代碼下載地址:(在CSDN中,個人下載積分是0,窮苦好久了,因此下載積分設地比較高,看懂出對積分的渴望。因此有分的就給點分,沒分的就發郵件給我,我再把工程回覆給您就行了。原諒個人庸俗。呵呵)app
http://download.csdn.net/detail/zouxy09/5621059框架
1、設計目標tcp
經過利用Qt應用程序框架提供的QTcpSocket和QUdpSocket類進行網絡通訊,在windows平臺(支持跨平臺,將源碼在目標平臺重編譯便可)上實現了兩大功能:ide
1)實現客戶端與服務器端之間文件傳輸功能;函數
2)實現客戶端與服務器端之間的聊天功能;ui
2、系統設計框架
整個應用程序要實現聊天及文件傳輸的功能,主要由三大塊組成:
1)人機交互界面:用於顯示鏈接狀態、消息傳送、實現消息輸入等功能;
2)消息傳輸模塊:用於實現消息傳輸協議的封裝與解包工做,實現消息傳輸控制;
3)文件傳輸模塊:用於實現文件傳輸協議的封裝與解包工做,實現文件頭及文件的發送與接收控制。
3、設計細節
3.一、TCP/IP與UDP通訊原理
計算機網絡通訊主要是經過Socket(套接字)進行的。它提供了一個通訊端口,能夠經過這個端口與任何一個具備Socket接口的計算機通訊。應用程序在網絡上傳輸,接收的信息都經過這個Socket接口來實現。在應用開發中就像使用文件句柄同樣,能夠對Socket句柄進行讀,寫操做。
Qt提供了較爲完整的網絡應用服務,能夠實現大部分網絡應用程序的開發,提供了QTcpSocket和QUdpSocket類,用於實現TCP和UDP傳輸協議。提供了QTcpServer類建立服務器應用程序來處理引入的TCP鏈接。
3.二、聊天系統設計
聊天也就是信息(字符串)的收發,根據其實時性的要求與短消息傳遞的特色,採用UDP協議來實現。
在Qt中提供了QUdpSocket 類來進行UDP數據報(datagrams)的發送和接收。UDP的應用是很簡單的。咱們只須要在發送端執行writeDatagram()函數將信息發送到廣播地址的某一個端口,而後接收端綁定到這個端口,只要這個端口有數據,就會發送readyRead()信號,咱們接收到該信號後進行數據處理便可。
(1)客戶端
/***** 建立Udp套接字和綁定端口與槽函數 *********/
QUdpSocket *UdpSender = new QUdpSocket(this);
QUdpSocket *UdpReader = new QUdpSocket(this);
// 將UdpReader綁定到廣播地址的5566端口,接收發到這個端口的信息
UdpReader->bind(5566, QUdpSocket::ShareAddress);
// 將有數據的readyRead()信號綁定到讀信息的readMessage()槽函數
connect(UdpReader, SIGNAL(readyRead()), this, SLOT(readMessage()));
/***** 發送消息 *********/
void sendMessage()
{
QByteArray datagram = 「hello world!」;
UdpSender ->writeDatagram(datagram.data(),datagram.size(),
QHostAddress::Broadcast,5566);
}
/***** 接收消息 *********/
void readMessage()
{
//擁有等待的數據報
while(UdpReader->hasPendingDatagrams())
{
QByteArray datagram; //擁於存放接收的數據報
//讓datagram的大小爲等待處理的數據報的大小,這樣才能接收到完整的數據
datagram.resize(UdpReader->pendingDatagramSize());
//接收數據報,將其存放到datagram中
UdpReader->readDatagram(datagram.data(), datagram.size());
serverMessage = datagram;
}
}
(2)服務器端
過程與代碼和客戶端一致,只是由於是雙向通訊,因此在服務器端須要發送的端口號要不同。具體見工程代碼。
3.三、文件傳輸設計
文件傳送具備連續性和可靠性要求的特色,一般採用點對點通訊,並應用TCP服務來保證數據傳輸過程當中的準確性和順序性。
TCP協議的程序使用的是客戶端/服務器模式,在Qt中提供了QTcpSocket類來編寫客戶端程序,使用QTcpServer類編寫服務器端程序。咱們在服務器端進行端口的監聽,一旦發現客戶端的鏈接請求,就會發出newConnection()信號,咱們能夠關聯這個信號到咱們本身的槽函數,實現與客戶端的鏈接。創建鏈接後,任何一方均可以進行數據的發送。另外一端一旦有數據到來就會發出readyRead()信號,咱們能夠關聯此信號,進行數據的接收。
由於涉及到文件的拆分和增長文件頭信息等,還須要管理髮送和接收進度條,代碼比較複雜,這裏就不貼了,具體見工程代碼,代碼都有比較清晰的註釋。
4、人機交互界面設計
如圖4-1所示,交互界面包含3大功能塊:
1)聊天窗口:包括髮送和接收到的信息顯示框和信息的輸入框;
2)文件收發:包括須要填寫的創建TCP鏈接的服務器信息框和文件傳輸須要的功能和顯示進度框;
3)附加功能:附加「消息記錄」和「使用幫助」的對話框,點擊對應按鈕接口打開。
圖4-1:客戶端界面
服務器端界面和客戶端界面差異只在於「服務器信息」中的「創建會話」按鈕改成「開始監聽」按鈕,由於TCP鏈接是由客戶端申請,由服務器來監聽的。
圖4-2:服務器端界面
工程還實現了歷史消息界面和幫助界面。
5、附錄代碼
5.一、工程目錄
左邊是客戶端的項目文件,右邊是服務器端的項目文件。
工程說明:
注:加入Dialog後,須要先在 本項目名字 右鍵,選擇 執行qmake,再執行run才能夠編譯經過,不然會出現 Dialog的鏈接錯誤 LNK2019
5.二、服務器端的兩個主要的代碼(客戶端大部分相同)
MainWindow.h
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include <QtNetwork> #include "Dialog.h" #include "hismesdialog.h" namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private slots: /********** Send and receive message though UCP protocol **************/ void sendMessage(); // 發送消息 void readMessage(); // 接受信息 void showMessage(bool isSend); // 顯示消息 /********** Send and receive files though TCP protocol **************/ void acceptConnection(); // 接收客戶端請求 void listening(); // 監聽 void displayError(QAbstractSocket::SocketError); //顯示錯誤 /************* Send file **********************/ void openFile(); //打開文件 void startTransferFile(); //發送文件大小等信息 void updateClientProgress(qint64); //發送數據,更新進度條 /************* Receive file *******************/ void updateServerProgress(); //接收數據,更新進度條 /************************* Button Event ****************************/ void on_openButton_clicked(); void on_listenButton_clicked(); void on_disconectButton_clicked(); void on_sendMesButton_clicked(); void on_sendFileButton_clicked(); void on_helpButton_clicked(); void on_mesHistoryButton_clicked(); void on_quitButton_clicked(); void on_textBrowser_textChanged(); private: Ui::MainWindow *ui; /********** Send and receive message though UCP protocol **************/ QUdpSocket *UdpSender; QUdpSocket *UdpReader; QString localMessage; // 存放本地要發送的信息 QString serverMessage; //存放從服務器接收到的信息 /********** Send and receive files though TCP protocol **************/ QTcpServer *tcpServer; QTcpSocket *tcpSocket; /************* Send file **********************/ quint16 blockSize; //存放接收到的信息大小 QFile *localFile; //要發送的文件 qint64 totalBytes; //數據總大小 qint64 bytesWritten; //已經發送數據大小 qint64 bytesToWrite; //剩餘數據大小 qint64 loadSize; //每次發送數據的大小 QString fileName; //保存文件路徑 QByteArray outBlock; //數據緩衝區,即存放每次要發送的數據 /************* Receive file *******************/ //qint64 totalBytes; //存放總大小信息 qint64 bytesReceived; //已收到數據的大小 qint64 fileNameSize; //文件名的大小信息 //QString fileName; //存放文件名 //QFile *localFile; //本地文件 QByteArray inBlock; //數據緩衝區 Dialog *helpDialog; hisMesDialog *histroyMessageDialog; QFile *historyMessage; QString hostIP; QString getHostIP(); }; #endif // MAINWINDOW_H
MainWindow.cpp
// Chat and file Transceiver // Author : Zouxy // Date : 2013-6-20 // HomePage : http://blog.csdn.net/zouxy09 // Email : zouxy09@qq.com #include "MainWindow.h" #include "ui_MainWindow.h" #include <QFileDialog> #include <QObject> #include <QMessageBox> #include <QTextCodec> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); /********** Send and receive message though UCP protocol **************/ UdpSender = new QUdpSocket(this); UdpReader = new QUdpSocket(this); // 將UdpReader綁定到廣播地址的6666端口,接收發到這個端口的信息 UdpReader->bind(6666, QUdpSocket::ShareAddress); connect(UdpReader, SIGNAL(readyRead()), this, SLOT(readMessage())); /********** Send and receive files though TCP protocol **************/ loadSize = 4*1024; //將整個大的文件分紅不少小的部分進行發送,每部分爲4字節 totalBytes = 0; bytesWritten = 0; bytesToWrite = 0; bytesReceived = 0; fileNameSize = 0; tcpServer = new QTcpServer(this); tcpSocket = new QTcpSocket(this); //當發現新鏈接時發出newConnection()信號 connect(tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection())); ui->sendFileButton->setEnabled(false); //開始使」發送文件「按鈕不可用 ui->sendMesButton->setEnabled(true); //開始使」發送信息「按鈕可用 // 服務器的缺省值 hostIP = getHostIP(); if (hostIP == 0) ui->hostLineEdit->setText("127.0.0.1"); else ui->hostLineEdit->setText(hostIP); ui->portLineEdit->setText("5555"); helpDialog = new Dialog; helpDialog->hide(); histroyMessageDialog = new hisMesDialog; histroyMessageDialog->hide(); historyMessage = new QFile("historyMessage.txt"); if(!historyMessage->open(QFile::Append)) { qDebug() << "writing history Message file error!"; return; } } MainWindow::~MainWindow() { delete ui; } // 監聽 void MainWindow::listening() { //初始化已發送字節爲0 bytesWritten = 0; bytesReceived = 0; blockSize = 0; //初始化其爲0 ui->listenButton->setEnabled(false); if(!tcpServer->listen(QHostAddress::LocalHost, ui->portLineEdit->text().toInt())) { qDebug() << tcpServer->errorString(); close(); return; } ui->currentStatusLabel->setText("Status: listening..."); } // 鏈接後的對話框提示 void MainWindow::acceptConnection() { //QMessageBox box_success; //box_success.information(NULL, "提示", "會話創建成功", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); tcpSocket = tcpServer->nextPendingConnection(); // 當有數據發送成功時,咱們更新進度條 connect(tcpSocket, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64))); // 當有數據接收成功時,咱們更新進度條 connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(updateServerProgress())); // 綁定錯誤處理 connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError))); tcpServer->close(); ui->currentStatusLabel->setText("Status: Waiting for file transfer..."); } // 發送信息 void MainWindow::sendMessage() { localMessage = ui->inputTextEdit->toPlainText(); QByteArray datagram = localMessage.toUtf8(); //進行UDP數據報(datagrams)的發送 UdpSender->writeDatagram(datagram.data(), datagram.size(), QHostAddress::Broadcast, 5566); // 顯示消息 showMessage(true); } // 接收信息 void MainWindow::readMessage() { //擁有等待的數據報 while(UdpReader->hasPendingDatagrams()) { QByteArray datagram; //擁於存放接收的數據報 //讓datagram的大小爲等待處理的數據報的大小,這樣才能接收到完整的數據 datagram.resize(UdpReader->pendingDatagramSize()); //接收數據報,將其存放到datagram中 UdpReader->readDatagram(datagram.data(), datagram.size()); //將數據報內容顯示出來 serverMessage = datagram; } // 顯示消息 showMessage(false); } // 顯示消息 void MainWindow::showMessage(bool isSend) { QDateTime time = QDateTime::currentDateTime(); //獲取系統如今的時間 QString str = time.toString("hh:mm:ss ddd"); //設置顯示格式 QString str4file = time.toString("yyyy-MM-dd hh:mm:ss"); //設置顯示格式 blockSize = 0; QFont font; font.setPixelSize(18); ui->textBrowser->setFont(font); QTextStream stream(historyMessage); if (isSend) { // 用不一樣的顏色顯示信息所屬和當前時間 ui->textBrowser->setTextColor(QColor(0, 0, 0)); QString entraMess = "I say: " + str; ui->textBrowser->append(entraMess); stream<<"I say: "<<str4file<<"\n"; // 寫入 歷史信息 文件時須要保存日期 ui->textBrowser->setTextColor(QColor(0, 0, 255)); ui->textBrowser->append(localMessage); ui->inputTextEdit->clear(); ui->currentStatusLabel->setText("Status: send message successfully..."); stream<<localMessage<<"\n"; } else { // 用不一樣的顏色顯示信息所屬和當前時間 ui->textBrowser->setTextColor(QColor(0, 0, 0)); QString entraMess = "He/she: " + str; ui->textBrowser->append(entraMess); stream<<"He/she: "<<str4file<<"\n"; ui->textBrowser->setTextColor(QColor(255, 0, 0)); ui->textBrowser->append(serverMessage); ui->currentStatusLabel->setText("Status: new message coming..."); stream<<localMessage<<"\n"; } } // 打開文件 void MainWindow::openFile() { fileName = QFileDialog::getOpenFileName(this); if(!fileName.isEmpty()) { ui->sendFileButton->setEnabled(true); QString currentFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1); ui->currentStatusLabel->setText(tr("Status: open file %1 successfully!").arg(currentFileName)); } } // 實現文件大小等信息的發送 void MainWindow::startTransferFile() { localFile = new QFile(fileName); if(!localFile->open(QFile::ReadOnly)) { qDebug() << "open file error!"; return; } totalBytes = localFile->size(); //文件總大小 QDataStream sendOut(&outBlock, QIODevice::WriteOnly); sendOut.setVersion(QDataStream::Qt_5_0); QString currentFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1); //依次寫入總大小信息空間,文件名大小信息空間,文件名 sendOut << qint64(0) << qint64(0) << currentFileName; //這裏的總大小是文件名大小等信息和實際文件大小的總和 totalBytes += outBlock.size(); //返回outBolock的開始,用實際的大小信息代替兩個qint64(0)空間 sendOut.device()->seek(0); sendOut<<totalBytes<<qint64((outBlock.size() - sizeof(qint64)*2)); //發送完頭數據後剩餘數據的大小 bytesToWrite = totalBytes - tcpSocket->write(outBlock); ui->currentStatusLabel->setText("Status: start transfering..."); outBlock.resize(0); } // 更新進度條,實現文件的傳送 void MainWindow::updateClientProgress(qint64 numBytes) { //已經發送數據的大小 bytesWritten += (int)numBytes; if(bytesToWrite > 0) //若是已經發送了數據 { //每次發送loadSize大小的數據,這裏設置爲4KB,若是剩餘的數據不足4KB,就發送剩餘數據的大小 outBlock = localFile->read(qMin(bytesToWrite, loadSize)); //發送完一次數據後還剩餘數據的大小 bytesToWrite -= (int)tcpSocket->write(outBlock); //清空發送緩衝區 outBlock.resize(0); } else { //若是沒有發送任何數據,則關閉文件 localFile->close(); } //更新進度條 ui->serverProgressBar->setMaximum(totalBytes); ui->serverProgressBar->setValue(bytesWritten); if(bytesWritten == totalBytes) //發送完畢 { QString currentFileName = fileName.right(fileName.size() - fileName.lastIndexOf('/')-1); ui->currentStatusLabel->setText(tr("Status: transfer file %1 ...").arg(currentFileName)); localFile->close(); bytesWritten = 0; // clear fot next send bytesToWrite = 0; totalBytes = 0; //tcpSocket->close(); } } // 更新進度條,實現文件的接收 void MainWindow::updateServerProgress() { QDataStream in(tcpSocket); in.setVersion(QDataStream::Qt_5_0); //若是接收到的數據小於等於16個字節,那麼是剛開始接收數據,咱們保存爲頭文件信息 if(bytesReceived <= sizeof(qint64)*2) { //接收數據總大小信息和文件名大小信息 if((tcpSocket->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize == 0)) { in >> totalBytes >> fileNameSize; bytesReceived += sizeof(qint64) * 2; } //接收文件名,並創建文件 if((tcpSocket->bytesAvailable() >= fileNameSize) && (fileNameSize != 0)) { in >> fileName; ui->currentStatusLabel->setText(tr("Accept file %1 ...").arg(fileName)); bytesReceived += fileNameSize; localFile = new QFile(fileName); if(!localFile->open(QFile::WriteOnly)) { qDebug() << "writing file error!"; return; } } else return; } //若是接收的數據小於總數據,那麼寫入文件 if(bytesReceived < totalBytes) { bytesReceived += tcpSocket->bytesAvailable(); inBlock = tcpSocket->readAll(); localFile->write(inBlock); inBlock.resize(0); } //更新進度條 ui->serverProgressBar->setMaximum(totalBytes); ui->serverProgressBar->setValue(bytesReceived); //接收數據完成時 if(bytesReceived == totalBytes) { //tcpSocket->close(); localFile->close(); ui->listenButton->setEnabled(true); ui->currentStatusLabel->setText(tr("Status: receive file %1 successfully!").arg(fileName)); bytesReceived = 0; // clear for next receive totalBytes = 0; fileNameSize = 0; } } // 顯示錯誤 void MainWindow::displayError(QAbstractSocket::SocketError) { qDebug() << tcpSocket->errorString(); tcpSocket->close(); ui->serverProgressBar->reset(); ui->currentStatusLabel->setText(tr("Status: connect error! open client and retry")); ui->listenButton->setEnabled(true); //開始使」監聽「按鈕可用 ui->sendFileButton->setEnabled(false); } // 開始監聽 void MainWindow::on_listenButton_clicked() { listening(); } // 斷開鏈接 void MainWindow::on_disconectButton_clicked() { tcpSocket->disconnectFromHost(); ui->listenButton->setEnabled(true); //開始使」監聽「按鈕可用 ui->sendFileButton->setEnabled(false); //開始使」發送文件「按鈕不可用 } // 發送信息 void MainWindow::on_sendMesButton_clicked() { sendMessage(); } // 打開文件 void MainWindow::on_openButton_clicked() { openFile(); } // 發送文件 void MainWindow::on_sendFileButton_clicked() { startTransferFile(); } // 幫助說明 void MainWindow::on_helpButton_clicked() { helpDialog->show(); } // 消息記錄 void MainWindow::on_mesHistoryButton_clicked() { histroyMessageDialog->showHisMesDialog(); histroyMessageDialog->show(); } // 關閉程序 void MainWindow::on_quitButton_clicked() { this->close(); } // 文字多了會增長滾動條並顯示最後的信息 void MainWindow::on_textBrowser_textChanged() { ui->textBrowser->moveCursor(QTextCursor::End); } QString MainWindow::getHostIP() { QList<QHostAddress> list = QNetworkInterface::allAddresses(); foreach (QHostAddress address, list) { //咱們使用IPv4地址 if(address.protocol() == QAbstractSocket::IPv4Protocol) return address.toString(); } return 0; }