本节旨在基于 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)的数据结构,分配内存时的顺序是相反的:最后分配的内存块会最先释放。
栈的特点:
- 自动分配和释放:当函数调用时,函数内的局部变量会被自动分配在栈上;当函数执行完毕,栈上的局部变量会被自动释放,不需要程序员手动管理。
- 内存限制:栈内存通常比堆内存小,适用于小而频繁分配的变量。
- 高效:栈内存的分配和释放非常快,操作系统通过移动栈指针来管理内存。
- 生命周期短:栈上的变量仅在函数作用域内有效,当函数返回后,这些变量就失效了。
哪些变量在栈上:
- 局部变量:在函数或方法内部声明的基本数据类型(如
int
、float
、char
)和指针类型。 - 函数参数:大多数情况下,函数的参数会被复制到栈上。
- 简单对象:如果一个对象是在栈上声明的,则该对象的内存会分配在栈上。
class MyClass {
int value;
};
void func(int a, int b) {
// 函数参数 a 和 b 在栈上
int x = 10; // 局部变量 x 在栈上分配
MyClass obj; // obj 在栈上分配
}
堆(Heap)
堆是一块较大的动态内存区域,由程序员手动控制。与栈不同,堆上的内存是动态分配的,程序员必须显式地进行分配和释放。
堆的特点:
- 动态分配和释放:内存的分配和释放由程序员通过
new
和delete
(或malloc
和free
)来管理。如果忘记释放内存,就会导致内存泄漏。 - 内存空间大:堆的内存空间通常比栈大很多,但分配速度较慢。
- 灵活的生命周期:堆上的变量不会随着函数返回而自动释放,可以在整个程序生命周期内存活,直到显式释放。
- 较慢的分配和释放:由于堆需要动态查找可用内存块,分配和释放操作会比栈慢。
哪些变量在堆上:
- 动态分配的对象或数组:任何通过
new
或malloc
分配的变量或对象都在堆上分配。 - 动态分配的大型数据结构:例如,动态数组、链表、树等较大或需要在程序中长期使用的数据结构通常放在堆上。
- 对象的指针成员变量:如果类的成员变量是指针类型并且通过
new
进行分配,那么这些成员变量的内存也会在堆上。
内存泄漏和访问冲突:
- 内存泄漏:堆上分配的内存需要手动释放。如果忘记调用
delete
,堆上的内存就不会被释放,导致内存泄漏。 - 访问冲突:如果使用未初始化的堆指针或释放后的指针,可能会导致访问无效内存,进而产生访问冲突(如
0xC0000005
错误)。
TCP连接助手
- windows:网上有很多,此处分享一个我用的链接:http://www.eastcent.com//upload/file/software/tcpassistv11.zip
- arm64:
- 下载源码:https://github.com/justsure/mNetAssist_arm64,将所有文件移至指定目录,如:`/usr/tcp/`;
- 在
/etc/ld.so.conf.d/
目录下 添加mNetAssist.conf
文件,内容为mNetAssist
路径,此例应该为:/usr/tcp/
; - 在终端执行:
sudo ldconfig
命令; - 切换到
tcp
目录,执行./mNetAssist