客户端与服务端


本节旨在基于 QT 实现客户端与服务端的连接与数据传输。

TCP

  • 面向连接的协议,即发送数据之前要先建立连接。
  • 面向字节流,发送数据时以字节为单位。
  • 只支持点对点通信。
  • UDP 是无连接的协议,类似于广播,传输数据为报文,UDP可以一对一、一对多、多对一、多对多。

三次握手

  • 第一次握手:客户端什么情况都不知道,服务端确定了对方发送数据正常;

  • 第二次握手:

    • 客户端确认了:自己发送与接收正常、服务器接收与发送正常;
    • 服务端确认了:自己接收正常,客户端发送正常;
  • 第三次握手:

    • 客户端确认了:自己发送与接收正常、服务器发送与接收正常;
    • 服务端确认了:自己接收与发送正常,客户端发送与接收正常;
  • 两次握手会导致:

    • 无法保证客户端的接收能力: 在两次握手中,客户端发送了一个请求包,服务器回应确认包,但此时服务器无法确认客户端是否准备好接收数据。如果服务器立即开始发送数据,客户端可能还未准备好接收,造成数据丢失。

    • 旧的连接请求可能被误处理: 在两次握手中,如果客户端的请求在网络中因延迟而滞后,服务器可能会收到一个很久以前发出的连接请求,误认为是新的请求而建立连接,导致数据不一致。

四次挥手

  • 第一次挥手:客户端向服务器说:“我要关闭连接了”(Client将FIN置为1,发送一个序列号seq给Server;进入FIN_WAIT_1状态)
  • 第二次挥手:服务端向客户端传递,”收到,你要关闭连接了“ (Server收到FIN之后,发送一个ACK=1,acknowledge number=收到的序列号+1;进入CLOSE_WAIT状态)
  • 第三次挥手:服务器向客户端说,“我要关闭连接了”(Server将FIN置1,发送一个序列号给Client;进入LAST_ACK状态)
  • 第四次挥手:客户端向服务端传输:“已收到你要关闭连接”(Client收到服务器的FIN后,进入TIME_WAIT状态;接着将ACK置1,发送一个 acknowledge number=序列号+1给服务器;服务器收到后,确认acknowledge number后,变为 CLOSED状态,不再向客户端发送数据。客户端等待2*MSL(报文段最长寿命)时间后,也进入 CLOSED状态。完成四次挥手。)
  • 二三次挥手没有合并的原因:服务器收到客户端断开连接的请求时,可能还有一些数据没有发完,这时先回复ACK,表示接 收到了断开连接的请求。等到数据发完之后再发FIN,断开服务器到客户端的数据传送。

客户端与服务端

客户端

客户端主要实现流程:

  • 创建并初始化 QTcpSocket 对象 socket
  • socket 调用 connectToHost() 连接服务器;
  • socket 调用成员函数 write,发送数据给服务器;
  • 连接 QTcpSocket 对象的 connected() 信号槽,当客户端成功连接到服务器后触发 connected() 信号;
  • 连接 QTcpsocket 对象的 readyread() 信号槽,当客户端接收到服务端发来数据时触发 readyread() 信号;
  • 连接 QTcpsocket 对象的 disconnected 信号槽,当客户端对象调用成员函数 close,会触发 QTcpsocket 对象的 disconnected 信号,进而触发槽函数进行相应处理。

代码实现:

class TcpClient : public QObject {
    Q_OBJECT
public:
    void Init();
    
private slots:
    void serverInfoSlot();
    
private:
    QTcpSocket *_socket;
};

