小成開發日記---利用Qt/C++實現基於Udp協議的網絡聊天室(分服務端和客戶端的開發【輕聊v1.0.1】)

作者:小成Charles
原創作品
轉載請標註原創文章地址:https://blog.csdn.net/weixin_42999453/article/details/112363393


一、引言
最近一直在聯繫網絡通信的項目,一開始準備使用純C/C++去寫服務器的,雖說這樣效率很高,但是寫起來超級麻煩啊,第一個C/S項目,我就利用QtQnetwork模塊去編寫了,話說我中學的夢想就是能夠自己做一個社交軟件,最近開啓了名爲“輕聊”的項目,今年要考研,也不知道能寫到多少…話不多說,上一下輕聊v1.0.1客戶端的的截圖;
(ps:界面很醜,只是實現了功能,下一步將繼續完善功能和界面)
在這裏插入圖片描述
二、功能實現
目前實現了最基本的功能:





  1. 登錄服務器
  2. 退出服務器
  3. 實時更新當前服務器在線的所有用戶
  4. 雙擊用戶列表可實現發送消息給指定用戶

三、設計思路
我對服務器的設計思路毫無頭緒,都是想到什麼寫什麼,雖然功能都能實現,但是性能低,後期會繼續完善優化。
項目實現分爲客戶端服務端,服務端在linux環境下運行,衆所周知服務器是不帶界面的。客戶端在登錄,退出,發送消息等等需要和服務器交互的數據統一是通過Json數據包傳送,這樣方便管理和提供接口等等。
服務端收到json數據包消息後會將json數據傳送給sendServer.h,這個類是專門用來解析json數據包並將解析後的結果傳送給相應的客戶端,這裏我就是創建了兩個類,一個送來接收消息一個用來發送消息,這樣做的目的是方便代碼管理以及高性能做準備。
客戶端就簡單點,只要對接收到的消息進行處理分析就可,後面代碼分析會講解。
由於本項目是基於UDP協議的,也就是面向無連接的,這樣收發消息很方便,但是這樣的話對用戶登錄和下線的處理比較麻煩,在這裏雖然也解決了,但是後期還是將會利用TCP協議進行登錄連接,UDP進行收發消息,據說騰訊QQ就是這種模式。
這裏我對我項目的大致邏輯畫了一個圖,方便大家理解!
在這裏插入圖片描述
四、客戶端代碼講解
(1)登錄和退出的代碼塊
登錄和退出的實現就是點擊綁定端口和解除綁定,這時候如果你點擊綁定端口,你就會給服務器發送一個json數據包,其中鍵“way”對應的值就是“log”,相反對應的值就是“quit”,同時這裏點擊接觸綁定的同時會把當前在線列表清空









void MainWindow::on_actBind_triggered()
{
   
   //開始綁定 就是上線
    quint16 port=ui->editPort->text().toUInt();
    QString     serverAddrEdit=ui->serverAddr->text(); //目標IP
    QHostAddress    serverAddr(serverAddrEdit);

    quint16     ServerPort=ui->ServerPort->text().toUInt();//目標port
    if (udpSocket->bind(port))//綁定端口成功
    {
   
   
        ui->chatPlainTextEdit->appendPlainText("**已成功綁定");
        ui->chatPlainTextEdit->appendPlainText("**綁定端口:"
                                               +QString::number(udpSocket->localPort()));
        QJsonDocument document=logOrQuit("log");
        QByteArray jsonArry=document.toJson(QJsonDocument::Compact);
        udpSocket->writeDatagram(jsonArry,serverAddr,ServerPort);
        ui->actBind->setEnabled(false);
        ui->actBort->setEnabled(true);
    }
    else
        ui->chatPlainTextEdit->appendPlainText("**綁定失敗");
}

void MainWindow::on_actBort_triggered()
{
   
   //解除綁定 就是下線
    QString     serverAddrEdit=ui->serverAddr->text(); //目標IP
    QHostAddress    serverAddr(serverAddrEdit);
    quint16     ServerPort=ui->ServerPort->text().toUInt();//目標port
    QJsonDocument document=logOrQuit("quit");
    QByteArray jsonArry=document.toJson(QJsonDocument::Compact);
    udpSocket->writeDatagram(jsonArry,serverAddr,ServerPort);
    ui->listWidget->clear();
    udpSocket->abort(); //不能解除綁定
    ui->actBind->setEnabled(true);
    ui->actBort->setEnabled(false);
    ui->chatPlainTextEdit->appendPlainText("**已解除綁定");
}

