參考資料
TCP
TCP
協(xié)議是一個(gè)面向連接的可靠性交付協(xié)議贝椿。
- 由于是面向連接的想括,所以在服務(wù)器上需要為其分配內(nèi)存來存儲客戶端連接,同樣客戶端也需要存儲服務(wù)器的烙博。
- 由于保證可靠性瑟蜈,所以引入了很多保證可靠性的機(jī)制,比如定時(shí)重傳機(jī)制渣窜,如
SYN
/ACK
機(jī)制等铺根。
TCP傳輸控制協(xié)議是一種面向連接的、可靠的乔宿、基于字節(jié)流的通信協(xié)議位迂,數(shù)據(jù)在傳輸前需要建立連接,傳輸完畢后需要斷開連接详瑞〉嗔郑客戶端在收發(fā)數(shù)據(jù)前要使用connect()
函數(shù)和服務(wù)器建立連接,建立連接的目的是保證IP地址坝橡、端口泻帮、物理鏈路等正確無誤,為數(shù)據(jù)的傳輸開辟通道计寇。
TCP與UDP的異同點(diǎn)是什么呢锣杂?
TCP是面向連接的傳輸協(xié)議脂倦,建立連接時(shí)需要經(jīng)過三次握手,斷開連接時(shí)需要經(jīng)過四次揮手元莫,中間傳輸數(shù)據(jù)時(shí)也需要回復(fù)ACK
包確認(rèn)赖阻。多種機(jī)制保證了數(shù)據(jù)能夠正確到達(dá),不會丟失或出錯(cuò)踱蠢。UDP是非連接的傳輸協(xié)議火欧,沒有連接和斷開連接的過程,只是簡單地把數(shù)據(jù)丟到網(wǎng)絡(luò)中朽基,也不需要ACK
包確認(rèn)布隔。
UDP傳輸數(shù)據(jù)好像郵寄包裹,郵寄前需要填好寄件人和收件人的地址稼虎,之后送到快遞公司即可。但包裹是否正確到達(dá)招刨,是否損壞是無法得知也無法保證的霎俩。UDP協(xié)議只管將數(shù)據(jù)包發(fā)送到網(wǎng)絡(luò),然后就不管了沉眶。如果數(shù)據(jù)包丟失或損壞打却,發(fā)送端是無法知道的,當(dāng)然也不會重發(fā)谎倔。
既然如此柳击,TCP應(yīng)該是更加優(yōu)質(zhì)的傳輸協(xié)議嗎?
如果只是考慮可靠性片习,TCP確實(shí)比UDP要好捌肴,但是UDP在結(jié)構(gòu)上比TCP更加簡潔。因?yàn)椴粫l(fā)送ACK
的應(yīng)答消息也不會給數(shù)據(jù)包分配SEQ
序號藕咏,所以UDP的傳輸效率又是會比TCP高出很多状知,另外編程中實(shí)現(xiàn)UDP也比TCP簡單。
UDP的可靠性雖然不及TCP孽查,但也不會像想象中那樣頻繁地發(fā)生數(shù)據(jù)損毀饥悴,在更加重視傳輸效率而非可靠性的情況下。UDP是一種很好的選擇盲再,比如視頻通信或音頻通信西设,就非常適合采用UDP協(xié)議。通信時(shí)數(shù)據(jù)必須高效傳輸才不會出現(xiàn)卡頓現(xiàn)象答朋,用戶體驗(yàn)才能更加流暢贷揽,如果丟失幾個(gè)數(shù)據(jù)包,視頻畫面可能會出現(xiàn)雪花點(diǎn)绿映,音頻可能會夾雜一些雜音擒滑,這些都是無妨的腐晾。
與UDP相比,TCP的生命在于流控制丐一,這保證了數(shù)據(jù)傳輸?shù)恼_性藻糖。
最后需要說明的是TCP的速度無法超越UDP,但是在收發(fā)某些類型的數(shù)據(jù)時(shí)有可能會接近UDP库车。例如巨柒,每次交換的數(shù)據(jù)越大,TCP的傳輸效率就接近于UDP柠衍。
TCP數(shù)據(jù)報(bào)結(jié)構(gòu)
帶陰影的重點(diǎn)字段
- SEQ(Sequence Number) 序號洋满,序號占32位,用來標(biāo)識從計(jì)算機(jī)A發(fā)送到計(jì)算機(jī)B的數(shù)據(jù)包的序號珍坊,計(jì)算機(jī)發(fā)送數(shù)據(jù)時(shí)對此進(jìn)行標(biāo)記牺勾。
- ACK(Acknowledge Number) 確認(rèn)號,確認(rèn)號占32位阵漏,客戶端和服務(wù)器都可以發(fā)送驻民,ACK = SEQ + 1。
- 標(biāo)志位:每個(gè)標(biāo)志位占1Bit履怯,共有6個(gè)分別是
- URG 緊急指針(Urgent Pointer)有效
- ACK 確認(rèn)序號有效
- PSH 接收方應(yīng)盡快將這個(gè)報(bào)文交給應(yīng)用層
- RST 重置連接
- SYN 建立一個(gè)新連接
- FIN 斷開一個(gè)連接
TCP通信過程
- 三次握手:建立TCP連接通道
- 數(shù)據(jù)傳輸
- 四次揮手:斷開TCP連接通道
1. 三次握手
TCP建立連接時(shí)需要傳輸三個(gè)數(shù)據(jù)包回还,簡稱三次握手(Three-way Handshaking)。使用connect()
建立連接時(shí)叹洲,客戶端和服務(wù)器會相互發(fā)送三個(gè)數(shù)據(jù)包柠硕。
當(dāng)客戶端調(diào)用socket()
函數(shù)創(chuàng)建套接字后,因?yàn)闆]有建立連接,所以套接字處于CLOSED
狀態(tài)。服務(wù)器調(diào)用listen()
函數(shù)后安吁,套接字進(jìn)入LISTEN
狀態(tài)认境,并開始監(jiān)聽客戶端請求。
此時(shí)客戶端開始發(fā)起請求
- 當(dāng)客戶端調(diào)用
connect()
函數(shù)后,TCP協(xié)議會組建一個(gè)數(shù)據(jù)包,被設(shè)置SYN
標(biāo)志位,表示該數(shù)據(jù)包是用來建立同步連接的坎缭。
- 客戶端同時(shí)會生成一個(gè)隨機(jī)數(shù)1000,填充
SEQ
序號字段签钩,表示該數(shù)據(jù)包的序號掏呼。 - 當(dāng)完成這些工作后開始向服務(wù)器發(fā)送數(shù)據(jù)包,客戶端就進(jìn)入了
SYN-SEND
狀態(tài)铅檩。
- 服務(wù)器接收到數(shù)據(jù)包憎夷,檢測到已經(jīng)設(shè)置了
SYN
標(biāo)志位,知道這是客戶端發(fā)來建立連接的“請求包”昧旨。
- 服務(wù)器會組建一個(gè)數(shù)據(jù)包拾给,并設(shè)置
SYN
和ACK
標(biāo)志位祥得,SYN
表示該數(shù)據(jù)包是用來建立連接的,ACK
表示用來確認(rèn)收到了剛才客戶端發(fā)送的數(shù)據(jù)包蒋得。 - 服務(wù)器會生成一個(gè)隨機(jī)數(shù)2000级及,填充
SEQ
序列字段,2000和客戶端數(shù)據(jù)包沒有關(guān)系额衙。 - 服務(wù)器將客戶端數(shù)據(jù)包序號1000加1后得到1001饮焦,并使用這個(gè)數(shù)字填充
ACK
確認(rèn)號字段。 - 服務(wù)器將數(shù)據(jù)包發(fā)出并進(jìn)入
SYN-RECV
狀態(tài)
- 客戶端接收到數(shù)據(jù)包窍侧,檢測到已經(jīng)設(shè)置了
SYN
和ACK
標(biāo)志位县踢,就知道這是服務(wù)器發(fā)過來的確認(rèn)包。
- 客戶端會檢測確認(rèn)號
ACK
字段看它是否為1000+1伟件,如果是就說明連接建立成功硼啤。 - 客戶端會繼續(xù)組件數(shù)據(jù)包并設(shè)置ACK標(biāo)志位,表示客戶端正確接受了服務(wù)器發(fā)過來的確認(rèn)包斧账。
- 客戶端同時(shí)將剛才服務(wù)發(fā)送過來的數(shù)據(jù)包序號2000加1得到2001丙曙,并使用2001填充
ACK
確認(rèn)號字段。 - 客戶端將數(shù)據(jù)包發(fā)出并進(jìn)入
ESTABLISHED
狀態(tài)其骄,表示連接成功建立。
- 服務(wù)器接收到數(shù)據(jù)包扯旷,檢測到已經(jīng)設(shè)置了
ACK
標(biāo)志位拯爽,就知道是客戶端發(fā)過來的確認(rèn)包。
- 服務(wù)器會檢測
ACK
確認(rèn)號钧忽,看它是否等于2000+1毯炮,如果是就說明連接建立成功。 - 服務(wù)器則進(jìn)入
ESTABLISHED
狀態(tài)耸黑。
到此為止桃煎,客戶端和服務(wù)器都進(jìn)入了ESTABLISHED
狀態(tài),表示連接通道建立成功大刊,接下來就可以收發(fā)數(shù)據(jù)了为迈。
三次握手的關(guān)鍵是要確認(rèn)雙方收到了都收到了自己的數(shù)據(jù)包,這個(gè)目標(biāo)是通過ACK
確認(rèn)號字段實(shí)現(xiàn)的缺菌,計(jì)算機(jī)會記錄下自己發(fā)送的數(shù)據(jù)包序號SEQ
葫辐,待收到雙方的數(shù)據(jù)包后會檢測ACK確認(rèn)號,當(dāng)看到ACK = SEQ + 1成立后則說明對方正確的接收到了自己的數(shù)據(jù)包伴郁。
2. 數(shù)據(jù)傳輸
當(dāng)TCP建立連接后兩臺主機(jī)就可以相互傳輸數(shù)據(jù)了耿战。
例如:主機(jī)A分2次,也就是分2個(gè)數(shù)據(jù)包向主機(jī)B傳遞200字節(jié)數(shù)據(jù)的過程焊傅。
首先剂陡,主機(jī)A通過1個(gè)數(shù)據(jù)包發(fā)送100個(gè)字節(jié)的數(shù)據(jù)狈涮,數(shù)據(jù)包的SEQ
序列號設(shè)置為1200,主機(jī)B為了確認(rèn)這一點(diǎn)鸭栖,先主機(jī)A發(fā)送了ACK
確認(rèn)序列有效的數(shù)據(jù)包歌馍,并將ACK
號設(shè)置為1301。
為了保證數(shù)據(jù)能夠準(zhǔn)確到達(dá)纤泵,目標(biāo)機(jī)器在接收到數(shù)據(jù)包骆姐,包括SYN
包、FIN
包捏题、普通數(shù)據(jù)包等玻褪。必須立即回傳ACK
包,這樣發(fā)送方才能確認(rèn)數(shù)據(jù)傳輸成功公荧。
此時(shí)ACK
號為1301而不是1201带射,原因在于ACK
號的增量為傳輸?shù)臄?shù)據(jù)字節(jié)數(shù),假設(shè)每次ACK
號不添加傳輸?shù)淖止?jié)數(shù)循狰,這樣雖然可以確認(rèn)數(shù)據(jù)包的傳輸窟社,但是卻無法明確100字節(jié)全部正確傳輸還是丟失了一部分,比如說只傳輸了80字節(jié)绪钥,因此按ACK號 = SEQ號 + 傳遞的字節(jié)數(shù) + 1
的公式確認(rèn)ACK
號灿里。與三次握手協(xié)議相同的是,最后加1是為了告訴對方要傳輸?shù)腟EQ號程腹。
TCP套接字傳輸過程中發(fā)生錯(cuò)誤匣吊,在數(shù)據(jù)傳輸過程中數(shù)據(jù)丟失的情況。
例如:通過SEQ序號1301數(shù)據(jù)包向主機(jī)B傳遞了100字節(jié)的數(shù)據(jù)寸潦,但是中間卻發(fā)生了錯(cuò)誤色鸳,主機(jī)B未收到。經(jīng)過一段時(shí)間后见转,主機(jī)A仍未收到對于SEQ 1301的ACK確認(rèn)命雀,因此會嘗試重傳數(shù)據(jù)。為了完成數(shù)據(jù)包的重傳斩箫,TCP套接字每次發(fā)送數(shù)據(jù)包時(shí)都會啟動定時(shí)器吏砂。如果在一段時(shí)間內(nèi)沒有收到目標(biāo)機(jī)器傳回的ACK包,數(shù)據(jù)包會重傳校焦。當(dāng)然赊抖,也會有ACK包丟失的情況,一樣會重傳寨典。
重傳超時(shí)時(shí)間(RTO, Retransmission Time Out)
重傳超時(shí)時(shí)間RTO的值如果設(shè)置過大會導(dǎo)致不必要的等待氛雪,如果太小則會導(dǎo)致不必要的重傳,理論上最好是網(wǎng)絡(luò)往返時(shí)間RTT
(Round-Trip Time)時(shí)間耸成,但又受制于網(wǎng)絡(luò)距離和瞬態(tài)時(shí)延變化报亩,所以實(shí)際上使用自適應(yīng)的動態(tài)算法來確定超時(shí)時(shí)間浴鸿。
網(wǎng)絡(luò)往返時(shí)間RTT(Round-Trip Time)
表示從發(fā)送數(shù)據(jù)開始,到發(fā)送端接收到來自接收端的ACK
確認(rèn)包(接收端接收到數(shù)據(jù)后便會立即確認(rèn))弦追,總共經(jīng)歷的時(shí)延岳链。
重傳次數(shù)
TCP數(shù)據(jù)包重傳次數(shù)會根據(jù)系統(tǒng)設(shè)置的不同而有所區(qū)別,有些系統(tǒng)一個(gè)數(shù)據(jù)包只會被重傳三次劲件,如果重傳三次之后還未接收到該數(shù)據(jù)包的ACK確認(rèn)掸哑,就不再會嘗試重傳。但有些要求很高的業(yè)務(wù)系統(tǒng)零远,會不斷地重傳丟失的數(shù)據(jù)包苗分,以盡最大可能保證業(yè)務(wù)數(shù)據(jù)的正常交互。
最后需要說明的是牵辣,發(fā)送端只有在接收到對方的ACK
確認(rèn)包之后摔癣,才會清空輸出緩沖區(qū)中的數(shù)據(jù)。
3. 四次揮手
TCP四次揮手?jǐn)嚅_連接的過程中纬向,首先需要理解建立連接是非常重要的择浊,它是數(shù)據(jù)正確傳輸?shù)那疤帷嚅_連接同樣重要逾条,它讓計(jì)算機(jī)釋放不再使用的資源琢岩。如果連接不能正常斷開,不僅會造成數(shù)據(jù)傳輸錯(cuò)誤师脂,還會導(dǎo)致套接字不能關(guān)閉粘捎,持續(xù)占用資源,如果并發(fā)量比較高的話危彩,服務(wù)器壓力堪憂。
當(dāng)連接建立后泳桦,客戶端和服務(wù)器都處于ESTABLISHED
狀態(tài)汤徽,此時(shí)客戶端發(fā)起斷開連接的請求。
- 客戶端調(diào)用
close()
函數(shù)后灸撰,向服務(wù)器發(fā)送FIN
數(shù)據(jù)包谒府,并進(jìn)入FIN_WAIT_1
狀態(tài)。FIN
是Finish
的縮寫表示完成任務(wù)需要斷開連接浮毯。 - 服務(wù)器接收到數(shù)據(jù)包后完疫,檢測到設(shè)置了
FIN
標(biāo)志位,知道要斷開連接债蓝。于是向客戶端發(fā)送確認(rèn)包并進(jìn)入CLOSE_WAIT
狀態(tài)壳鹤。需要注意的是,服務(wù)器接收到請求后并不是立即斷開連接饰迹,而是先向客戶端發(fā)送確認(rèn)包芳誓,告訴它我知道了余舶,我需要準(zhǔn)備一下才能斷開連接。 - 客戶端接收到確認(rèn)包后進(jìn)入
FIN_WAIT_2
狀態(tài)锹淌,并等待服務(wù)器準(zhǔn)備完畢后再次發(fā)送數(shù)據(jù)包匿值。 - 客戶端等待片刻后,服務(wù)器準(zhǔn)備完畢赂摆,可以斷開連接挟憔。于是服務(wù)器再主動向客戶端發(fā)送
FIN
包,并告訴它我準(zhǔn)備好了烟号,斷開連接吧绊谭。然后進(jìn)入LAST_ACK
狀態(tài)。 - 客戶端接收到服務(wù)器的
FIN
包后再次向服務(wù)器發(fā)送ACK
包褥符,并告訴它你斷開連接吧龙誊,然后進(jìn)入TIME_WAIT
狀態(tài)。 - 服務(wù)器接收到客戶端的
ACK
包后立即斷開連接并關(guān)閉套接字進(jìn)入CLOSED
狀態(tài)喷楣。
TIME_WAIT
狀態(tài)
客戶端最后一次發(fā)送ACK包后進(jìn)入TIME_WAIT
狀態(tài)而不是直接進(jìn)入CLOSED
狀態(tài)關(guān)閉連接趟大,這是為什么呢?
TCP是面向連接的傳輸方式铣焊,必須保證數(shù)據(jù)能夠正確地到達(dá)目標(biāo)機(jī)器逊朽,不能丟失或出錯(cuò),但網(wǎng)絡(luò)是不穩(wěn)定的曲伊,隨時(shí)可能會損壞數(shù)據(jù)叽讳,所以機(jī)器A每次先機(jī)器B發(fā)送數(shù)據(jù)包后,都會要求機(jī)器B確認(rèn)回傳ACK包坟募,告訴機(jī)器A我收到了岛蚤,這樣機(jī)器A才能知道數(shù)據(jù)傳送成功了。如果機(jī)器B沒有回傳ACK包懈糯,機(jī)器A會重新發(fā)送直到機(jī)器B回傳ACK包涤妒。
客戶端最后一次向服務(wù)器回傳ACK包時(shí),有可能會因?yàn)榫W(wǎng)絡(luò)問題導(dǎo)致服務(wù)器接收不到赚哗,服務(wù)器會再次發(fā)送FIN
包她紫,如果此時(shí)客戶端完全關(guān)閉了連接,那么服務(wù)器無論如何也接收不到ACK
包屿储,所以客戶端需要等待片刻贿讹,確認(rèn)對方接收到ACK
包之后才能進(jìn)入CLOSED
狀態(tài)。那么需要等待多久呢够掠?
數(shù)據(jù)在網(wǎng)絡(luò)中是有生存時(shí)間的民褂,超過這個(gè)時(shí)間還未到達(dá)目標(biāo)主機(jī)就會被丟棄,并通知源主機(jī)。這成為報(bào)文最大生存時(shí)間MSL, Maximum Segment Lifetime
助赞。TIME_WAIT
要等待2MSL
才會進(jìn)入CLOSED
狀態(tài)买羞。ACK
包到達(dá)服務(wù)器需要MSL
時(shí)間,服務(wù)器重傳FIN
也需要MSL
時(shí)間雹食,2MSL
是數(shù)據(jù)包往返的最大時(shí)間畜普,如果2MSL
后還未收到服務(wù)器重傳的FIN
包就說明服務(wù)器已經(jīng)收到了ACK
包。
TCP狀態(tài)轉(zhuǎn)換
TCP三次握手建立連接通道和四次揮手?jǐn)嚅_連接通道過程中狀態(tài)變遷以及數(shù)據(jù)傳輸?shù)倪^程群叶,根據(jù)TCP狀態(tài)轉(zhuǎn)換圖可分為上下兩段:上半部分是TCP三次握手過程的狀態(tài)變遷吃挑,下半部分是TCP四次揮手過程的狀態(tài)變遷。
1. TCP三次握手過程的狀態(tài)變遷
-
CLOSED
起始點(diǎn)
在超時(shí)或連接關(guān)閉時(shí)進(jìn)入此狀態(tài)街立,這并不是一個(gè)真正的狀態(tài)舶衬,而是狀態(tài)圖的假想七點(diǎn)和重點(diǎn)。 -
LISTEN
服務(wù)器等待連接的狀態(tài)
服務(wù)器經(jīng)過socket
赎离、bind
逛犹、listen
函數(shù)之后進(jìn)入此狀態(tài),開始監(jiān)聽客戶端發(fā)過來的連接請求梁剔,又稱為應(yīng)用程序被動打開虽画,等待客戶端連接請求。 -
SYN_SENT
第一次握手階段客戶端發(fā)起連接
客戶端調(diào)用connect
發(fā)送SYN
給服務(wù)器荣病,然后進(jìn)入SYN_SENT
狀態(tài)等待服務(wù)器確定(三次握手中的第二個(gè)報(bào)文)码撰。如果服務(wù)器不能連接則之直接進(jìn)入CLOSED
狀態(tài)。 -
SYN_RCVD
第二次握手發(fā)生階段
SYN_RCVD
階段與SYN_SEND
階段對應(yīng)个盆,這里是服務(wù)器接收到了客戶端的SYN
脖岛,此時(shí)服務(wù)器由LISTEN
狀態(tài)進(jìn)入SYN_RCVD
狀態(tài),同時(shí)服務(wù)器回應(yīng)一個(gè)ACK
然后再發(fā)送一個(gè)SYN
即SYN+ACK
給客戶端颊亮。狀態(tài)圖中還描述了這樣一種情況柴梆,當(dāng)客戶端在發(fā)送SYN
的同時(shí)也接收到服務(wù)器的SYN
請求,即兩個(gè)同時(shí)發(fā)起連接請求终惑,那么客戶端就會從SYN_SEND
轉(zhuǎn)為SYN_REVD
狀態(tài)轩性。 -
ESTABLISHED
第三次握手發(fā)生階段
客戶端接收到服務(wù)器的ACK
包(ACK
、SYN
)之后 狠鸳,會發(fā)送一個(gè)ACK
確認(rèn)包,客戶端進(jìn)入ESTABLISHED
狀態(tài)悯嗓,表明客戶端端這邊已經(jīng)準(zhǔn)備好件舵,但TCP需要兩端都在準(zhǔn)備好才可以進(jìn)行數(shù)據(jù)傳輸。服務(wù)器接收到客戶端的ACK
之后會從SYN_RCVD
狀態(tài)轉(zhuǎn)移到ESTABLISHED
狀態(tài)脯厨,表明服務(wù)器也準(zhǔn)備好進(jìn)入ESTABLISHED
铅祸,也就是說是一個(gè)數(shù)據(jù)傳送狀態(tài)。
以上就是TCP三次握手過程的狀態(tài)變遷,結(jié)合三次握手過程圖临梗,從報(bào)文的角度看狀態(tài)變遷:
-
SYN_SENT
狀態(tài)表示客戶端已經(jīng)發(fā)送了SYN
報(bào)文 -
SYN_RCVD
狀態(tài)表示服務(wù)器已經(jīng)接收到了SYN
報(bào)文
2. TCP四次揮手過程的狀態(tài)變遷
-
FIN_WAIT_1
第一次揮手
主動關(guān)閉的一方(執(zhí)行主動關(guān)閉的一方既可以是客戶端也可以是服務(wù)器涡扼,這里以客戶端執(zhí)行主動關(guān)閉為例),終止連接時(shí)發(fā)送FIN
給對方盟庞,然后等待對方返回ACK
吃沪。調(diào)用close()
方法第一揮手就進(jìn)入此狀態(tài)。 -
CLOSE_WAIT
接收到FIN
之后什猖,被動關(guān)閉一方進(jìn)入此狀態(tài)票彪。
具體動作是接收到FIN
同時(shí)發(fā)送ACK
,之所以叫CLOSE_WAIT
可以理解為被動關(guān)閉的一方此時(shí)正在等待上層應(yīng)用程序發(fā)出關(guān)閉連接指令不狮。TCP關(guān)閉是全雙工過程降铸,這里客戶端執(zhí)行了主動關(guān)閉,被動方服務(wù)器接收到FIN
后也需要調(diào)用close()
方法進(jìn)行關(guān)閉摇零,這個(gè)CLOSE_WAIT
就是處于這個(gè)狀態(tài)推掸,等待發(fā)送FIN
,發(fā)送FIN
后則進(jìn)入LAST_ACK
狀態(tài)驻仅。 -
FIN_WAIT_2
主動關(guān)閉方(這里是客戶端)先執(zhí)行主動關(guān)閉發(fā)送FIN
谅畅,然后接收到被動關(guān)閉方返回的ACK
后進(jìn)入此狀態(tài)。 -
LAST_ACK
被動關(guān)閉方(這個(gè)是服務(wù)器)發(fā)起關(guān)閉請求雾家,由CLOSE_WAIT
進(jìn)入此狀態(tài)铃彰,具體動作是發(fā)送FIN
給對方,同時(shí)在接收到ACK
時(shí)進(jìn)入CLOSED
狀態(tài)芯咧。 -
CLOSING
雙方同時(shí)發(fā)送關(guān)閉請求時(shí)牙捉,即主動關(guān)閉方發(fā)送FIN
等待被動關(guān)閉方返回ACK
,同時(shí)被動關(guān)閉方也發(fā)送了FIN
敬飒,主動關(guān)閉方接收到了FIN
之后發(fā)送ACK
給被動方邪铲,主動關(guān)閉方由FIN_WAIT_1
進(jìn)入此狀態(tài)等待被動關(guān)閉方返回ACK
。 -
TIME_WAIT
從狀態(tài)變遷圖中看到无拗,四次揮手操作最后都會經(jīng)過這樣一個(gè)狀態(tài)TIME_WAIT
然后進(jìn)入CLOSED
狀態(tài)带到,共有三個(gè)狀態(tài)會進(jìn)入該狀態(tài)TIME_WAIT
。
(1) 由CLOSING
進(jìn)入TIME_WAIT
同時(shí)發(fā)起關(guān)閉情況下英染,當(dāng)主動關(guān)閉方接收到ACK
后進(jìn)入TIME_WAIT
狀態(tài)揽惹,實(shí)際上這里同時(shí)發(fā)生的是這樣的情況:客戶端發(fā)起關(guān)閉請求,發(fā)送FIN
之后等待服務(wù)器回應(yīng)ACK
四康,但此時(shí)服務(wù)器同時(shí)也發(fā)起關(guān)閉請求搪搏,也發(fā)送了FIN
,并且被客戶端先于ACK
接收到闪金。
(2)由FIN_WAIT_1
進(jìn)入TIME_WAIT
當(dāng)發(fā)起關(guān)閉后發(fā)送了FIN
等待ACK
的時(shí)候疯溺,正好被動關(guān)閉方(服務(wù)器)也發(fā)起關(guān)閉請求论颅,發(fā)送了FIN
,此時(shí)客戶端接收到了先前ACK
也接收到了對方的FIN
然后發(fā)送ACK
(給對方FIN
的回應(yīng))囱嫩,與CLOSING
進(jìn)入的狀態(tài)不同的是接收到FIN
和ACK
的先后順序恃疯。
(3)由FIN_WAIT_2
進(jìn)入TIME_WAIT
這是不同時(shí)的情況,主動方在完成自身發(fā)起的主動關(guān)閉請求后墨闲,接收到對方發(fā)送過來的FIN
然后回應(yīng)ACK
今妄。
從上面進(jìn)入TIME_WAIT
狀態(tài)的三個(gè)狀態(tài)動作來看,都是主動方最后回一個(gè)ACK
损俭,CLOSING
實(shí)際上前面的哪個(gè)FIN_WAIT_1
狀態(tài)就已經(jīng)回應(yīng)了ACK蛙奖。
先考慮這樣一種情況:加入這個(gè)最后 回應(yīng)的ACK
丟失了,也就是服務(wù)器接收不到這個(gè)ACK
杆兵,那么服務(wù)器將繼續(xù)發(fā)送它最終的那個(gè)FIN
雁仲,因此客戶端必須維持狀態(tài)信息TIME_WAIT
允許它重發(fā)最后的那個(gè)ACK
。如果沒有這個(gè)TIME_WAIT
狀態(tài)琐脏,客戶端處于CLOSED
狀態(tài)(CLOSED
狀態(tài)實(shí)際并不存在只是為了方便描述假想的)攒砖,那么客戶端將響應(yīng)RST
,服務(wù)器接收到后會將該RST
分節(jié)解釋成一個(gè)錯(cuò)誤日裙,也就不能實(shí)現(xiàn)最后的全雙工關(guān)閉了(可能是主動方單方的關(guān)閉)吹艇。所以要實(shí)現(xiàn)TCP全雙工連接的正常終止(兩方都關(guān)閉連接),必須處理終止過程中四個(gè)分節(jié)任何一個(gè)分節(jié)的丟失情況昂拂,那么主動關(guān)閉連接的主動端必須維持TIME_WAIT
狀態(tài)受神,最后一個(gè)回應(yīng)ACK
的是主動執(zhí)行關(guān)閉的那一端。從變遷圖可以看出格侯,如果沒有TIME_WAIT
狀態(tài)將沒有任何機(jī)制來保證最后一個(gè)ACK
能夠正常到達(dá)鼻听。前面的FIN
、ACK
正常到達(dá)均由相應(yīng)的狀態(tài)對應(yīng)联四。
這里還有一種情況:如果目前的通信雙方都已經(jīng)調(diào)用了close()
撑碴,都到達(dá)了CLOSED
狀態(tài),沒有TIME_WAIT
狀態(tài)時(shí)朝墩,會出現(xiàn)這樣一種情況醉拓,現(xiàn)在有一個(gè)新的連接被建立起來,使用的IP地址和端口和這個(gè)先前到達(dá)了CLOSED
狀態(tài)的完全相同收苏,假定原來的連接中還有是數(shù)據(jù)報(bào)殘存在網(wǎng)絡(luò)之中亿卤,這樣新的連接建立之后創(chuàng)數(shù)的數(shù)據(jù)極有可能就是原先的連接的數(shù)據(jù)報(bào),為了防止這一點(diǎn)鹿霸,TCP不允許從處于TIME_WAIT
狀態(tài)的Socket建立一個(gè)連接排吴。處于TIME_WAIT
狀態(tài)的Socket在等待了兩倍的MSL
時(shí)間后將會轉(zhuǎn)變?yōu)?code>CLOSED狀態(tài)。這里TIME_WAIT
狀態(tài)維持的時(shí)間是2MSL
(MSL
是任何IP數(shù)據(jù)報(bào)能夠在Internet中存活的最長時(shí)間)杜跷,足以讓這兩個(gè)方向上的數(shù)據(jù)包被丟棄(最長是2MSL)。通過實(shí)施這個(gè)規(guī)則芥永,九能保證每成功建立一個(gè)TCP連接時(shí)芥颈,來自該連接先前化生的老的重復(fù)分組都已經(jīng)在網(wǎng)絡(luò)中消逝了。
綜合來看止状,TIME_WAIT
存在的理由是
- 可靠地實(shí)現(xiàn)TCP全雙工連接的終止
- 允許老的重復(fù)分節(jié)(數(shù)據(jù)報(bào))在網(wǎng)絡(luò)中消逝
Socket
在UNIX/Linux系統(tǒng)中為了統(tǒng)一對各種硬件的操作以簡化接口淑趾,不同的硬件設(shè)備都被視為一個(gè)文件阳仔。對這些文件的操作等同于對磁盤上普通文件的操作。所以扣泊,UNIX/Linux中一切都是文件近范。
為了表示和區(qū)分已經(jīng)打開的文件,UNIX/Linux會給每個(gè)文件分配一個(gè)整型ID延蟹,這個(gè)整數(shù)的ID被稱為文件描述符(fd, File Descriptor)评矩。例如
- 通常使用0表示標(biāo)準(zhǔn)輸入文件
stdin
,它對應(yīng)的硬件設(shè)備是鍵盤阱飘。 - 通常使用1表示標(biāo)準(zhǔn)輸出文件
stdout
斥杜,它對應(yīng)的硬件設(shè)備是顯示器。
UNIX/Linux程序在執(zhí)行任何形式的I/O操作時(shí)都是在讀取或?qū)懭胍粋€(gè)文件描述符fd
沥匈,一個(gè)文件描述符fd
只是一個(gè)和打開的文件相關(guān)聯(lián)的整數(shù)蔗喂,它的背后可能是一個(gè)硬盤上的普通文件、FIFO高帖、管道缰儿、終端、鍵盤散址、顯示器乖阵,甚至是一個(gè)網(wǎng)絡(luò)連接。
Socket是應(yīng)用層與TCP/IP協(xié)議簇通信的中間軟件抽象層爪飘,是一組接口义起。
TCP/IP只是 一個(gè)協(xié)議棧,就像操作系統(tǒng)的運(yùn)行機(jī)制一樣师崎,它必須要有具體實(shí)現(xiàn)默终,同時(shí)還要提供對外的操作接口。就像操作系統(tǒng)會提供標(biāo)準(zhǔn)的編程接口犁罩,比如Win32編程接口有一樣齐蔽,TCP/IP也必須對外提供編程接口,這就是Socket編程接口床估。
在Socket編程接口中設(shè)計(jì)者提出了一個(gè)很重要的概念含滴,就是Socket。這個(gè)Socket和文件句柄很相似丐巫,實(shí)際上在BSD系統(tǒng)中就是跟文件句柄一樣存放在進(jìn)程句柄表中谈况。這個(gè)Socket其實(shí)就是一個(gè)序號勺美,表示在句柄表中的位置。操作系統(tǒng)中句柄分很多種碑韵,比如文件句柄赡茸、窗口句柄等。這些句柄其實(shí)代表系統(tǒng)中某些特定的對象祝闻,用于在各種函數(shù)中作為參數(shù)傳入占卧,以對特定的對象進(jìn)行操作 - 這其實(shí)是C語言的問題。在C++中這些句柄其實(shí)就是this
對象指針联喘。
Socket跟TCP/IP并沒有必然的聯(lián)系华蜒,Socket編程接口在設(shè)計(jì)的時(shí)候希望能夠適應(yīng)其他的網(wǎng)絡(luò)協(xié)議,所以Socket的出現(xiàn)只是為了更加方便地使用TCP/IP協(xié)議棧而已豁遭,通過對TCP/IP進(jìn)行抽象形成了幾個(gè)最基本的函數(shù)接口 叭喜,如create
、listen
堤框、accept
域滥、connect
、read
蜈抓、write
等启绰。
服務(wù)器先初始化Socket,然后與指定地址的端口進(jìn)行綁定沟使,接著對端口進(jìn)行監(jiān)聽委可,最后調(diào)用accept
阻塞,等待客戶端連接腊嗡。此時(shí)如果有客戶端初始化一個(gè)Socket后連接服務(wù)器着倾,如果連接成功,客戶端與服務(wù)器的連接就會建立成功燕少】ㄕ撸客戶端發(fā)送數(shù)據(jù)請求 ,服務(wù)器接收請求并處理客们,然后將回應(yīng)數(shù)據(jù)發(fā)送給客戶端崇决,客戶端讀取數(shù)據(jù),最后關(guān)閉連接底挫,完成一次交互過程恒傻。
Socket類型
Socket套接字有很多種類型,比如DARPA Internet地址(Internet套接字)建邓、本地節(jié)點(diǎn)的路徑名(UNIX套接字)盈厘、CCITT X.25地址(X.25套接字)等。這里我們所討論的是Internet套接字官边,它是最具代表也是最經(jīng)典最常用的沸手。
根據(jù)傳輸方式外遇,可以將Internet套接字分為兩種類型:流格式套接字SOCK_STREAM
、數(shù)據(jù)報(bào)格式套接字SOCK_DGRAM
流格式套接字SOCK_STREAM
流格式套接字Stream Sockets
也叫做面向連接的套接字契吉,代碼中使用SOCK_STREAM
表示臀规。流格式套接字是一個(gè)可靠地、雙向的通信數(shù)據(jù)流栅隐,數(shù)據(jù)可以準(zhǔn)確無誤的到達(dá)另一臺計(jì)算機(jī),如果損壞或丟失可以重新發(fā)送玩徊。流格式套接字有自己的糾錯(cuò)機(jī)制租悄。
流格式套接字具有以下幾個(gè)特征:數(shù)據(jù)在傳輸過程中不會消失、數(shù)據(jù)是按順序傳輸?shù)亩鞲ぁ?shù)據(jù)的發(fā)送和接收不是同步的(不存在數(shù)據(jù)邊界)
可以將流格式套接字比喻成一條傳送帶泣棋,只要傳送帶本身沒有問題(不會斷網(wǎng)),就能保證數(shù)據(jù)不丟失畔塔。同時(shí)較晚傳送的數(shù)據(jù)不會先到達(dá)潭辈,較早傳送的數(shù)據(jù)不會晚到達(dá),這就保證了數(shù)據(jù)是按順序傳遞的澈吨。
為什么流格式套接字可以達(dá)到高質(zhì)量的數(shù)據(jù)傳輸呢把敢?
這是因?yàn)樗褂昧薚CP傳輸控制協(xié)議,TCP協(xié)議會控制你的數(shù)據(jù)按照順序到達(dá)并且保證沒有錯(cuò)誤谅辣。對于TCP/IP協(xié)議族而言修赞,TCP是用來確保數(shù)據(jù)的正確性,IP網(wǎng)絡(luò)協(xié)議是用來控制數(shù)據(jù)如何從源頭到達(dá)目的地也就是常說的路由桑阶。
TCP是如何解決數(shù)據(jù)收發(fā)不同步的問題的呢柏副?
假設(shè)傳送帶傳送的是水果,接收者需要湊齊100個(gè)后才能裝箱蚣录,但是傳送帶可能會把100個(gè)水果分批傳送割择,比如第一批傳送20個(gè),第二批50個(gè)萎河,第三批30個(gè)荔泳。接收者不需要和傳送帶保持同步,只需要根據(jù)自己的節(jié)奏來裝箱即可公壤,不管傳送帶傳送多少批换可,也不用沒到一批就裝箱依次,可以等到湊夠100個(gè)在裝箱厦幅。
流格式套接字的內(nèi)部有一個(gè)字符數(shù)組的緩沖區(qū)沾鳄,通過Socket傳輸?shù)臄?shù)據(jù)將保存在這個(gè)緩沖區(qū)中。接收端在收到數(shù)據(jù)后并不一定立即讀取确憨,只要數(shù)據(jù)不超過緩沖區(qū)的容量译荞,接收端有可能在緩沖區(qū)被填滿以后一次性地讀取瓤的,也可能分成好幾次讀取。也就是說吞歼,不管數(shù)據(jù)分幾次傳送圈膏,接收端只需要根據(jù)自己的要求讀取,不用非得在數(shù)據(jù)到達(dá)時(shí)立即讀取篙骡。傳送段有自己的節(jié)奏稽坤,接收端也有自己的節(jié)奏,它們是不一致的糯俗。
流格式套接字有什么實(shí)際的應(yīng)用場景嗎尿褪?瀏覽器使用的HTTP協(xié)議是基于面向連接的套接字,因此必須確保數(shù)據(jù)準(zhǔn)確無誤得湘,否則加載的HTML將無法解析杖玲。
數(shù)據(jù)報(bào)格式套接字SOCK_DGRAM
數(shù)據(jù)報(bào)格式套接字(Datagram Sockets)也叫做無連接的套接字,在代碼中使用SOCK_DGRAM
表示淘正。
由于計(jì)算機(jī)只管傳輸數(shù)據(jù)不做數(shù)據(jù)校驗(yàn)摆马,如果數(shù)據(jù)在傳輸過程中損壞,或者沒有達(dá)到另一臺計(jì)算機(jī)鸿吆,是沒有辦法補(bǔ)救的囤采。也就是說,數(shù)據(jù)錯(cuò)誤就錯(cuò)了無法重傳惩淳。
由于數(shù)據(jù)報(bào)套接字所做的校驗(yàn)工作少所以在傳輸效率方面比流式套接字效率要高斑唬。
可以將數(shù)據(jù)報(bào)格式套接字比率成高速移動的摩托車快遞,它具有以下特征:強(qiáng)調(diào)快速傳輸而非傳輸順序黎泣、傳輸?shù)臄?shù)據(jù)可能丟失也可能損毀恕刘、限制每次傳輸數(shù)據(jù)的大小、數(shù)據(jù)的發(fā)生和接收是同步的(存在數(shù)據(jù)邊界)抒倚。
總所周知褐着,速度是快遞行業(yè)的生命,使用摩托車發(fā)往同一地點(diǎn)的兩件包裹無需保證順序托呕,只要以最快的速度交付給客戶即可含蓉。這種方式存在損壞或丟失的風(fēng)險(xiǎn),而且包裹大小有一定的限制项郊。因此馅扣,想要傳遞大量包裹,就得分批發(fā)送着降。另外差油,使用兩輛摩托車分別發(fā)送兩件包裹,接收者也需要分兩次接收,所以“數(shù)據(jù)的發(fā)送和接收是同步的”蓄喇,換句話說接收次數(shù)應(yīng)該和發(fā)送次數(shù)相同发侵。總之?dāng)?shù)據(jù)報(bào)套接字是一種不可靠的妆偏、不按順序傳遞的刃鳄、以追求速度為目的的套接字。
數(shù)據(jù)報(bào)套接字使用IP協(xié)議作路由钱骂,但不使用TCP叔锐,而是使用UDP用戶傳輸協(xié)議。例如QQ視頻聊天和語音聊天就是使用數(shù)據(jù)報(bào)套接字见秽,因?yàn)槭紫纫WC通信的效率掌腰,盡量減少延遲,而數(shù)據(jù)的正確性是次要的张吉,即使丟失很小一部分的數(shù)據(jù),視頻和音頻也可以正常解析催植,最多出現(xiàn)噪點(diǎn)或雜音肮蛹,不會對通信質(zhì)量有實(shí)質(zhì)上的影響。
面向連接套接字 VS 無連接套接字
流式套接字SOCK_STREAM
是基于TCP協(xié)議面向連接的套接字创南,數(shù)據(jù)報(bào)格式套接字SOCK_DGRAM
是基于UDP協(xié)議無連接的套接字伦忠。面向連接是可靠的通信,無連接的通信是不可靠的通信稿辙,實(shí)際情況是這樣的嗎昆码?
首先,不管是哪一種數(shù)據(jù)傳輸方式邻储,都必須通過整個(gè)Internet網(wǎng)絡(luò)的物理線路將數(shù)據(jù)傳輸過去赋咽,從這個(gè)層面 上來看,所有的Socket都是有物理連接的吨娜,那么為什么還會有無連接的Socket呢脓匿?
從字面上看,面向連接好像有一條管道宦赠,它連接發(fā)送和接收端陪毡,數(shù)據(jù)報(bào)都是通過這條管道來傳輸,當(dāng)然勾扭,兩臺計(jì)算機(jī)在通信之前必須先搭建好管道毡琉。而無連接好像是無頭蒼蠅亂撞,數(shù)據(jù)報(bào)從發(fā)送端到接收端并沒有固定的路線妙色,愛怎么走就怎么走桅滋,只要能到達(dá)就行。每個(gè)數(shù)據(jù)包都比較自私身辨,不會和別人分享自己的線路虱歪,但是最終都能殊途同歸蜂绎,到達(dá)接收端。
無連接的套接字
對于無連接的套接字笋鄙,每個(gè)數(shù)據(jù)報(bào)可以選擇不同的路徑师枣。每個(gè)數(shù)據(jù)包之間都是獨(dú)立的,各走各的的路萧落,誰也不影響誰践美,除了迷路或發(fā)生以外的數(shù)據(jù)報(bào),最后都到達(dá)目的地找岖。只是到達(dá)的順序是不確定的陨倡。對于無連接的套接字,數(shù)據(jù)包在傳輸過程中會發(fā)生各種不測许布,也會發(fā)生各種奇跡兴革。無連接套接字遵循的是“盡最大努力交付”的原則,就是盡力而為蜜唾,實(shí)在做不到了也沒有辦法杂曲,無連接套接字提供的是沒有質(zhì)量保證的服務(wù)。
面向連接的套接字
面向連接的套接字在正式通信之前需要先確定一條路徑袁余,沒有特殊情況的話擎勘,以后就會固定地使用這條路線來傳遞數(shù)據(jù)包。當(dāng)然颖榜,如果路徑被破壞的話棚饵,比如某個(gè)路由器斷電了,那么會重新建立路線掩完。固定線路是由路由器維護(hù)的噪漾,路徑上所有的路由器都要存儲路徑的信息,實(shí)際上只需要存儲上游和下游兩個(gè)路由器的位置即可且蓬。所以路由器是有開銷的怪与。
面向連接的套接字建立的固定路徑,又被稱為虛電路缅疟,也就是一條虛擬的通信電路分别。為了保證數(shù)據(jù)包準(zhǔn)確、順序地到達(dá)存淫,發(fā)送端在發(fā)送數(shù)據(jù)包以后耘斩,必須得到接收端的確認(rèn)后才會發(fā)送下一個(gè)數(shù)據(jù)包。如果數(shù)據(jù)包發(fā)送出去后一端時(shí)間仍沒有得到接收端的回應(yīng)桅咆,發(fā)送端會重新再發(fā)送一次括授,直到得到接收端的回應(yīng)。這樣一來,發(fā)送端發(fā)送的數(shù)據(jù)包都能到達(dá)接收端荚虚,并且是按照順序到達(dá)的薛夜。
發(fā)送端發(fā)送一個(gè)數(shù)據(jù)包,是如何得到接收端的確認(rèn)呢版述?
這個(gè)很簡單梯澜,為每個(gè)數(shù)據(jù)包分配一個(gè)ID,接收端接收到數(shù)據(jù)包以后渴析,再給發(fā)送端返回一個(gè)數(shù)據(jù)包晚伙,告訴發(fā)送端接收到的ID即可。
面向連接的套接字比無連接的套接字多出很多數(shù)據(jù)包俭茧,因?yàn)榘l(fā)送端每發(fā)送一個(gè)數(shù)據(jù)包咆疗,接收端會要返回一個(gè)數(shù)據(jù)包,此外建立連接和斷開連接的過程也會傳遞很多數(shù)據(jù)包母债。因此不但是數(shù)量多了午磁,每個(gè)數(shù)據(jù)包也會變大。除了源端口和目標(biāo)端口毡们,面向連接的套接字還包括序號迅皇、確認(rèn)信息、數(shù)據(jù)偏移漏隐、控制標(biāo)志(如URG、ACK奴迅、PSH青责、RST、SYN取具、FIN)脖隶、窗口、校驗(yàn)和暇检、緊急指針产阱、選項(xiàng)等信息。無連接的套接字則只包含長度和校驗(yàn)信息块仆。
總的來說构蹬,兩種套接字的傳輸方式各有優(yōu)缺
- 無連接套接字傳輸效率高,但不可靠有丟失數(shù)據(jù)包悔据、搗亂數(shù)據(jù)的風(fēng)險(xiǎn)庄敛。
- 有連接套接字非常可靠萬無一失科汗,但傳輸效率低消耗資源多藻烤。
Socket緩沖區(qū)
每個(gè)Socket被創(chuàng)建后都會分配兩個(gè)I/O緩沖區(qū):輸入緩沖區(qū)、輸出緩沖區(qū),I/O緩沖區(qū)的默認(rèn)大小一般是8K怖亭。
I/O緩沖區(qū)的特性是:I/O緩沖在每個(gè)TCP套接字中單獨(dú)存在涎显、I/O緩沖區(qū)在創(chuàng)建套接字時(shí)自動生成、即使關(guān)閉套接字也會繼續(xù)傳送輸出緩沖區(qū)中遺留的數(shù)據(jù)兴猩、關(guān)閉套接字將會丟失輸入緩沖區(qū)中的數(shù)據(jù)
TCP協(xié)議獨(dú)立于write()
或send()
函數(shù)期吓,數(shù)據(jù)有可能剛被寫入緩沖區(qū)就發(fā)送到網(wǎng)絡(luò),也有可能在緩沖區(qū)中 不斷積累峭跳,多次寫入的數(shù)據(jù)被一次性發(fā)送到網(wǎng)絡(luò)膘婶,這取決于當(dāng)時(shí)的網(wǎng)絡(luò)情況以及當(dāng)前線程是否 空閑等諸多因素,這不是由程序員能控制的蛀醉。read()
或recv()
函數(shù)也是如何悬襟,也從輸入緩沖區(qū)中讀取數(shù)據(jù),而不是直接從網(wǎng)絡(luò)中讀取數(shù)據(jù)拯刁。
write()
或send()
函數(shù)并不會立即向網(wǎng)絡(luò)中傳輸數(shù)據(jù)脊岳,而是先將數(shù)據(jù)寫入緩沖區(qū)中,再由TCP協(xié)議將數(shù)據(jù)從緩沖區(qū)發(fā)送到目標(biāo)機(jī)器垛玻。一旦將數(shù)據(jù)寫入到緩沖區(qū)割捅,函數(shù)就可以成功返回,不管它們有沒有到達(dá)目標(biāo)機(jī)器帚桩,也不管它們何時(shí)被發(fā)送到網(wǎng)絡(luò)亿驾,這些都是TCP協(xié)議負(fù)責(zé)的事情。
Socket的阻塞模式
所謂阻塞就是上一步動作還沒有完成账嚎,下一步動作將暫停莫瞬,直到上一步動作完成后才能繼續(xù),以保持同步性郭蕉。TCP套接字默認(rèn)情況下是阻塞模式的疼邀。
對于TCP套接字在默認(rèn)情況下,當(dāng)使用write()
或send()
函數(shù)發(fā)送數(shù)據(jù)時(shí)
- 首先會檢查緩沖區(qū)召锈,如果緩沖區(qū)的可用空間長度小于要發(fā)送的數(shù)據(jù)長度旁振,那么
write()
或send()
函數(shù)將會被阻塞也就時(shí)暫停執(zhí)行,直到緩沖區(qū)中的數(shù)據(jù)被發(fā)送到目標(biāo)機(jī)器涨岁,騰出足夠的空間才喚醒write()
或send()
函數(shù)繼續(xù)寫入數(shù)據(jù)拐袜。 - 如果TCP協(xié)議正在向網(wǎng)絡(luò)中發(fā)送數(shù)據(jù),那么輸出緩沖區(qū)會被鎖定梢薪,不允許寫入阻肿。
write()
或send()
函數(shù)也會被阻塞,直到數(shù)據(jù)發(fā)送完畢后緩沖區(qū)解鎖沮尿,write()
或send()
才會被喚醒丛塌。 - 如果寫入的數(shù)據(jù)大于緩沖區(qū)的最大長度將會分批寫入
- 直到所有數(shù)據(jù)被寫入緩沖區(qū)
write()
或send()
函數(shù)才會返回
對于TCP套接字在默認(rèn)情況下较解,當(dāng)使用read()
或recv()
函數(shù)讀取數(shù)據(jù)時(shí)
- 首先會檢查緩沖區(qū),如果緩沖區(qū)中有數(shù)據(jù)則直接讀取赴邻,否則函數(shù)會被阻塞直到網(wǎng)絡(luò)上有數(shù)據(jù)到來印衔。
- 如果要讀取的數(shù)據(jù)長度小于緩沖區(qū)中的數(shù)據(jù)長度,那么就不能一次性將緩沖區(qū)中的所有數(shù)據(jù)讀出姥敛,剩余數(shù)據(jù)將不斷積累直到有
read()
或recv()
函數(shù)再次讀取奸焙。 - 直到讀取到數(shù)據(jù)后
read()
或recv()
函數(shù)才會返回否則會一致被阻塞
UNIX BSD Socket
使用TCP/IP協(xié)議的應(yīng)用程序通常采用應(yīng)用編程接口UNIX BSD的Socket來實(shí)現(xiàn)網(wǎng)絡(luò)進(jìn)程之間的通信
在討論網(wǎng)絡(luò)中進(jìn)程通信之前需要解決的問題是如何唯一標(biāo)識一個(gè)進(jìn)程呢多糠?在本地可以通過進(jìn)程PID來唯一標(biāo)識一個(gè)進(jìn)程础拨,但在網(wǎng)絡(luò)中是行不通的。TCP/IP協(xié)議簇已經(jīng)幫助我們解決了這個(gè)問題巩那,網(wǎng)絡(luò)層的“IP地址”可以唯一標(biāo)識網(wǎng)絡(luò)中的主機(jī)墨榄,傳輸層的“協(xié)議+端口”可以唯一標(biāo)識主機(jī)中的應(yīng)用程序(進(jìn)程)玄糟。這樣利用“三元組 = IP地址 + 協(xié)議 + 端口”,就可以標(biāo)識網(wǎng)絡(luò)中的進(jìn)程袄秩,網(wǎng)絡(luò)中的進(jìn)程通信就可以利用這個(gè)三元組標(biāo)志與其它進(jìn)程進(jìn)行交互阵翎。
網(wǎng)絡(luò)中的進(jìn)程是通過Socket來通信的,那什么是Socket呢之剧,Socket其起源于UNIX郭卫,UNIX/Linux基本哲學(xué)之一是“一一切皆文件”,文件都可以使用“打開-讀寫-關(guān)閉”模式來操作背稼。而Socket其實(shí)是一種特殊的文件贰军,Socket函數(shù)其實(shí)就是對其進(jìn)行的操作。
TCP服務(wù)器編程的基本經(jīng)典三段式:創(chuàng)建一個(gè)監(jiān)聽socket
蟹肘,然后將其綁定到指定地址的端口上并開始監(jiān)聽词疼,然后循環(huán)不斷的accept
客戶端請求。
例如:使用C語言實(shí)現(xiàn)的TCP服務(wù)器
// 創(chuàng)建監(jiān)聽的socket
int sfd = socket(AF_INET, SOCK_STREAM, 0);// 返回服務(wù)端的文件描述符
// 綁定socket到指定地址的端口并開始監(jiān)聽
bind(sfd, (struct sockaddr *)(&s_addr), sizeof(struct sockaddr)) && listen(sfd, 10);
// 循環(huán)accept客戶端請求
while(1){
cfd = accept(sfd, (struct sockaddr *)(&cli_addr), &addr_size);//返回客戶端文件描述符
}
socket
int socket(int domain, int type, int protocol);
socket()
函數(shù)對應(yīng)于普通文件的打開操作疆前,普通文件的打開返回文件描述符fd
寒跳,socket()
函數(shù)用于創(chuàng)建一個(gè)Socket描述符sd, socket descriptor
聘萨,它能唯一標(biāo)識一個(gè)Socket竹椒。這個(gè)Socket描述符跟文件描述符fd
一樣,在后續(xù)的操作中都會使用到米辐,會將其作為參數(shù)通過它來進(jìn)行一些列的讀寫操作胸完。
創(chuàng)建Socket的時(shí)候,可以指定不同的參數(shù)創(chuàng)建不同的Socket描述符翘贮,socket()
函數(shù)有三個(gè)參數(shù)分別是:
-
domain
協(xié)議域又稱為協(xié)議族AF, address family
赊窥,常用的協(xié)議族有AF_INET
、AF_INET6
狸页、AF_LOCAL
(又稱為AF_UNIX
即UNIX域的Socket)锨能、AF_ROUTE
等扯再。協(xié)議族決定了Socket的地址類型,在通信中必須采用對應(yīng)的地址址遇,例如AF_INET
決定了要使用32位的IPv4地址與16位的端口號的組合熄阻,AF_UNIX
決定了要使用一個(gè)絕對路徑名作為地址。 -
type
指定Socket類型倔约,常用的Socket類型包括SOCK_STREAM
秃殉、SOCK_DGRAM
、SOCK_RAW
浸剩、SOCK_PACKET
钾军、SOCK_SEQPACKET
等。 -
protocol
指定協(xié)議绢要,常用的協(xié)議包括TCP傳輸協(xié)議IPPROTO_TCP
吏恭、UDP傳輸協(xié)議IPPROTO_UDP
、STCP傳輸協(xié)議IPPROTO_STCP
袖扛、TIPC傳輸協(xié)議IPPROTO_TIPC
等砸泛。
需要注意的是type
與protocol
并非可以隨意組合的,比如SOCK_STREAM
不可以跟IPPROTO_UDP
組合蛆封。當(dāng)protocol
為0時(shí)會自動選擇type
類型對應(yīng)的默認(rèn)協(xié)議唇礁。
當(dāng)調(diào)用socket()
函數(shù)創(chuàng)建一個(gè)Socket時(shí)返回的Socket描述符存在于協(xié)議族Address Family, AF_XXX
空間中惨篱,但沒有一個(gè)具體的地址盏筐。如果想要給它賦值一個(gè)地址,就必須調(diào)用bind()
函數(shù)否則調(diào)用connect()
砸讳、listen()
函數(shù)時(shí)系統(tǒng)會自動隨機(jī)分配一個(gè)端口琢融。
bind
bind()
函數(shù)會將一個(gè)地址簇中的特定地址賦給Socket,例如對應(yīng)AF_INET
簿寂、AF_INET6
會將一個(gè)IPv4或IPv6的地址和端口賦給Socket漾抬。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()
函數(shù)的三個(gè)參數(shù)分別是
-
int sockfd
表示Socket描述字,它是通過socket()
函數(shù)創(chuàng)建用于唯一標(biāo)識一個(gè)Socket常遂,bind()
函數(shù)就是將這個(gè)描述字綁定一個(gè)名字纳令。 -
const struct sockaddr *addr
表示一個(gè)const struct sockaddr *
指針,指向要綁定給sockfd
的協(xié)議地址克胳,這個(gè)地址結(jié)構(gòu)根據(jù)地址創(chuàng)建Socket時(shí)的地址協(xié)議族的不同而不同平绩。 -
socklen_t addlen
表示對應(yīng)的地址的長度
通常服務(wù)器在啟動時(shí)都會綁定一個(gè)總所周知的地址,比如IP地址+端口號漠另,用來提供服務(wù)捏雌,客戶端通過這個(gè)地址來連接服務(wù)器,客戶端自身是不用指定的因?yàn)橛邢到y(tǒng)會自動分配一個(gè)端口號和自身的IP地址組合笆搓。這就是為什么暢通服務(wù)器在監(jiān)聽listen
前會調(diào)用bind()
函數(shù)性湿,而客戶端不用調(diào)用直接使用connect()
時(shí)會由系統(tǒng)隨機(jī)生成一個(gè)纬傲。
listen
int listen(int sockfd, int backlog);
listen()
函數(shù)的第一個(gè)參數(shù)int sockfd
是需要監(jiān)聽的Socket的描述字,第二個(gè)參數(shù)int backlog
為相應(yīng)Socket可以排隊(duì)的最大連接個(gè)數(shù)肤频。socket()
函數(shù)創(chuàng)建的Socket默認(rèn)是一個(gè)主動類型的嘹锁,listen()
函數(shù)將Socket轉(zhuǎn)變?yōu)楸粍宇愋筒⒌却蛻舳说倪B接請求。
connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect()
函數(shù)的第一個(gè)參數(shù)為客戶端Socket描述符着裹,第二個(gè)參數(shù)是服務(wù)器的Socket地址领猾,第三個(gè)參數(shù)是Socket地址的長度『龋客戶端通過調(diào)用connect()
函數(shù)建立與TCP服務(wù)器的連接摔竿。
accept
TCP服務(wù)器一次調(diào)用socket()
、bind()
少孝、listen()
之后會監(jiān)聽指定Socket地址继低,TCP客戶端依次調(diào)用socket()
、connect()
之后會向TCP服務(wù)器發(fā)送一個(gè)連接請求稍走。TCP服務(wù)器監(jiān)聽到這個(gè)請求后會調(diào)用accept()
函數(shù)獲取并接收請求袁翁,這樣連接就建立好了。之后就可以開始網(wǎng)絡(luò)I/O操作了婿脸,類似普通文件的讀寫I/O操作一樣粱胜。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept()
函數(shù)的第一個(gè)參數(shù)int sockfd
是服務(wù)器的Socket描述字,第二個(gè)參數(shù)為指向struct sockaddr *
的指針用于返回客戶端的協(xié)議地址狐树,第三個(gè)參數(shù)socklen_t *addrlen
為協(xié)議地址的長度焙压。如果accept
成功返回值是由內(nèi)核自動生成的一個(gè)全新的描述符,代表與返回客戶的TCP連接抑钟。
需要注意的是accept()
函數(shù)的第一個(gè)參數(shù)是服務(wù)器的Socket描述符涯曲,是服務(wù)器開始調(diào)用socket()
函數(shù)創(chuàng)建生成的,又被稱為監(jiān)聽Socket描述符在塔。而accept()
函數(shù)返回的是已連接的Socket描述符幻件。一個(gè)服務(wù)器通常僅僅只創(chuàng)建一個(gè)監(jiān)聽Socket描述符,它在該服務(wù)器的生命周期內(nèi)一直會存在蛔溃。系統(tǒng)內(nèi)核會為每個(gè)由服務(wù)器進(jìn)程接收的客戶連接創(chuàng)建一個(gè)已經(jīng)連接的Socket描述符绰沥,當(dāng)服務(wù)器完成了對某個(gè)客戶端的服務(wù)時(shí)相應(yīng)的已連接Socket描述符就會被關(guān)閉。
close
int close(int fd);
close()
函數(shù)表示TCP Socket的缺省行為時(shí)把該Socket描述符標(biāo)記為關(guān)閉城榛,然后立即返回到調(diào)用進(jìn)程揪利。Socket描述符不能再由調(diào)用進(jìn)程使用态兴,也就是說不能再作為read
或write
的第一個(gè)參數(shù)使用狠持。需要注意的是close()
操作只是使相應(yīng)Socket描述符的引用計(jì)數(shù)減一,只有當(dāng)引用計(jì)數(shù)為0的時(shí)候才會觸發(fā)TCP客戶端向服務(wù)器發(fā)送終止連接請求瞻润。
TCP粘包
TCP協(xié)議的粘包問題實(shí)際上是針對數(shù)據(jù)無邊界性提出的喘垂,為什么這么說呢甜刻?
Socket緩沖區(qū)和數(shù)據(jù)傳輸過程中,可以發(fā)現(xiàn)數(shù)據(jù)的接收和發(fā)送是無關(guān)的正勒,read()
或recv()
讀函數(shù)不管數(shù)據(jù)發(fā)送多少次都回盡可能多的接收數(shù)據(jù)得院,都會盡可能多的接收數(shù)據(jù),也就是說read()
或recv()
讀函數(shù)和write()
或send()
寫函數(shù)的執(zhí)行次數(shù)可能不同章贞。
例如:write()
或send()
寫函數(shù)重復(fù)執(zhí)行三次祥绞,每次都發(fā)送字符串abc
,那么目標(biāo)機(jī)器上的read()
或recv()
讀函數(shù)可能分成三次接收鸭限,每次都接收abc
蜕径。也有可能分成兩次接收,第一次接收abcab
败京,第二次接收cabc
兜喻。也有可能一次就接收所有的字符串abcabcabc
。
假設(shè)希望客戶端每次發(fā)送一位學(xué)生的學(xué)號赡麦,就讓服務(wù)器返回學(xué)生的姓名朴皆、地址、成績等信息泛粹,此時(shí)就可能出現(xiàn)問題遂铡,服務(wù)器是不能夠區(qū)分學(xué)生的學(xué)號的。例如第一次發(fā)送1晶姊,第二次發(fā)送3忧便,服務(wù)器可能當(dāng)成13來處理,返回的信息顯然是錯(cuò)誤的帽借。
這就是數(shù)據(jù)的粘包問題珠增,客戶端發(fā)送的多個(gè)數(shù)據(jù)包被當(dāng)作一個(gè)數(shù)據(jù)包接收,也稱為數(shù)據(jù)的無邊界性砍艾。read()
或recv()
讀函數(shù)不知道數(shù)據(jù)包的開始或結(jié)束標(biāo)志蒂教,實(shí)際上也沒有任何開始或結(jié)束標(biāo)志,只是把它們當(dāng)作連續(xù)的數(shù)據(jù)流來處理脆荷。
Tornado TCPServer
Tornado有了tornado.ioloop
和tornado.iostream
兩個(gè)模塊的幫助可以實(shí)現(xiàn)異步Web服務(wù)器凝垛,tornado.httpserver
是Tornado的Web服務(wù)器模塊,該模塊中實(shí)現(xiàn)了HTTPServer
- 一個(gè)單線程HTTP服務(wù)器蜓谋,其實(shí)現(xiàn)是基于tornado.tcpserver
模塊的TCPServer
梦皮。
TCPServer
是一個(gè)非阻塞單線程的TCP服務(wù)器,負(fù)責(zé)處理TCP協(xié)議部分的內(nèi)容桃焕,并預(yù)留handle_stream
抽象接口方法針對相應(yīng)的應(yīng)用層協(xié)議編寫服務(wù)器剑肯。所以,在分析Tornado的HTTP服務(wù)器實(shí)現(xiàn)之前观堂,需要先理解tornado.tcpserver.TCPServer
的實(shí)現(xiàn)让网。
tornado.tcpserver
模塊中只定義了一個(gè)TCPServer
類呀忧,由于其實(shí)現(xiàn)不涉及到具體的應(yīng)用層協(xié)議,加上有IOLoop
和IOStream
的支持溃睹,其實(shí)現(xiàn)相對簡單而账。
#! /usr/bin/env python3
# -*- coding=utf-8 -*-
from tornado.tcpserver import TCPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
define("port", type=int, default=8000)
# Tornado實(shí)現(xiàn)TCPServer需子類實(shí)例化TCPServer
class Server(TCPServer):
def handle_stream(self, sockfd, client_addr):
sockfd.read_until_close(self.handle_recv)
def handl_recv(self, data):
print(data)
if __name__=="__main__":
options.parse_command_line()
server = Server()
server.listen(options.port, address="127.0.0.1")
IOLoop.instance().start()
Tornado的TCPServer類定義在tcpserver.py
文件中
from tornado.tcpserver import TCPServer
Tornado的TCPServer兩種用法分別是bind + start
和listen
- 使用
bind + start
綁定開啟的方式用于多進(jìn)程 - 使用
listen
監(jiān)聽的方式用于單進(jìn)程
def listen(self, port, address=""):
sockets = bind_sockets(port, address=address)
self.add_sockets(sockets)
listen
方法會接收兩個(gè)參數(shù)分別是端口port
和 主機(jī)地址address
,進(jìn)入后首先會使用bind_sockets
方法接收地址和端口來創(chuàng)建sockets
列表并綁定地址端口并監(jiān)聽因篇,也就完成了TCP三部曲的前兩步泞辐。然后,使用add_sockets
在這些sockets
上注冊read/timeout
事件竞滓。
由于Tornado采用的是單進(jìn)程單線程異步IO的網(wǎng)絡(luò)模型 铛碑,所以可以看作是單線程事件驅(qū)動模式的服務(wù)器,TCP三部曲中的第三步accept
就被分隔到了事件回調(diào)中虽界,因此要在所有的文件描述符fd
上監(jiān)聽事件汽烦。當(dāng)完成上述操作后就可以安心的調(diào)用ioloop
單例的start
方法開始循環(huán)監(jiān)聽事件。
簡單來說莉御,基于事件驅(qū)動的服務(wù)器需要做的就是:創(chuàng)建socket
撇吞,綁定bind
到指定地址的端口上并監(jiān)聽listen
,然后注冊事件和對應(yīng)的回調(diào)礁叔,最后在和回調(diào)中accept
客戶端的最新請求牍颈。
Tornado的HTTPServer是派生自TCPServer的,從協(xié)議上講是再自然不過的琅关。從TCPServer的實(shí)現(xiàn)上看煮岁,它是一個(gè)通用的Server架構(gòu),基本是按照BSD Socket的思想設(shè)計(jì)的涣易,因此create-bind-listen
三段式一個(gè)都不少画机。
Tornado中TCPServer類的實(shí)現(xiàn)代碼位于tornado/tcpserver.py
文件中,TCPServer是一個(gè)非阻塞(non-blocking)單線程(single-threaded)的TCPServer新症,關(guān)于這一點(diǎn)如何理解呢步氏?
首先是非阻塞non-blocking
表示服務(wù)器沒有使用阻塞式API,為什么是阻塞式設(shè)計(jì)呢徒爹?例如在BSD Socket中recv
函數(shù)默認(rèn)是阻塞式的荚醒。當(dāng)使用recv
讀取客戶端數(shù)據(jù)時(shí),如果客戶端并未發(fā)送數(shù)據(jù)隆嗅,此時(shí)這個(gè)API就會一直阻塞在那里不返回界阁,這樣服務(wù)器的設(shè)計(jì)就不得不使用多線程或多進(jìn)程的方式,避免因?yàn)橐粋€(gè)API的阻塞導(dǎo)致服務(wù)器沒有去做其他的事情胖喳。
阻塞式的API是非常常見的泡躯,可以認(rèn)為阻塞式設(shè)計(jì)是:“不管有沒有是數(shù)據(jù),服務(wù)器都會派API去讀,如果讀不到API就不會回來交差”精续。而非阻塞式的對于recv
來說,區(qū)別在于當(dāng)沒有數(shù)據(jù)可讀時(shí)服務(wù)器不會在那里死等粹懒,會直接返回重付。由于服務(wù)器無法預(yù)知到有沒有數(shù)據(jù)可讀,因此不得不反復(fù)派recv
函數(shù)去讀凫乖,這樣不會浪費(fèi)大量的CPU資源嗎确垫?
Tornado的非阻塞設(shè)計(jì)的要高級很多,基本上是另一種思路:服務(wù)器并不主動的讀取數(shù)據(jù)帽芽,它和操作系統(tǒng)合作實(shí)現(xiàn)了一種監(jiān)視器(Linux上Epoll的IO網(wǎng)絡(luò)模型删掀,UNIX的kqueue),TCP連接也就是監(jiān)視器的監(jiān)視對象导街。當(dāng)某個(gè)連接上有數(shù)據(jù)到來時(shí)披泪,操作系統(tǒng)會按照事先約定通知服務(wù)器:“xxx號客戶端連接上有數(shù)據(jù)到來了,你去處理于一下”搬瑰。服務(wù)器此時(shí)才會派API去取數(shù)據(jù)款票。因此,服務(wù)器不用創(chuàng)建大量線程來阻塞式的處理每個(gè)連接泽论,也不用不停地派API去檢查連接上是否有數(shù)據(jù)艾少,它只需要坐在那里等待操作系統(tǒng)的通知,這也保證了recv
函數(shù)這個(gè)API一旦出手就不會落空翼悴。
Tornado另一個(gè)被強(qiáng)調(diào)的特性的單線程single-threaded
缚够,由于與操作系統(tǒng)何實(shí)現(xiàn)的監(jiān)視器非常高效,因此可以在一個(gè)線程中監(jiān)視成千上萬個(gè)連接的狀態(tài)鹦赎,基本上不需要再動用線程來分流谍椅。實(shí)測表明,單線程比阻塞式多進(jìn)程或多進(jìn)程設(shè)計(jì)更加高效古话,當(dāng)然這也依賴于操作系統(tǒng)的大力配合毯辅。不過,現(xiàn)代主流操作系統(tǒng)都提供了非常高效的監(jiān)視器機(jī)制煞额。
handle_stream
TCPServer
是一個(gè)非阻塞的單線程TCP服務(wù)器思恐,它提供了一個(gè)抽象接口方法handle_stream
供子類實(shí)現(xiàn),同時(shí)支持多進(jìn)程的運(yùn)行方式膊毁。
TCPServer類一般不直接被實(shí)例化胀莹,而是由它派生出子類,再用子類實(shí)例化婚温。為了強(qiáng)化這個(gè)設(shè)計(jì)思想描焰,TCPServer定義了一個(gè)未直接實(shí)現(xiàn)的接口handle_stream()
。這個(gè)技巧就是強(qiáng)制讓子類覆蓋此方法,否則給報(bào)錯(cuò)荆秦。
def handle_stream(self, stream, address):
raise NotImplementedError()
例如:使用Tornado實(shí)現(xiàn)TCPServer聊天功能
實(shí)現(xiàn)聊天服務(wù)器篱竭,當(dāng)客戶端連接服務(wù)器后發(fā)出消息,服務(wù)器將該消息推送到當(dāng)前連接服務(wù)器的每個(gè)客戶端上步绸。
$ vim server.py
#! /usr/bin/env python3
#encoding=utf-8
from tornado.tcpserver import TCPServer
from tornado.ioloop import IOLoop
# 連接類
class Connection(object):
# 聲明一個(gè)空集合clients用來存儲所有連接到服務(wù)器的客戶端對象
clients = set()
def __init__(self, stream, address):
Connection.clients.add(self)
# _stream可以抽象成一座架在服務(wù)器與客戶端之間的橋梁掺逼,在其上進(jìn)行數(shù)據(jù)傳輸操作。
self._stream = stream
#_address是客戶端地址和端口瓤介,是一個(gè)元組對象
self._address = address
# EOF用來作為客戶端發(fā)送消息完畢的標(biāo)識
self.EOF = b"\n"
# set_clolse_callback()函數(shù)用于注冊一個(gè)回調(diào)函數(shù)吕喘,在Stream關(guān)閉時(shí)會被激活。
self._stream.set_close_callback(self.on_close)
# read_message()負(fù)責(zé)讀取客戶端發(fā)送的消息刑桑,是連接類的核心方法氯质。
self.read_message()
print("client entered ", address)
# 廣播消息
def broadcast_messages(self, data):
print("broadcast message: ", data[:-1], self._address)
for conn in Connection.clients:
conn.send_message(data)
self.read_message()
# 負(fù)責(zé)讀取客戶端發(fā)送過來的消息
def read_message(self):
print("read message")
# 從緩沖區(qū)讀取數(shù)據(jù)當(dāng)遇到EOF時(shí)讀取完成并激活回調(diào)函數(shù)
# tornado.iostream.BaseIOStream類的read_until(delimiter, callback)方法
# 將會從緩沖區(qū)中直到讀到截止標(biāo)記時(shí)會產(chǎn)生一次回調(diào)
# 如果沒有截止標(biāo)記緩沖區(qū)就繼續(xù)積攢數(shù)據(jù),直到截止標(biāo)記出現(xiàn)祠斧,才會生成回調(diào)闻察。
# 緩沖區(qū)默認(rèn)最大尺寸max_buffer_size = 102857600
self._stream.read_until(self.EOF, self.broadcast_messages)
def send_message(self, data):
print("send message:", data)
self._stream.write(data)
def on_close(self):
print("client close ", self._address)
Connection.clients.remove(self)
class Server(TCPServer):
def handle_stream(self, stream, address):
print("client connection ", address, stream)
Connection(stream, address)
print("client connection num is ", len(Connection.clients))
if __name__=="__main__":
print("server start")
server = Server()
server.listen(8000)
IOLoop.instance().start()
#! /usr/bin/env python3
# -*- coding=utf-8 -*-
from tornado.tcpserver import TCPServer
from tornado.ioloop import IOLoop
from tornado.options import define, options
define("host", type=str, default="127.0.0.1")
define("port", type=int, default=8000)
# 服務(wù)器連接類
class Connection:
# 聲明空集合clients用于存儲所有連接到服務(wù)器的客戶端對象
clients = set()
# 初始化方法
def __init__(self, stream, address):
print("client connect", address)
# 添加客戶端連接
Connection.clients.add(self)
# _stream可抽象成一座架在客戶端和服務(wù)器之間的橋梁,在其上進(jìn)行數(shù)據(jù)傳輸?shù)炔僮鳌? self._stream = stream
# _address是客戶端地址和端口琢锋,是一個(gè)元素對象
self._address = address
# EOF用來作為客戶端發(fā)送消息完畢的 標(biāo)識
self.EOF = b'\n'
# set_close_callback方法用于注冊一個(gè)回調(diào)函數(shù)蜓陌,當(dāng)stream關(guān)閉時(shí)會被激活。
self._stream.set_close_callback(self.on_close)
# read_message用于讀取客戶端發(fā)送過來的消息吩蔑,是服務(wù)器連接類的核心方法钮热。
self.read_message()
# 讀取客戶端發(fā)送過來的消息
def read_message(self):
print("read client message")
# read_until方法負(fù)責(zé)從緩沖區(qū)讀取數(shù)據(jù),當(dāng)遇到EOF標(biāo)識后讀取完成并激活結(jié)束回調(diào)函數(shù)
self._stream.read_until(self.EOF, self.broadcast_message)
# 將客戶端發(fā)送的消息廣播給每個(gè)已經(jīng)連接的客戶端
def broadcast_message(self, data):
print("broadcast clients message:", data)
try:
data = tornado.escape.to_unicode(data)
# 遍歷Connection.clients所有客戶端連接并保持監(jiān)聽每個(gè)客戶端發(fā)送的消息
for conn in Connection.clients:
conn.send_message(data)
self.read_message()
except StreamClosedError as e:
# 出現(xiàn)異常pass斷開stream時(shí)的報(bào)錯(cuò)
pass
# 客戶端發(fā)送消息
def send_message(self, data):
print("send message")
# 將數(shù)據(jù)轉(zhuǎn)換為bytes字節(jié)類型后通過stream_write方法寫入緩沖區(qū)
data = str(self._address) + ":" + data
self._stream.write(bytes(data.encode("utf-8")))
# 當(dāng)客戶端斷開連接時(shí)將其從客戶端集合中刪除
def on_close(self):
print("client close")
Connection.clients.remove(self)
# 服務(wù)器類
# Tornado實(shí)現(xiàn)TCPServer需繼承tornado.tcpserver.TCPServer并重寫handle_stream()方法
class Server(TCPServer):
# 重寫TCPServer的handle_stream方法
def handle_stream(self, sockfd, client_addr):
# 實(shí)例化服務(wù)器連接類
Connection(sockfd, client_addr)
# 入口方法烛芬,運(yùn)行服務(wù)器隧期。
if __name__=="__main__":
options.parse_command_line()
# 創(chuàng)建服務(wù)器實(shí)例
server = Server()
# 監(jiān)聽指定地址的端口
server.listen(options.port, options.host)
# 運(yùn)行IOLoop實(shí)例并開啟事件循環(huán)
IOLoop.instance().start()
$ vim client.py
#! /usr/bin/python3
#encoding=utf-8
import socket
import time
HOST = "127.0.0.1"
PORT = 8000
sfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sfd.connect((HOST, PORT))
print("client connect")
sfd.sendall(bytes("hello\n", "utf-8"))
time.sleep(3)
sfd.sendall(bytes("world\n", "utf-8"))
print("client send message")
data = sfd.recv(1024)
print("client recieve: ", repr(data))
time.sleep(60)
sfd.close()
#! /usr/bin/python3
#encoding=utf-8
import socket,threading
from tornado.iostream import IOStream
from tornado.ioloop import IOLoop
# 定義客戶端類
class Client:
# 初始化
def __init__(self, host, port):
self._host = host
self._port = port
# 創(chuàng)建客戶端時(shí)還未與服務(wù)器建立連接,所以_stream初始值位None赘娄。
self._stream = None
# EOF設(shè)置為消息的結(jié)尾仆潮,當(dāng)讀取到這個(gè)標(biāo)識的時(shí)候標(biāo)識一條消息輸入完畢
self.EOF = b'\n'
# 建立連接
def connect(self):
# 建立流
self.get_stream()
# 指定地址和端口連接服務(wù)器,并注冊回調(diào)函數(shù)為開始客戶端運(yùn)行的函數(shù)遣臼。
self._stream.connect((self._host, self._port), self.start)
# 獲取Socket性置,通過tornado.iostream.IOStream創(chuàng)建_stream
def get_stream(self):
# 創(chuàng)建Socket描述符,AF_INET表示協(xié)議族為IPv4揍堰,SOCK_STREAM表示連接類型為流式基于TCP鹏浅,0表示系統(tǒng)根據(jù)情況決定協(xié)議類型。
sockfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
# 創(chuàng)建流
self._stream = IOStream(sockfd)
# 設(shè)置關(guān)閉流時(shí)的回調(diào)函數(shù)
self._stream.set_close_callback(self.on_close)
# 開始并運(yùn)行客戶端
def start(self):
# 使用多線程同時(shí)通知收發(fā)消息
# 使用多線程時(shí)如果退出程序就必須要結(jié)束線程否則會拋出異常屏歹,程序何時(shí)結(jié)束取決于用戶隐砸。
t1 = threading.Thread(target = self.read_msg)
t2 = threading.Thread(target = self.send_msg)
# 為解決這個(gè)問題,將線程設(shè)置為daemon守護(hù)線程蝙眶,daemon線程可也i也在主程序結(jié)束時(shí)自動結(jié)束季希。
t1.daemon, t2.daemon = True, True
# 開啟線程
t1.start()
t2.start()
# 讀取消息
def read_msg(self):
self._stream.read_until(self.EOF, self.show_msg)
# 屏幕打印讀取的消息
def show_msg(self, data):
print(to_unicode(data))
self.read_msg()
# 發(fā)送消息
def send_msg(self):
# 使用while循環(huán)保持輸入狀態(tài)
while True:
# 讀取客戶端輸入
data = input()
# 當(dāng)輸入完畢后將消息轉(zhuǎn)換為字節(jié)byte型并拼接結(jié)束標(biāo)識后發(fā)送
self._stream.write(bytes(data) + self.EOF)
# 用戶退出時(shí)關(guān)閉_stream會激活_close函數(shù)
def on_close(self):
print("exit")
quit()
if __name__ == "__main__":
client = Client("127.0.0.1", 8000)
client.connect()
IOLoop.instance().start()
運(yùn)行測試
$ python3 server.py
server start
client connection ('127.0.0.1', 52088) <tornado.iostream.IOStream object at 0x7f565b8ee7f0>
read message
client entered ('127.0.0.1', 52088)
client connection num is 1
WARNING:tornado.general:error on read: '>=' not supported between instances of 'int' and 'method'
client close ('127.0.0.1', 52088)
$ python3 client.py
client connect
client send message
client recieve: b''
服務(wù)器出現(xiàn)警告信息
WARNING:tornado.general:error on read: '>=' not supported between instances of 'int' and 'method'
client close ('127.0.0.1', 52088)
TCPServer SSL
TCPServer的構(gòu)造函數(shù)中會對非None
得ssl_options
選項(xiàng)參數(shù)進(jìn)行檢查,要求必須包括certfile
和keyfile
選項(xiàng)并且選線需要指定文件保存路徑,但不會檢查文件內(nèi)容式塌。至于文件內(nèi)容得檢查博敬,會推遲到客戶端連接建立時(shí)。ssl_options
是一個(gè)字典dictionary
類型峰尝,在Python3.2+之后可以使用ssl.SSLContext
實(shí)例來代替偏窝。
TCPServer是支持SSL的,由于強(qiáng)大的Python支持SSL非常簡單境析,因此只要啟動一個(gè)支持SSL的TCPServer時(shí)告訴它你的certfile
和keyfile
即可囚枪。
TCPServer(
ssl_options = {
"certfile" : os.path.join(datadir, "domain.crt"'),
"kefile" : os.path.join(datadir, "domain.key")
}
)
TCPServer初始化
- 使用
listen
的單進(jìn)程形式
通過使用TCPServer的listen
方法派诬,程序?qū)⒁詥芜M(jìn)程的方式運(yùn)行服務(wù)器實(shí)例劳淆。
import tornado.tcpserver from TCPServer
import tornado.ioloop from IOLoop
server = TCPServer()
server.listen(8000)
IOLoop.current().start()
TCPServer提供的listen
方法可以立即啟動在指定地址端口上進(jìn)行監(jiān)聽,并將相應(yīng)的Socket加入到IOLoop事件循環(huán)中默赂。listen
方法可以多次調(diào)用沛鸵,即同時(shí)監(jiān)聽多個(gè)端口。由于需要IOLoop事件循環(huán)來驅(qū)動缆八,所以必須確保相應(yīng)的IOLoop實(shí)例已經(jīng)啟動曲掰。
- 使用
bind + start
的多進(jìn)程方式
通過使用TCPServer的bind
和start
方法,程序可以以多進(jìn)程的方式運(yùn)行服務(wù)器實(shí)例奈辰。
import tornado.tcpserver from TCPServer
import tornado.ioloop from IOLoop
server = TCPServer()
server.bind(8000)
server.starat(0) # fork派生創(chuàng)建多個(gè)子進(jìn)程
IOLoop.current().start()
bind
方法可以將服務(wù)器綁定到指定地址栏妖,并通過start
方法啟動多個(gè)子進(jìn)程,以達(dá)到多進(jìn)程運(yùn)行的模式奖恰。
start
方法通過參數(shù)num_processes
來指定以單進(jìn)程或多進(jìn)程方式運(yùn)行服務(wù)器吊趾,num_processes
參數(shù)的默認(rèn)值為1,即以單進(jìn)程方式運(yùn)行瑟啃。當(dāng)設(shè)置為None
或小于0時(shí)將嘗試使用與CPU核心數(shù)量相同的子進(jìn)程運(yùn)行论泛。當(dāng)設(shè)置為大于1時(shí)將以該值指定的子進(jìn)程數(shù)量運(yùn)行。不過蛹屿,如果是以單進(jìn)程方式運(yùn)行服務(wù)器的話屁奏,一般都會直接使用listen
方式。
在多進(jìn)程模式啟動時(shí)错负,不能將IOLoop對象傳遞給TCPServer的構(gòu)造函數(shù)坟瓢,如果這樣做將會導(dǎo)致TCPServer直接按單進(jìn)程方式啟動。
- 高級多進(jìn)程形式
TCPServer的bind
和start
方法內(nèi)部實(shí)際封裝的是綁定監(jiān)聽端口和啟動子進(jìn)程的業(yè)務(wù)邏輯犹撒,你也可以不使用這兩個(gè)方式载绿,而是執(zhí)行調(diào)用綁定函數(shù)bind_sockets
和fork
進(jìn)程來達(dá)到多進(jìn)程運(yùn)行服務(wù)器實(shí)例的目的。
sockets = bind_sockets(8000)
tornado.process.fork_processes(0)
server = TCPServer()
server.add_sockets(socktes)
IOLoop.current().starat()
bind_sockets
函數(shù)定義在tornado.netutil
模塊中油航,pork_processes
函數(shù)定義在tornado.process
模塊中崭庸。
通過調(diào)用bind_sockets
函數(shù)可以創(chuàng)建一個(gè)或多個(gè)鑒定指定端口的Socket,注意一個(gè)域名hostname
可能會綁定到多個(gè)IO地址上。
通過調(diào)用fork_processes
方法可以fork創(chuàng)建出多個(gè)子進(jìn)程怕享,其中主進(jìn)程調(diào)用負(fù)責(zé)監(jiān)聽子進(jìn)程的狀態(tài)而不會返回执赡,子進(jìn)程會繼續(xù)執(zhí)行后續(xù)代碼。
實(shí)際上TCPServer的bind
和start
方法內(nèi)部也是通過調(diào)用bind_sockets
和fork_processes
函數(shù)實(shí)現(xiàn)的函筋。
高級多進(jìn)程形式的主要優(yōu)點(diǎn)是tornado.process.fork_processes(0)
為進(jìn)程的創(chuàng)建提供多的靈活性沙合。
TCPServer類
__init__
TCPServer類的初始化方法__init__
可以接收一個(gè)io_loop
參數(shù),實(shí)際上io_loop
對TCPServer來說并不是可有可無的跌帐,它是必須的首懈。不過TCPServer提供了多種渠道來與一個(gè)io_loop
綁定,初始化參數(shù)只是其中一種綁定方式而已谨敛。
listen
在創(chuàng)建服務(wù)器實(shí)例后第一個(gè)被調(diào)用的是listen
方法究履,TCPServer類的listen
函數(shù)是開始接受指定地址端口上的連接。注意脸狸,這個(gè)listen
與BSD Socket中的listen
并不等價(jià)最仑,它做的事比BSD socket() + bind() + listen()
還要多。
listen
函數(shù)注釋中提到的一句話是這樣的:你可以在一個(gè)Server的實(shí)例中多次調(diào)用listen
以實(shí)現(xiàn)一個(gè)Server監(jiān)聽多個(gè)端口炊甲。這個(gè)應(yīng)該怎么來理解呢泥彤?
在BSD Socket架構(gòu)中是不可能在一個(gè)Socket上同時(shí)監(jiān)聽多個(gè)端口的,反推不難想到卿啡,TCPServer的listen
函數(shù)內(nèi)部一定是執(zhí)行了全套的BSD Socket三段式create->bind->listen
吟吝,使得每調(diào)用一次listen
實(shí)際上是創(chuàng)建一個(gè)新的Socket。
def listen(self, port, address=""):
# 創(chuàng)建Socket
sockets = bind_sockets(port, address=address)
# 將創(chuàng)建的Socket添加到監(jiān)聽隊(duì)列中
self.add_sockets(sockets)
bind_sockets
bind_sockets
函數(shù)的主要作用是創(chuàng)建Socket并綁定Socket到指定地址的端口颈娜,并開啟監(jiān)聽剑逃。
bind_sockets
函數(shù)并不是TCPServer的成員,它定義在netutil.py
文件中揭鳞,原型為:
def bind_sockets(
port,
address=None,
family=socket.AF_UNSPEC,
backlog=128,
flags=None
):
端口列表
-
port
端口 -
address
地址
address
地址可以是IP地址炕贵,也可以是域名hostname
,如果是localhost
則可以監(jiān)聽域名對應(yīng)的所有IP野崇。如果address
是空字符串""
或None
則會監(jiān)聽主機(jī)上的所有端口称开。 -
family
網(wǎng)絡(luò)層協(xié)議類型
family
網(wǎng)絡(luò)層協(xié)議類型可選AF_INET
和AF_INET6
,默認(rèn)情況下兩則都會被啟用乓梨。此參數(shù)是在BSD Sockett創(chuàng)建時(shí)的sockaddr_in.sin_family
參數(shù)鳖轰。 -
backlog
監(jiān)聽隊(duì)列的長度
backlog
指的是其實(shí)時(shí)BSDlisten(n)
中的n
值。 -
flag
位標(biāo)志
flag
位標(biāo)志是用來傳遞給socket.getaddrinfo()
函數(shù)的扶镀,比如socket.AI_PASSIVE
等蕴侣。
當(dāng)在IPV4和IPV6混用的情況下,bind_socket
函數(shù)的返回值是一個(gè)Socket列表臭觉,此時(shí)一個(gè)address
地址參數(shù)可能對應(yīng)一個(gè)IPV4和一個(gè)IPV6地址昆雀,但是它們的Socket是不通的辱志,會各自獨(dú)立創(chuàng)建。
# bind_socket的參數(shù)賦值流程
sockets = []
if address == "":
address = None
if not socket.has_ipv6 and family == socket.AF_UNSPEC:
family = socket.AF_INET
if flags is None:
flags = socket.AI_PASSIVE
接下來是一個(gè)循環(huán)狞膘,之所以使用循環(huán)是因?yàn)镮Pv4和IPv6混用情況下getaddrinfo
方法會返回多個(gè)地址的信息揩懒。
for res in set(socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags)):
socket.getaddrinfo()
方法是Python標(biāo)準(zhǔn)庫中的函數(shù),其作用是將所接收的參數(shù)重組為一個(gè)結(jié)構(gòu)res
挽封,res
類型將可以直接作為socket.socket()
的參數(shù)已球,跟BSD Socket中的getaddrinfo
函數(shù)相似。
循環(huán)體內(nèi)是針對單個(gè)地址辅愿,會直接獲取getaddrinfo
方法的返回值來創(chuàng)建Socket智亮。
af, socktype, proto, canonname, sockaddr=res
try:
sock = socket.socket(af, socktype, proto)
except socket.error as e:
if e.args[0] == errno.EAFNOSUPPORT:
continue
raise
首先會從res
這個(gè)元組tuple
中拆分出5個(gè)參數(shù),然后根據(jù)需要來創(chuàng)建Socket点待。
接下來會設(shè)置進(jìn)程退出時(shí)對sock
的操作
set_close_exec(sock.fileno())
例如:使用Tornado實(shí)現(xiàn)的TCPServer和TCPClient
服務(wù)器
- 創(chuàng)建一個(gè)繼承于
TCPServer
類的實(shí)例陌凳,監(jiān)聽端口后開啟服務(wù) 煎殷,啟動消息循環(huán)處理客戶端請求烁峭,服務(wù)器開始運(yùn)行非春。
from tornado.tcpserver import TCPServer
class Server(TCPServer):
server = Server()
server.listen(9000)
server.start()
IOLoop.current().start()
- 如果有客戶端連接過來扮念,Tornado會創(chuàng)建一個(gè)
iostream
猜揪,然后調(diào)用handle_stream
方法捂襟,調(diào)用時(shí)傳入兩個(gè)參數(shù)iostream
和client
的地址秽褒。
handle_stream(self, stream, address)
- 服務(wù)器每收到一段20字符以內(nèi)的內(nèi)容就將其反序回傳坝冕,如果收到
over
則表示斷開連接徒探。
msg = yield stream.read_bytes(20, partial = True)
yield stream.write(msg[::-1])
if msg == "over":
stream.close()
- 斷開連接不使用
yield
調(diào)用,無論是誰主動斷開連接喂窟,連接雙方都會各自觸發(fā)一個(gè)StreamClosedError
錯(cuò)誤测暗。
except iostream.StreamClosedError:
pass
運(yùn)行服務(wù)器
$ vim server.py
#! /usr/bin/python3
# encoding=utf-8
from tornado import iostream, gen
from tornado.tcpserver import TCPServer
from tornado.ioloop import IOLoop
class Server(TCPServer):
@gen.coroutine
def handle_stream(self, stream, address):
try:
while True:
msg = yield stream.read_bytes(20, partial = True)
print(msg, "from", address)
yield stream.write(msg[::-1])
if msg == "over":
stream.close()
except iostream.StreamClosedError:
pass
if __name__ == "__main__":
server = Server()
server.listen(9000)
server.start()
IOLoop.current().start()
$ python3 server.py
客戶端
$ vim client.py
#! /usr/bin/python3
# encoding=utf-8
from tornado import gen, iostream
from tornado.tcpclient import TCPClient
from tornado.ioloop import IOLoop
@gen.coroutine
def Trans():
stream = yield TCPClient().connect("192.168.56.103", 9000)
try:
while True:
data = input("Enter: ")
back = yield stream.read_bytes(20, partial = True)
msg = yield stream.read_bytes(20, partial = True)
print(back, msg)
if data == "over":
break
except iostream.StreamClosedError:
pass
if __name__ == "__main__":
IOLoop.current().run_sync(Trans)
$ python3 client.py
使用TCPClient
的connect
方法連接到服務(wù)器,此時(shí)會返回iostream
對象磨澡,向服務(wù)器發(fā)送一些字符串碗啄,它都會反序發(fā)回。最后發(fā)一個(gè)over
則讓服務(wù)器斷開連接稳摄。
未完待續(xù)...