Java语言获取TCP流的实现步骤
作者:半夏之沫
正文
一. 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流,其实就可以根据这个报文的SeqNum和AckNum,向前和向后查找符合序列号和确认号变化规则的报文,只要符合规则,那么这个报文就是属于TCP流的。
在Java语言中,要实现TCP流的获取,可以先借助io.pkts工具把网络包先解开,然后把每个报文封装为我们自定义的Entity(io.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流的资料请关注脚本之家其它相关文章!