(2)發送消息模塊
這裏就是將輸入的消息傳送給sendMsg(QString msg)這個函數,這裏將數據封裝成json數據包,並且指定需要發送的客戶端的地址,封裝好後,發送給服務端。

void MainWindow::on_btnSend_clicked()
{
   
   //發送消息

    QString  msg=ui->msgEdit->text();//發送的消息內容
    sendMsg(msg);
    ui->chatPlainTextEdit->appendPlainText("[out] "+msg);
    ui->msgEdit->clear();
    ui->msgEdit->setFocus();
}
void MainWindow::sendMsg(QString msg)
{
   
   
    QString     serverAddrEdit=ui->serverAddr->text(); //目標IP
    QHostAddress    serverAddr(serverAddrEdit);
    quint16     serverPort=ui->ServerPort->text().toUInt();//目標port

    QJsonObject toObject;
    toObject.insert("addr",ui->targetAddrEdit->text());
    toObject.insert("port",ui->targetPortEdit->text());
    toObject.insert("msg",msg);
    QJsonObject msgObject;
    msgObject.insert("way","sendMsg");
    msgObject.insert("to",QJsonValue(toObject));
    QJsonDocument msgDocument;
    msgDocument.setObject(msgObject);
    QByteArray msgJsonArry=msgDocument.toJson(QJsonDocument::Compact);
    udpSocket->writeDatagram(msgJsonArry,serverAddr,serverPort); //發出數據報
}

(3)接收消息的模塊
這裏接收到消息之後就先把數據包發送給getWay()函數。這個函數將會判斷”way“對應的值什麼,然後執行相應的函數。

void MainWindow::onSocketReadyRead()
{
   
   //接受消息
    while(udpSocket->hasPendingDatagrams())
    {
   
   
        QByteArray   datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        QHostAddress    peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);

        //分析json包
        QJsonParseError jsonError;
        QJsonDocument parseDocument=QJsonDocument::fromJson(datagram.data(),&jsonError);
        QJsonObject object=parseDocument.object();
        getWay(object);

    }
}
void MainWindow::getWay(QJsonObject object)
{
   
   
    QString way =  object.value("way").toString();
    if(way=="receiveMsg")
    {
   
   
        getMsg( object);
    }
    if(way=="updateUsers")
    {
   
   
        updateUsers(object);
    }
}

(4)更新用戶在線列表
這裏用到的空間是QlistWidge來存放用戶數據列表
這裏就是如果收到的數據的wayupdateUsers,將會執行如下函數功能,json數據中心傳輸過來的addrport兩個數組一一對應,遍歷並獲得數據,創建QlistWidgetItem對象,設置用戶地址和端口;我這裏每次都會先清空所用列表再一一重新創建來更新列表,這樣效率很差,不建議使用,後期會優化這一部分,因爲客戶端界面沒有開始做就不講究效率問題了。

void MainWindow::updateUsers(QJsonObject object)
{
   
   //接收json包分析數據 並實現更新在線用戶列表   if(object.contains("way"))
    {
   
   
        QJsonValue value=object.value("way");
        if(value.isString())
        {
   
   
            QString way=value.toString();
            qDebug()<<way;
            if(way=="updateUsers")
            {
   
   
                ui->listWidget->clear();                if(object.contains("addr")&&object.contains("port"))
                {
   
   
                    QJsonValue addrValue=object.value("addr");
                    QJsonValue portValue=object.value("port");
                    QJsonArray addrArry=addrValue.toArray();
                    QJsonArray portArry=portValue.toArray();
                    for (int i = 0; i < addrArry.size(); ++i) {
   
   
                        QString addrStr=addrArry.at(i).toString();
                        QString portStr=portArry.at(i).toString();
                        onlineAddr.append(addrStr);
                        onlinePort.append(portStr);
                        QListWidgetItem *item=new QListWidgetItem();
                        item->setData(i,i);
                        item->setText("用戶IP:"+addrStr+"用戶端口:"+portStr);
                        ui->listWidget->addItem(item);
                    }
                }
            }
        }
    }
}

