基於Qt的P2P局域網聊天及文件傳送軟件設計

基於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;
}
相關文章
相關標籤/搜索