Java語(yǔ)言獲取TCP流的實(shí)現(xiàn)步驟
正文
一. TCP流概念
如果去搜索引擎搜索:什么是TCP流,那么大概率是很難得到一個(gè)有效的答案的,而在Wireshark中,選中一個(gè)TCP報(bào)文并單擊右鍵時(shí),在菜單的追蹤流中可以選擇到TCP流這個(gè)功能,如下所示。

當(dāng)點(diǎn)擊TCP流后,Wireshark會(huì)把選中的TCP報(bào)文對(duì)應(yīng)的TCP連接的所有TCP報(bào)文過(guò)濾出來(lái)并順序展示,那么這里就知道了,TCP流就是一次TCP連接中,從連接建立,到數(shù)據(jù)傳輸,再到連接斷開(kāi)整個(gè)過(guò)程中的TCP報(bào)文集合。
那么Wireshark憑什么可以從那么多TCP報(bào)文中,精確的把某一條TCP連接的TCP報(bào)文過(guò)濾出來(lái)并順序展示呢,其實(shí)就是基于TCP報(bào)文的序列號(hào)和確認(rèn)號(hào)。下面是TCP報(bào)文頭的格式。

可以看到每個(gè)TCP報(bào)文都有一個(gè)序列號(hào)SeqNum和確認(rèn)號(hào)AckNum,并且他們的含義如下。
- 序列號(hào):表示本次傳輸?shù)臄?shù)據(jù)的起始字節(jié)在整個(gè)TCP連接傳輸?shù)淖止?jié)流中的編號(hào)。舉個(gè)例子,某個(gè)TCP報(bào)文的SeqNum為500,然后報(bào)文長(zhǎng)度length為100,則表示本次傳輸數(shù)據(jù)的起始字節(jié)在整個(gè)TCP流中的序列號(hào)為100,并且本次傳輸?shù)臄?shù)據(jù)的序列號(hào)范圍是500到599,根據(jù)序列號(hào),能夠?qū)鬏數(shù)臄?shù)據(jù)有序的排列組合起來(lái),以解決網(wǎng)絡(luò)傳輸中的數(shù)據(jù)亂序問(wèn)題;
- 確認(rèn)號(hào):用來(lái)告訴對(duì)端本端期望下一次收到的數(shù)據(jù)的序列號(hào),換言之,告訴對(duì)端本端已經(jīng)正常接收了序列號(hào)等于AckNum之前的所有數(shù)據(jù),根據(jù)確認(rèn)號(hào),可以解決網(wǎng)絡(luò)傳輸中的數(shù)據(jù)丟包問(wèn)題。
那么序列號(hào)和確認(rèn)號(hào)的變化有什么規(guī)則呢,規(guī)則總結(jié)如下。
- 本次發(fā)送報(bào)文的SeqNum等于上一次發(fā)送報(bào)文的SeqNum加上上一次發(fā)送報(bào)文的length;
- 本次發(fā)送報(bào)文的AckNum等于上一次接收?qǐng)?bào)文的SeqNum加上上一次接收?qǐng)?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,向前和向后查找符合序列號(hào)和確認(rèn)號(hào)變化規(guī)則的報(bào)文,只要符合規(guī)則,那么這個(gè)報(bào)文就是屬于TCP流的。
在Java語(yǔ)言中,要實(shí)現(xiàn)TCP流的獲取,可以先借助io.pkts工具把網(wǎng)絡(luò)包先解開(kāi),然后把每個(gè)報(bào)文封裝為我們自定義的Entity(io.pkts工具包解開(kāi)后的報(bào)文對(duì)象不太易用),最后就是根據(jù)序列號(hào)和確認(rèn)號(hào)的變化規(guī)則,來(lái)得到某一個(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)文載荷長(zhǎng)度。
*/
private int length;
/**
* ACK報(bào)文標(biāo)識(shí)。
*/
private boolean ack;
/**
* FIN報(bào)文標(biāo)識(shí),
*/
private boolean fin;
/**
* SYN報(bào)文標(biāo)識(shí)。
*/
private boolean syn;
/**
* RST報(bào)文標(biāo)識(shí)。
*/
private boolean rst;
/**
* 序列號(hào)。
*/
private long seqNum;
/**
* 確認(rèn)號(hào)。
*/
private long ackNum;
/**
* 報(bào)文到達(dá)時(shí)間戳。
*/
private long arriveTimestamp;
/**
* 報(bào)文體。
*/
private String body;
}
現(xiàn)在假設(shè)已經(jīng)拿到了網(wǎng)絡(luò)包對(duì)應(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來(lái)包裝網(wǎng)絡(luò)包文件的輸入流,同時(shí)因?yàn)槲覀円@取的是TCP流,所以我們只處理有TCP協(xié)議的報(bào)文,并會(huì)在convertTcpPacket2TcpPackage() 方法中完成到TcpPackage結(jié)構(gòu)的轉(zhuǎn)換,convertTcpPacket2TcpPackage() 方法實(shí)現(xiàn)如下所示。
public static TcpPackage convertTcpPacket2TcpPackage(TCPPacket tcpPacket) {
// 報(bào)文長(zhǎng)度=IP報(bào)文長(zhǎng)度-IP報(bào)文頭長(zhǎng)度-TCP報(bào)文頭長(zhǎng)度
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)文載荷長(zhǎng)度的獲取,我們能夠拿到的數(shù)據(jù)是IP報(bào)文長(zhǎng)度,IP報(bào)文頭長(zhǎng)度和TCP報(bào)文頭長(zhǎng)度,所以IP報(bào)文長(zhǎng)度減去IP報(bào)文頭長(zhǎng)度可以得到TCP報(bào)文長(zhǎng)度,再拿TCP報(bào)文長(zhǎng)度減去TCP報(bào)文頭長(zhǎng)度就能得到TCP報(bào)文載荷長(zhǎng)度。
現(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)拿到對(duì)應(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í)就是把序列號(hào)和確認(rèn)號(hào)的變化規(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語(yǔ)言如何從網(wǎng)絡(luò)包中獲得TCP流就介紹完畢。
總結(jié)
TCP流就是一次TCP連接中,從連接建立,到數(shù)據(jù)傳輸,再到連接斷開(kāi)整個(gè)過(guò)程中的TCP報(bào)文集合,而獲取TCP流是基于TCP報(bào)文序列號(hào)和確認(rèn)號(hào)的變化規(guī)則,規(guī)則如下。
- 本次發(fā)送報(bào)文的SeqNum等于上一次發(fā)送報(bào)文的SeqNum加上上一次發(fā)送報(bào)文的length;
- 本次發(fā)送報(bào)文的AckNum等于上一次接收?qǐng)?bào)文的SeqNum加上上一次接收?qǐng)?bào)文的length;
- SYN報(bào)文和FIN報(bào)文的length默認(rèn)為1,而不是0。
使用Java語(yǔ)言解析網(wǎng)絡(luò)包并得到TCP流,步驟總結(jié)如下。
- 使用io.pkts工具解開(kāi)網(wǎng)絡(luò)包;
- 將網(wǎng)絡(luò)包中的TCP報(bào)文轉(zhuǎn)換為自定義的可讀性更強(qiáng)的數(shù)據(jù)結(jié)構(gòu);
- 選中一個(gè)TCP報(bào)文;
- 根據(jù)序列號(hào)和確認(rèn)號(hào)變化獲取TCP流。
以上就是Java語(yǔ)言獲取TCP流的實(shí)現(xiàn)步驟的詳細(xì)內(nèi)容,更多關(guān)于Java獲取TCP流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
劍指Offer之Java算法習(xí)題精講二叉搜索樹(shù)與數(shù)組查找
跟著思路走,之后從簡(jiǎn)單題入手,反復(fù)去看,做過(guò)之后可能會(huì)忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會(huì)發(fā)現(xiàn)質(zhì)的變化2022-03-03
Java8?Stream?collect(Collectors.toMap())的使用
這篇文章主要介紹了Java8?Stream?collect(Collectors.toMap())的使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
spring boot上傳文件出錯(cuò)問(wèn)題如何解決
這篇文章主要介紹了spring boot上傳文件出錯(cuò)問(wèn)題如何解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
提升java開(kāi)發(fā)效率工具lombok使用爭(zhēng)議
這篇文章主要介紹了提升java開(kāi)發(fā)效率工具lombok使用爭(zhēng)議到底該不該使用的分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
解析Java?中for循環(huán)和foreach循環(huán)哪個(gè)更快
這篇文章主要介紹了Java中for循環(huán)和foreach循環(huán)哪個(gè)更快示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Java字節(jié)流 從文件輸入輸出到文件過(guò)程解析
這篇文章主要介紹了Java字節(jié)流 從文件輸入 輸出到文件過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
JDK8新特性-java.util.function-Function接口使用
這篇文章主要介紹了JDK8新特性-java.util.function-Function接口使用,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04

