Java语言获取TCP流

前言

使用Wireshark 分析网络包时,一个很常用的功能就是选中一个TCP 报文,然后查看这个TCP 报文的TCP 流,从而可以进一步分析建连是否慢了,断连是否正常等情况,那么本文就TCP 流的概念以及在Java中如何获取,做一个简单的学习。

正文

一. TCP流概念

如果去搜索引擎搜索:什么是TCP 流,那么大概率是很难得到一个有效的答案的,而在Wireshark 中,选中一个TCP 报文并单击右键时,在菜单的追踪流中可以选择到TCP流这个功能,如下所示。

当点击TCP 流后,Wireshark 会把选中的TCP 报文对应的TCP 连接的所有TCP 报文过滤出来并顺序展示,那么这里就知道了,TCP 流就是一次TCP 连接中,从连接建立,到数据传输,再到连接断开整个过程中的TCP报文集合。

那么Wireshark 凭什么可以从那么多TCP 报文中,精确的把某一条TCP 连接的TCP 报文过滤出来并顺序展示呢,其实就是基于TCP 报文的序列号和确认号。下面是TCP报文头的格式。

可以看到每个TCP 报文都有一个序列号SeqNum 和确认号AckNum,并且他们的含义如下。

  • 序列号 :表示本次传输的数据的起始字节在整个TCP 连接传输的字节流中的编号。举个例子,某个TCP 报文的SeqNum 为500,然后报文长度length 为100,则表示本次传输数据的起始字节在整个TCP流中的序列号为100,并且本次传输的数据的序列号范围是500到599,根据序列号,能够将传输的数据有序的排列组合起来,以解决网络传输中的数据乱序问题;
  • 确认号 :用来告诉对端本端期望下一次收到的数据的序列号,换言之,告诉对端本端已经正常接收了序列号等于AckNum之前的所有数据,根据确认号,可以解决网络传输中的数据丢包问题。

那么序列号和确认号的变化有什么规则呢,规则总结如下。

  1. 本次发送报文的SeqNum 等于上一次发送报文的SeqNum 加上上一次发送报文的length
  2. 本次发送报文的AckNum 等于上一次接收报文的SeqNum 加上上一次接收报文的length
  3. SYN 报文和FIN 报文的length默认为1,而不是0。

结合下面一张图,可以更好的理解上面的变化规则。

二. TCP流获取的Java实现

结合第一节的内容,想要获取某一个TCP 报文所属TCP 连接的TCP 流,其实就可以根据这个报文的SeqNumAckNum ,向前和向后查找符合序列号和确认号变化规则的报文,只要符合规则,那么这个报文就是属于TCP流的。

Java 语言中,要实现TCP 流的获取,可以先借助io.pkts 工具把网络包先解开,然后把每个报文封装为我们自定义的Entityio.pkts 工具包解开后的报文对象不太易用),最后就是根据序列号和确认号的变化规则,来得到某一个报文所属的TCP流。

现在进行实操,先引入io.pkts工具的依赖,如下所示。

xml 复制代码
<dependency>
    <groupId>io.pkts</groupId>
    <artifactId>pkts-streams</artifactId>
    <version>3.0.10</version>
</dependency>
<dependency>
    <groupId>io.pkts</groupId>
    <artifactId>pkts-core</artifactId>
    <version>3.0.10</version>
</dependency>

同时自定义一个TCP 报文的Entity,如下所示。

java 复制代码
/**
 * TCP报文Entity。
 */
@Getter
@Setter
@AllArgsConstructor
public class TcpPackage {

    /**
     * 源地址IP。
     */
    private String sourceIp;
    /**
     * 源地址端口。
     */
    private int sourcePort;
    /**
     * 目的地址IP。
     */
    private String destinationIp;
    /**
     * 目的地址端口。
     */
    private int destinationPort;
    /**
     * 报文载荷长度。
     */
    private int length;
    /**
     * ACK报文标识。
     */
    private boolean ack;
    /**
     * FIN报文标识,
     */
    private boolean fin;
    /**
     * SYN报文标识。
     */
    private boolean syn;
    /**
     * RST报文标识。
     */
    private boolean rst;
    /**
     * 序列号。
     */
    private long seqNum;
    /**
     * 确认号。
     */
    private long ackNum;
    /**
     * 报文到达时间戳。
     */
    private long arriveTimestamp;
    /**
     * 报文体。
     */
    private String body;

}

