手動發(fā)包只握手兩次,我發(fā)現(xiàn)了TCP的秘密
星球提問
TCP三次握手這個話題,沒有一萬,也有九千篇文章寫過了。
今天寫這篇文章,是因為有球友在我的知識星球里提了這么一個問題:
總結一下三個小問題:
客戶端發(fā)送完第三次握手后,是不是不管服務器有沒有收到,直接就發(fā)送數(shù)據(jù)?
TCP的第三次握手能不能攜帶數(shù)據(jù)?
如果因為各種原因,服務端并未收到客戶端發(fā)來的第三次握手包,那客戶端后續(xù)發(fā)送的數(shù)據(jù),服務端如何處理?
我的回答
以下是我的回答:
首先來回答這位球友最開始的問題:客戶端發(fā)送完第三個握手后,是不是不管服務器有沒有收到,直接就發(fā)送數(shù)據(jù)?
你可以從理論上來猜測一下,如果上面這個問題的答案是否定的話,也就是說客戶端還得要確認服務器收到自己的第三次握手包以后才能發(fā)送數(shù)據(jù)。那怎么確認呢?是不是服務端還得回復自己一下:我收到了你的第三次握手包了,你可以發(fā)送數(shù)據(jù)了。
但如果這樣一來,那是不是就變成了四次握手,而不是三次握手了呢?
所以反過來想,這個問題的答案就是肯定的,即:客戶端發(fā)送完第三次握手包后,不再需要服務端的確認,立即可以發(fā)送數(shù)據(jù)。
下面是《TCP/IP協(xié)議詳解》(卷1)中的連接建立示意圖,你可以看到客戶端這一側,發(fā)送完第三次握手包以后,狀態(tài)就別變成了ESTABLISH狀態(tài)了,并未等待服務器確認,就開始在傳輸數(shù)據(jù)了。
光理論不夠,我們再來抓包看一下,下面是我用抓包軟件抓了一個TCP連接建立的握手時序圖,同樣你可以看到,在第三次握手包發(fā)送后,左側的客戶端立即就發(fā)出了正式的數(shù)據(jù)傳輸:一個HTTP請求包。
所以這個問題的答案就清楚了。
接下來看第二個問題:客戶端在發(fā)送第三次握手包的時候是不是會攜帶數(shù)據(jù)一起傳輸過去?
其實從上面的2個圖中你可以看出,TCP三次握手并未攜帶有效的應用層數(shù)據(jù),數(shù)據(jù)的傳輸是在握手完成以后才開始的。但是如果我們非得問一句:客戶端在發(fā)送第三次握手數(shù)據(jù)包的時候,到底能不能順帶攜帶一些數(shù)據(jù)過去呢?
關于這一問題,最權威的答案還是得看RFC標準文檔,關于TCP標準協(xié)議的規(guī)范,是記錄在編號793的RFC793一文中,鏈接如下:
https://www.rfc-editor.org/rfc/rfc793.html
文檔有點長,而且是英文版,看起來可能有些吃力。
在事件處理這一節(jié)里面,會找到下面這段文字:
大意是說:如果我們的同步包SYN已經得到了確認,就把連接狀態(tài)改為ESTABLISHED,然后發(fā)送的第三次握手包中可能會包含數(shù)據(jù)(如果已經有數(shù)據(jù)在排隊等待傳輸?shù)脑?
這就說的很清楚了:TCP標準協(xié)議規(guī)范中,第三次握手包是允許傳輸數(shù)據(jù)的!
最后一個問題:如果因為各種原因,服務端并未收到客戶端發(fā)來的第三次握手包,那客戶端后續(xù)發(fā)送的數(shù)據(jù),服務端如何處理?
這里先賣個關子,接著往下看。
接下來才是這篇文章的精華部分:
實驗論證
TCP建立連接的三次握手,是操作系統(tǒng)內核協(xié)議棧自動完成的,作為底層服務,這個過程對應用程序是透明的,我們開發(fā)應用程序的時候,只需要使用應用層編程接口就行了,比如套接字接口。
所以,大部分人對TCP三次握手的概念還是建立在書本上,博客里,公眾號文章里,今天,我們自己來發(fā)送TCP數(shù)據(jù)包來實現(xiàn)三次握手!
自己發(fā)包,來驗證我們上面的結論!
使用的工具,是之前一篇文章中提到的神器:scapy。
為了方便查看數(shù)據(jù),我找了一個沒有HTTPS的網站,通過ping它的域名,拿到了IP地址,向其進行握手并發(fā)送GET請求包。
- from scapy.all import *
- def tcp_test(ip, port, data):
- # 第一次握手,發(fā)送SYN包
- # 請求端口和初始序列號隨機生成
- # 使用sr1發(fā)送而不用send發(fā)送,因為sr1會接收返回的內容
- ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)
- # 假定此刻對方已經發(fā)來了第二次握手包:ACK+SYN
- # 對方回復的目標端口,就是我方使用的請求端口(上面隨機生成的那個)
- sport = ans[TCP].dport
- s_seq = ans[TCP].ack
- d_seq = ans[TCP].seq + 1
- # 第三次握手,發(fā)送ACK確認包
- send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A'), verbose=False)
- # 發(fā)起GET請求
- send(IP(dst=ip)/TCP(dport=port, sport=sport, seq=s_seq, ack=d_seq, flags=24)/data, verbose=False)
- if __name__ == '__main__':
- data = 'GET / HTTP/1.1\n'
- data += 'Host: www.chengtu.com\n'
- data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
- data += 'Accept: text/html'
- data += '\r\n\r\n'
- tcp_test("150.138.151.65", 80, data)
執(zhí)行上面這段代碼,來抓包看一下:
可以看到,成功的完成了三次握手動作,服務器還返回了數(shù)據(jù),證明手動編程來握手是可行的。
下面論證星球中,球友提出的問題:第三個握手包里面能不能攜帶數(shù)據(jù)呢?
我們來試一下就知道了:
- from scapy.all import *
- def tcp_test_2(ip, port, data):
- # 第一次握手,發(fā)送SYN包
- # 請求端口和初始序列號隨機生成
- # 使用sr1發(fā)送而不用send發(fā)送,因為sr1會接收返回的內容
- ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)
- # 假定此刻對方已經發(fā)來了第二次握手包:ACK+SYN
- # 對方回復的目標端口,就是我方使用的請求端口(上面隨機生成的那個)
- sport = ans[TCP].dport
- s_seq = ans[TCP].ack
- d_seq = ans[TCP].seq + 1
- # 第三次握手,發(fā)送ACK確認包,順帶把數(shù)據(jù)一起帶上
- send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A')/data, verbose=False)
- if __name__ == '__main__':
- data = 'GET / HTTP/1.1\n'
- data += 'Host: www.chengtu.com\n'
- data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
- data += 'Accept: text/html'
- data += '\r\n\r\n'
- tcp_test_2("150.138.151.65", 80, data)
看到了吧,在第三次握手中,我的GET請求就帶過去了,TCP協(xié)議仍然能夠正常工作!
這是Linux的情況,我又找了我們大學的網站試了一下,因為學校網站沒用HTTPS(就很離譜),而且是ASP.NET技術棧做的(別問我怎么知道的),服務器是Windows,依然可以正常工作,說明Windows的協(xié)議棧也支持這種操作。
接下來驗證另一個問題:如果第三次握手包服務器沒有收到,就直接發(fā)送數(shù)據(jù),會發(fā)生什么?
怎么驗證,很簡單,直接把發(fā)送第三次握手的那一行注釋掉,不發(fā)送第三次握手,直接發(fā)送GET請求就行了:
- from scapy.all import *
- def tcp_test(ip, port, data):
- # 第一次握手,發(fā)送SYN包
- # 請求端口和初始序列號隨機生成
- # 使用sr1發(fā)送而不用send發(fā)送,因為sr1會接收返回的內容
- ans = sr1(IP(dst=ip) / TCP(dport=port, sport=RandShort(), seq=RandInt(), flags='S'), verbose=False)
- # 假定此刻對方已經發(fā)來了第二次握手包:ACK+SYN
- # 對方回復的目標端口,就是我方使用的請求端口(上面隨機生成的那個)
- sport = ans[TCP].dport
- s_seq = ans[TCP].ack
- d_seq = ans[TCP].seq + 1
- # 第三次握手,發(fā)送ACK確認包
- # send(IP(dst=ip) / TCP(dport=port, sport=sport, ack=d_seq, seq=s_seq, flags='A'), verbose=False)
- # 發(fā)起GET請求
- send(IP(dst=ip)/TCP(dport=port, sport=sport, seq=s_seq, ack=d_seq, flags='A')/data, verbose=False)
- if __name__ == '__main__':
- data = 'GET / HTTP/1.1\n'
- data += 'Host: www.chengtu.com\n'
- data += 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36\n'
- data += 'Accept: text/html'
- data += '\r\n\r\n'
- tcp_test("150.138.151.65", 80, data)
結果發(fā)現(xiàn)依然能正常工作!分析了一下,發(fā)現(xiàn)這種方式其實和上面那種情況是等價的:直接在第三次握手包中帶了數(shù)據(jù)。
這里雖然把第三次握手那一行注釋了,但直接發(fā)送的那個GET請求包中,ACK標記是置位了的,所以服務端就把這個GET包當成了第三次握手了。
所以結論就是:如果第三次握手包服務器沒有收到,就直接發(fā)送數(shù)據(jù),服務器將這個攜帶應用數(shù)據(jù)的包當做第三次握手(前提是這一個包中攜帶有ACK標記)。
除了我上面的回答外,這位球友又評論補充了一個問題:
其實看到這里,這個問題的答案想必已經心中有數(shù)了,但咱們還是來實驗模擬一下:先發(fā)送帶數(shù)據(jù)的請求包,然后再發(fā)送第三次握手包,看看會發(fā)生什么?
從圖中可以看到,直接發(fā)送的那個帶數(shù)據(jù)的請求包,被當做了第三次握手包,而后面再發(fā)送的那個名義上的第三次握手包,也就是圖中黑色的那一行,被當作了重復發(fā)送的無效包,被忽略掉了,對通信沒有造成影響。
以上就是我對這位球友問題的全部解答。
本文轉載自微信公眾號「編程技術宇宙」,可以通過以下二維碼關注。轉載本文請聯(lián)系編程技術宇宙公眾號。