Tornado TCP

參考資料

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)

TCP數(shù)據(jù)報(bào)結(jié)構(gòu)

帶陰影的重點(diǎn)字段

  1. 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)記牺勾。
  2. ACK(Acknowledge Number) 確認(rèn)號,確認(rèn)號占32位阵漏,客戶端和服務(wù)器都可以發(fā)送驻民,ACK = SEQ + 1。
  3. 標(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通信過程

  1. 三次握手:建立TCP連接通道
  2. 數(shù)據(jù)傳輸
  3. 四次揮手:斷開TCP連接通道
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ā)起請求

  1. 當(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)铅檩。
  1. 服務(wù)器接收到數(shù)據(jù)包憎夷,檢測到已經(jīng)設(shè)置了SYN標(biāo)志位,知道這是客戶端發(fā)來建立連接的“請求包”昧旨。
  • 服務(wù)器會組建一個(gè)數(shù)據(jù)包拾给,并設(shè)置SYNACK標(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)
  1. 客戶端接收到數(shù)據(jù)包窍侧,檢測到已經(jīng)設(shè)置了SYNACK標(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)其骄,表示連接成功建立。
  1. 服務(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ù)了耿战。

數(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ù)丟失的情況。

數(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ā)起斷開連接的請求。

  1. 客戶端調(diào)用close()函數(shù)后灸撰,向服務(wù)器發(fā)送FIN數(shù)據(jù)包谒府,并進(jìn)入FIN_WAIT_1狀態(tài)。FINFinish的縮寫表示完成任務(wù)需要斷開連接浮毯。
  2. 服務(wù)器接收到數(shù)據(jù)包后完疫,檢測到設(shè)置了FIN標(biāo)志位,知道要斷開連接债蓝。于是向客戶端發(fā)送確認(rèn)包并進(jìn)入CLOSE_WAIT狀態(tài)壳鹤。需要注意的是,服務(wù)器接收到請求后并不是立即斷開連接饰迹,而是先向客戶端發(fā)送確認(rèn)包芳誓,告訴它我知道了余舶,我需要準(zhǔn)備一下才能斷開連接。
  3. 客戶端接收到確認(rèn)包后進(jìn)入FIN_WAIT_2狀態(tài)锹淌,并等待服務(wù)器準(zhǔn)備完畢后再次發(fā)送數(shù)據(jù)包匿值。
  4. 客戶端等待片刻后,服務(wù)器準(zhǔn)備完畢赂摆,可以斷開連接挟憔。于是服務(wù)器再主動向客戶端發(fā)送FIN包,并告訴它我準(zhǔn)備好了烟号,斷開連接吧绊谭。然后進(jìn)入LAST_ACK狀態(tài)。
  5. 客戶端接收到服務(wù)器的FIN包后再次向服務(wù)器發(fā)送ACK包褥符,并告訴它你斷開連接吧龙誊,然后進(jìn)入TIME_WAIT狀態(tài)。
  6. 服務(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)變遷。

TCP狀態(tài)轉(zhuǎn)化圖

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è)SYNSYN+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包(ACKSYN)之后 狠鸳,會發(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)不同的是接收到FINACK的先后順序恃疯。
    (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á)鼻听。前面的FINACK正常到達(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í)間是2MSLMSL是任何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

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é)議簇通信的中間軟件抽象層爪飘,是一組接口义起。

Socket位置

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ù)接口 叭喜,如createlisten堤框、accept域滥、connectread蜈抓、write等启绰。

Socket通信

服務(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é)的事情。

TCP套接字的I/O緩沖區(qū)

Socket的阻塞模式

所謂阻塞就是上一步動作還沒有完成账嚎,下一步動作將暫停莫瞬,直到上一步動作完成后才能繼續(xù),以保持同步性郭蕉。TCP套接字默認(rèn)情況下是阻塞模式的疼邀。

對于TCP套接字在默認(rèn)情況下,當(dāng)使用write()send()函數(shù)發(fā)送數(shù)據(jù)時(shí)

  1. 首先會檢查緩沖區(qū)召锈,如果緩沖區(qū)的可用空間長度小于要發(fā)送的數(shù)據(jù)長度旁振,那么write()send()函數(shù)將會被阻塞也就時(shí)暫停執(zhí)行,直到緩沖區(qū)中的數(shù)據(jù)被發(fā)送到目標(biāo)機(jī)器涨岁,騰出足夠的空間才喚醒write()send()函數(shù)繼續(xù)寫入數(shù)據(jù)拐袜。
  2. 如果TCP協(xié)議正在向網(wǎng)絡(luò)中發(fā)送數(shù)據(jù),那么輸出緩沖區(qū)會被鎖定梢薪,不允許寫入阻肿。write()send()函數(shù)也會被阻塞,直到數(shù)據(jù)發(fā)送完畢后緩沖區(qū)解鎖沮尿,write()send()才會被喚醒丛塌。
  3. 如果寫入的數(shù)據(jù)大于緩沖區(qū)的最大長度將會分批寫入
  4. 直到所有數(shù)據(jù)被寫入緩沖區(qū)write()send()函數(shù)才會返回