现在假设已经拿到了网络包对应的MultipartFile ,下面给出基于io.pkts工具解析网络包的实现,如下所示。

java 复制代码
public static List<TcpPackage> parseTcpPackagesFromFile(MultipartFile multipartFile) {
    List<TcpPackage> tcpPackages = new ArrayList<>();
    try (InputStream inputStream = multipartFile.getInputStream()) {
        GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream);
        Pcap pcap = Pcap.openStream(gzipInputStream);
        pcap.loop(packet -> {
            if (packet.hasProtocol(Protocol.TCP)) {
                TCPPacket tcpPacket = (TCPPacket) packet.getPacket(Protocol.TCP);
                tcpPackages.add(convertTcpPacket2TcpPackage(tcpPacket));
            }
            return true;
        });
        return tcpPackages;
    } catch (Exception e) {
        String message = "从网络包解析TCP报文失败";
        log.error(message);
        throw new RuntimeException(message, e);
    }
}

上述实现中,假定网络包是gzip 的压缩格式,所以使用了GZIPInputStream 来包装网络包文件的输入流,同时因为我们要获取的是TCP 流,所以我们只处理有TCP 协议的报文,并会在convertTcpPacket2TcpPackage() 方法中完成到TcpPackage 结构的转换,convertTcpPacket2TcpPackage() 方法实现如下所示。

java 复制代码
public static TcpPackage convertTcpPacket2TcpPackage(TCPPacket tcpPacket) {
    // 报文长度=IP报文长度-IP报文头长度-TCP报文头长度
    IPPacket ipPacket = tcpPacket.getParentPacket();
    int length = ipPacket.getTotalIPLength() - ipPacket.getHeaderLength() - tcpPacket.getHeaderLength();
    Buffer bodyBuffer = tcpPacket.getPayload();
    String body = ObjectUtils.isNotEmpty(bodyBuffer)
            ? bodyBuffer.toString() : StringUtils.EMPTY;
    long arriveTimestamp = tcpPacket.getArrivalTime() / 1000;
    return new TcpPackage(ipPacket.getSourceIP(), tcpPacket.getSourcePort(), ipPacket.getDestinationIP(), tcpPacket.getDestinationPort(),
            length, tcpPacket.isACK(), tcpPacket.isFIN(), tcpPacket.isSYN(), tcpPacket.isRST(), tcpPacket.getSequenceNumber(),
            tcpPacket.getAcknowledgementNumber(), arriveTimestamp, body);
}

上述方法需要注意的一点就是TCP 报文载荷长度的获取,我们能够拿到的数据是IP 报文长度,IP 报文头长度和TCP 报文头长度,所以IP 报文长度减去IP 报文头长度可以得到TCP 报文长度,再拿TCP 报文长度减去TCP 报文头长度就能得到TCP报文载荷长度。

现在我们已经拿到网络包里面所有TCP 报文的集合了,并且这些报文是按照时间先后顺序进行正序排序的,我们随机选中一个报文,拿到这个TCP 报文以及其在集合中的索引,然后我们就可以基于下面的实现拿到对应的TCP流。

java 复制代码
public static List<TcpPackage> getTcpStream(List<TcpPackage> tcpPackages, int index) {
    LinkedList<TcpPackage> tcpStream = new LinkedList<>();
    TcpPackage beginTcpPackage = tcpPackages.get(index);
    long currentSeqNum = beginTcpPackage.getSeqNum();
    long currentAckNum = beginTcpPackage.getAckNum();
    // 从index位置向前查找
    for (int i = index - 1; i >=0; i--) {
        TcpPackage previousTcpPackage = tcpPackages.get(i);
        long previousSeqNum = previousTcpPackage.getSeqNum();
        long previousAckNum = previousTcpPackage.getAckNum();
        if (isPreviousTcpPackageSatisfied(currentSeqNum, currentAckNum, previousSeqNum, previousAckNum)) {
            tcpStream.addFirst(previousTcpPackage);
            currentSeqNum = previousSeqNum;
            currentAckNum = previousAckNum;
        }
    }
    // index位置的报文也要放到tcp流中
    tcpStream.add(beginTcpPackage);
    currentSeqNum = beginTcpPackage.getSeqNum();
    currentAckNum = beginTcpPackage.getAckNum();
    // 从index位置向后查找
    for (int i = index + 1; i < tcpPackages.size(); i++) {
        TcpPackage nextTcpPackage = tcpPackages.get(i);
        long nextSeqNum = nextTcpPackage.getSeqNum();
        long nextAckNum = nextTcpPackage.getAckNum();
        if (isNextTcpPackageSatisfied(currentSeqNum, currentAckNum, nextSeqNum, nextAckNum)) {
            tcpStream.add(nextTcpPackage);
            currentSeqNum = nextSeqNum;
            currentAckNum = nextAckNum;
        }
    }
    return tcpStream;
}

