作者:小成Charles
原創作品
轉載請標註原創文章地址:https://blog.csdn.net/weixin_42999453/article/details/112363393
一、引言
最近一直在聯繫網絡通信的項目,一開始準備使用純C/C++
去寫服務器的,雖說這樣效率很高,但是寫起來超級麻煩啊,第一個C/S
項目,我就利用Qt
的Qnetwork
模塊去編寫了,話說我中學的夢想就是能夠自己做一個社交軟件,最近開啓了名爲“輕聊”
的項目,今年要考研,也不知道能寫到多少…話不多說,上一下輕聊v1.0.1
客戶端的的截圖;
(ps:界面很醜,只是實現了功能,下一步將繼續完善功能和界面)
二、功能實現
目前實現了最基本的功能:
- 登錄服務器
- 退出服務器
- 實時更新當前服務器在線的所有用戶
- 雙擊用戶列表可實現發送消息給指定用戶
三、設計思路
我對服務器的設計思路毫無頭緒,都是想到什麼寫什麼,雖然功能都能實現,但是性能低,後期會繼續完善優化。
項目實現分爲客戶端
和服務端
,服務端在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
來存放用戶數據列表
這裏就是如果收到的數據的way
是updateUsers
,將會執行如下函數功能,json數據中心傳輸過來的addr
和port
兩個數組一一對應,遍歷並獲得數據,創建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)更新用戶列表模塊
這裏有兩個函數,分別是inserNewUsers
和pushQuitUsers
,代碼邏輯大相徑庭,就是對從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
,然後執行對應的函數塊,sendLOgUser
和sendQuitUser
分別觸發了newUsers
和quitUsers
這兩個信號,並將數據傳送出去,對應上面講的那兩個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