WireShark抓包分析TCP数据传输过程与内容详解
引言
网上有很多关于 WireShark 抓包分析 TCP 三次握手和四次挥手的教程,这个个人认为属于八股的范畴,可能面试的时候比较有用,但是实际开发中应该不关心连接建立的过程
从现有的工作经验看,更关注连接建立后,上下的通信是否正常,也就是说,上位机发送的指令,下位机是否收到并且有相应的应答返回上来
本篇博客主要尝试对上下层通信建立后通信内容进行分析,通过 WireShark 抓包并且提取出发送和应答的内容
Qt实现TCP客户端服务端
服务端代码
// Server.h
#ifndef SERVER_H
#define SERVER_H#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>class Server : public QObject
{Q_OBJECT
public:explicit Server(QObject *parent = nullptr);void startServer(quint16 port);signals:void newMessage(const QString &msg);private slots:void onNewConnection();void onReadyRead();void onDisconnected();private:QTcpServer *m_server;QList<QTcpSocket*> m_clients;
};#endif // SERVER_H
// Server.cpp
#include "Server.h"
#include <QDebug>Server::Server(QObject *parent) : QObject(parent)
{m_server = new QTcpServer(this);connect(m_server, &QTcpServer::newConnection, this, &Server::onNewConnection);
}void Server::startServer(quint16 port)
{if(!m_server->listen(QHostAddress::Any, port)) {emit newMessage(QString("服务器启动失败: %1").arg(m_server->errorString()));return;}emit newMessage(QString("服务器已启动,监听端口: %1").arg(port));
}void Server::onNewConnection()
{QTcpSocket *clientSocket = m_server->nextPendingConnection();connect(clientSocket, &QTcpSocket::readyRead, this, &Server::onReadyRead);connect(clientSocket, &QTcpSocket::disconnected, this, &Server::onDisconnected);m_clients.append(clientSocket);emit newMessage(QString("新的客户端连接: %1:%2").arg(clientSocket->peerAddress().toString()).arg(clientSocket->peerPort()));
}void Server::onReadyRead()
{QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());if(!clientSocket) return;QByteArray data = clientSocket->readAll();emit newMessage(QString("收到数据: %1").arg(data.toHex()));// 广播给所有客户端for(QTcpSocket *client : qAsConst(m_clients)) {client->write(data);}
}void Server::onDisconnected()
{QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());if(!clientSocket) return;m_clients.removeOne(clientSocket);emit newMessage(QString("客户端断开连接: %1:%2").arg(clientSocket->peerAddress().toString()).arg(clientSocket->peerPort()));clientSocket->deleteLater();
}
客户端代码
// Client.h
#ifndef CLIENT_H
#define CLIENT_H#include <QObject>
#include <QTcpSocket>class Client : public QObject
{Q_OBJECT
public:explicit Client(QObject *parent = nullptr);void connectToServer(const QString &host, quint16 port);void sendMessage(const QByteArray &message);signals:void newMessage(const QString &msg);void connected();void disconnected();private slots:void onConnected();void onReadyRead();void onDisconnected();private:QTcpSocket *m_socket;
};#endif // CLIENT_H
// Client.cpp
#include "Client.h"
#include <QDebug>Client::Client(QObject *parent) : QObject(parent)
{m_socket = new QTcpSocket(this);connect(m_socket, &QTcpSocket::connected, this, &Client::onConnected);connect(m_socket, &QTcpSocket::readyRead, this, &Client::onReadyRead);connect(m_socket, &QTcpSocket::disconnected, this, &Client::onDisconnected);
}void Client::connectToServer(const QString &host, quint16 port)
{m_socket->connectToHost(host, port);emit newMessage(QString("正在连接到服务器 %1:%2...").arg(host).arg(port));
}void Client::sendMessage(const QByteArray &message)
{if(m_socket->state() == QTcpSocket::ConnectedState) {m_socket->write(message);emit newMessage(QString("发送: %1").arg(message.toHex()));}
}void Client::onConnected()
{emit newMessage("已连接到服务器");emit connected();
}void Client::onReadyRead()
{QByteArray data = m_socket->readAll();emit newMessage(QString("收到: %1").arg(data.toHex()));
}void Client::onDisconnected()
{emit newMessage("与服务器断开连接");emit disconnected();
}
使用示例
服务端使用
// main_server.cpp
#include <QCoreApplication>
#include "Server.h"int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);Server server;QObject::connect(&server, &Server::newMessage, [](const QString &msg) {qDebug() << "[Server]" << msg;});server.startServer(12345); // 监听12345端口return a.exec();
}
客户端使用
// main_client.cpp
#include <QCoreApplication>
#include <QTimer>
#include "Client.h"int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);Client client;QObject::connect(&client, &Client::newMessage, [](const QString &msg) {qDebug() << "[Client]" << msg;});client.connectToServer("127.0.0.1", 12345);// 连接成功后发送消息QObject::connect(&client, &Client::connected, [&client]() {QTimer::singleShot(1000, [&client]() {client.sendMessage(QByteArray::fromHex("AB99CD66EE"));});});return a.exec();
}
功能说明
- 服务端功能:
- 监听指定端口
- 接受多个客户端连接
- 接收客户端消息并广播给所有客户端
- 处理客户端断开连接
- 客户端功能:
- 连接到指定服务器
- 发送消息到服务器
- 接收服务器消息
- 处理连接断开
编译运行
- 将服务端代码和服务端main文件一起编译
- 将客户端代码和客户端main文件一起编译
- 先运行服务端,再运行客户端
18:18:04: Starting E:\Code\QtTcpServer\build\Desktop_Qt_6_7_3_MSVC2019_64bit-Debug\QtTcpServer.exe...
[Server] "服务器已启动,监听端口: 12345"
[Server] "新的客户端连接: ::ffff:127.0.0.1:13908"
[Server] "收到数据: ab99cd66ee"
18:18:08: Starting E:\Code\QTcpClient\build\Desktop_Qt_6_7_3_MSVC2019_64bit-Debug\QTcpClient.exe...
[Client] "正在连接到服务器 127.0.0.1:12345..."
[Client] "已连接到服务器"
[Client] "发送: ab99cd66ee"
[Client] "收到: ab99cd66ee"
示例中是模拟实际中可能会发送的十六进制格式的控制指令(“AB99CD66EE”)
WireShark抓包分析
最前面三行是在进行三次握手,此处不展开,主要看后面四行
因为是在本地回环上测试,因此 SRC 和 DST 都是 127.0.0.1
Client的通信端口是13908
,Server的通信端口是12345
Demo 实现的功能是客户端给服务端发什么,服务端都会无条件的转发回去
第四行13908→12345
,在Data
段可以看到客户端给服务端发送消息的具体内容,和代码中是一致的
第六行12345→13908
,在Data
段同样可以看到服务端回复给客户端相同的内容,与预期一致
抓包标志位详细分析
在 Wireshark 抓包分析中,[PSH, ACK]
是 TCP 协议标志位(Flags)的组合,表示数据包的两种关键行为。以下是详细解释:
1. 标志位含义
PSH
(Push)- 发送方通知接收方立即将数据交给上层应用,而不是等待缓冲区填满。
- 避免数据在 TCP 缓冲区中滞留,提高实时性(如交互式应用)。
ACK
(Acknowledgment)- 表示该数据包包含对已接收数据的确认(确认号有效)。
- 绝大多数 TCP 数据包都会带有
ACK
标志。
2. [PSH, ACK]
的典型场景
-
实时数据传输
例如:SSH 输入、HTTP 请求/响应、在线聊天消息等需要即时处理的场景。
Client → Server: [PSH, ACK] "GET /index.html" Server → Client: [PSH, ACK] "HTTP/1.1 200 OK"
-
避免缓冲延迟
当发送方希望接收方立即处理数据时(如用户敲击键盘后发送字符)。
3. 技术细节
-
数据流示例
1. Client → Server [SYN] # 建立连接 2. Server → Client [SYN, ACK] 3. Client → Server [ACK] 4. Client → Server [PSH, ACK] # 携带实际数据(如HTTP请求) 5. Server → Client [PSH, ACK] # 携带响应数据
-
Wireshark 中的显示
-
在包列表中以
[PSH, ACK]
标记。 -
在包详情中可查看具体标志位:
Transmission Control Protocol (TCP)Flags: 0x018 (PSH, ACK)...000.. = Reserved: Not set....1... = PSH: Push.....1.. = ACK: Acknowledgment
-
4. 常见问题
Q1: 为什么有些数据包没有 PSH
?
- 如果数据量较小或无需立即处理,TCP 可能合并多个小包(Nagle 算法),延迟发送
PSH
。
Q2: PSH
和 URG
的区别?
PSH
:要求正常数据立即上传给应用。URG
:标记紧急数据(如中断信号),需配合紧急指针使用(现代应用较少使用)。
Q3: 如何过滤 [PSH, ACK]
包?
在 Wireshark 过滤栏输入:
tcp.flags.push == 1 and tcp.flags.ack == 1
5. 实际意义
- 网络调试:频繁出现
[PSH, ACK]
可能表明应用需要低延迟(如视频会议)。 - 性能优化:若发现
PSH
过多,可检查是否禁用 Nagle 算法(TCP_NODELAY
)。
总结
行为 | 说明 |
---|---|
PSH | 催促接收方立即处理数据,避免缓冲延迟 |
ACK | 确认已接收数据,保证可靠性 |
[PSH, ACK] | 同时携带数据和确认信息,常见于需要实时响应的应用(如HTTP、SSH、DNS) |
通过分析 [PSH, ACK]
包,可以深入理解应用的通信模式和性能特征。
当主机A向主机B发送一个**[PSH, ACK]
数据包后,主机B通常会回复一个[ACK]
(纯确认包),这是TCP协议的标准行为**
TCP协议通过**确认机制(ACK)**保证可靠传输,具体逻辑如下:
[PSH, ACK]
的作用:PSH
:要求接收方(B)立即将数据推送(Push)给上层应用(如HTTP服务)。ACK
:携带对之前数据的确认号(Acknowledge Number),表示"我已收到你之前的数据"。
- B的响应逻辑:
- 当B收到**
[PSH, ACK]
**后,需要做两件事:- 确认接收:通过**
[ACK]
**包告知A"数据已收到"(确认号 = A发送的序列号 + 数据长度)。 - 处理数据:将数据立即提交给应用层(如Web服务器处理HTTP请求)。
- 确认接收:通过**
- 当B收到**
以HTTP请求为例:
A → B: [PSH, ACK] Seq=100, Ack=200, Len=50 # 携带HTTP请求数据
B → A: [ACK] Seq=200, Ack=150 # 确认收到50字节(100+50=150)
B → A: [PSH, ACK] Seq=200, Ack=150, Len=80 # 携带HTTP响应数据
A → B: [ACK] Seq=150, Ack=280 # 确认收到80字节(200+80=280)
如果B需要立即回复数据(如HTTP响应),可能会将**[ACK]
和[PSH, ACK]
**合并,直接回复:
A → B: [PSH, ACK] Seq=100, Ack=200, Len=50 # HTTP请求
B → A: [PSH, ACK] Seq=200, Ack=150, Len=80 # 同时确认请求+发送响应
此时不会单独出现纯**[ACK]
**包
- 默认情况下,TCP会尝试延迟发送
[ACK]
(通常200ms),等待是否有数据需要一起发送(减少包数量) - 如果B没有数据要回复,最终仍会单独发送**
[ACK]
**