對于TCP套接字在默認(rèn)情況下较解,當(dāng)使用read()recv()函數(shù)讀取數(shù)據(jù)時(shí)

  1. 首先會檢查緩沖區(qū),如果緩沖區(qū)中有數(shù)據(jù)則直接讀取赴邻,否則函數(shù)會被阻塞直到網(wǎng)絡(luò)上有數(shù)據(jù)到來印衔。
  2. 如果要讀取的數(shù)據(jù)長度小于緩沖區(qū)中的數(shù)據(jù)長度,那么就不能一次性將緩沖區(qū)中的所有數(shù)據(jù)讀出姥敛,剩余數(shù)據(jù)將不斷積累直到有read()recv()函數(shù)再次讀取奸焙。
  3. 直到讀取到數(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_INETAF_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_DGRAMSOCK_RAW浸剩、SOCK_PACKET钾军、SOCK_SEQPACKET等。
  • protocol指定協(xié)議绢要,常用的協(xié)議包括TCP傳輸協(xié)議IPPROTO_TCP吏恭、UDP傳輸協(xié)議IPPROTO_UDP、STCP傳輸協(xié)議IPPROTO_STCP袖扛、TIPC傳輸協(xié)議IPPROTO_TIPC等砸泛。

需要注意的是typeprotocol并非可以隨意組合的,比如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)程使用态兴,也就是說不能再作為readwrite的第一個(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.iolooptornado.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é)議,加上有IOLoopIOStream的支持溃睹,其實(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 + startlisten

  • 使用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ù)中會對非Nonessl_options選項(xiàng)參數(shù)進(jìn)行檢查,要求必須包括certfilekeyfile選項(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í)告訴它你的certfilekeyfile即可囚枪。

TCPServer(
  ssl_options = {
    "certfile" : os.path.join(datadir, "domain.crt"'), 
    "kefile" : os.path.join(datadir, "domain.key")
  }
)

TCPServer初始化

  1. 使用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)啟動曲掰。

  1. 使用bind + start的多進(jìn)程方式

通過使用TCPServer的bindstart方法,程序可以以多進(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)程方式啟動。

  1. 高級多進(jìn)程形式

TCPServer的bindstart方法內(nèi)部實(shí)際封裝的是綁定監(jiān)聽端口和啟動子進(jìn)程的業(yè)務(wù)邏輯犹撒,你也可以不使用這兩個(gè)方式载绿,而是執(zhí)行調(diào)用綁定函數(shù)bind_socketsfork進(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的bindstart方法內(nèi)部也是通過調(diào)用bind_socketsfork_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_INETAF_INET6,默認(rèn)情況下兩則都會被啟用乓梨。此參數(shù)是在BSD Sockett創(chuàng)建時(shí)的sockaddr_in.sin_family參數(shù)鳖轰。
  • backlog 監(jiān)聽隊(duì)列的長度
    backlog指的是其實(shí)時(shí)BSD listen(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ù)器

  1. 創(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()
  1. 如果有客戶端連接過來扮念,Tornado會創(chuàng)建一個(gè)iostream猜揪,然后調(diào)用handle_stream方法捂襟,調(diào)用時(shí)傳入兩個(gè)參數(shù)iostreamclient的地址秽褒。
handle_stream(self, stream, address)
  1. 服務(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()
  1. 斷開連接不使用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

使用TCPClientconnect方法連接到服務(wù)器,此時(shí)會返回iostream對象磨澡,向服務(wù)器發(fā)送一些字符串碗啄,它都會反序發(fā)回。最后發(fā)一個(gè)over則讓服務(wù)器斷開連接稳摄。

未完待續(xù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末稚字,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子厦酬,更是在濱河造成了極大的恐慌胆描,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仗阅,死亡現(xiàn)場離奇詭異昌讲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)减噪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門短绸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來车吹,“玉大人,你說我怎么就攤上這事醋闭±窀椋” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵目尖,是天一觀的道長馒吴。 經(jīng)常有香客問我,道長瑟曲,這世上最難降的妖魔是什么饮戳? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮洞拨,結(jié)果婚禮上扯罐,老公的妹妹穿的比我還像新娘。我一直安慰自己烦衣,他們只是感情好歹河,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著花吟,像睡著了一般秸歧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上衅澈,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天键菱,我揣著相機(jī)與錄音,去河邊找鬼今布。 笑死经备,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的部默。 我是一名探鬼主播侵蒙,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼傅蹂!你這毒婦竟也來了纷闺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤贬派,失蹤者是張志新(化名)和其女友劉穎急但,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搞乏,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡波桩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了请敦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片镐躲。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡储玫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出萤皂,到底是詐尸還是另有隱情撒穷,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布裆熙,位于F島的核電站端礼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏入录。R本人自食惡果不足惜蛤奥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望僚稿。 院中可真熱鬧凡桥,春花似錦、人聲如沸蚀同。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蠢络。三九已至衰猛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谢肾,已是汗流浹背腕侄。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工小泉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留芦疏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓微姊,卻偏偏與公主長得像酸茴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子兢交,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容