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