为了使用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(); }