Java语言获取TCP流的实现步骤

 更新时间:2023年11月29日 08:30:42   作者:半夏之沫  
使用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之前的所有数据,根据确认号,可以解决网络传输中的数据丢包问题。

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

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

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

二. TCP流获取的Java实现

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

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

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

<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,如下所示。

/**
 * 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工具解析网络包的实现,如下所示。

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() 方法实现如下所示。

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流。

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() 方法,而这两个方法其实就是把序列号和确认号的变化规则翻译成了代码,如下所示。

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报文序列号和确认号的变化规则,规则如下。

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

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

  • 使用io.pkts工具解开网络包;
  • 将网络包中的TCP报文转换为自定义的可读性更强的数据结构;
  • 选中一个TCP报文;
  • 根据序列号和确认号变化获取TCP流。

以上就是Java语言获取TCP流的实现步骤的详细内容,更多关于Java获取TCP流的资料请关注脚本之家其它相关文章!

相关文章

  • 剑指Offer之Java算法习题精讲二叉搜索树与数组查找

    剑指Offer之Java算法习题精讲二叉搜索树与数组查找

    跟着思路走,之后从简单题入手,反复去看,做过之后可能会忘记,之后再做一次,记不住就反复做,反复寻求思路和规律,慢慢积累就会发现质的变化
    2022-03-03
  • Java8 Stream collect(Collectors.toMap())的使用

    Java8 Stream collect(Collectors.toMap())的使用

    这篇文章主要介绍了Java8 Stream collect(Collectors.toMap())的使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-05-05
  • spring boot上传文件出错问题如何解决

    spring boot上传文件出错问题如何解决

    这篇文章主要介绍了spring boot上传文件出错问题如何解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • 深入了解MyBatis参数

    深入了解MyBatis参数

    今天小编就为大家分享一篇关于深入了解MyBatis参数,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • 提升java开发效率工具lombok使用争议

    提升java开发效率工具lombok使用争议

    这篇文章主要介绍了提升java开发效率工具lombok使用争议到底该不该使用的分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 解析Java 中for循环和foreach循环哪个更快

    解析Java 中for循环和foreach循环哪个更快

    这篇文章主要介绍了Java中for循环和foreach循环哪个更快示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Java实现顺序栈的示例代码

    Java实现顺序栈的示例代码

    线性表和栈都是我们常用的数据结构,栈可以看成一种特殊状态的线性表。线性表分为顺序表和链表,使用线性表中的顺序表来实现栈时这种栈被称为顺序栈。这篇文章总结了如何使用顺序表实现栈,需要的可以参考一下
    2022-11-11
  • Java字节流 从文件输入输出到文件过程解析

    Java字节流 从文件输入输出到文件过程解析

    这篇文章主要介绍了Java字节流 从文件输入 输出到文件过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09
  • JDK8新特性-java.util.function-Function接口使用

    JDK8新特性-java.util.function-Function接口使用

    这篇文章主要介绍了JDK8新特性-java.util.function-Function接口使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • Spring Boot中的Properties的使用详解

    Spring Boot中的Properties的使用详解

    这篇文章主要介绍了Spring Boot中的Properties的使用详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02

最新评论