void TcpClient::Init(){
    // 1.创建并初始化 QTcpSocket 对象
    _socket = new QTcpSocket();
    QString IP = "xxx.xxx.xxx.xxx";
    int Port = 8000;
    // 2.连接服务器
    _socket->connectToHost(IP, Port);
    // 3.客户端成功连接到服务器后触发的槽函数
    connect(_socket, &QTcpSocket::connected, [this](){
        std::cout << "connect successful!" <<std::endl;
    });
    // 3.客户端断开与服务器连接后触发的槽函数
    connect(_socket, &QTcpSocket::disconnected, [this](){
        std::cout << "connect disconnected!" <<std::endl;
    });
    // 3.客户端接收到服务端发来数据时触发的槽函数
    connect(_socket, &QTcpSocket::readyRead, this, &TcpClient::serverInfoSlot);
}

void TcpClient::serverInfoSlot() {
    while (socket.canReadLine()) {
    	QString msg = _socket->readAll();
    	std::cout << msg.toStdString() << std::endl;
        // 4.数据发送
        _socket.write(QString::fromStdString("Received successfully!").toUtf8());
        _socket.flush();
    }
}

服务端

基于QT实现服务端,无显示界面,消息在终端显示;

服务端基于 QTcpServer 实现,具体实现流程:

  • 创建 QTcpServer 对象 server
  • server 设置监听端口,server->listen ;
  • 连接信号和槽;客户端发起连接时, server 会发出 newConnection 信号,将该信号连接到一个自定义的槽函数,来处理新连接;
  • 处理新连接; 在响应 newConnection 信号的槽函数中,使用 QTcpServer::nextPendingConnection() 方法获取与新客户端连接相关的 QTcpSocket 对象。

代码实现:

class TcpServer : public QObject {
    Q_OBJECT
public:
    void Init();
    
private slots:
    void newClientHandler();
    void clientInfoSlot();
};

void TcpServer::Init() {
    // 1.创建 QTcpServer 对象 
    _server = new QTcpServer(this);
    // 2.设置监听端口及地址,监听地址
    if (!_server->listen(QHostAddress::Any, PORT)) {
        std::cerr << "Failed to start server: " << _server->errorString().toStdString() << std::endl;
        return;
    }
    // 3.连接信号和槽;客户端发起连接时,触发 newClientHandler 函数处理连接
    connect(_server, &QTcpServer::newConnection, this, &TcpServer::newClientHandler);
}

void TcpServer::newClientHandler() {
    // 建立TCP连接
    QTcpSocket *socket = _server->nextPendingConnection();
    if (socket) {
        // 获取客户端地址与端口号
        std::cout << socket->peerAddress().toString().toStdString() << ": " << socket->peerPort() << std::endl;
    }
    // 服务器收到客户端的消息,socket发出readyread信号
    connect(socket, &QTcpSocket::readyRead, this, &TcpServer::clientInfoSlot);
}

void TcpServer::clientInfoSlot() {
    // 在槽函数里面使用 sender() 可以获取信号的发出者
    QTcpSocket *s = (QTcpSocket *)sender();
    while (s->canReadLine()) { // 逐行读取
        QString msg = s->readLine();
        std::cout << msg.toStdString() << std::endl;
    }
}

客户端添加重连机制

class TcpClient : public QObject {
    Q_OBJECT
public:
    void Init();
    
private slots:
    void serverInfoSlot();
    
private:
    QTcpSocket *_socket;
    QTimer *reconnectTimer = nullptr;
    QString IP;
    int Port;
private:
    void reconnectToHost(const QString &socket_address, quint16 port);
};

void TcpClient::Init(){
    // 1.创建并初始化 QTcpSocket 对象
    _socket = new QTcpSocket();
    IP = "xxx.xxx.xxx.xxx";
    Port = 8000;
    // 2.连接服务器
    _socket->connectToHost(IP, Port);
    // 3.客户端成功连接到服务器后触发的槽函数
    connect(_socket, &QTcpSocket::connected, [this](){
        std::cout << "connect successful!" <<std::endl;
    });
    // 3.客户端断开与服务器连接后触发的槽函数
    connect(_socket, &QTcpSocket::disconnected, [this](){
        std::cout << "connect disconnected!" <<std::endl;
        reconnectTimer->start(3000); // 每3秒尝试重连一次
    });
    // 3.客户端接收到服务端发来数据时触发的槽函数
    connect(_socket, &QTcpSocket::readyRead, this, &TcpClient::serverInfoSlot);
    
    reconnectTimer = new QTimer();
    QObject::connect(reconnectTimer, &QTimer::timeout, [&]() {
        if (socket.state() != QTcpSocket::ConnectedState) {
            reconnectToHost(IP, Port);
        }
    });
    reconnectToHost(IP, Port);
}