(5)最後就是將消息添加到聊天框

void MainWindow::getMsg(QJsonObject object)
{
   
   
    QJsonObject fromObject=object.value("from").toObject();
    QString fromAddr=fromObject.value("addr").toString();
    QString fromPort=fromObject.value("port").toString();
    QString fromMsg=fromObject.value("msg").toString();
    QString peer="[From "+fromAddr+":"+fromPort+"] ";
     ui->chatPlainTextEdit->appendPlainText(peer+fromMsg);
}

五、服務端代碼講解
(1)接收數據模塊
這裏就是接收到消息之後創建一個SendServer類的對象,這個類主要做數據處理和轉發數據的,創建了兩個connect函數,分別監聽如果有用戶登錄和用戶退出的事件,然後將數據傳出來進行更新在線用戶列表,記住這裏最後一定要將這個對象delete掉,不然最後可能會導致內存泄漏等問題!

void udpserver::onReadyRead()
{
   
   //server to revecive
    QByteArray   datagram;
    datagram.resize(udpServer->pendingDatagramSize());
    QHostAddress    peerAddr;
    quint16 peerPort;
    udpServer->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
    qDebug()<<datagram.data();
    SendServer *send=new SendServer ();
    connect(send,SIGNAL(newUsers(QStringList)),this,SLOT(inserNewUsers(QStringList)));//connect the signals before the emit
    connect(send,SIGNAL(quitUsers(QStringList)),this,SLOT(pushQuitUsers(QStringList)));
    send->getReceiveData(datagram.data(),datagram.size(),peerAddr,peerPort);
    delete send;//free object

}

(2)更新用戶列表模塊
這裏有兩個函數,分別是inserNewUserspushQuitUsers,代碼邏輯大相徑庭,就是對從SendServer類中監聽到的數據進行判斷操作的槽函數,這裏也創建了一個SendServer對象把onlineUserList(存放在線用戶的容器)傳送給了這個類中的函數sendAllCilentsTheUsers(onlineUserList);,它將實現把數據發送給所用客戶端

void udpserver::inserNewUsers(QStringList logUserInfo)
{
   
   //update the userList
    for (int i=0;i<onlineUserList.length();++i) {
   
   
        if(onlineUserList.at(i)==logUserInfo)
        {
   
   
            qDebug()<<"the same log users";
            SendServer *updateSend=new SendServer ();
            updateSend->sendAllCilentsTheUsers(onlineUserList);
            delete updateSend;
            return;
        }
    }
    onlineUserList.append(logUserInfo);
    SendServer *updateSend=new SendServer ();
    updateSend->sendAllCilentsTheUsers(onlineUserList);
    delete updateSend;
    qDebug()<<onlineUserList;

}
void udpserver::pushQuitUsers(QStringList quitUserInfo)
{
   
   
    for (int i=0;i<onlineUserList.length();++i) {
   
   
        if(onlineUserList.at(i)==quitUserInfo)
        {
   
   
            qDebug()<<"it can be deleted";
            onlineUserList.removeAt(i);
            qDebug()<<"removved:"<<onlineUserList;
            SendServer *updateSend=new SendServer ();
            updateSend->sendAllCilentsTheUsers(onlineUserList);
            delete updateSend;
            return;
        }else {
   
   
            qDebug()<<"reQuited!";
        }
    }
}

(3)最後貼出SendServer.h所用的代碼塊
感興趣可以自己看一下,這裏getWay函數實現分析way,然後執行對應的函數塊,sendLOgUsersendQuitUser分別觸發了newUsersquitUsers這兩個信號,並將數據傳送出去,對應上面講的那兩個connect函數,然後又去執行sendAllCilentsTheUsers實現實時更新用戶列表,如果是執行sendMsgToClients這個函數,就會發送者ip和端口以及消息打包成json發送給指定的客戶端。

 SendServer::SendServer(QObject *parent) : QObject(parent)
{
   
   
    udpSendServer =new QUdpSocket ();

}

