java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java获取TCP流

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

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

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

您可能感兴趣的文章:
阅读全文