void TcpClient::reconnectToHost(const QString &socket_address, quint16 port) {
    if (socket.state() == QTcpSocket::UnconnectedState) {
        socket.connectToHost(QString::fromStdString(socket_address), g_Utils::get_socket_port());
        if (!socket.waitForConnected(5000)) {
            qDebug() << "Reconnection failed: " << socket.errorString();
        }
    }
    if (socket.state() == QTcpSocket::ConnectedState) {
        std::cout << "reconnect successful!" << std::endl;
        reconnectTimer->stop();
    }
}

客户端问题

QObject: Cannot create children for a parent that is in a different thread.

上述问题可以理解为:不能为属于不同线程的父对象创建子对象。

示例程序:

#ifndef TCP2_SERVER_H
#define TCP2_SERVER_H
#include "qthread.h"
#include <iostream>
#include <string>
#include <QTcpSocket>
#include <QThread>
#include <QEventLoop>
class Th2 : public QThread {
public:
    Th2() {
        qDebug() << "Th2::Th2, thread = " << QThread::currentThread();
    };
    ~Th2(){};
    void Init();
    void run() override;

public:
    QTcpSocket *_socket;
};
#endif // TCP2_SERVER_H

void Th2::Init() {
    _socket = new QTcpSocket();
    _socket->connectToHost("xxx.xxx.xxx.xxx", 8000);
    QObject::connect(_socket, &QTcpSocket::connected, [&]() {
        std::cout << "Th2 connected successful!" << std::endl;
    });

    QObject::connect(_socket, &QTcpSocket::disconnected, [&]() {
        std::cout << "Th2 disconnected from server." << std::endl;
    });
}

void Th2::run() {
    qDebug() << "Th2::run, thread = " << QThread::currentThread();
    qDebug() << "Th2::run, Th2 thread = " << this->thread();
    QString str = "Th2 data";
    _socket->write(str.toUtf8());
    _socket->flush();
    QEventLoop loop;
    loop.exec();
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
    Th2 *th2 = new Th2();
    th2->Init();
    th2->start();
    return a.exec();
}
/*
打印结果:
Th2::Th2, thread =  QThread(0x587d600)	
Th2::run, thread =  QThread(0x58848e0)
Th2::run, Th2 thread =  QThread(0x587d600)
*/

结果分析:0x587d600 表示此时在main线程里面,0x58848e0 为run方法的线程地址,所以会报:不能为属于不同线程的父对象创建子对象。

解决方法是通过 QThread->moveToThread(QThread) 将子线程也搬到主线程上去;

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
    Th2 *th2 = new Th2();
    th2->moveToThread(th2);
    th2->Init();
    th2->start();
    return a.exec();
}
/*
打印结果:
Th2::Th2, thread =  QThread(0x5877d20)
Th2::run, thread =  QThread(0x587e5e0)
Th2::run, Th2 thread =  QThread(0x587e5e0)
*/

建议初始化socket时放在run方法里面初始化,如:

#ifndef TCP2_SERVER_H
#define TCP2_SERVER_H
#include "qthread.h"
#include <iostream>
#include <string>
#include <QTcpSocket>
#include <QThread>
#include <QEventLoop>
class Th2 : public QThread {
public:
    Th2() {
        qDebug() << "Th2::Th2, thread = " << QThread::currentThread();
    };
    ~Th2(){};
    void InitSocket();
    void run() override;

public:
    QTcpSocket *_socket;
};
#endif // TCP2_SERVER_H