void SendServer::getWay( QJsonDocument parseDocument)
{
   
   
    QJsonObject object=parseDocument.object();
    if(object.contains("way"))
    {
   
   
        QJsonValue value=object.value("way");
        if(value.isString())
        {
   
   
            QString way=value.toString();
            qDebug()<<way;
            if(way=="log")
            {
   
   
                sendLOgUser(object);
            }
            if(way=="sendMsg")
            {
   
   
                sendMsgToClients(object);
            }
            if(way=="quit")
            {
   
   
                sendQuitUser(object);
            }

        }
    }
}
void SendServer::getReceiveData(QByteArray data,int size,QHostAddress &peerAddr,quint16 &peerPort)
{
   
   
    QJsonParseError jsonError;
    QJsonDocument parseDocument=QJsonDocument::fromJson(data,&jsonError);
    if(!parseDocument.isNull())
    {
   
   
        temtLOgAddr =peerAddr;
        temtLogPort = peerPort;
        getWay(parseDocument);
    }
}
void SendServer::sendLOgUser(QJsonObject object)
{
   
   //send the log user to clients

    logUserInfo.append(temtLOgAddr.toString());
    logUserInfo.append(QString::number(temtLogPort));
    qDebug()<<logUserInfo;
    emit newUsers(logUserInfo);
udpSendServer>writeDatagram(jsonArry,temtLOgAddr,temtLogPort);
}

void SendServer::sendQuitUser(QJsonObject object)
{
   
   //send Quit users
    quitUserInfo.append(temtLOgAddr.toString());
    quitUserInfo.append(QString::number(temtLogPort));
    qDebug()<<quitUserInfo;
    emit quitUsers(quitUserInfo);

}

void SendServer::sendAllCilentsTheUsers(QList <QStringList> onlineUserList)
{
   
   


    bool ok;
    QJsonArray addrArry;
    QJsonArray portArry;

    for (int i = 0;i<onlineUserList.length();i++) {
   
   
        QString addr=onlineUserList.at(i).at(0);
        QString port=onlineUserList.at(i).at(1);
        //        qDebug()<<"enter 1";
        addrArry.append(addr);
        portArry.append(port);
    }


    QJsonObject jsonObject;
    jsonObject.insert("way","updateUsers");
    jsonObject.insert("addr",QJsonValue(addrArry) );
    jsonObject.insert("port",QJsonValue(portArry));

    QJsonDocument document;
    document.setObject(jsonObject);
    QByteArray jsonArry=document.toJson(QJsonDocument::Compact);
    //forward the json to all clients

    for (int j = 0;j<onlineUserList.length();j++) {
   
   
        //        qDebug()<<"enter 2";
        QHostAddress addr(onlineUserList.at(j).at(0));
        quint16 port= onlineUserList.at(j).at(1).toUInt();
        qDebug()<<addr<<port;
        udpSendServer->writeDatagram(jsonArry,addr,port);
    }
}


void SendServer::sendMsgToClients(QJsonObject object)
{
   
   
    QJsonObject toObject=object.value("to").toObject();
    QString toAddr=toObject.value("addr").toString();
    QString toPort=toObject.value("port").toString();
    QString msg=toObject.value("msg").toString();
    QHostAddress targetAddr(toAddr);
    quint16 targetPort=toPort.toUInt();


    QJsonObject fromObject;
    fromObject.insert("addr",temtLOgAddr.toString());
    fromObject.insert("port",QString::number( temtLogPort));
    fromObject.insert("msg",msg);
    QJsonObject sendObject;
    sendObject.insert("way","receiveMsg");
    sendObject.insert("from",QJsonValue(fromObject));
    QJsonDocument sendDoucment;
    sendDoucment.setObject(sendObject);
    QByteArray sendJsonArry=sendDoucment.toJson(QJsonDocument::Compact);
    udpSendServer->writeDatagram(sendJsonArry,targetAddr,targetPort);
}

五、總結
沒有總結,接下來繼續更新完善更能,繼續更新博客,立個flag輕聊5.0將達到商用級別!

作者:小成Charles
原創作品
轉載請標註原創文章地址:https://blog.csdn.net/weixin_42999453/article/details/112363393

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章