文章目录
Qt 网络模块:
-
Qt基础 | 主机信息查询 | QHostInfo的介绍和使用 | QNetworkInterface的介绍和使用
-
Qt基础 | TCP通信 | TCP服务端与客户端程序设计 | QTcpServer | QTcpSocket
-
Qt基础 | UDP通信 | UDP单播、广播、组播的介绍与实现
-
Qt基础 | 基于HTTP协议的网络文件下载
一、TCP 通信
1.TCP 通信概述
TCP 是一种被大多数 Internet 网络协议(如 HTTP 和 FTP) 用于数据传输的低级网络协议,它是可靠的、面向流 、面向连接的传输协议,特别适合用于连续数据传输。
TCP 通信必须先建立 TCP 连接,通信端分为客户端和服务器端。Qt 提供 QTcpSocket 类和 QTcpServer 类用于建立 TCP 通信应用程序。服务器端程序必须使用 QTcpServer 用于端口监听,建立服务器;QTcpSocket 用于建立连接后使用套接字(Socket)进行通信。
QTcpServer 是从 QObject 继承的类,它主要用于服务器端建立网络监听,创建网络 Socket 连接。
服务器端程序首先需要用 QTcpServer::listen()
开始服务器端监听,可以指定监听的 IP 地址和端口,一般一个服务程序只监听某个端口的网络连接。当有新的客户端接入时,QTcpServer内部的 incomingConnection()
函数会创建一个与客户端连接的 QTcpSocket 对象,并添加到内部可用新连接列表,然后发射 newConnection()
信号。在 该信号的槽函数中,可以用 nextPendingConnection()
函数接受客户端的连接,然后使用 QTcpSocket 与客户端通信。在客户端与服务器建立 TCP 连接后,具体的数据通信是通过 QTcpSocket 完成的。
QTcpServer 类的主要接口函数:
QTcpSocket 类是从 QIODevice 间接继承的类,所以具有流数据读写的功能。一个 QTcpSocket 实例既可以接收数据也可以发送数据,且接收与发射是异步工作的,有各自的缓冲区。QTcpSocket 类除了构造函数和析构函数,其他函数都是从 QAbstractSocket 继承或重定义的。
TCP 客户端使用 QTcpSocket 与 TCP 服务器建立连接并通信。客户端的 QTcpSocket 实例首先通过 connectToHost()
尝试连接到服务器,需要指定服务器的 IP 地址和端口。connectToHost()
以异步方式连接服务器,即以非阻塞程序运行,connectToHost()
成功连接到服务器发射 connected()
信号。如果需要使用阻塞方式连接服务器,则使用 waitForConnected()
函数阻塞程序运行,直到建立 socket 连接。与服务器端建立 socket 连接后,就可以向缓冲区写数据或从接收缓冲区读取数据,实现数据的通信。当缓冲区有新数据进入时 会发射 readyRead() 信号,一般在此信号的槽函数里读取缓冲区数据。
QAbstractSocket 用于 TCP 通信的主要接口函数:
2.TCP 服务器端程序设计
TCP 服务器端程序应具有以下功能:
- 根据指定 IP 地址和端口打开网络监听,有客户端连接时创建 socket 连接
- 采用基于行的数据通信协议,可以接收客户端发来的消息,也可以向客户端发送消息
- 在状态栏显示服务器监听状态和 socket 的状态
2.1 主窗口定义与构造函数
TCPServer 是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:
主窗口类 MainWindow 定义如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QLabel>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QLabel *LabListen;//状态栏标签
QLabel *LabSocketState;//状态栏标签
QTcpServer *tcpServer; //TCP服务器
QTcpSocket *tcpSocket;//TCP通讯的Socket
QString getLocalIP();//获取本机IP地址
protected:
void closeEvent(QCloseEvent *event);
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onNewConnection();//QTcpServer的newConnection()信号
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onClientConnected(); //Client Socket connected
void onClientDisconnected();//Client Socket disconnected
void onSocketReadyRead();//读取socket传入的数据
//UI生成的
void on_actStart_triggered();
void on_actStop_triggered();
void on_actClear_triggered();
void on_btnSend_clicked();
void on_actHostInfo_triggered();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
私有变量:
- tcpServer:用于建立 TCP 服务器
- tcpSocket:用于与客户端进行 socket 连接和通信。
- LabListen:用于显示服务器监听状态
- LabSocketState:用于显示 socket 的状态
槽函数:
-
onNewConnection()
函数:当有新客户端接入时,会发射 newConnection()信号,连接到该槽函数,在该槽函数中可以调用
nextPendingConnection()
函数获取与接入连接进行通信的 socket,然后将 socket 的几个信号与相应的槽函数连接起来。 -
onClientConnected()
函数:当 tcpSocket 与服务器连接建立时,发射 connected() 信号,连接到该槽函数。
-
onClientDisconnected()
函数:当 tcpSocket 与服务器连接断开时,发射 disconnected() 信号,连接到该槽函数。
-
onSocketStateChange()
函数:当 tcpSocket 状态发生变化时,发射 stateChanged() 信号,连接到该槽函数。
-
onSocketReadyRead()
函数:当 tcpSocket 发射 readyRead() 信号时,连接到该槽函数。
构造函数:
构造函数创建状态栏上的标签用于信息显示,调用自定义函数 getLocalIP()
获取本机 IP 地址,并显示到标题栏上。创建 QTcpServer 实例 tcpServer,并将其 newConnection() 信号与 onNewConnection() 槽函数关联。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
LabListen=new QLabel("监听状态:");
LabListen->setMinimumWidth(150);
ui->statusBar->addWidget(LabListen);
LabSocketState=new QLabel("Socket状态:");//
LabSocketState->setMinimumWidth(200);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
ui->comboIP->addItem(localIP);
tcpServer=new QTcpServer(this);
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(onNewConnection()));
}
QString MainWindow::getLocalIP()
{//获取本机IPv4地址
QString hostName=QHostInfo::localHostName();//本地主机名
QHostInfo hostInfo=QHostInfo::fromName(hostName);
QString localIP="";
QList<QHostAddress> addList=hostInfo.addresses();//
if (!addList.isEmpty())
for (int i=0;i<addList.count();i++)
{
QHostAddress aHost=addList.at(i);
if (QAbstractSocket::IPv4Protocol==aHost.protocol())
{
localIP=aHost.toString();
break;
}
}
return localIP;
}
2.2 网络监听与 socket 连接的建立
网络监听:
作为 TCP 服务器,QTcpServer 类需要调用 listen()
函数在本机某个 IP 地址和端口上开始 TCP 监听,以等待 TCP 客户端的接入。单击 “开始监听” 按钮,会触发槽函数,代码如下:
void MainWindow::on_actStart_triggered()
{//开始监听
QString IP=ui->comboIP->currentText();//IP地址
quint16 port=ui->spinPort->value();//端口
QHostAddress addr(IP);
tcpServer->listen(addr,port);//
// tcpServer->listen(QHostAddress::LocalHost,port);// Equivalent to QHostAddress("127.0.0.1").
ui->plainTextEdit->appendPlainText("**开始监听...");
ui->plainTextEdit->appendPlainText("**服务器地址:"
+tcpServer->serverAddress().toString());
ui->plainTextEdit->appendPlainText("**服务器端口:"
+QString::number(tcpServer->serverPort()));
ui->actStart->setEnabled(false);
ui->actStop->setEnabled(true);
LabListen->setText("监听状态:正在监听");
}
- IP 地址可以是表示本机的 “127.0.0.1”,或是本机的实际 IP 地址。
socket 连接的建立:
tcpServer 开始监听后,TCPClient 就可以通过 IP 地址和端口连接到此服务器。当有客户端接入时, tcpServer 会发射 newConnection() 信号,此信号关联到 onNewConnection()
槽函数。在该槽函数中可以调用 nextPendingConnection()
函数获取与接入连接进行通信的 socket,然后将 socket 的几个信号与相应的槽函数连接起来。代码如下:
void MainWindow::onNewConnection()
{
// ui->plainTextEdit->appendPlainText("有新连接");
tcpSocket = tcpServer->nextPendingConnection(); //创建socket
connect(tcpSocket, SIGNAL(connected()),
this, SLOT(onClientConnected()));
onClientConnected();//
connect(tcpSocket, SIGNAL(disconnected()),
this, SLOT(onClientDisconnected()));
connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
onSocketStateChange(tcpSocket->state());
connect(tcpSocket,SIGNAL(readyRead()),
this,SLOT(onSocketReadyRead()));
}
QTcpSocket 的这几个信号的作用是:
- connected()信号,客户端 socket 连接建立时发射此信号
- disconnected() 信号,客户端 socket 连接断开时发射此信号
- stateChanged() 信号,socket 状态变化时发射此信号
- readyRead() 信号,socket 的读取缓冲区有新数据时发射此信号
这几个信号对应的槽函数如下:
void MainWindow::onClientConnected()
{//客户端接入时
ui->plainTextEdit->appendPlainText("**client socket connected");
ui->plainTextEdit->appendPlainText("**peer address:"+
tcpSocket->peerAddress().toString());
ui->plainTextEdit->appendPlainText("**peer port:"+
QString::number(tcpSocket->peerPort()));
}
void MainWindow::onClientDisconnected()
{//客户端断开连接时
ui->plainTextEdit->appendPlainText("**client socket disconnected");
tcpSocket->deleteLater();
// deleteLater();//QObject::deleteLater();
}
void MainWindow::onSocketStateChange(QAbstractSocket::SocketState socketState)
{//socket状态变化时
switch(socketState)
{
case QAbstractSocket::UnconnectedState:
LabSocketState->setText("scoket状态:UnconnectedState");
break;
case QAbstractSocket::HostLookupState:
LabSocketState->setText("scoket状态:HostLookupState");
break;
case QAbstractSocket::ConnectingState:
LabSocketState->setText("scoket状态:ConnectingState");
break;
case QAbstractSocket::ConnectedState:
LabSocketState->setText("scoket状态:ConnectedState");
break;
case QAbstractSocket::BoundState:
LabSocketState->setText("scoket状态:BoundState");
break;
case QAbstractSocket::ClosingState:
LabSocketState->setText("scoket状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
LabSocketState->setText("scoket状态:ListeningState");
}
}
void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
while(tcpSocket->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}
停止监听:
TCP 服务器停止监听,只需调用 QTcpServer 的 close () 函数即可。单击 “停止监听” 按钮,其响应槽函数代码如下:
void MainWindow::on_actStop_triggered()
{//停止监听
if (tcpServer->isListening()) //tcpServer正在监听
{
tcpServer->close();//停止监听
ui->actStart->setEnabled(true);
ui->actStop->setEnabled(false);
LabListen->setText("监听状态:已停止监听");
}
}
2.3 与 TCP 客户端进行数据通信
TCP 服务器端和客户端之间通过 QTcpSocket 通信时,需要规定两者之间的通信协议,即传输的数据内容如何解析。 QTcpSocket 间接继承于 QIODevice,所以支待流读写功能。
Socket 之间的数据通信协议一般有两种方式:基于行的或基于数据块的。
-
基于行的数据通信协议一般用于纯文本数据的通信,每一行数据以一个换行符结束。一般是通过调用
canReadLine()
函数判断是否有新的一行数据需要读取,再调用readLine()
函数读取一行数据,例如://读取缓冲区行文本 while(tcpSocket->canReadLine()) ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
-
基于块的数据通信协议用于一般的二进制数据的传输,需要自定义具体的格式。
发送数据:
将输入文本框中的字符串转换为 QByteArray 类型的字节数组,然后调用 QIODevice 的 write()
函数写入缓冲区,这样就完成向客户端发送一行文字。单击 “发送信息” 按钮,会触发上述内容,代码如下:
void MainWindow::on_btnSend_clicked()
{//发送一行字符串,以换行符结束
QString msg=ui->editMsg->text();
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
QByteArray str=msg.toUtf8();
str.append('\n');//添加一个换行符
tcpSocket->write(str);
}
读取数据:
QTcpSocket 接收到来自客户端的数据后,会发射 readyRead() 信号,在其槽函数中需要读取行缓冲区的数据。代码如下:
void MainWindow::onSocketReadyRead()
{//读取缓冲区行文本
while(tcpSocket->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpSocket->readLine());
}
这样,TCPServer 就可以与 TCPClient 之间进行双向通信了,且这个连接将一直存在,直到某一方的 QTcpSocket 对象调用 disconnectFromHost() 函数断开 socket 连接。
运行效果图:
3.TCP 客户端程序设计
客户端程序 TCPClient 只需要使用一个 QTcpSocket 对象,就可以和服务器端程序 TCPServer 进行通信。
TCP 客户端程序应具有以下功能:
- 通过 IP 地址和端口号连接到服务器
- 采用基于行的数据通信协议,与服务器端收发消息
- 处理 QTcpSocket 的 StateChange() 信号,在状态栏显示 socket 的状态
3.1 主窗口定义与构造函数
TCPClient 是一个基于 QMainWindow 的应用程序,界面是由 UI 设计器设计,设计如下:
主窗口定义如下:
mainwindow.h:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpSocket>
#include <QLabel>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QTcpSocket *tcpClient; //socket
QLabel *LabSocketState; //状态栏显示标签
QString getLocalIP();//获取本机IP地址
protected:
void closeEvent(QCloseEvent *event);
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
//自定义槽函数
void onConnected();
void onDisconnected();
void onSocketStateChange(QAbstractSocket::SocketState socketState);
void onSocketReadyRead();//读取socket传入的数据
//
void on_actConnect_triggered();
void on_actDisconnect_triggered();
void on_actClear_triggered();
void on_btnSend_clicked();
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
私有变量:
- tcpClient:用于socket 连接与通信
- LabSocketState:用于显示 socket 状态
槽函数:自定义了几个槽函数,用于与 tcpClient 的相关信号关联。
构造函数:
构造函数主要功能是创建 tcpClient,设置状态栏标签,往下拉列表组件中添加本机 IP,最后建立信号与槽函数的关联。
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
tcpClient=new QTcpSocket(this); //创建socket变量
LabSocketState=new QLabel("Socket状态:");//状态栏标签
LabSocketState->setMinimumWidth(250);
ui->statusBar->addWidget(LabSocketState);
QString localIP=getLocalIP();//本机IP
this->setWindowTitle(this->windowTitle()+"----本机IP:"+localIP);
ui->comboServer->addItem(localIP);
connect(tcpClient,SIGNAL(connected()),this,SLOT(onConnected()));
connect(tcpClient,SIGNAL(disconnected()),this,SLOT(onDisconnected()));
connect(tcpClient,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
connect(tcpClient,SIGNAL(readyRead()),
this,SLOT(onSocketReadyRead()));
}
3.2 与服务器端建立 socket 连接
通过调用 QTcpSocket 的函数 connectToHost()
连接到服务器,也可以使用 disconnectFromHost()
函数断开与服务器的连接。其代码如下:
void MainWindow::on_actConnect_triggered()
{//连接到服务器
QString addr=ui->comboServer->currentText();
quint16 port=ui->spinPort->value();
tcpClient->connectToHost(addr,port);
// tcpClient->connectToHost(QHostAddress::LocalHost,port);
}
void MainWindow::on_actDisconnect_triggered()
{//断开与服务器的连接
if (tcpClient->state()==QAbstractSocket::ConnectedState)
tcpClient->disconnectFromHost();
}
void MainWindow::onConnected()
{ //connected()信号槽函数
ui->plainTextEdit->appendPlainText("**已连接到服务器");
ui->plainTextEdit->appendPlainText("**peer address:"+
tcpClient->peerAddress().toString());
ui->plainTextEdit->appendPlainText("**peer port:"+
QString::number(tcpClient->peerPort()));
ui->actConnect->setEnabled(false);
ui->actDisconnect->setEnabled(true);
}
void MainWindow::onDisconnected()
{//disConnected()信号槽函数
ui->plainTextEdit->appendPlainText("**已断开与服务器的连接");
ui->actConnect->setEnabled(true);
ui->actDisconnect->setEnabled(false);
}
3.3 与 TCPServer 的数据收发
TCPClient 与 TCPServer 之间采用基于行的数据通信协议。
发送数据:
将输入文本框中的字符串转换为 QByteArray 类型的字节数组,然后调用 QIODevice 的 write()
函数写入缓冲区,这样就完成向服务器端发送一行文字。单击 “发送信息” 按钮,会触发上述内容,代码如下:
void MainWindow::on_btnSend_clicked()
{//发送数据
QString msg=ui->editMsg->text();
ui->plainTextEdit->appendPlainText("[out] "+msg);
ui->editMsg->clear();
ui->editMsg->setFocus();
QByteArray str=msg.toUtf8();
str.append('\n');
tcpClient->write(str);
}
接收数据:
QTcpSocket 接收到来自服务器端的数据后,会发射 readyRead() 信号,在其槽函数中需要读取行缓冲区的数据。代码如下:
void MainWindow::onSocketReadyRead()
{//readyRead()信号槽函数
while(tcpClient->canReadLine())
ui->plainTextEdit->appendPlainText("[in] "+tcpClient->readLine());
}
运行结果:
4.小结
上述客户端与服务端程序只是为了演示 TCP 通信的基本原理,TCPServer 只允许一个TCPClient 客户端接入。而一般的 TCP 服务器程序允许多个客户端接入,为了使每个 socket 连接独立通信互不影响,一般采用多线程 ,即为一个 socket 连接创建一个线程。
上述的数据通信采用基于行的通信协议,这种 socket 通信方式只能传输字符串数据。QTcpSocket 间接继承于 QIODevice,可以使用数据流的方式传输二进制数据流,例如传输图片、任意格式文件等,但是这涉及到服务器端和客户端之间通信协议的定义。