本篇文章在Qt5.9.1环境下实现网络编程。Qt自带网络编程模块,网络编程主要是TCP,UDP和HTTP通信,Qt提供用于编写TCP/IP客户端和服务器端程序的各种类如用于TCP通信的QTcpServer和QTcpSocket,用于UDP通信的QUdpSocket,还有用于实现HTTP、FTP等普通网络通信的高级类如QNetworkRequest,QNetworkReply和QNetworkAccessManager。本文是网络编程的第一篇文章,主要在TCP协议下实现服务器端与客户端之间的通信,大大减轻了编程量,通过本文可以初步学会在Qt下TCP通信的相关操作。
开发环境:
1、Qt5.9.1
2、Win11
–
TCP协议(Transmission Control Protocol)全称是传输控制协议,他是一种被大多数Internet网络协议(如HTTP和FTP)用于数据传输的低级网络协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,特别适用于连续数据传输。
TCP被TCP通信必须先建立TCP连接,通信端分为客户端和服务端。服务端通过QTcpServer监听某个端口来监听是否有客户端连接到来,如果有连接到来,则建立新的socket连接,QTcpSocket用于建立连接后使用套接字(Socket)进行通信。客户端通过ip和port 连接服务端,当成功建立连接之后,就可进行数据的收发了。需要注意的是,在Qt中,Qt把socket当成输入输出流来对待的,数据的收发是通过read()和write()来进行的,需要与我们常见的send()与recv()进行区分。
TCP 客户端与服务端通信示意图如下。
QTcpServer是从QObject继承的类,他主要用于服务器端建立网络监听,创建网络Socket连接。QTcpServer的主要接口函数如下表所示(重要函数已加深颜色)
服务器端程序首先需要用QTcpServer::listen()开始服务器端监听,可以指定监听的IP地址和端口,一般一个服务程序只监听某个端口的网络连接。当有新的客户端接入时,QTcpServer内部的incomingConnection()函数会创建一个与客户端连接的QTcpSocket对象,然后发射信号newConnection(),在newConnection()信号的槽函数中,可以用nextPendingConnection()接受客户端的连接,然后使用QTcpSocket与客户端通信。
在客户端与服务器端建立TCP连接后,具体的数据通信是通过QTcpSocket完成的,QTcpSocket类提供TCP协议的接口,可以用QTcpSocket类实现标准网络通信协议。QTcpSocket类除了构造函数和析构函数,其它函数都是从QAbstractSocket继承或重定义的。其继承关系如下图所示:
QAbstractSocket用于TCP通信的主要接口函数如下表所示(重要函数已加深颜色)
TCP客户端使用QTcpSocket与TCP服务器端建立连接并通信。
客户端的QTcpSocket实例首先通过connectToHost()指定服务器端的IP地址和端口并尝试连接到服务器。connectToHost()是异步方式连接服务器,不会阻塞程序运行(如果需要阻塞方式连接服务器可以通过waitForConnection()函数),连接后发射connected()信号。与服务器建立连接后就可以向缓冲区写数据或从接收缓冲区读取数据,实现数据的通信,当缓冲区有新数据进入时,会发射readyRead()信号,一般在此信号的槽函数里读取缓冲区数据。
服务端主要功能是监听以及发送接收消息。监听分别用两个开关设置其状态,通过发送按钮发送编辑框里的值,通过文本浏览器接收来自客户端的消息,其界面搭建如下所示:
控件命名如下所示:
清空文本按钮由于是直接清空文本浏览器的内容,都有对应的信号与槽,所以直接在界面上设置信号与槽的关联:
要在Qt中使用网络模块,需要在项目配置文件中加入一条配置语句:
QT += core gui
QT += network //手动加入的网络模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
#ifndef WIDGET_H
#define WIDGET_H
#include
#include //加入服务器端server头文件
#include //加入服务器端socket头文件
#include //加入查询主机信息的头文件
#include //加入网络接口相关头文件
#include //调试
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
QTcpSocket* tcpSocket; //声明socket指针
QTcpServer* tcpServer; //声明server指针
QList<QHostAddress> IPlist; //声明搜寻的主机IP列表
void getLocalHostIP(); //声明当地主机IP地址函数
private slots:
void clientConnected(); //自定义槽函数,表示有新客户端连接时的处理
void receiveMessages(); //自定义槽函数,表示服务器端接收消息时处理
void socketStateChange(QAbstractSocket::SocketState); //自定义槽函数,表示socket状态
void on_pbtnListenStart_clicked(); //由按钮生成的槽函数,表示监听开始
void on_pbtnListenStop_clicked(); //由按钮生成的槽函数,表示监听结束
void on_pbtnSendMessage_clicked(); //由按钮生成的槽函数,表示消息发送
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
tcpServer = new QTcpServer(this);
tcpSocket = new QTcpSocket(this);
ui->spinBox->setRange(10000,99999); //设置端口号范围
ui->lineEdit->setText("我是服务器端的哦"); //设置发送编辑框默认文字
getLocalHostIP(); //调用获取IP函数
//当有新客户端连接信号时调用我们定义的槽函数
connect(tcpServer, SIGNAL(newConnection()),this, SLOT(clientConnected()));
}
Widget::~Widget()
{
delete ui;
}
// 搜寻当地的IP地址并输入到组合框内
void Widget::getLocalHostIP()
{
QList<QNetworkInterface> list = QNetworkInterface::allInterfaces(); //返回主机上所有接口的网络列表
foreach (QNetworkInterface interface, list)
{
QList<QNetworkAddressEntry> entryList = interface.addressEntries(); //返回该网络接口的IP地址列表
foreach (QNetworkAddressEntry entry, entryList)
{
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) //只显示IPv4地址
{
ui->comboBox->addItem(entry.ip().toString()); //打印到组合框
IPlist<<entry.ip();
}
}
}
}
// 当有新客户端连接时调用此槽函数
void Widget::clientConnected()
{
tcpSocket = tcpServer->nextPendingConnection(); //返回下一个等待接入的连接
QString ip = tcpSocket->peerAddress().toString(); //获取客户端ip
quint16 port = tcpSocket->peerPort(); //获取客户端端口
ui->textBrowser->append("客户端已连接");
ui->textBrowser->append("客户端 ip 地址:" + ip);
ui->textBrowser->append("客户端端口:" + QString::number(port));
//当缓冲区有数据时调用自定义的数据接收槽函数
connect(tcpSocket, SIGNAL(readyRead()),this, SLOT(receiveMessages()));
//当socket状态改变时调用自定义的状态改变槽函数
connect(tcpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(socketStateChange(QAbstractSocket::SocketState)));
}
// 当缓冲区有数据时调用此槽函数
void Widget::receiveMessages()
{
QString messages = "客户端:" + tcpSocket->readAll(); //读取缓冲区数据
ui->textBrowser->append(messages);
}
// 当状态改变时调用此槽函数
void Widget::socketStateChange(QAbstractSocket::SocketState state)
{
switch (state) //不同状态打印不同信息
{
case QAbstractSocket::UnconnectedState:
ui->textBrowser->append("scoket 状态:UnconnectedState");
break;
case QAbstractSocket::ConnectedState:
ui->textBrowser->append("scoket 状态:ConnectedState");
break;
case QAbstractSocket::ConnectingState:
ui->textBrowser->append("scoket 状态:ConnectingState");
break;
case QAbstractSocket::HostLookupState:
ui->textBrowser->append("scoket 状态:HostLookupState");
break;
case QAbstractSocket::ClosingState:
ui->textBrowser->append("scoket 状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
ui->textBrowser->append("scoket 状态:ListeningState");
break;
case QAbstractSocket::BoundState:
ui->textBrowser->append("scoket 状态:BoundState");
break;
default:
break;
}
}
//按钮生成槽函数,监听开始
void Widget::on_pbtnListenStart_clicked()
{
if (ui->comboBox->currentIndex() != -1)
{
qDebug()<<"start listen"<<endl;
tcpServer->listen(IPlist[ui->comboBox->currentIndex()],ui->spinBox->value()); //设置监听ip和端口
ui->textBrowser->append("服务器 IP 地址:"+ ui->comboBox->currentText());
ui->textBrowser->append("正在监听端口:"+ ui->spinBox->text());
}
}
//按钮生成槽函数,监听结束
void Widget::on_pbtnListenStop_clicked()
{
qDebug()<<"stop listen"<<endl;
tcpServer->close(); //关闭监听
if (tcpSocket->state() == tcpSocket->ConnectedState)
tcpSocket->disconnectFromHost(); //断开socket
ui->textBrowser->append("已停止监听端口:"+ ui->spinBox->text());
}
//按钮生成槽函数,服务器端发送
void Widget::on_pbtnSendMessage_clicked()
{
if(NULL == tcpSocket)
return;
if(tcpSocket->state() == tcpSocket->ConnectedState)
{
tcpSocket->write(ui->lineEdit->text().toUtf8().data()); //socket写入编辑框的值
ui->textBrowser->append("服务端:" + ui->lineEdit->text());
}
}
客户端与服务端界面类似,只是将监听开关换成了连接服务器开关。其界面搭建如下所示:
控件命名如下所示:
清空文本按钮由于是直接清空文本浏览器的内容,都有对应的信号与槽,所以直接在界面上设置信号与槽的关联:
要在Qt中使用网络模块,需要在项目配置文件中加入一条配置语句:
QT += core gui
QT += network //手动加入的网络模块
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
#ifndef WIDGET_H
#define WIDGET_H
#include
//#include //客户端只需要socket即可
#include
#include
#include
#include
#include
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
QTcpSocket *tcpSocket;
QList<QHostAddress> IPlist;
void getLocalHostIP();
private slots:
void connected(); //自定义槽函数,表示连接之后的处理
void disconnected(); //自定义槽函数,表示断开连接之后的处理
void receiveMessages(); //自定义槽函数,表示客户端接收消息时处理
void socketStateChange(QAbstractSocket::SocketState); //自定义槽函数,表示socket状态
void on_pbtnConnect_clicked(); //由按钮生成的槽函数,表示连接服务器
void on_pbtnDisconnect_clicked(); //由按钮生成的槽函数,表示断开连接
void on_pbtnSendMessage_clicked(); //由按钮生成的槽函数,表示向服务器发送消息
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
/* 客户端很多操作都与服务器端类似,所以这里就不重复注释了。 */
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
tcpSocket = new QTcpSocket(this);
ui->spinBox->setRange(10000, 99999);
ui->lineEdit->setText("我是客户端哦");
getLocalHostIP();
connect(tcpSocket, SIGNAL(connected()),this, SLOT(connected()));
connect(tcpSocket, SIGNAL(disconnected()),this, SLOT(disconnected()));
connect(tcpSocket, SIGNAL(readyRead()),this, SLOT(receiveMessages()));
connect(tcpSocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
this,SLOT(socketStateChange(QAbstractSocket::SocketState)));
}
Widget::~Widget()
{
delete ui;
}
void Widget::connected()
{
ui->textBrowser->append("已经连上服务端");
}
void Widget::disconnected()
{
ui->textBrowser->append("已经断开服务端");
}
void Widget::getLocalHostIP()
{
QList<QNetworkInterface> list = QNetworkInterface::allInterfaces();
foreach (QNetworkInterface interface, list)
{
QList<QNetworkAddressEntry> entryList = interface.addressEntries();
foreach (QNetworkAddressEntry entry, entryList)
{
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol)
{
ui->comboBox->addItem(entry.ip().toString());
IPlist<<entry.ip();
}
}
}
}
void Widget::receiveMessages()
{
QString messages = tcpSocket->readAll();
ui->textBrowser->append("服务端:" + messages);
}
void Widget::socketStateChange(QAbstractSocket::SocketState state)
{
switch (state) //不同状态打印不同信息
{
case QAbstractSocket::UnconnectedState:
ui->textBrowser->append("scoket 状态:UnconnectedState");
break;
case QAbstractSocket::ConnectedState:
ui->textBrowser->append("scoket 状态:ConnectedState");
break;
case QAbstractSocket::ConnectingState:
ui->textBrowser->append("scoket 状态:ConnectingState");
break;
case QAbstractSocket::HostLookupState:
ui->textBrowser->append("scoket 状态:HostLookupState");
break;
case QAbstractSocket::ClosingState:
ui->textBrowser->append("scoket 状态:ClosingState");
break;
case QAbstractSocket::ListeningState:
ui->textBrowser->append("scoket 状态:ListeningState");
break;
case QAbstractSocket::BoundState:
ui->textBrowser->append("scoket 状态:BoundState");
break;
default:
break;
}
}
void Widget::on_pbtnConnect_clicked()
{
if (tcpSocket->state() != tcpSocket->ConnectedState)
{
tcpSocket->connectToHost(IPlist[ui->comboBox->currentIndex()],ui->spinBox->value()); //连接服务器
}
}
void Widget::on_pbtnDisconnect_clicked()
{
tcpSocket->disconnectFromHost(); //断开连接
tcpSocket->close(); //关闭socket
}
void Widget::on_pbtnSendMessage_clicked()
{
if(NULL == tcpSocket)
return;
if(tcpSocket->state() == tcpSocket->ConnectedState) //确认是已连接的状态
{
tcpSocket->write(ui->lineEdit->text().toUtf8().data()); //写入编辑框里的值
ui->textBrowser->append("客户端:" + ui->lineEdit->text());
}
else
ui->textBrowser->append("please connect server!");
}
1.服务器端打开监听
2.客户端链接服务器
3.客户端发送消息
4.服务器端发送消息
5.客户端断开连接
6.服务器端停止监听
本次设计了解如何通过Qt自带的网络模块编写TCP通信的服务端和客户端并实现两者的交互。设计中只实现了一些重要代码的功能,其他辅助代码需要读者自己去完善,比如界面的一些优化,按钮的互斥等等,本次设计参考【正点原子】I.MX6U嵌入式Qt开发指南V1.1以及Qt5.9 C++开发指南。