1 環境搭建
建議閱讀此文前先了解TCP的原理。此文章僅爲了加深對TCP的理解。
爲了在抓包過程中捕獲儘可能多種類的TCP報文,本文需要自己編寫java socket程序,並安裝Wireshark配套軟件。
爲了方便理解TCP傳輸過程,僅客戶端向服務端發送數據。
1.1 編寫java程序
程序中需要注意的幾點:
- 客戶端發送數據,服務端接受數據。將服務端buffer大小設置的明顯小於客戶端,是爲了捕獲
流量控制
報文。 - 客戶端發送的數據(即d://bb.jpg),應該選擇合適的大小。本文中bb.jpg大小爲7M(推薦),這是爲了捕獲足夠多的樣本來進行分析。
- 服務端中並沒有關閉socket,這是爲了捕獲
reset
報文。
/**
* 服務端
*
* @author youngaoo
* @created 2018年5月16日上午11:09:51
*/
public class Server {
public static void main(String[] args) {
Server s = new Server();
s.doServer();
}
public void doServer() {
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8081));
SocketChannel socketChannel = serverSocketChannel.accept();
ByteBuffer dst = ByteBuffer.allocate(1024);
FileOutputStream fileOut = new FileOutputStream("d://cc.jpg");
FileChannel fileChannel = fileOut.getChannel();
int len = 0;
while ((len = socketChannel.read(dst)) != -1) {
dst.flip();
fileChannel.write(dst);
dst.clear();
}
fileOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 客戶端
*
* @author youngaoo
* @created 2018年5月15日下午12:06:15
*/
public class Client {
public static void main(String[] args) {
Client c = new Client();
c.doClent();
}
public void doClent() {
try {
FileInputStream fileInputStream = new FileInputStream("d://bb.jpg");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer dst = ByteBuffer.allocate(1024*10);
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8081));
int len = 0;
while ((len = fileChannel.read(dst)) != -1) {
dst.flip();
socketChannel.write(dst);
dst.clear();
}
fileInputStream.close();
socketChannel.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
1.2 安裝並配置Wireshark
安裝和Wireshark和npcap。並打開Wireshark,做如下配置:
- 網卡選擇本地迴環
- 過濾器填寫
port 8081 and host 127.0.0.1
2 抓包並分析
開啓Wireshark抓包功能,然後依次運行服務端和客戶端,得到tcp報文段,如圖(圖中報文段並不完整,省略了多個意義重複的報文段):
可以看到,圖中包含了多種報文類型。編號爲1的紅框爲三次握手
過程;編號爲3的代表“二次揮手”
和reset
報文段。
圖中凡是帶有中括號[]的文字,均是Wireshark添加的註釋,並不是TCP協議的內容。可以看到傳輸數據過程中,發生了多次流量控制,具體體現在[TCP Window Update]報文和編號爲②的報文段上。
下面,將會詳細分析幾種報文的含義,和出現的原因。
2.1 TCP協議首部格式
TCP協議的幾乎所有功能都與首部相關,因此這裏放上此圖,供後文說明使用。
2.2 三次握手
編號爲①的紅框爲三次握手的過程。在這個階段,通信雙方通過協商,得出了一系列信息,包括初始序列號
,自己的接收窗口
大小,最大報文段長度MSS
,窗口擴大選項WS
,選擇確認SACK
。三次握手完成以後,通信雙方就可以根據這些信息,分別構建出自己發送窗口,MSS等。此時,一條邏輯上的連接就建立成功了。
紅框中的內容,就對應TCP首部的各個部分。同樣,使用中括號[]括起來的文字是Wireshark添加的,便於使用者理解。
2.3 傳送數據
三次握手下面緊接着,就是傳送數據的報文。31306 → 8081 [ACK] Seq=1 Ack=1 Win=8192 Len=1460
。其中Seq
代表本報文段發送數據的第一個字節的序號;Win
代表發送本報文段一方的接收窗口大小,在這裏,即代表客戶端的大小;Len
即發送的數據的長度(單位爲字節);當建立連接完畢以後,所發送的所有報文段,Ack
字段都必須爲1。
該報文段發送的數據長度(Len),受到MSS值控制。之所以要協商MSS值,是因爲從網絡利用率來考慮。
2.4 流量控制
流量控制發生在接收方來不及處理數據時,接收方要求發送發降低發送速率,即減小發送方的發送窗口大小。在第一個[Tcp Window Update]報文和其前一個報文,即流量控制報文。前一個報文將Win改爲768,即告訴客戶端,我的接收窗口爲768個字節,你應該據此修改自己的發送窗口。後面的[Tcp Window Update]報文又將自己的接受窗口增大至4096字節。
最壞的情況是服務端的應用程序讀取數據的速度太慢,導致接受緩存達到最大容量,使接收窗口變爲0。那麼此時服務端就應該發送流量控制報文,[TCP ZeroWindow] 8081 → 31306 [ACK] Seq=1 Ack=31421 Win=0 Len=0
,將Win設置爲0,通知客戶端不要再發送報文段了,等我處理完積壓數據再通知你。注意,在[TCP ZeroWindow]報文前,發送了一個[TCP Window Full]報文。此報文是客戶端根據服務端的接收窗口大小,MSS值計算出來的。計算方法爲:5120/1460=3…740。
當服務器處理完畢積壓數據,又有了一些緩存空間,於是發送Win=3584的報文段。在Wireshark中,凡是擴大窗口的報文段都被註釋爲[Tcp Window Update]。
2.5 RESET報文
編號爲③的報文段中,前兩個是四次揮手
的前兩次揮手,此時客戶端到服務端的單向連接已經被關閉。接着應該進行服務端到客戶端連接的關閉。但是服務端的應用程序並沒有向TCP發送close命令,當應用程序進程結束後,操作系統的TCP就向客戶端發送了reset報文。
2.6 PSH報文
發生在TCP層清空緩存時,纔將push置爲1。