Java語言獲取TCP流的實(shí)現(xiàn)步驟
正文
一. TCP流概念
如果去搜索引擎搜索:什么是TCP流,那么大概率是很難得到一個(gè)有效的答案的,而在Wireshark中,選中一個(gè)TCP報(bào)文并單擊右鍵時(shí),在菜單的追蹤流中可以選擇到TCP流這個(gè)功能,如下所示。
當(dāng)點(diǎn)擊TCP流后,Wireshark會把選中的TCP報(bào)文對應(yīng)的TCP連接的所有TCP報(bào)文過濾出來并順序展示,那么這里就知道了,TCP流就是一次TCP連接中,從連接建立,到數(shù)據(jù)傳輸,再到連接斷開整個(gè)過程中的TCP報(bào)文集合。
那么Wireshark憑什么可以從那么多TCP報(bào)文中,精確的把某一條TCP連接的TCP報(bào)文過濾出來并順序展示呢,其實(shí)就是基于TCP報(bào)文的序列號和確認(rèn)號。下面是TCP報(bào)文頭的格式。
可以看到每個(gè)TCP報(bào)文都有一個(gè)序列號SeqNum和確認(rèn)號AckNum,并且他們的含義如下。
- 序列號:表示本次傳輸?shù)臄?shù)據(jù)的起始字節(jié)在整個(gè)TCP連接傳輸?shù)淖止?jié)流中的編號。舉個(gè)例子,某個(gè)TCP報(bào)文的SeqNum為500,然后報(bào)文長度length為100,則表示本次傳輸數(shù)據(jù)的起始字節(jié)在整個(gè)TCP流中的序列號為100,并且本次傳輸?shù)臄?shù)據(jù)的序列號范圍是500到599,根據(jù)序列號,能夠?qū)鬏數(shù)臄?shù)據(jù)有序的排列組合起來,以解決網(wǎng)絡(luò)傳輸中的數(shù)據(jù)亂序問題;
- 確認(rèn)號:用來告訴對端本端期望下一次收到的數(shù)據(jù)的序列號,換言之,告訴對端本端已經(jīng)正常接收了序列號等于AckNum之前的所有數(shù)據(jù),根據(jù)確認(rèn)號,可以解決網(wǎng)絡(luò)傳輸中的數(shù)據(jù)丟包問題。
那么序列號和確認(rèn)號的變化有什么規(guī)則呢,規(guī)則總結(jié)如下。
- 本次發(fā)送報(bào)文的SeqNum等于上一次發(fā)送報(bào)文的SeqNum加上上一次發(fā)送報(bào)文的length;
- 本次發(fā)送報(bào)文的AckNum等于上一次接收報(bào)文的SeqNum加上上一次接收報(bào)文的length;
- SYN報(bào)文和FIN報(bào)文的length默認(rèn)為1,而不是0。
結(jié)合下面一張圖,可以更好的理解上面的變化規(guī)則。
二. TCP流獲取的Java實(shí)現(xiàn)
結(jié)合第一節(jié)的內(nèi)容,想要獲取某一個(gè)TCP報(bào)文所屬TCP連接的TCP流,其實(shí)就可以根據(jù)這個(gè)報(bào)文的SeqNum和AckNum,向前和向后查找符合序列號和確認(rèn)號變化規(guī)則的報(bào)文,只要符合規(guī)則,那么這個(gè)報(bào)文就是屬于TCP流的。
在Java語言中,要實(shí)現(xiàn)TCP流的獲取,可以先借助io.pkts工具把網(wǎng)絡(luò)包先解開,然后把每個(gè)報(bào)文封裝為我們自定義的Entity(io.pkts工具包解開后的報(bào)文對象不太易用),最后就是根據(jù)序列號和確認(rèn)號的變化規(guī)則,來得到某一個(gè)報(bào)文所屬的TCP流。
現(xiàn)在進(jìn)行實(shí)操,先引入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>
同時(shí)自定義一個(gè)TCP報(bào)文的Entity,如下所示。
/** * TCP報(bào)文Entity。 */ @Getter @Setter @AllArgsConstructor public class TcpPackage { /** * 源地址IP。 */ private String sourceIp; /** * 源地址端口。 */ private int sourcePort; /** * 目的地址IP。 */ private String destinationIp; /** * 目的地址端口。 */ private int destinationPort; /** * 報(bào)文載荷長度。 */ private int length; /** * ACK報(bào)文標(biāo)識。 */ private boolean ack; /** * FIN報(bào)文標(biāo)識, */ private boolean fin; /** * SYN報(bào)文標(biāo)識。 */ private boolean syn; /** * RST報(bào)文標(biāo)識。 */ private boolean rst; /** * 序列號。 */ private long seqNum; /** * 確認(rèn)號。 */ private long ackNum; /** * 報(bào)文到達(dá)時(shí)間戳。 */ private long arriveTimestamp; /** * 報(bào)文體。 */ private String body; }
現(xiàn)在假設(shè)已經(jīng)拿到了網(wǎng)絡(luò)包對應(yīng)的MultipartFile,下面給出基于io.pkts工具解析網(wǎng)絡(luò)包的實(shí)現(xiàn),如下所示。
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 = "從網(wǎng)絡(luò)包解析TCP報(bào)文失敗"; log.error(message); throw new RuntimeException(message, e); } }
上述實(shí)現(xiàn)中,假定網(wǎng)絡(luò)包是gzip的壓縮格式,所以使用了GZIPInputStream來包裝網(wǎng)絡(luò)包文件的輸入流,同時(shí)因?yàn)槲覀円@取的是TCP流,所以我們只處理有TCP協(xié)議的報(bào)文,并會在convertTcpPacket2TcpPackage() 方法中完成到TcpPackage結(jié)構(gòu)的轉(zhuǎn)換,convertTcpPacket2TcpPackage() 方法實(shí)現(xiàn)如下所示。
public static TcpPackage convertTcpPacket2TcpPackage(TCPPacket tcpPacket) { // 報(bào)文長度=IP報(bào)文長度-IP報(bào)文頭長度-TCP報(bào)文頭長度 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); }
上述方法需要注意的一點(diǎn)就是TCP報(bào)文載荷長度的獲取,我們能夠拿到的數(shù)據(jù)是IP報(bào)文長度,IP報(bào)文頭長度和TCP報(bào)文頭長度,所以IP報(bào)文長度減去IP報(bào)文頭長度可以得到TCP報(bào)文長度,再拿TCP報(bào)文長度減去TCP報(bào)文頭長度就能得到TCP報(bào)文載荷長度。
現(xiàn)在我們已經(jīng)拿到網(wǎng)絡(luò)包里面所有TCP報(bào)文的集合了,并且這些報(bào)文是按照時(shí)間先后順序進(jìn)行正序排序的,我們隨機(jī)選中一個(gè)報(bào)文,拿到這個(gè)TCP報(bào)文以及其在集合中的索引,然后我們就可以基于下面的實(shí)現(xiàn)拿到對應(yīng)的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位置的報(bào)文也要放到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; }
上述方法中,向前查找時(shí)判斷TCP報(bào)文是否屬于TCP流是基于isPreviousTcpPackageSatisfied() 方法,向后查找時(shí)判斷TCP報(bào)文是否屬于TCP流是基于isNextTcpPackageSatisfied() 方法,而這兩個(gè)方法其實(shí)就是把序列號和確認(rèn)號的變化規(guī)則翻譯成了代碼,如下所示。
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語言如何從網(wǎng)絡(luò)包中獲得TCP流就介紹完畢。
總結(jié)
TCP流就是一次TCP連接中,從連接建立,到數(shù)據(jù)傳輸,再到連接斷開整個(gè)過程中的TCP報(bào)文集合,而獲取TCP流是基于TCP報(bào)文序列號和確認(rèn)號的變化規(guī)則,規(guī)則如下。
- 本次發(fā)送報(bào)文的SeqNum等于上一次發(fā)送報(bào)文的SeqNum加上上一次發(fā)送報(bào)文的length;
- 本次發(fā)送報(bào)文的AckNum等于上一次接收報(bào)文的SeqNum加上上一次接收報(bào)文的length;
- SYN報(bào)文和FIN報(bào)文的length默認(rèn)為1,而不是0。
使用Java語言解析網(wǎng)絡(luò)包并得到TCP流,步驟總結(jié)如下。
- 使用io.pkts工具解開網(wǎng)絡(luò)包;
- 將網(wǎng)絡(luò)包中的TCP報(bào)文轉(zhuǎn)換為自定義的可讀性更強(qiáng)的數(shù)據(jù)結(jié)構(gòu);
- 選中一個(gè)TCP報(bào)文;
- 根據(jù)序列號和確認(rèn)號變化獲取TCP流。
以上就是Java語言獲取TCP流的實(shí)現(xiàn)步驟的詳細(xì)內(nèi)容,更多關(guān)于Java獲取TCP流的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
劍指Offer之Java算法習(xí)題精講二叉搜索樹與數(shù)組查找
跟著思路走,之后從簡單題入手,反復(fù)去看,做過之后可能會忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會發(fā)現(xiàn)質(zhì)的變化2022-03-03Java8?Stream?collect(Collectors.toMap())的使用
這篇文章主要介紹了Java8?Stream?collect(Collectors.toMap())的使用,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05解析Java?中for循環(huán)和foreach循環(huán)哪個(gè)更快
這篇文章主要介紹了Java中for循環(huán)和foreach循環(huán)哪個(gè)更快示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09JDK8新特性-java.util.function-Function接口使用
這篇文章主要介紹了JDK8新特性-java.util.function-Function接口使用,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04