QTcpSocket 支持两种通用的网络编程方法
异步(非阻塞)方法:
当控制返回到 Qt 的事件循环时,操作会被调度和执行。 当操作完成时,QTcpSocket 会发出一个信号。 例如,QTcpSocket::connectToHost() 立即返回,当连接建立后,QTcpSocket 发出 connected()。
同步(阻塞)方法:
在非 GUI 和多线程应用程序中,您可以调用 waitFor…() 函数(例如,QTcpSocket::waitForConnected())来暂停调用线程直到操作完成,而不是连接到信号。
tcp异步编程
Tcp异步编程的核心就是Qt的信号槽机制
,通过关联对应的信号,等信号来了之后在做出相应的响应。显然,异步操作是可以主线程(GUI)中使用,因为它不会阻塞主线程,这也是与同步根本的区别。下面展示tcp异步编程的基本步骤,包含客户端和服务端。
1. 客户端
- 在widget类中声明一个
QTcpSocket*指针
或者QTcpSocket对象
,下面以指针为例,并声明几个常用的关联信号的函数(函数名自定义即可)。
//client.h
class Client : public Dialog{//当然这里也可以直接子类化QTcpSocket
Q_OBJECT
public:
explicit Client(QWidget *parent = nullptr);
...
private slots:
void requestData(); //请求数据
void readData(); //读数据
void displayError(QAbstractSocket::SocketError socketError);//有错误发生
...
private:
QTcpSocket *tcpSocket = nullptr;
}
- 在.cpp中的构造函数中关联上QTcpSocket对应的信号。
//client.cpp
//下面的函数都是简写
void Client::Client(QWidget *parent)
:QDialog(parent),
tcpSocket(new QTcpSocket(this){
...
/*异步核心代码*/
//假设前面声明了一个按钮,点击操作即调用请求操作
connect(ReqButton, &QAbstractButton::clicked, this, &Client::requestData);
//连接建立成功!
connect(tcpSocket,&QAbstractSocket::connected,[=](){
...
//简单的打印一个消息,你也可以在这里做其他操作
qDebug() << "连接成功!!!";
...
});
//新数据到来
connect(tcpSocket, &QIODevice::readyRead, this, &Client::readData);
//有错误发生
connect(tcpSocket, &QAbstractSocket::errorOccurred,
this, &Client::displayError);
...
}
- 在.cpp中实现信号对应的槽函数
void Client::requestData(){
...
//准备连接服务器端,等待服务器的连接,连接成功会发出connected()信号
tcpSocket.connectToHost(主机ip,主机端口号);
...
}
void Client::readData(){
...
//读数据操作 (除了自带的方法,也可以关联数据流(QDataStream)来读取序列化数据)
auto data = tcpSocket.readAll();
//将数据显示或者存储
...
}
void Client::::displayError(QAbstractSocket::SocketError socketError){
//打印错误信息
...
}
至此,客户端主要代码完成!(更多细节根据自己需要实现)
2. 服务端
- 在widget类中声明一个
QTcpServer*指针
或者QTcpServer对象
,下面以指针为例,并声明几个常用的关联信号的函数(函数名自定义即可)。
class Server: public Dialog{
Q_OBJECT
public:
explicit Server(QWidget *parent = nullptr);
...
private slots:
void sendData(); //请求数据
...
private:
QTcpServer *tcpServer = nullptr;
}
- 在.cpp中关联信号,并实现对应的槽函数
Server::Server(QWidget *parent)
: QDialog(parent)
, tcpServer(new QTcpServer(this)) //初始化套接字
{
if (!tcpServer->listen()) { //监听连接
QMessageBox::critical(this, tr("Server"),
tr("Unable to start the server: %1.")
.arg(tcpServer->errorString()));
close();
return;
}
...
//将服务端地址和端口号告知客户端(可以通过两个label显示下面这两个字符串)
//tcpServer.serverAddress() 和 tcpServer.serverPort()
...
//每次有新连接到来时,也即客户端调用connectToHost()
connect(tcpServer, &QTcpServer::newConnection, this, &Server::sendData();
...
}
void Server::sendData(){
...
准备数据data
...
//返回一个代办的连接
QTcpSocket *clientConnection = tcpServer->nextPendingConnection();
//关闭连接之后发出这个信号,触发删除事件,随后删除这个连接对象
connect(clientConnection, &QAbstractSocket::disconnected,
clientConnection, &QObject::deleteLater);
//将数据写进这个socket
clientConnection->write(data);
//等数据写完之后,关闭连接
clientConnection->disconnectFromHost();
}
至此服务端代码完成(更多细节根据自己需要实现)
tcp同步编程
tcp同步编程的核心就是QTcpSocket提供的阻塞API
,以waitFor…()开头的函数,由于要调用阻塞API,如果网络操作还在主线程中进行,那么势必会冻结用户界面,这是非常不友好的,所以应该在另外的线程中处理这些同步操作
。下面介绍同步编程的基本步骤,包含客户端和服务端。
1. 客户端
- 包含两个核心类,
BlockingClient(继承窗口类)
,BlockingThread(继承Thread)
对于BlockingThread类,它是实际进行网络操作的线程类:
//BlockingThread.h
class BlockingThread : public QThread
{
Q_OBJECT
public:
BlockingThread(QObject *parent = nullptr);
~BlockingThread();
void requestData(const QString &hostName, quint16 port);//请求新的数据
void run() override; //再此方法中进行网络操作
signals:
void newData(const QString &fortune); //回显数据信号
void error(int socketError, const QString &message); //发生错误信号
private:
QString hostName;
quint16 port;
QMutex mutex; //互斥锁,保证数据的写操作只能有一个线程
QWaitCondition cond; //条件变量,同步操作
bool quit; //run函数的退出条件
}
在.cpp中调用阻塞api进行同步操作
BlockingThread::BlockingThread(QObject *parent)
: QThread(parent), quit(false)
{
//constructor is very simple
}
BlockingThread::~BlockingThread()
{
mutex.lock();
quit = true;
cond.wakeOne();//这里将唤醒线程完成最后的迭代操作
mutex.unlock();
wait();//QThread::wait():返回当线程完成最后操作,即run函数执行完毕,或者线程尚未启动直接返回
}
//这个方法实际在窗口类中进行调用和传参
void BlockingThread::requestData(const QString &hostName, quint16 port){
QMutexLocker locker(&mutex); //这里需锁定,防止获取数据时,其他线程调用此方法来修改数据
this->hostName = hostName;
this->port = port;
if (!isRunning())
start(); //如果线程还没启动,则启动该线程来向Server请求fortune
else
cond.wakeOne(); //如果线程已经在运行且是等待状态,调用这个函数唤醒该线程,表示又是一次新的请求数据的过程
}
run函数进行实际的网络操作
void BlockingThread::run()
{
//这里防止在获取数据时,其他线程访问这些数据,QString是可重入的,但不是线程安全的
mutex.lock();
QString serverName = hostName;
quint16 serverPort = port;
mutex.unlock();
while (!quit) {
const int Timeout = 5 * 1000;
QTcpSocket socket;
socket.connectToHost(serverName, serverPort);
//waitForConnected():等待套接字连接,最多 mssecs 毫秒,期间将阻塞线程.
if (!socket.waitForConnected(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}
//这里通过数据流来读数据
QDataStream in(&socket);
auto data;
//一次do{}while()代表一次完整的读新数据的过程
do {
//waitForReadyRead():等待可读的新数据,期间将阻塞线程
if (!socket.waitForReadyRead(Timeout)) {
emit error(socket.error(), socket.errorString());
return;
}
//开启事务
in.startTransaction();
in >> data; //写入新数据
} while (!in.commitTransaction()); //事务提交成功,那么说明写入成功,退出循环
mutex.lock();
emit newData(data); //通知客户端显示fortune
//在这里这个线程将会阻塞(休眠状态或是等待状态),直至调用cont.waitOne or cont.waitAll() 来唤醒该线程 ,也即客户端请求新的data来调用requestData()
cond.wait(&mutex); //等待结束后,锁定的Mutex将返回到相同的锁定状态
serverName = hostName; //这里的主机名或者端口号可能已经被其他线程改变了,所以需要重新赋值
serverPort = port;
mutex.unlock();
}
}
- 对于BlockingClient类,主要进行显示操作和回调线程类方法,当然也处理在线程中可能发生的网络错误信息
//BlockingClient.h
class BlockingClient : public QWidget
{
Q_OBJECT
public:
BlockingClient(QWidget *parent = nullptr);
private slots:
void requestData();
void showData(const QString &fortune);
void displayError(int socketError, const QString &message);
private:
BlockingThread thread; //线程类
};
//BlockingClient.cpp
BlockingClient::BlockingClient(QWidget *parent)
: QWidget(parent)
{
...
//初始化窗口
...
//请求新数据
connect(getFortuneButton, &QPushButton::clicked,
this, &BlockingClient::requestData);
//新数据在其他线程已就绪,在主线程进行显示
connect(&thread, &BlockingThread::newData, this, &BlockingClient::showData);
//网络操作在其他线程发生错误,通知主线程处理
connect(&thread, &BlockingThread::error, this, &BlockingClient::displayError);
...
}
void BlockingClient::requestData()
{
...
//实际调用线程类的方法
thread.requestData(host, port);
...
}
void BlockingClient::showData(const QString &nextFortune)
{
//显示数据
...
}
void BlockingClient::displayError(int socketError, const QString &message)
{
//打印错误信息
...
}
至此,阻塞客户端主要代码完成!(更多细节根据自己需要实现)
2. 服务端
- 服务端使用3个核心类来完成,BlockingServer(继承QTcpServer)、BlockingThread(继承QThread),Dialog(继承QDialog)
- 对于BlockingServer类,主要是对于每个新到来的连接,新开一个线程,实际网络操作在另外的线程中完成。
//BlockingServer.h
class BlockingServer : public QTcpServer
{
Q_OBJECT
public:
BlockingServer(QObject *parent = nullptr);
protected:
//注意,要将连接操作在另外的线程中处理需要重写这个函数
void incomingConnection(qintptr socketDescriptor) override;
private:
QStringList data;
};
//BlockingServer.cpp
BlockingServer::FortuneServer(QObject *parent)
: QTcpServer(parent)
{
//准备数据data
}
void BlockingServer::incomingConnection(qintptr socketDescriptor)
{
//每个连接都是用一个单独的线程处理,并传递socketDescriptor参数到另一个线程处理
BlockingThread *thread = new BlockingThread(socketDescriptor, data, this);
connect(thread, &BlockingThread::finished, thread, &BlockingThread::deleteLater);//关联线程的finished信号( run()函数结束 )来删除这个线程对象
thread->start(); //启动线程
}
- 对于BlockingThread类,实际处理网络操作的线程类
//BlockingThread.h
class BlockingThread : public QThread
{
Q_OBJECT
public:
BlockingThread(int socketDescriptor, const QString &data, QObject *parent);
void run() override;
signals:
void error(QTcpSocket::SocketError socketError);
private:
int socketDescriptor;
QString text;
};
//BlockingThread.cpp
BlockingThread::BlockingThread(int socketDescriptor, const QString &data, QObject *parent)
: QThread(parent),
socketDescriptor(socketDescriptor), text(data)//初始化套接字描述符,和准备的数据
{
//constructor is very simple
}
void BlockingThread::run()
{
QTcpSocket tcpSocket;
//通过调用 QTcpSocket::setSocketDescriptor() 初始化套接字,将套接字描述符作为参数传递
if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
emit error(tcpSocket.error());
return;
}
//使用 QDataStream 将流编码为 QByteArray。
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly);
out << text;
tcpSocket.write(block);
tcpSocket.disconnectFromHost();//等数据写完之后,关闭连接
//这里可以采取阻塞api,因为在单独的线程中运行,所以 GUI 将保持响应。
tcpSocket.waitForDisconnected();
}
- 对于Dialog窗口类,主要是用来监听客户端的连接
//Dialog.h
class Dialog : public QWidget
{
Q_OBJECT
public:
Dialog(QWidget *parent = nullptr);
private:
BlockingServer server; //tcpServer对象
};
//Dialog.cpp
Dialog::Dialog(QWidget *parent)
: QWidget(parent)
{
...
if (!server.listen()) { //监听来自客户端的连接
QMessageBox::critical(this, tr("Server"),
tr("Unable to start the server: %1.")
.arg(server.errorString()));
close();
return;
}
//将服务端地址和端口号告知客户端(可以通过两个label显示下面这两个字符串)
//tcpServer.serverAddress() 和 tcpServer.serverPort()
...
}
至此服务端代码完成(更多细节根据自己需要实现)
总结:一般tcp异步编程较多,但同步编程也会用到,根据实际需求选择即可~