Qt學習之路_4(Qt UDP的初步使用)
爲了使用Qt自帶的Socket進行網絡編程,先必須熟悉Socket編程的原理,另外還需對Qt一些基本類的操作比較熟悉。由於剛接觸不久,所以還是以看人家的代碼來學習。這次主要是學Qt下UDP的編程,且熟悉一些Qt下代碼的編寫流程,所以本文參照的是《Qt及Qt Quick開發實戰精解》一書中的第5個例子:局域網聊天工具中的UDP聊天部分。
另外http://www.yafeilinux.com/ 上有其源碼和相關教程下載。
該程序實現的功能是:局域網內,每個用戶登錄到聊天軟件,則軟件界面的右端可以顯示在線用戶列表,分別顯示的是用戶名,主機名,ip地址。軟件左邊那大塊是聊天內容顯示界面,這裏局域網相當於qq中的qq羣,即羣聊。每個人可以在聊天輸入界面中輸入文字併發送。其聊天界面如下:
該程序實現的是每個用戶登錄既是客戶端又是服務器端,這就需要看你站在哪個角度看問題了。簡單的說,當用戶發送信息給別人時就是客戶端,當接收別人的信息是就可以看做是服務器端。
下面分服務器端和客戶端2部分來介紹。
服務器端:建立一個UDP Socket並綁定在固定端口後,用信號與槽的方式進行監聽是否有數據來臨。如果用,接收其數據並分析數據的消息類型,如果消息是新用戶登錄則更新用戶列表並在聊天顯示窗口中添加新用戶上線通知;同理,如果是用戶下線,則在用戶列表中刪除該用戶且在聊天顯示窗口中顯示下線通知;如果是聊天消息,則接收該消息並且在窗口中顯示。其流程圖如下:
客戶端:首先當客戶端登錄時,獲取本機的用戶名,計算機名和ip地址,並廣播給局域網的服務器更新用戶列表。然後當客戶端需要發送信息時,則在聊天輸入欄中輸入信息並按發送鍵發送聊天內容,當然於此同時也廣播本地系統的各種信息。其流程圖如下:
程序主要代碼和註釋如下:
widget.h:
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> class QUdpSocket; 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(); private: Ui::Widget *ui; QUdpSocket *udpSocket; qint16 port; private slots: void processPendingDatagrams(); void on_sendButton_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> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); udpSocket = new QUdpSocket(this);//創建一個QUdpSocket類對象,該類提供了Udp的許多相關操作 port = 45454; //此處的bind是個重載函數,連接本機的port端口,採用ShareAddress模式(即允許其它的服務連接到相同的地址和端口,特別是 //用在多客戶端監聽同一個服務器端口等時特別有效),和ReuseAddressHint模式(重新連接服務器) udpSocket->bind(port, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint); //readyRead()信號是每當有新的數據來臨時就被觸發 connect(udpSocket, SIGNAL(readyRead()), this, SLOT(processPendingDatagrams())); sendMessage(NewParticipant);//打開軟件時就向外發射本地信息,讓其他在線用戶得到通知 } Widget::~Widget() { delete ui; } // 使用UDP廣播發送信息,MessageType是指頭文件中的枚舉數據類型 //sendMessage即把本機的主機名,用戶名+(消息內容後ip地址)廣播出去 void Widget::sendMessage(MessageType type, QString serverAddress) { QByteArray data; //字節數組 //QDataStream類是將序列化的二進制數據送到io設備,因爲其屬性爲只寫 QDataStream out(&data, QIODevice::WriteOnly); QString localHostName = QHostInfo::localHostName();//返回主機名,QHostInfo包含了一些關於主機的靜態函數 QString address = getIP(); //調用自己類中的getIP()函數 //將type,getUserName(),localHostName按照先後順序送到out數據流中,消息類型type在最前面 out << type << getUserName() << localHostName; switch(type) { case Message : if (ui->messageTextEdit->toPlainText() == "") { //將輸入框裏的文字轉化成純文本發送 //當發送的文本爲空時創建一個警告信息窗口,tr函數爲譯本函數,即譯碼後面的text內容 QMessageBox::warning(0,tr("警告"),tr("發送內容不能爲空"),QMessageBox::Ok); return; } out << address << getMessage();//將ip地址和得到的消息內容輸入out數據流 ui->messageBrowser->verticalScrollBar() //返回垂直條 ->setValue(ui->messageBrowser->verticalScrollBar()->maximum());//設置垂直滑動條的最大值 break; case NewParticipant : out << address; //爲什麼此時只是輸出地址這一項呢?因爲此時不需要傳遞聊天內容 break; case ParticipantLeft : break; case FileName : break; case Refuse : break; } //一個udpSocket已經於一個端口bind在一起了,這裏的data是out流中的data,最多可以傳送8192個字節,但是建議不要超過 //512個字節,因爲這樣雖然可以傳送成功,但是這些數據需要在ip層分組,QHostAddress::Broadcast是指發送數據的目的地址 //這裏爲本機所在地址的廣播組內所有機器,即局域網廣播發送 udpSocket->writeDatagram(data,data.length(),QHostAddress::Broadcast, port);//將data中的數據發送 } // 接收UDP信息 void Widget::processPendingDatagrams() { //hasPendingDatagrams返回true時表示至少有一個數據報在等待被讀取 while(udpSocket->hasPendingDatagrams()) { QByteArray datagram; //pendingDatagramSize爲返回第一個在等待讀取報文的size,resize函數是把datagram的size歸一化到參數size的大小一樣 datagram.resize(udpSocket->pendingDatagramSize()); //將讀取到的不大於datagram.size()大小數據輸入到datagram.data()中,datagram.data()返回的是一個字節數組中存儲 //數據位置的指針 udpSocket->readDatagram(datagram.data(), datagram.size()); QDataStream in(&datagram, QIODevice::ReadOnly);//因爲其屬性爲只讀,所以是輸入 int messageType; //此處的int爲qint32,在Qt中,qint8爲char,qint16爲uint in >> messageType; //讀取1個32位長度的整型數據到messageTyep中 QString userName,localHostName,ipAddress,message; QString time = QDateTime::currentDateTime() .toString("yyyy-MM-dd hh:mm:ss");//將當前的時間轉化到括號中的形式 switch(messageType) { case Message: //in>>後面如果爲Qstring,則表示讀取一個直到出現'\0'的字符串 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("[ " +localHostName+" ] "+ 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: break; case Refuse: break; } } } // 處理新用戶加入 void Widget::newParticipant(QString userName, QString localHostName, QString ipAddress) { //此處的findItems表示找到與內容localHostName匹配的item,其匹配是基於變體的匹配模式 bool isEmpty = ui->userTableWidget->findItems(localHostName, Qt::MatchExactly).isEmpty(); if (isEmpty) { //沒有找到相應的主機名 //新建3個小的item,分別爲user,host,ip QTableWidgetItem *user = new QTableWidgetItem(userName); QTableWidgetItem *host = new QTableWidgetItem(localHostName); QTableWidgetItem *ip = new QTableWidgetItem(ipAddress); ui->userTableWidget->insertRow(0);//先設置的是第0行,即新來的用戶放在最上面 ui->userTableWidget->setItem(0,0,user);//第0行的第1列... 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)); //arg爲返回後面文本的一個副本,%1表示輸出的內容按照第1個.arg後面的輸出? 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); //此句執行完後,rowCount()內容會自動減1 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地址,獲取本機ip地址(其協議爲ipv4的ip地址) QString Widget::getIP() { //QList是Qt中一個容器模板類,是一個數組指針? QList<QHostAddress> list = QNetworkInterface::allAddresses();//此處的所有地址是指ipv4和ipv6的地址 //foreach (variable, container),此處爲按照容器list中條目的順序進行迭代 foreach (QHostAddress address, list) { if(address.protocol() == QAbstractSocket::IPv4Protocol) return address.toString(); } return 0; } // 獲取用戶名 QString Widget::getUserName() { QStringList envVariables; //將後面5個string存到envVariables環境變量中 envVariables << "USERNAME.*" << "USER.*" << "USERDOMAIN.*" << "HOSTNAME.*" << "DOMAINNAME.*"; //系統中關於環境變量的信息存在environment中 QStringList environment = QProcess::systemEnvironment(); foreach (QString string, envVariables) { //indexOf爲返回第一個匹配list的索引,QRegExp類是用規則表達式進行模式匹配的類 int index = environment.indexOf(QRegExp(string)); if (index != -1) { //stringList中存的是environment.at(index)中出現'='號前的字符串 QStringList stringList = environment.at(index).split('='); if (stringList.size() == 2) { return stringList.at(1);//at(0)爲文字"USERNAME.",at(1)爲用戶名 break; } } } return "unknown"; } // 獲得要發送的消息 QString Widget::getMessage() { QString msg = ui->messageTextEdit->toHtml();//轉化成html語言進行發送 ui->messageTextEdit->clear();//發送完後清空輸入框 ui->messageTextEdit->setFocus();//重新設置光標輸入焦點,即焦點保持不變 return msg; } // 發送消息 void Widget::on_sendButton_clicked() { sendMessage(Message); }
main:
#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(); }