上述方法中,向前查找时判断TCP 报文是否属于TCP 流是基于isPreviousTcpPackageSatisfied() 方法,向后查找时判断TCP 报文是否属于TCP 流是基于isNextTcpPackageSatisfied() 方法,而这两个方法其实就是把序列号和确认号的变化规则翻译成了代码,如下所示。

java 复制代码
public static boolean isPreviousTcpPackageSatisfied(long currentSeqNum, long currentAckNum,
                                                    long previousSeqNum, long previousAckNum) {
    boolean condition1 = currentSeqNum == previousSeqNum && currentSeqNum != 0;
    boolean condition2 = currentAckNum == previousAckNum && currentAckNum != 0;
    boolean condition3 = currentSeqNum == previousAckNum;
    boolean condition4 = currentAckNum - 1 == previousSeqNum;
    return condition1 || condition2 || condition3 || condition4;
}

public static boolean isNextTcpPackageSatisfied(long currentSeqNum, long currentAckNum,
                                                long nextSeqNum, long nextAckNum) {
    boolean condition1 = currentSeqNum == nextSeqNum && currentSeqNum != 0;
    boolean condition2 = currentAckNum == nextAckNum && currentAckNum != 0;
    boolean condition3 = currentAckNum == nextSeqNum;
    boolean condition4 = currentSeqNum + 1 == nextAckNum;
    return condition1 || condition2 || condition3 || condition4;
}

至此,使用Java 语言如何从网络包中获得TCP流就介绍完毕。

总结

TCP 流就是一次TCP 连接中,从连接建立,到数据传输,再到连接断开整个过程中的TCP 报文集合,而获取TCP 流是基于TCP报文序列号和确认号的变化规则,规则如下。

  1. 本次发送报文的SeqNum 等于上一次发送报文的SeqNum 加上上一次发送报文的length
  2. 本次发送报文的AckNum 等于上一次接收报文的SeqNum 加上上一次接收报文的length
  3. SYN 报文和FIN 报文的length默认为1,而不是0。

使用Java 语言解析网络包并得到TCP流,步骤总结如下。

  1. 使用io.pkts工具解开网络包;
  2. 将网络包中的TCP报文转换为自定义的可读性更强的数据结构;
  3. 选中一个TCP报文;
  4. 根据序列号和确认号变化获取TCP流。
相关推荐
AWS官方合作商1 小时前
在CSDN发布AWS Proton解决方案:实现云原生应用的标准化部署
java·云原生·aws
gadiaola2 小时前
【JVM】Java虚拟机(二)——垃圾回收
java·jvm
coderSong25684 小时前
Java高级 |【实验八】springboot 使用Websocket
java·spring boot·后端·websocket
Mr_Air_Boy5 小时前
SpringBoot使用dynamic配置多数据源时使用@Transactional事务在非primary的数据源上遇到的问题
java·spring boot·后端
豆沙沙包?6 小时前
2025年- H77-Lc185--45.跳跃游戏II(贪心)--Java版
java·开发语言·游戏
年老体衰按不动键盘6 小时前
快速部署和启动Vue3项目
java·javascript·vue
咖啡啡不加糖6 小时前
Redis大key产生、排查与优化实践
java·数据库·redis·后端·缓存
liuyang-neu6 小时前
java内存模型JMM
java·开发语言
利刃大大7 小时前
【在线五子棋对战】二、websocket && 服务器搭建
服务器·c++·websocket·网络协议·项目
UFIT7 小时前
NoSQL之redis哨兵
java·前端·算法