最近在讀《Unix網(wǎng)絡(luò)編程》,感覺以前看的太粗糙复哆,很多細(xì)節(jié)都沒有深究欣喧,這次重讀重新整理下筆記,以期對(duì)網(wǎng)絡(luò)編程的一些細(xì)枝末節(jié)能有一個(gè)較好的梳理梯找,這是第一篇唆阿,實(shí)例分析TCP協(xié)議通信流程。
1 協(xié)議分層
一次網(wǎng)絡(luò)請(qǐng)求是要經(jīng)過很多層的锈锤,如底層的物理層驯鳖,再上面的鏈路層、網(wǎng)絡(luò)層久免、傳輸層以及應(yīng)用層浅辙。當(dāng)然我們一般工作是針對(duì)應(yīng)用層,但是也需要對(duì)傳輸層有很深刻的了解妄壶,傳輸層個(gè)人感覺也是最復(fù)雜的摔握。下圖是TCP/IP協(xié)議分層圖,注意丁寄,雖然ARP和RARP協(xié)議都劃分在鏈路層氨淌,實(shí)際上IP、ARP和RARP數(shù)據(jù)報(bào)都需要以太網(wǎng)驅(qū)動(dòng)程序來封裝成幀伊磺;同樣的盛正,ICMP和IGMP協(xié)議雖然劃分在網(wǎng)絡(luò)層,實(shí)際它們都需要IP協(xié)議來封裝成數(shù)據(jù)報(bào)屑埋。
圖1中沒有畫出的物理層豪筝,指的是電信號(hào)的傳遞方式,比如現(xiàn)在以太網(wǎng)通用的網(wǎng)線(雙絞線)、早期以太網(wǎng)采用的的同軸電纜(現(xiàn)在主要用于有線電視)续崖、光纖等都屬于物理層的概念敲街。物理層的能力決定了最大傳輸速率、傳輸距離严望、抗干擾性等多艇。
鏈路層有以太網(wǎng)、令牌環(huán)網(wǎng)等標(biāo)準(zhǔn)像吻,鏈路層負(fù)責(zé)網(wǎng)卡設(shè)備的驅(qū)動(dòng)峻黍、幀同步、沖突檢測(cè)拨匆、數(shù)據(jù)差錯(cuò)校驗(yàn)等工作姆涩,現(xiàn)在我們平時(shí)接觸到的基本是以太網(wǎng)。交換機(jī)是工作在鏈路層的網(wǎng)絡(luò)設(shè)備惭每,可以在不同的鏈路層網(wǎng)絡(luò)之間轉(zhuǎn)發(fā)數(shù)據(jù)幀(比如十兆以太網(wǎng)和百兆以太網(wǎng)之間骨饿、以太網(wǎng)和令牌環(huán)網(wǎng)之間),由于不同鏈路層的幀格式可能不同洪鸭,交換機(jī)要將進(jìn)來的數(shù)據(jù)包拆掉鏈路層首部重新封裝之后再轉(zhuǎn)發(fā)样刷。
網(wǎng)絡(luò)層則主要是經(jīng)常提及的IP協(xié)議仑扑,IP協(xié)議不保證數(shù)據(jù)傳輸?shù)目煽啃?/strong>览爵,數(shù)據(jù)包在傳輸過程中可能丟失,可靠性可以在上層協(xié)議或應(yīng)用程序中提供支持镇饮。路由器是工作在第三層的網(wǎng)絡(luò)設(shè)備蜓竹,同時(shí)兼有交換機(jī)的功能,可以在不同的鏈路層接口之間轉(zhuǎn)發(fā)數(shù)據(jù)包储藐,因此路由器需要將進(jìn)來的數(shù)據(jù)包拆掉網(wǎng)絡(luò)層和鏈路層兩層首部并重新封裝俱济。
傳輸層則是TCP和UDP協(xié)議,TCP協(xié)議保證數(shù)據(jù)收發(fā)的可靠性钙勃,丟失的數(shù)據(jù)包自動(dòng)重發(fā)蛛碌,上層應(yīng)用程序收到的總是可靠的數(shù)據(jù)流。UDP協(xié)議不面向連接辖源,也不保證可靠性蔚携。
應(yīng)用層則是我們自己的應(yīng)用程序。而在應(yīng)用程序里面發(fā)送的數(shù)據(jù)克饶,在網(wǎng)絡(luò)上則不僅僅是那些數(shù)據(jù)本身酝蜒,還有各個(gè)協(xié)議頭部,數(shù)據(jù)包的封裝過程如圖二所示矾湃。各層協(xié)議頭部和內(nèi)容在接下來會(huì)通過一個(gè)例子來分析亡脑。
2 基于TCP協(xié)議的編程
2.1 以太網(wǎng)幀和ARP協(xié)議
從圖2可以看到,數(shù)據(jù)最終都是封裝成以太網(wǎng)幀在網(wǎng)絡(luò)中傳輸。以太網(wǎng)幀的格式如圖3所示:
在TCP編程中霉咨,兩端通信之前蛙紫,需要先通過ARP協(xié)議來確定指定IP的機(jī)器的物理地址,也就是它的網(wǎng)卡的硬件地址(MAC)途戒,MAC地址長度為48位惊来,在網(wǎng)卡出廠的時(shí)候固化在網(wǎng)卡里面的,可以通過命令 ifconfig
來查看網(wǎng)卡地址棺滞。ARP協(xié)議的數(shù)據(jù)報(bào)格式如下:
比如當(dāng)我們運(yùn)行命令 ping 192.168.1.100
裁蚁,那么需要先arp協(xié)議獲取192.168.1.100
這個(gè)ip對(duì)應(yīng)的MAC地址,下面是一個(gè)ARP協(xié)議的請(qǐng)求和響應(yīng)包继准。通常操作系統(tǒng)會(huì)有ARP緩存枉证,所以一次請(qǐng)求后,只要緩存沒有過期移必,下次就可以從緩存中取而不需要發(fā)送ARP請(qǐng)求來獲取目的IP地址的MAC地址了室谚。
請(qǐng)求包
響應(yīng)包
對(duì)照前面貼出來的以太網(wǎng)幀格式,很容易分析這兩個(gè)數(shù)據(jù)包崔泵。如以太網(wǎng)幀的頭部包含的14個(gè)字節(jié)秒赤,分別是目的地址,源地址以及協(xié)議類型憎瘸。最初因?yàn)椴恢滥康膇p地址的mac地址入篮,所以目的地址填的是ff:ff:ff:ff:ff:ff
進(jìn)行廣播,協(xié)議類型是0x0806幌甘。而ARP協(xié)議的內(nèi)容可以參照上圖潮售,分布是硬件類型(以太網(wǎng),標(biāo)志1)锅风,協(xié)議類型為IPV4(0x0800)酥诽,硬件地址長度(6個(gè)字節(jié)),協(xié)議地址長度(IPV4皱埠,4個(gè)字節(jié))肮帐,操作碼(ARP請(qǐng)求類型為1,ARP響應(yīng)類型為2)边器,發(fā)送方的MAC地址(本機(jī)mac地址)训枢,發(fā)送方ip地址(請(qǐng)求包里是192.168.1.106,響應(yīng)包是192.168.1.100)饰抒,目標(biāo)MAC地址(請(qǐng)求時(shí)不知道目標(biāo)MAC地址肮砾,所以填全0,響應(yīng)包會(huì)填寫目標(biāo)MAC地址)袋坑,目標(biāo)IP地址(請(qǐng)求包是192.168.1.100仗处,響應(yīng)包是192.168.1.106)眯勾。
2.2 Socket編程
接下來要開始socket編程了,先寫一個(gè)簡單的客戶端-服務(wù)端婆誓。
#服務(wù)端:server.py
import socket
def start_server(ip, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((ip, port))
sock.listen(1)
while True:
conn, cliaddr = sock.accept()
print 'server connect from: ', cliaddr
while True:
data = conn.recv(1024)
if not data:
print 'client closed:', cliaddr
break
conn.send(data.upper())
conn.close()
except Exception, ex:
print 'exception occured:', ex
finally:
sock.close()
if __name__ == "__main__":
start_server('127.0.0.1', 7777)
#客戶端:client.py
from socket import *
import sys
def start_client(ip, port):
try:
sock = socket(AF_INET, SOCK_STREAM, 0)
sock.connect((ip, port))
print 'connected'
while True:
data = sys.stdin.readline().strip()
print 'input data:', data
if not data: break
sock.send(data)
result = sock.recv(1024)
if not result:
print 'other side has closed'
else:
print 'response from server:%s' % result
sock.close()
except Exception, ex:
print ex
if __name__ == "__main__":
start_client('127.0.0.1', 7777)
TCP的通信流程如下圖所示吃环,可以看到前面建立連接有三次握手,后面關(guān)閉連接有四次握手洋幻。注意圖中是C語言的函數(shù)郁轻,對(duì)應(yīng)到python的里面發(fā)送用的是send函數(shù),讀取用的是recv函數(shù)文留。要注意的是好唯,一個(gè)TCP連接的套接字對(duì)是一個(gè)四元組,包括源IP燥翅,源端口骑篙,目的IP,目的端口森书“卸耍客戶端和服務(wù)端的狀態(tài)并不是同步的,如果客戶端的ACK發(fā)送失敗凛膏,可能客戶端的連接是ESTABLISHED杨名,而服務(wù)端對(duì)應(yīng)連接還是SYN_RCVD狀態(tài)。
3 TCP通信流程實(shí)例分析
接下來要做的就是通過wireshark來觀察這個(gè)流程猖毫。先在一個(gè)終端運(yùn)行 python server.py
台谍,然后在第二個(gè)終端運(yùn)行python client.py
,這個(gè)時(shí)候我們看到第二個(gè)終端輸出connected
鄙麦,表示連接上了典唇。wireshark輸出如下:
從圖中可以看到三次握手的過程镊折,這里我們拿出第一個(gè)SYN包來分析下數(shù)據(jù)包的格式胯府,以太網(wǎng)幀的格式上面我們已經(jīng)提過了,前面12個(gè)字節(jié)是目的地址和源地址恨胚,因?yàn)槭潜镜氐刂仿钜颍赃@都是00,然后兩個(gè)字節(jié)幀類型是0800赃泡,即IP協(xié)議寒波。后面就是IP協(xié)議棧和TCP協(xié)議棧的內(nèi)容。
先看IP協(xié)議棧的格式如圖7所示升熊,數(shù)據(jù)包如圖8所示俄烁,第一個(gè)字節(jié)0x45中,前4位為版本IPV4级野,接著4位5為首部長度页屠,代表的是4*5=20個(gè)字節(jié),這是指的整個(gè)IP數(shù)據(jù)包的長度。第二個(gè)字節(jié)0x00為服務(wù)類型TOS辰企,有3個(gè)位用來指定IP數(shù)據(jù)報(bào)的優(yōu)先級(jí)风纠,現(xiàn)在幾乎不用起宽。然后的16位0x003c為IP包的總長度60(首部20字節(jié)+數(shù)據(jù)40字節(jié))赦颇,可以看到伶唯,從IP數(shù)據(jù)包的第一個(gè)字節(jié)45開始到最后一共是60個(gè)字節(jié)舌涨。緊接著的16位0xe963為標(biāo)識(shí)淹冰,如果IP包大小超過了MTU羞延,則需要進(jìn)行拆分拷淘,這個(gè)標(biāo)識(shí)字段就是用于標(biāo)識(shí)哪些包分拆前是同一組的撇他。接著的16位0x4000竹习,前3位為標(biāo)志位速址,其中最高位保留為0,第二位為DF(don't fragment)位為1由驹,也就是不分片芍锚,第三位為MF(more fragments,更多分片)位為0蔓榄,因?yàn)槲覀冞@里沒有分片; 接著的13位是片的偏移并炮,這里沒有分片,所以為0甥郑。接下來的8位0x40為TTL逃魄,值為64(TTL在traceroute時(shí)就很有用,TTL是這樣用的:源主機(jī)為數(shù)據(jù)包設(shè)定一個(gè)生存時(shí)間澜搅,比如64伍俘,每過一個(gè)路由器就把該值減1,如果減到0就表示路由已經(jīng)太長了仍然找不到目的主機(jī)的網(wǎng)絡(luò)勉躺,就丟棄該包癌瘾,因此這個(gè)生存時(shí)間的單位不是秒,而是跳(hop))饵溅,再8位是協(xié)議字段妨退,指示上層協(xié)議是TCP,UDP還是ICMP,IGMP等蜕企。我們這里是TCP咬荷,所以值為0x06。然后16位0x5356是首部校驗(yàn)和轻掩,只校驗(yàn)IP首部幸乒,數(shù)據(jù)的校驗(yàn)由更高層協(xié)議負(fù)責(zé)。然后的32位7f000001是源IP地址127.0.0.1唇牧,而接著的32位是目的IP地址127.0.0.1罕扎,選項(xiàng)為空基茵,然后接下來的是TCP協(xié)議棧內(nèi)容。
3.1 三次握手?jǐn)?shù)據(jù)包解析
TCP段格式和數(shù)據(jù)段實(shí)例如圖9壳影,10所示拱层。最開始16位0xdbb8為源端口56280,然后的16位0x1e61為目的端口7777宴咧。接著是32位序號(hào)0x2d4a6c26即759852070根灯,注意我們看到wireshark中為了顯示友好,顯示的值為0掺栅,那是相對(duì)序號(hào)(可以在右鍵的Protocol Preference中取消相對(duì)序號(hào)選項(xiàng)就可以看到絕對(duì)序號(hào)了)烙肺。接著是32位的確認(rèn)序號(hào)0x00000000。接著的16位中的前4位是首部長度0xa氧卧,也就是4*10=40
個(gè)字節(jié)桃笙。我們可以看到這里的TCP段正好是40個(gè)字節(jié),也就是說沒有數(shù)據(jù)部分沙绝,只有首部搏明。接著的6位是保留位,這16位的最后6位是6個(gè)標(biāo)志位0x002闪檬,分布是URG,ACK,PSH,RST,SYN,FIN星著,其中URG先不管,ACK是確認(rèn)標(biāo)志粗悯,PSH是盡快推送數(shù)據(jù)到接收進(jìn)程標(biāo)志虚循,RST是復(fù)位連接標(biāo)志,SYN是同步序號(hào)標(biāo)志样傍,F(xiàn)IN是完成數(shù)據(jù)發(fā)送標(biāo)志横缔。我們這里看到只有SYN標(biāo)志置位為1,表示是同步序號(hào)衫哥。而后16位0xaaaa茎刚,表示窗口大小為43690。再接著就是16位校驗(yàn)和0xfe30炕檩,然后是16為緊急指針為0x0000斗蒋,接著是選項(xiàng)字段。
TCP選項(xiàng)字段格式分為3部分笛质,kind為選項(xiàng)的類型,length為該選項(xiàng)的總長度(這個(gè)總長度包括了kind和length這兩個(gè)字節(jié)在內(nèi))捞蚂,info為選項(xiàng)的值妇押,下表是常見的選項(xiàng)值和含義,更多選項(xiàng)值參見參考資料2姓迅。
kind (1字節(jié)) | length (1字節(jié)) | info (n字節(jié)) |含義
--------------------|------------------|-----------------------|
0 | 空 | 空 |表示選項(xiàng)表結(jié)束
1 | 空| 空| 空操作nop敲霍,一般用于填充tcp選項(xiàng)的總長度為4的倍數(shù)
2 | 4 | MSS| 最大段長度
3 | 3 |window scale| 滑動(dòng)窗口擴(kuò)大因子
4 | 2 | SACK | 選擇性確認(rèn)
8 | 10 | timestamp |時(shí)間戳值4字節(jié)+時(shí)間戳回顯應(yīng)答4字節(jié)
可以看到選項(xiàng)字段先是 0x02 04 ff d7表示是MSS俊马,長度為4字節(jié),值為65495肩杈。MSS通常等于MTU-20-20柴我,而MTU一般設(shè)置為1500,所以一般MSS為1460扩然,當(dāng)然艘儒,考慮到TCP的選項(xiàng)值可能會(huì)占據(jù)最多20個(gè)字節(jié),所以MSS也可能是1460-12-8=1440夫偶。而通常的MTU為1500界睁,lo特殊,為65536兵拢,所以這里的MSS不是1440翻斟,而是一個(gè)比較大的值0xffd7=65495。MTU是服務(wù)器可配置的说铃,在TCP通信過程中客戶端和服務(wù)器端會(huì)協(xié)商最小的MSS作為最終值访惜。TCP有了MSS限制,就可以保證在IP層不用分片了腻扇。再接著是0x0402是選擇性確認(rèn)選項(xiàng)疾牲,類別為4,總長度為2字節(jié)衙解,這里表示沒有info阳柔。接著的0x08 0a ff ff 85 45 00 00 00 00是時(shí)間戳,類別是8蚓峦,總長度為10舌剂,內(nèi)容為時(shí)間戳0xffff8545=4294935877,時(shí)間戳回顯值為0x0000000=0暑椰。然后是1字節(jié)的0x01霍转,類別為1,表示空操作一汽,用于填充的避消。接著是0x03 03 07。類別為3召夹,長度為3岩喷,值為7,表示窗口擴(kuò)大因子為7监憎。
那么第二階段SYN+ACK和第三階段的ACK的數(shù)據(jù)包類似纱意,分別如下所示,SYN+ACK中的標(biāo)記是SYN和ACK置位鲸阔,然后時(shí)間戳回顯為當(dāng)前的時(shí)間偷霉;第三階段ACK數(shù)據(jù)包是ACK標(biāo)記置位和時(shí)間戳回顯迄委。
3.2 發(fā)送數(shù)據(jù)的數(shù)據(jù)包解析
下面看看發(fā)送數(shù)據(jù)的包,我們?cè)赾lient.py的終端輸入 haha类少,可以看到捕獲到4個(gè)數(shù)據(jù)包叙身,第一個(gè)包是客戶端發(fā)往服務(wù)端的,PSH和ACK標(biāo)志置位硫狞,同時(shí)數(shù)據(jù)為haha信轿;第二個(gè)包是服務(wù)端發(fā)往客戶端的,ACK標(biāo)志置位妓忍;第三個(gè)包還是服務(wù)端發(fā)往客戶端的虏两,PSH和ACK標(biāo)志置位,這是服務(wù)端發(fā)送的內(nèi)容為HAHA世剖;第四個(gè)包是客戶端發(fā)往服務(wù)端的定罢,ACK標(biāo)志置位,確認(rèn)收到了數(shù)據(jù)旁瘫。需要注意的是祖凫,客戶端和服務(wù)端各自維護(hù)了一個(gè)序號(hào),這是因?yàn)門CP是全雙工通信酬凳。如果某種面向連接的協(xié)議是半雙工的惠况,通訊過程只能采用一問一答的方式,收和發(fā)兩個(gè)方向不能同時(shí)傳輸宁仔,在同一時(shí)間只允許一個(gè)方向的數(shù)據(jù)傳輸稠屠,則只需要一套序號(hào)就夠了,不需要通訊雙方各自維護(hù)一套序號(hào)翎苫。服務(wù)端發(fā)往客戶端的ACK是5权埠,這是因?yàn)槭盏降臄?shù)據(jù)長度為4,所以新的請(qǐng)求序號(hào)為5(注意都是相對(duì)序號(hào))煎谍,同理看到后面客戶端發(fā)送數(shù)據(jù)的序號(hào)是5了攘蔽,序號(hào)跟數(shù)據(jù)長度是相關(guān)的。
3.3 關(guān)閉連接四次握手?jǐn)?shù)據(jù)包解析
關(guān)閉TCP連接時(shí)呐粘,會(huì)有四次握手满俗,主動(dòng)關(guān)閉的一方會(huì)處于TIME_WAIT狀態(tài)一段時(shí)間再徹底關(guān)閉。TIME_WAIT的時(shí)間是系統(tǒng)配置參數(shù)tcp_fin_timeout設(shè)定的作岖,Linux里面一般是60秒唆垃,當(dāng)然我們也可以調(diào)短它,關(guān)于為什么要有TIME_WAIT的狀態(tài)鳍咱,主要原因有兩點(diǎn):其一是為了可靠的實(shí)現(xiàn)TCP全雙工連接的終止降盹,其二是為了允許老的重復(fù)分節(jié)在網(wǎng)絡(luò)中消逝。第一點(diǎn)谤辜,假設(shè)最后客戶端發(fā)送的ACK服務(wù)端沒有收到蓄坏,則服務(wù)端會(huì)重發(fā)FIN,這個(gè)時(shí)候處于TIME_WAIT狀態(tài)的客戶端連接還可以重發(fā)ACK丑念。否則涡戳,如果連接已經(jīng)關(guān)閉,則客戶端會(huì)發(fā)送RST的一個(gè)分節(jié)脯倚,這樣服務(wù)端會(huì)解析為一個(gè)錯(cuò)誤渔彰,這個(gè)不是我們想要的。第二點(diǎn)推正,如果沒有TIME_WAIT恍涂,那么可能在連接關(guān)閉后,新建立一個(gè)連接植榕,IP地址和端口跟之前的一樣再沧,TCP必須防止老的重復(fù)分組或者延遲的分組在該連接后終止后再現(xiàn),不然無法區(qū)分分組來自老的連接尊残,還是新的連接的炒瘸,如下圖所示。當(dāng)然這個(gè)情況很難出現(xiàn)寝衫,因?yàn)樾碌倪B接和老的連接必須是兩邊的IP地址和端口都一樣顷扩,而且老的分組的ISN編號(hào)也要有效。通常慰毅,新的連接會(huì)采用一個(gè)隨機(jī)的端口號(hào)隘截,很難這么湊巧跟之前一樣,而要之前老的分組的序列號(hào)ISN也幾乎不可能有效汹胃。因此婶芭,TCP禁止處于TIME_WAIT狀態(tài)的端口發(fā)起新的連接,在TIME_WAIT時(shí)間過后统台,建立新的連接雕擂,基本可以保證該連接以前的老的化身的重復(fù)分組已經(jīng)消逝。
在第二個(gè)終端贱勃,CTRL+C關(guān)閉client.py井赌,可以看到wireshark捕獲到的數(shù)據(jù)包如下,這里看到服務(wù)端的FIN和ACK合并在了一個(gè)數(shù)據(jù)包里贵扰。而且可以看到客戶端連接處于TIME_WAIT狀態(tài)仇穗,過一段時(shí)間后才關(guān)閉。
4 總結(jié)
這是TCP/IP協(xié)議的第一篇戚绕,總結(jié)下各個(gè)協(xié)議的格式和數(shù)據(jù)包的分析纹坐,第二篇會(huì)重點(diǎn)分析下諸如SO_REUSEADDR, backlog
等參數(shù)的意義,特別是backlog參數(shù)舞丛,是查了好多資料才明白一二耘子。
5 參考資料
- 《Linux C 一站式編程》網(wǎng)絡(luò)編程部分果漾,大部分協(xié)議圖也來自這里。
- TCP頭部選項(xiàng)
- time-wait-and-its-design-implications-for-protocols-and-scalable-servers
- 《Unix網(wǎng)絡(luò)編程》部分章節(jié)