推薦閱讀:備戰(zhàn)2020——iOS全新面試題總結(jié)
一偿洁、TCP的特點(diǎn)和報(bào)文結(jié)構(gòu)
1撒汉、面向連接、可靠傳輸涕滋、面向字節(jié)流睬辐、全雙工服務(wù)
2、TCP的報(bào)文結(jié)構(gòu)
TCP報(bào)文段由首部字段和一個(gè)數(shù)據(jù)字段組成。
數(shù)據(jù)字段包含一塊應(yīng)用數(shù)據(jù)溯饵。最大報(bào)文長(zhǎng)度MSS
(Maximum Segment Size)限制了報(bào)文段數(shù)據(jù)字段的最大長(zhǎng)度侵俗。MSS
選項(xiàng)用于在TCP連接建立時(shí),收發(fā)雙方協(xié)商通信時(shí)每一個(gè)報(bào)文段所能承載的最大數(shù)據(jù)長(zhǎng)度丰刊。
所以當(dāng)TCP發(fā)送一個(gè)大文件(比如一張高清圖片)時(shí)隘谣,通常是將該文件劃分為MSS
長(zhǎng)度的若干塊(最后一塊除外,通常會(huì)小于MSS
)啄巧。而實(shí)際交互式應(yīng)用通常傳送長(zhǎng)度小于MSS
的數(shù)據(jù)塊寻歧。
如圖,與UDP一樣秩仆,首部包括源端口號(hào)和目的端口號(hào)码泛,用于多路復(fù)用/分解來(lái)自上層或送到上層應(yīng)用的數(shù)據(jù)。TCP首部也同樣包括檢驗(yàn)和字段
TCP首部還包含下列字段:
- 32比特的序號(hào)字段Seq(sequence number field)和32比特的確認(rèn)號(hào)字段Ack(acknowledge number field)
- 16比特的接收窗口字段RW(receive window field),該字段用于流量控制澄耍,用于指示接收方愿意接收的字節(jié)數(shù)量噪珊。
- 4比特的首部長(zhǎng)度字段(header length field),該字段指示了以32比特的字為單位的TCP首部長(zhǎng)度齐莲。由于TCP選項(xiàng)字段的原因痢站,TCP首部長(zhǎng)度是可變的。(通常选酗,選項(xiàng)字段為空瑟押,所以TCP首部的典型長(zhǎng)度就是20字節(jié))
- 可選和變長(zhǎng)的選項(xiàng)字段(option field),該字段用于發(fā)送方和接收方協(xié)商最大報(bào)文段長(zhǎng)度(MSS)時(shí)星掰,或用作窗口調(diào)節(jié)因子時(shí)使用多望。
- 6比特的標(biāo)志字段(flag field)。ACK比特用于指示確認(rèn)字段中的值是有效的氢烘,即該報(bào)文段包括一個(gè)對(duì)已被接收?qǐng)?bào)文段的確認(rèn)怀偷。RST、SYN播玖、FIN比特用于連接建立和拆除椎工。
PSH比特指示接收方應(yīng)立即將數(shù)據(jù)交給上層。URG比特用于指示報(bào)文段里存在著被發(fā)送端的上層實(shí)體置為“緊急”的數(shù)據(jù)蜀踏。緊急數(shù)據(jù)的最后一個(gè)字節(jié)由16比特的緊急數(shù)據(jù)指針字段指出维蒙。當(dāng)緊急數(shù)據(jù)存在并給出指向緊急數(shù)據(jù)尾的指針的時(shí)候,TCP必須通知接收端的上層實(shí)體果覆。在實(shí)踐中颅痊,PSH、URG和緊急數(shù)據(jù)指針并沒(méi)有使用局待。
3斑响、序號(hào)字段Seq和確認(rèn)號(hào)字段Ack
- 在TCP通訊中菱属,無(wú)論是建立連接,數(shù)據(jù)傳輸舰罚,友好斷開(kāi)纽门,強(qiáng)制斷開(kāi),都離不開(kāi)Seq值和Ack值营罢,它們是TCP傳輸?shù)目煽勘WC赏陵。
序號(hào)Seq:
TCP把數(shù)據(jù)看成一個(gè)無(wú)結(jié)構(gòu)的、有序的字節(jié)流饲漾。一個(gè)報(bào)文段的序號(hào)因此是該報(bào)文段的首字節(jié)的字節(jié)流編號(hào)蝙搔。
比如數(shù)據(jù)流由一個(gè)包含100000
字節(jié)的文件組成,其MSS
是1000
字節(jié)能颁,數(shù)據(jù)流的首字節(jié)編號(hào)是0。該TCP將為該數(shù)據(jù)流構(gòu)建100
個(gè)報(bào)文段倒淫。給第一個(gè)報(bào)文段分配序號(hào)0伙菊,第二個(gè)則是1000
,第三個(gè)是2000
敌土,以此類(lèi)推镜硕。每一個(gè)序號(hào)被填入到相應(yīng)TCP報(bào)文段首部的序號(hào)字段中。
確認(rèn)號(hào)Ack:
TCP是全雙工服務(wù)的返干,因此主機(jī)A在向主機(jī)B發(fā)送數(shù)據(jù)的同時(shí)兴枯,也許也在接收主機(jī)B的數(shù)據(jù)。
主機(jī)A填充進(jìn)報(bào)文段的確認(rèn)號(hào)是主機(jī)A期望從主機(jī)B收到的下一個(gè)字節(jié)的序號(hào)矩欠。
在上個(gè)例子中财剖,假如服務(wù)端已經(jīng)接收包含字節(jié)0-999
的報(bào)文段和包含字節(jié)2000-2999
的報(bào)文段,但由于某種原因癌淮,還未收到包含字節(jié)1000-1999
的報(bào)文段躺坟,那么將仍會(huì)等待字節(jié)1000
(及其后的字節(jié))。因此服務(wù)端發(fā)給客戶端的下一個(gè)報(bào)文段將在確認(rèn)號(hào)Ack字段中包含1000
乳蓄。
因?yàn)門(mén)CP只確認(rèn)該流中至第一個(gè)丟失字節(jié)為止的字節(jié)咪橙,所以TCP被稱為累積確認(rèn)。
二虚倒、三次握手
-數(shù)據(jù)開(kāi)始傳輸前美侦,需要通過(guò) 三次握手
來(lái)建立連接
事實(shí)上我認(rèn)為,這里稱呼三步握手
(three-way handshake)才更貼切些
第一步:
- 客戶端的TCP首先向服務(wù)端的TCP發(fā)送一條特殊的TCP報(bào)文段魂奥。該報(bào)文段不包含應(yīng)用層數(shù)據(jù)菠剩,該報(bào)文段首部中的一個(gè)
標(biāo)志位(SYN比特)
被置為1,所以該報(bào)文段被稱為SYN報(bào)文段耻煤。另外赠叼,客戶會(huì)隨機(jī)選擇一個(gè)初始序號(hào)client_isn
擦囊,并將該序號(hào)放置于該起始的TCPSYN
報(bào)文段的序號(hào)字段中。 - 客戶端和服務(wù)端最開(kāi)始都處于
CLOSED
狀態(tài)嘴办,發(fā)送過(guò)該SYN
報(bào)文段后瞬场,客戶端TCP進(jìn)入SYN_SENT
狀態(tài),等待服務(wù)端確認(rèn)并將SYN比特置為1的報(bào)文段涧郊。
第二步:
- 收到SYN報(bào)文段后贯被,服務(wù)端會(huì)為該TCP連接分配TCP緩存和變量,服務(wù)端TCP會(huì)進(jìn)入
SYN_RCVD
狀態(tài)妆艘,等待客戶端TCP發(fā)送確認(rèn)報(bào)文段彤灶。 - 并向該客戶端TCP發(fā)送允許連接的報(bào)文段,該報(bào)文段同樣不包含應(yīng)用層數(shù)據(jù)批旺。該報(bào)文段首部的
SYN
比特被置為1幌陕,確認(rèn)號(hào)字段被置為client_isn
+1。服務(wù)端還會(huì)選擇自己的初始序號(hào)server_isn
汽煮,放到報(bào)文段首部的序號(hào)段中搏熄。該連接被稱為SYNACK報(bào)文段。
第三步:
- 收到SYNACK報(bào)文段后暇赤,客戶端也要為該TCP連接分配緩存和變量心例,客戶端TCP進(jìn)入
ESTABLISHED
狀態(tài),在此狀態(tài)鞋囊,客戶端就能發(fā)送和接收包含有效載荷數(shù)據(jù)的報(bào)文段了止后。 - 并向服務(wù)端TCP發(fā)送一個(gè)報(bào)文段:這最后一個(gè)報(bào)文段對(duì)服務(wù)端的允許連接的報(bào)文表示了確認(rèn)(將
server_isn
+ 1放到報(bào)文段首部的確認(rèn)字段中)。因?yàn)檫B接已經(jīng)建立了溜腐,所以該SYN
比特被置為0译株。這個(gè)階段,可以在報(bào)文段負(fù)載中攜帶應(yīng)用層數(shù)據(jù)挺益。 - 收到客戶端該報(bào)文段后古戴,服務(wù)端TCP也會(huì)進(jìn)入
ESTABLISHED
狀態(tài),可以發(fā)送和接收包含有效載荷數(shù)據(jù)的報(bào)文段矩肩。
三现恼、四次揮手
參與TCP連接的兩個(gè)進(jìn)程中的任何一個(gè)都能終止該連接,當(dāng)連接結(jié)束后黍檩,主機(jī)中的資源(緩存和變量)會(huì)被釋放叉袍。
上邊說(shuō)到,SYN和FIN標(biāo)志位分別對(duì)應(yīng)著TCP連接的建立和拆除刽酱。
如圖喳逛,
第一步:
- 客戶應(yīng)用進(jìn)程發(fā)出一個(gè)關(guān)閉連接的指令。會(huì)引起客戶端TCP向服務(wù)端發(fā)送一個(gè)特殊的TCP報(bào)文段棵里。該報(bào)文段即是將首部的一個(gè)標(biāo)志位
FIN
比特置為1润文。 - 同時(shí)姐呐,客戶端進(jìn)入
FIN_WAIT_1
狀態(tài),等待服務(wù)端的帶有確認(rèn)的TCP報(bào)文段典蝌。
第二步:
- 收到該報(bào)文段后會(huì)向客戶端發(fā)送一個(gè)確認(rèn)報(bào)文段曙砂。
- 服務(wù)端TCP進(jìn)入
CLOSE_WAIT
狀態(tài),對(duì)應(yīng)客戶端的TIME_WAIT
骏掀,表示被動(dòng)關(guān)閉鸠澈。 - 客戶端收到該報(bào)文段后,進(jìn)入
FIN_WAIT_2
狀態(tài)截驮,等待服務(wù)端的FIN
比特置為1的報(bào)文段笑陈。
第三步:
- 服務(wù)端發(fā)送自己的終止報(bào)文段,同樣是把報(bào)文段首部的標(biāo)志位
FIN
比特置為1葵袭。 - 服務(wù)端TCP進(jìn)入
LAST_ACK
狀態(tài)涵妥,等待服務(wù)端最后的確認(rèn)報(bào)文段。
第四步:
- 客戶端收到服務(wù)端的終止報(bào)文段后坡锡,向服務(wù)端發(fā)送一個(gè)確認(rèn)報(bào)文段蓬网。同時(shí),客戶端進(jìn)入
TIME_WAIT
狀態(tài)娜氏。 - 假如ACK丟失拳缠,
TIME_WAIT
狀態(tài)使TCP客戶重傳最后的確認(rèn)報(bào)文墩新,TIME_WAIT
通常會(huì)等待2MSL(Maximum Segment Lifetime 最長(zhǎng)報(bào)文段壽命)贸弥。經(jīng)過(guò)等待后,連接就正式關(guān)閉海渊,重新進(jìn)入CLOSED
狀態(tài)绵疲,客戶端所有資源將被釋放。 - 服務(wù)端收到該報(bào)文段后臣疑,同樣也會(huì)關(guān)閉盔憨,重新進(jìn)入
CLOSED
狀態(tài),釋放所有服務(wù)端TCP資源讯沈。
四郁岩、一些問(wèn)題
1、問(wèn):為什么建立連接只用三次握手缺狠,而斷開(kāi)連接卻要四次揮手问慎?
- 首先,當(dāng)客戶端數(shù)據(jù)已發(fā)送完畢挤茄,且知道服務(wù)端也全部接收到了時(shí)如叼,就會(huì)去斷開(kāi)連接即向服務(wù)端發(fā)送FIN
- 服務(wù)端接收到客戶端的FIN,為了表示接收到了穷劈,就會(huì)向客戶端發(fā)送ACK
- 但此時(shí)笼恰,服務(wù)端可能還在發(fā)送數(shù)據(jù)踊沸,并沒(méi)有關(guān)閉TCP窗口的意思,所以服務(wù)端的FIN和ACK并不是同步發(fā)的社证,只有當(dāng)數(shù)據(jù)發(fā)送完了逼龟,才會(huì)發(fā)送FIN
-
答:服務(wù)端的FIN和ACK需要分開(kāi)發(fā),并不是像三次握手中那樣猴仑,SYN可以和ACK同步發(fā)审轮,所以就需要四次揮手
2、在四次揮手中辽俗,客戶端為什么在TIME_WAIT后必須等待2MSL時(shí)間呢疾渣?
這個(gè)ACK
報(bào)文段有可能丟失,因而使處在LAST_ACK
端的服務(wù)端收不到對(duì)已發(fā)送的FIN
報(bào)文段的ACK
報(bào)文段崖飘,從而服務(wù)端會(huì)去不斷重傳FIN
報(bào)文段榴捡。
而客戶端就能在2MSL
時(shí)間內(nèi)收到重傳的FIN
報(bào)文段。接著客戶端重傳一次確認(rèn)朱浴,重新啟動(dòng)2MSL
計(jì)時(shí)器吊圾。直至服務(wù)端收到后,客戶端和服務(wù)端就都會(huì)進(jìn)入CLOSED
狀態(tài)翰蠢,關(guān)閉TCP連接项乒。
而如果客戶端不等待2MSL
時(shí)間,而是在發(fā)送完ACK
確認(rèn)后立即釋放資源梁沧,關(guān)閉連接檀何,那么就無(wú)法收到服務(wù)端重傳的FIN
報(bào)文段,因而也不會(huì)再發(fā)送一次ACK
確認(rèn)報(bào)文段廷支,這樣频鉴,服務(wù)端就無(wú)法正常進(jìn)入CLOSED
狀態(tài),資源就一直無(wú)法釋放了恋拍。
-
答:為了保證客戶端發(fā)送的最后一個(gè)
ACK
報(bào)文段能夠到達(dá)服務(wù)端垛孔。
3、TCP在創(chuàng)建連接時(shí)施敢,為什么需要三次握手而不是兩次或四次周荐?
一個(gè)簡(jiǎn)單的例子:
-
三次握手
:
“喂,你聽(tīng)得到嗎僵娃?”
“我聽(tīng)得到呀概作,你聽(tīng)得到我嗎?”
“我能聽(tīng)到你悯许,今天balabala……” -
兩次握手
:
“喂仆嗦,你聽(tīng)得到嗎?”
“我聽(tīng)得到呀先壕,你聽(tīng)得到我嗎瘩扼?”
“喂谆甜,你聽(tīng)得到嗎?”
“……誰(shuí)在說(shuō)話集绰?”
“喂规辱,你聽(tīng)得到嗎?”
“……” -
四次握手
:
“喂栽燕,你聽(tīng)得到嗎罕袋?”
“我聽(tīng)得到呀”“你能聽(tīng)到我嗎?”
“……不想跟傻逼說(shuō)話”
之所以不用四次握手
的原因很容易理解碍岔,就是浪費(fèi)資源浴讯,服務(wù)端的SYN
和ACK
可以一起發(fā),完全沒(méi)必要分開(kāi)兩次蔼啦。
而如果是兩次握手
:
客戶端發(fā)出的第一個(gè)連接請(qǐng)求SYN
報(bào)文段并沒(méi)有丟失榆纽,而是在某個(gè)網(wǎng)絡(luò)結(jié)點(diǎn)長(zhǎng)時(shí)間的滯留了,以致延誤到連接釋放以后的某個(gè)時(shí)間才到達(dá)服務(wù)端捏肢。本來(lái)這是一個(gè)早已失效的報(bào)文段奈籽。但服務(wù)端收到此失效的連接請(qǐng)求SYN
報(bào)文段后,就誤認(rèn)為是客戶端再次發(fā)出的一個(gè)新的連接請(qǐng)求SYN
報(bào)文段鸵赫。于是就向客戶端發(fā)出ACK
確認(rèn)報(bào)文段衣屏,同意建立連接。假設(shè)不采用三次握手
辩棒,那么只要服務(wù)端發(fā)出確認(rèn)狼忱,新的連接就建立了。
由于現(xiàn)在客戶端并沒(méi)有發(fā)出建立連接的SYN
請(qǐng)求盗温,因此不會(huì)理睬服務(wù)端的確認(rèn)藕赞,也不會(huì)向服務(wù)端發(fā)送數(shù)據(jù)成肘。但服務(wù)端卻以為新的運(yùn)輸連接已經(jīng)建立卖局,并一直等待客戶端發(fā)來(lái)數(shù)據(jù)。這樣双霍,服務(wù)端的很多資源就白白浪費(fèi)掉了砚偶。
事實(shí)上:TCP對(duì)有數(shù)據(jù)的TCP報(bào)文段必須確認(rèn)的原則,所以洒闸,客戶端對(duì)服務(wù)端的SYN
報(bào)文段必須回復(fù)一個(gè)ACK
報(bào)文段表示確認(rèn)染坯。并且,TCP不會(huì)為沒(méi)有數(shù)據(jù)的ACK
超時(shí)重傳丘逸,那么當(dāng)服務(wù)端沒(méi)收到客戶端的ACK
確認(rèn)報(bào)文段時(shí)单鹿,會(huì)超時(shí)重傳自己的SYN
報(bào)文段,一直到收到客戶端的ACK
為止深纲。
-
答:兩次握手會(huì)可能導(dǎo)致已失效的連接請(qǐng)求報(bào)文段突然又傳送到了服務(wù)端產(chǎn)生錯(cuò)誤仲锄,四次握手又太浪費(fèi)資源
五劲妙、代碼實(shí)現(xiàn)
2019 iOS面試-----一個(gè)基于UDP的簡(jiǎn)單的聊天Demo(用C語(yǔ)言、python儒喊、GCDAsyncUdpSocket來(lái)實(shí)現(xiàn)UDP通信)
參考UDP的代碼镣奋,其實(shí)TCP在代碼實(shí)現(xiàn)上也很相似,首先·socket·初始化時(shí)不再用·SOCK_DGRAM·怀愧,而是用·SOCK_STREAM·
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
TCP服務(wù)端要多了一道監(jiān)聽(tīng)侨颈、接受連接的過(guò)程:
int listen_ret = listen(fd,5);
int listen_socket = accept(_fd,(sockaddr *)&addr,&addr_len);
UDP則是多了一道連接的過(guò)程:
int ret = connect(_fd, (struct sockaddr *) &addr, sizeof(addr));
然后就是在接收和發(fā)送數(shù)據(jù)時(shí),不用再傳主機(jī)和端口了芯义。即由recvfrom
哈垢、sendto
改為recv
和send
。
send(_fd, [buffer bytes], [buffer length], 0);
recv(_fd, receiveBuffer, sizeof(receiveBuffer), 0);
python的客戶端代碼如下:
from socket import *
serverName = '127.0.0.1'
serverPort = 12000
clientSocket = socket(AF_INET,SOCK_STREAM)
clientSocket.connect((serverName,serverPort))
sentence = raw_input('Input lowercase:\n')
clientSocket.send(sentence)
modifiedSentence = clientSocket.recv(1029)
print 'From server:\n',modifiedSentence
clientSocket.close()
服務(wù)端代碼:
from socket import *
serverPort = 12000
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.bind(('',serverPort))
serverSocket.listen(1)
print 'server is ready to receive'
connectionSocket,addr = serverSocket.accept()
sentence = connectionSocket.recv(1029)
capitalizeSentence = sentence.upper()
print capitalizeSentence
connectionSocket.send(capitalizeSentence)
connectionSocket.close()