Qt-Tcp编程(异步与同步解读)

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异步编程较多,但同步编程也会用到,根据实际需求选择即可~

你可能感兴趣的:(Qt与QML,qt5,tcp,网络编程)