void Th2::InitSocket() {
    _socket = new QTcpSocket();
    _socket->connectToHost("192.168.1.216", 65529);
    QObject::connect(_socket, &QTcpSocket::connected, [&]() {
        std::cout << "Th2 connected successful!" << std::endl;
    });

    QObject::connect(_socket, &QTcpSocket::disconnected, [&]() {
        std::cout << "Th2 disconnected from server." << std::endl;
    });
}

void Th2::run() {
    InitSocket();
    qDebug() << "Th2::run, thread = " << QThread::currentThread();
    qDebug() << "Th2::run, Th2 thread = " << this->thread();
    QString str = "Th2 data";
    _socket->write(str.toUtf8());
    _socket->flush();
    QEventLoop loop;
    loop.exec();
}

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv);
    Th2 *th2 = new Th2();
    th2->moveToThread(th2);
    th2->start();
    return a.exec();
}

栈与堆扩展

在客户端与服务端的实现过程中,遇到一些问题,解决过程中了解了 C++ 中堆与栈的相关概念,在此处进行记录。

栈(Stack)

栈是一种高效、自动管理的内存区域。它是“后进先出”(LIFO,Last In First Out)的数据结构,分配内存时的顺序是相反的:最后分配的内存块会最先释放。

栈的特点:

  • 自动分配和释放:当函数调用时,函数内的局部变量会被自动分配在栈上;当函数执行完毕,栈上的局部变量会被自动释放,不需要程序员手动管理。
  • 内存限制:栈内存通常比堆内存小,适用于小而频繁分配的变量。
  • 高效:栈内存的分配和释放非常快,操作系统通过移动栈指针来管理内存。
  • 生命周期短:栈上的变量仅在函数作用域内有效,当函数返回后,这些变量就失效了。

哪些变量在栈上:

  • 局部变量:在函数或方法内部声明的基本数据类型(如 intfloatchar)和指针类型。
  • 函数参数:大多数情况下,函数的参数会被复制到栈上。
  • 简单对象:如果一个对象是在栈上声明的,则该对象的内存会分配在栈上。
class MyClass {
    int value;
};
void func(int a, int b) {
    // 函数参数 a 和 b 在栈上
    int x = 10;  // 局部变量 x 在栈上分配
    MyClass obj;  // obj 在栈上分配
}

堆(Heap)

堆是一块较大的动态内存区域,由程序员手动控制。与栈不同,堆上的内存是动态分配的,程序员必须显式地进行分配和释放。

堆的特点:

  • 动态分配和释放:内存的分配和释放由程序员通过 newdelete(或 mallocfree)来管理。如果忘记释放内存,就会导致内存泄漏
  • 内存空间大:堆的内存空间通常比栈大很多,但分配速度较慢。
  • 灵活的生命周期:堆上的变量不会随着函数返回而自动释放,可以在整个程序生命周期内存活,直到显式释放。
  • 较慢的分配和释放:由于堆需要动态查找可用内存块,分配和释放操作会比栈慢。

哪些变量在堆上:

  • 动态分配的对象或数组:任何通过 newmalloc 分配的变量或对象都在堆上分配。
  • 动态分配的大型数据结构:例如,动态数组、链表、树等较大或需要在程序中长期使用的数据结构通常放在堆上。
  • 对象的指针成员变量:如果类的成员变量是指针类型并且通过 new 进行分配,那么这些成员变量的内存也会在堆上。

内存泄漏和访问冲突

  • 内存泄漏:堆上分配的内存需要手动释放。如果忘记调用 delete,堆上的内存就不会被释放,导致内存泄漏。
  • 访问冲突:如果使用未初始化的堆指针或释放后的指针,可能会导致访问无效内存,进而产生访问冲突(如 0xC0000005 错误)。

TCP连接助手


文章作者: LSJune
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LSJune !
评论
  目录