微信公眾號(hào)【Java技術(shù)江湖】一位阿里 Java 工程師的技術(shù)小站。(關(guān)注公眾號(hào)后回復(fù)”Java“即可領(lǐng)取 Java基礎(chǔ)、進(jìn)階有额、項(xiàng)目和架構(gòu)師等免費(fèi)學(xué)習(xí)資料辣恋,更有數(shù)據(jù)庫(kù)、分布式、微服務(wù)等熱門(mén)技術(shù)學(xué)習(xí)視頻,內(nèi)容豐富,兼顧原理和實(shí)踐金句,另外也將贈(zèng)送作者原創(chuàng)的Java學(xué)習(xí)指南、Java程序員面試指南等干貨資源)
老曹眼中的網(wǎng)絡(luò)編程基礎(chǔ)
轉(zhuǎn)自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA
我們是幸運(yùn)的吕嘀,因?yàn)槲覀儞碛芯W(wǎng)絡(luò)违寞。網(wǎng)絡(luò)是一個(gè)神奇的東西,它改變了你和我的生活方式币他,改變了整個(gè)世界坞靶。 然而,網(wǎng)絡(luò)的無(wú)標(biāo)度和小世界特性使得它又是復(fù)雜的蝴悉,無(wú)所不在彰阴,無(wú)所不能,以致于我們無(wú)法區(qū)分甚至無(wú)法描述拍冠。
對(duì)于一個(gè)碼農(nóng)而言尿这,了解網(wǎng)絡(luò)的基礎(chǔ)知識(shí)可能還是從了解定義開(kāi)始簇抵,認(rèn)識(shí)OSI的七層協(xié)議模型,深入Socket內(nèi)部射众,進(jìn)而熟練地進(jìn)行網(wǎng)絡(luò)編程碟摆。
關(guān)于網(wǎng)絡(luò)
關(guān)于網(wǎng)絡(luò),在詞典中的定義是這樣的:
在電的系統(tǒng)中叨橱,由若干元件組成的用來(lái)使電信號(hào)按一定要求傳輸?shù)碾娐坊蜻@種電路的部分典蜕,叫網(wǎng)絡(luò)。
作為一名從事過(guò)TMN開(kāi)發(fā)的通信專業(yè)畢業(yè)生罗洗,固執(zhí)地認(rèn)為網(wǎng)絡(luò)是從通信系統(tǒng)中誕生的愉舔。通信是人與人之間通過(guò)某種媒介進(jìn)行的信息交流與傳遞。傳統(tǒng)的通信網(wǎng)絡(luò)(即電話網(wǎng)絡(luò))是由傳輸伙菜、交換和終端三大部分組成轩缤,通信網(wǎng)絡(luò)是指將各個(gè)孤立的設(shè)備進(jìn)行物理連接,實(shí)現(xiàn)信息交換的鏈路贩绕,從而達(dá)到資源共享和通信的目的火的。通信網(wǎng)絡(luò)可以從覆蓋范圍,拓?fù)浣Y(jié)構(gòu)淑倾,交換方式等諸多視角進(jìn)行分類...... 滿滿的回憶馏鹤,還是留在書(shū)架上吧。
網(wǎng)絡(luò)的概念外延被不斷的放大著踊淳,抽象的思維能力是人們創(chuàng)新乃至創(chuàng)造的根源假瞬。網(wǎng)絡(luò)用來(lái)表示諸多對(duì)象及其相互聯(lián)系,數(shù)學(xué)上的圖迂尝,物理學(xué)上的模型,交通網(wǎng)絡(luò),人際網(wǎng)絡(luò)剪芥,城市網(wǎng)絡(luò)等等垄开,總之,網(wǎng)絡(luò)被總結(jié)成從同類問(wèn)題中抽象出來(lái)用數(shù)學(xué)中的圖論科學(xué)來(lái)表達(dá)并研究的一種模型税肪。
很多伙伴認(rèn)為溉躲,了解這些之后呢,然并卵益兄。我們關(guān)心的只是計(jì)算機(jī)網(wǎng)絡(luò)锻梳,算機(jī)網(wǎng)絡(luò)是用通信線路和設(shè)備將分布在不同地點(diǎn)的多臺(tái)計(jì)算機(jī)系統(tǒng)互相連接起來(lái),按照網(wǎng)絡(luò)協(xié)議净捅,分享軟硬件功能疑枯,最終實(shí)現(xiàn)資源共享的系統(tǒng)。特別的蛔六,我們談到的網(wǎng)絡(luò)只是互聯(lián)網(wǎng)——Internet荆永,或者移動(dòng)互聯(lián)網(wǎng)废亭,需要的是寫(xiě)互連網(wǎng)應(yīng)用程序。但是具钥,一位工作了五六年的編程高手曾對(duì)我說(shuō)豆村,現(xiàn)在終于了解到基礎(chǔ)知識(shí)有多重要,技術(shù)在不斷演進(jìn)骂删,而相對(duì)不變的就是那些原理和編程模型了掌动。
老碼農(nóng)深以為然,編程實(shí)踐就是從具體到抽象宁玫,再到具體坏匪,循環(huán)往復(fù),螺旋式上升的過(guò)程撬统。了解前世今生适滓,只是為了可能觸摸到“勢(shì)”×底罚基礎(chǔ)越扎實(shí)凭迹,建筑就會(huì)越有想象的空間。 對(duì)于網(wǎng)絡(luò)編程的基礎(chǔ)苦囱,大概要從OSI的七層協(xié)議模型開(kāi)始了嗅绸。
七層模型
七層模型(OSI,Open System Interconnection參考模型)撕彤,是參考是國(guó)際標(biāo)準(zhǔn)化組織制定的一個(gè)用于計(jì)算機(jī)或通信系統(tǒng)間互聯(lián)的標(biāo)準(zhǔn)體系鱼鸠。它是一個(gè)七層抽象的模型,不僅包括一系列抽象的術(shù)語(yǔ)和概念羹铅,也包括具體的協(xié)議蚀狰。 經(jīng)典的描述如下:
簡(jiǎn)述每一層的含義:
物理層(Physical Layer):建立、維護(hù)职员、斷開(kāi)物理連接麻蹋。
數(shù)據(jù)鏈路層 (Link):邏輯連接、進(jìn)行硬件地址尋址焊切、差錯(cuò)校驗(yàn)等扮授。
網(wǎng)絡(luò)層 (Network):進(jìn)行邏輯尋址,實(shí)現(xiàn)不同網(wǎng)絡(luò)之間的路徑選擇专肪。
傳輸層 (Transport):定義傳輸數(shù)據(jù)的協(xié)議端口號(hào)刹勃,及流控和差錯(cuò)校驗(yàn)。
會(huì)話層(Session Layer):建立嚎尤、管理荔仁、終止會(huì)話。
表示層(Presentation Layer):數(shù)據(jù)的表示、安全咕晋、壓縮雹拄。
應(yīng)用層 (Application):網(wǎng)絡(luò)服務(wù)與最終用戶的一個(gè)接口
每一層利用下一層提供的服務(wù)與對(duì)等層通信,每一層使用自己的協(xié)議掌呜。了解了這些滓玖,然并卵。但是质蕉,這一模型確實(shí)是絕大多數(shù)網(wǎng)絡(luò)編程的基礎(chǔ)势篡,作為抽象類存在的,而TCP/IP協(xié)議棧只是這一模型的一個(gè)具體實(shí)現(xiàn)模暗。
TCP/IP 協(xié)議模型
TCP/IP是Internet的基礎(chǔ)禁悠,是一組協(xié)議的代名詞,包括許多協(xié)議兑宇,組成了TCP/IP協(xié)議棧碍侦。TCP/IP 有四層模型和五層模型之說(shuō),區(qū)別在于數(shù)據(jù)鏈路層是否作為獨(dú)立的一層存在隶糕。個(gè)人傾向于5層模型瓷产,這樣2層和3層的交換設(shè)備更容易弄明白。當(dāng)談到網(wǎng)絡(luò)的2層或3層交換機(jī)的時(shí)候枚驻,就知道指的是那些協(xié)議濒旦。
數(shù)據(jù)是如何傳遞呢?這就要了解網(wǎng)絡(luò)層和傳輸層的協(xié)議再登,我們熟知的IP包結(jié)構(gòu)是這樣的:
IP協(xié)議和IP地址是兩個(gè)不同的概念尔邓,這里沒(méi)有涉及IPV6的。不關(guān)注網(wǎng)絡(luò)安全的話锉矢,對(duì)這些結(jié)構(gòu)不必耳熟能詳?shù)奶菟浴鬏攲邮褂眠@樣的數(shù)據(jù)包進(jìn)行傳輸,傳輸層又分為面向連接的可靠傳輸TCP和數(shù)據(jù)報(bào)UDP沈撞。TCP的包結(jié)構(gòu):
TCP 連接建立的三次握手肯定是必知必會(huì)慷荔,在系統(tǒng)調(diào)優(yōu)的時(shí)候,內(nèi)核中關(guān)于網(wǎng)絡(luò)的相關(guān)參數(shù)與這個(gè)圖息息相關(guān)缠俺。UDP是一種無(wú)連接的傳輸層協(xié)議,提供的是簡(jiǎn)單不可靠的信息傳輸贷岸。協(xié)議結(jié)構(gòu)相對(duì)簡(jiǎn)單壹士,包括源和目標(biāo)的端口號(hào),長(zhǎng)度以及校驗(yàn)和偿警□锞龋基于TCP和UDP的數(shù)據(jù)封裝及解析示例如下:
還是然并卵么?一個(gè)數(shù)據(jù)包的大小了解了,會(huì)發(fā)現(xiàn)什么呢盒使?PayLoad到底是多少崩掘?在設(shè)計(jì)協(xié)議通信的時(shí)候,這些都為我們提供了粒度定義的依據(jù)少办。進(jìn)一步苞慢,通過(guò)一個(gè)例子看看吧。
模型解讀示例
FTP是一個(gè)比較好的例子英妓。為了方便起見(jiàn)挽放,假設(shè)兩條計(jì)算機(jī)分別是A 和 B,將使用FTP 將A上的一個(gè)文件X傳輸?shù)紹上蔓纠。
首先辑畦,計(jì)算機(jī)A和B之間要有物理層的連接,可以是有線比如同軸電纜或者雙絞線通過(guò)RJ-45的電路接口連接腿倚,也可以是無(wú)線連接例如WIFI纯出。先簡(jiǎn)化一下,考慮局域網(wǎng)敷燎,暫不討論路由器和交換機(jī)以及WIFI熱點(diǎn)暂筝。這些物理層的連接建立了比特流的原始傳輸通路。
接下來(lái)懈叹,數(shù)據(jù)鏈路層登場(chǎng)乖杠,建立兩臺(tái)計(jì)算機(jī)的數(shù)據(jù)鏈路。如果A和B所在的網(wǎng)絡(luò)上同時(shí)連接著計(jì)算機(jī)C澄成,D胧洒,E等等,A和B之間如何建立的數(shù)據(jù)鏈路呢墨状?這一過(guò)程就是物理尋址卫漫,A要在眾多的物理連接中找到B,依賴的是計(jì)算機(jī)的物理地址即MAC地址肾砂,對(duì)就是網(wǎng)卡上的MAC地址列赎。以太網(wǎng)采用CSMA/CD方式來(lái)傳輸數(shù)據(jù),數(shù)據(jù)在以太網(wǎng)的局域網(wǎng)中都是以廣播方式傳輸?shù)母淙罚麄€(gè)局域網(wǎng)中的所有節(jié)點(diǎn)都會(huì)收到該幀包吝,只有目標(biāo)MAC地址與自己的MAC地址相同的幀才會(huì)被接收。A通過(guò)差錯(cuò)控制和接入控制找到了B的網(wǎng)卡源葫,建立可靠的數(shù)據(jù)通路诗越。
那IP地址呢? 數(shù)據(jù)鏈路建立起來(lái)了息堂,還需要IP地址么嚷狞?我們FTP 命令中制定的是IP地址而不是MAC地址呀块促?IP地址是邏輯地址,包括網(wǎng)絡(luò)地址和主機(jī)地址床未。如果A和B在不同的局域網(wǎng)中竭翠,中間有著多個(gè)路由器,A需要對(duì)B進(jìn)行邏輯尋址才可以的薇搁。物理地址用于底層的硬件的通信斋扰,邏輯地址用于上層的協(xié)議間的通信。在以太網(wǎng)中:邏輯地址就是IP地址只酥,物理地址就是MAC 地址褥实。在使用中,兩種地址是用一定的算法將他們兩個(gè)聯(lián)系起來(lái)的裂允。所以损离,IP是用來(lái)在網(wǎng)絡(luò)上選擇路由的,在FTP的命令中绝编,IP中的原地址就是A的IP地址僻澎,目標(biāo)地址就是B的IP地址。這應(yīng)該就是網(wǎng)絡(luò)層十饥,負(fù)責(zé)將分組數(shù)據(jù)從源端傳輸?shù)侥康亩恕?/p>
A向B傳輸一個(gè)文件時(shí)窟勃,如果文件中有部分?jǐn)?shù)據(jù)丟失,就可能會(huì)造成在B上無(wú)法正常閱讀或使用逗堵。所以需要一個(gè)可靠的連接秉氧,能夠確保傳輸過(guò)程的完整性,這就是傳輸層的TCP協(xié)議蜒秤,F(xiàn)TP就是建立在TCP之上的汁咏。TCP的三次握手確定了雙方數(shù)據(jù)包的序號(hào)、最大接受數(shù)據(jù)的大小(window)以及MSS(Maximum Segment Size)作媚。TCP利用IP完成尋址攘滩,TCP中的提供了端口號(hào),F(xiàn)TP中目的端口號(hào)一般是21纸泡。傳輸層的端口號(hào)對(duì)應(yīng)主機(jī)進(jìn)程漂问,指本地主機(jī)與遠(yuǎn)程主機(jī)正在進(jìn)行的會(huì)話。
會(huì)話層用來(lái)建立女揭、維護(hù)蚤假、管理應(yīng)用程序之間的會(huì)話,主要功能是對(duì)話控制和同步吧兔,編程中所涉及的session是會(huì)話層的具體體現(xiàn)勤哗。表示層完成數(shù)據(jù)的解編碼,加解密掩驱,壓縮解壓縮等,例如FTP中bin命令,代表了二進(jìn)制傳輸欧穴,即所傳輸層數(shù)據(jù)的格式民逼。 HTTP協(xié)議里body中的Json,XML等都可以認(rèn)為是表示層涮帘。應(yīng)用層就是具體應(yīng)用的本身了拼苍,F(xiàn)TP中的PUT,GET等命令都是應(yīng)用的具體功能特性调缨。
簡(jiǎn)單地疮鲫,物理層到電纜連接,數(shù)據(jù)鏈路層到網(wǎng)卡弦叶,網(wǎng)絡(luò)層路由到主機(jī)俊犯,傳輸層到端口,會(huì)話層維持會(huì)話伤哺,表示層表達(dá)數(shù)據(jù)格式燕侠,應(yīng)用層就是具體FTP中的各種命令功能了。
Socket
了解了7層模型就可以編程了么立莉,拿起編程語(yǔ)言就可以耍了么绢彤?剛開(kāi)始上手嘗試還是可以的,如果要進(jìn)一步蜓耻,老碼農(nóng)覺(jué)得還是看看底層實(shí)現(xiàn)的好茫舶,因?yàn)橐磺袣w根到底都會(huì)歸結(jié)為系統(tǒng)調(diào)用。到了操作系統(tǒng)層面如何看網(wǎng)絡(luò)呢刹淌?Socket登場(chǎng)了饶氏。
在Linux世界,“一切皆文件”芦鳍,操作系統(tǒng)把網(wǎng)絡(luò)讀寫(xiě)作為IO操作嚷往,就像讀寫(xiě)文件那樣,對(duì)外提供出來(lái)的編程接口就是Socket柠衅。所以皮仁,socket(套接字)是通信的基石,是支持TCP/IP協(xié)議網(wǎng)絡(luò)通信的基本操作單元菲宴。socket實(shí)質(zhì)上提供了進(jìn)程通信的端點(diǎn)贷祈。進(jìn)程通信之前,雙方首先必須各自創(chuàng)建一個(gè)端點(diǎn)喝峦,否則是沒(méi)有辦法建立聯(lián)系并相互通信的势誊。一個(gè)完整的socket有一個(gè)本地唯一的socket號(hào),這是由操作系統(tǒng)分配的谣蠢。
從設(shè)計(jì)模式的角度看粟耻, Socket其實(shí)是一個(gè)外觀模式查近,它把復(fù)雜的TCP/IP協(xié)議棧隱藏在Socket接口后面,對(duì)用戶來(lái)說(shuō)挤忙,一組簡(jiǎn)單的Socket接口就是全部霜威。當(dāng)應(yīng)用程序創(chuàng)建一個(gè)socket時(shí),操作系統(tǒng)就返回一個(gè)整數(shù)作為描述符(descriptor)來(lái)標(biāo)識(shí)這個(gè)套接字册烈。然后戈泼,應(yīng)用程序以該描述符為傳遞參數(shù),通過(guò)調(diào)用函數(shù)來(lái)完成某種操作(例如通過(guò)網(wǎng)絡(luò)傳送數(shù)據(jù)或接收輸入的數(shù)據(jù))赏僧。
在許多操作系統(tǒng)中大猛,Socket描述符和其他I/O描述符是集成在一起的,操作系統(tǒng)把socket描述符實(shí)現(xiàn)為一個(gè)指針數(shù)組淀零,這些指針指向內(nèi)部數(shù)據(jù)結(jié)構(gòu)挽绩。進(jìn)一步看,操作系統(tǒng)為每個(gè)運(yùn)行的進(jìn)程維護(hù)一張單獨(dú)的文件描述符表窑滞。當(dāng)進(jìn)程打開(kāi)一個(gè)文件時(shí)琼牧,系統(tǒng)把一個(gè)指向此文件內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針寫(xiě)入文件描述符表,并把該表的索引值返回給調(diào)用者 哀卫。
既然Socket和操作系統(tǒng)的IO操作相關(guān)巨坊,那么各操作系統(tǒng)IO實(shí)現(xiàn)上的差異會(huì)導(dǎo)致Socket編程上的些許不同〈烁模看看我Mac上的Socket.so 會(huì)發(fā)現(xiàn)和CentOS上的還是些不同的趾撵。
進(jìn)程進(jìn)行Socket操作時(shí),也有著多種處理方式共啃,如阻塞式IO占调,非阻塞式IO,多路復(fù)用(select/poll/epoll)移剪,AIO等等究珊。
多路復(fù)用往往在提升性能方面有著重要的作用。select系統(tǒng)調(diào)用的功能是對(duì)多個(gè)文件描述符進(jìn)行監(jiān)視纵苛,當(dāng)有文件描述符的文件讀寫(xiě)操作完成以及發(fā)生異辰虽蹋或者超時(shí),該調(diào)用會(huì)返回這些文件描述符攻人。select 需要遍歷所有的文件描述符取试,就遍歷操作而言,復(fù)雜度是 O(N)怀吻。
epoll相關(guān)系統(tǒng)調(diào)用是在Linux 2.5 后的某個(gè)版本開(kāi)始引入的瞬浓。該系統(tǒng)調(diào)用針對(duì)傳統(tǒng)的select/poll不足,設(shè)計(jì)上作了很大的改動(dòng)蓬坡。select/poll 的缺點(diǎn)在于:
每次調(diào)用時(shí)要重復(fù)地從用戶模式讀入?yún)?shù)猿棉,并重復(fù)地掃描文件描述符磅叛。
每次在調(diào)用開(kāi)始時(shí),要把當(dāng)前進(jìn)程放入各個(gè)文件描述符的等待隊(duì)列铺根。在調(diào)用結(jié)束后宪躯,又把進(jìn)程從各個(gè)等待隊(duì)列中刪除。
epoll 是把 select/poll 單個(gè)的操作拆分為 1 個(gè) epollcreate位迂,多個(gè) epollctrl和一個(gè) wait。此外详瑞,操作系統(tǒng)內(nèi)核針對(duì) epoll 操作添加了一個(gè)文件系統(tǒng)掂林,每一個(gè)或者多個(gè)要監(jiān)視的文件描述符都有一個(gè)對(duì)應(yīng)的inode 節(jié)點(diǎn),主要信息保存在 eventpoll 結(jié)構(gòu)中坝橡。而被監(jiān)視的文件的重要信息則保存在 epitem 結(jié)構(gòu)中泻帮,是一對(duì)多的關(guān)系。由于在執(zhí)行 epollcreate 和 epollctrl 時(shí)计寇,已經(jīng)把用戶模式的信息保存到內(nèi)核了锣杂, 所以之后即便反復(fù)地調(diào)用 epoll_wait,也不會(huì)重復(fù)地拷貝參數(shù)番宁,不會(huì)重復(fù)掃描文件描述符,也不反復(fù)地把當(dāng)前進(jìn)程放入/拿出等待隊(duì)列。
所以双炕,當(dāng)前主流的Server側(cè)Socket實(shí)現(xiàn)大都采用了epoll的方式袖肥,例如Nginx, 在配置文件可以顯式地看到 use epoll
棋电。
網(wǎng)絡(luò)編程
了解了7層協(xié)議模型和操作系統(tǒng)層面的Socket實(shí)現(xiàn)茎截,可以方便我們理解網(wǎng)絡(luò)編程。
在系統(tǒng)架構(gòu)的時(shí)候赶盔,有重要的一環(huán)就是拓?fù)浼軜?gòu)企锌,這里涉及了網(wǎng)絡(luò)等基礎(chǔ)設(shè)施,那么7層協(xié)議下四層就會(huì)有助于我們對(duì)業(yè)務(wù)系統(tǒng)網(wǎng)絡(luò)結(jié)構(gòu)的觀察和判斷于未。在系統(tǒng)設(shè)計(jì)的時(shí)候撕攒,往往采用面向接口的設(shè)計(jì),而接口也往往是基于HTTP協(xié)議的Restful API沉眶。 那接口的粒度就可以將data segment作為一個(gè)約束了打却,同時(shí)可以關(guān)注到移動(dòng)互聯(lián)網(wǎng)中的弱網(wǎng)環(huán)境。
不同的編程語(yǔ)言谎倔,有著不同的框架和庫(kù)柳击,真正的編寫(xiě)網(wǎng)絡(luò)程序代碼并不復(fù)雜,例如片习,用Erlang 中 gen_tcp 用于編寫(xiě)一個(gè)簡(jiǎn)單的Echo服務(wù)器:
Start_echo_server()->
{ok,Listen}= gen_tcp:listen(1234,[binary,{packet,4},{reuseaddr,true},{active,true}]),
{ok,socket}=get_tcp:accept(Listen),
gen_tcp:close(Listen),
loop(Socket).
loop(Socket) ->
receive
{tcp,Socket,Bin} ->
io:format(“serverreceived binary = ~p~n”,[Bin])
Str= binary_to_term(Bin),
io:format(“server (unpacked) ~p~n”,[Str]),
Reply= lib_misc:string2value(Str),
io:format(“serverreplying = ~p~n”,[Reply]),
gen_tcp:send(Socket,term_to_binary(Reply)),
loop(Socket);
{tcp_closed,Socket} ->
Io:format(“ServerSocket closed ~n”)
end.
然而捌肴,寫(xiě)出漂亮的服務(wù)器程序仍然是一件非常吃功夫的事情蹬叭,例如,個(gè)人非常喜歡的python Tornado 代碼, 在ioloop.py 中有對(duì)多路復(fù)用的選擇:
@classmethod
def configurable_default(cls):
if hasattr(select, "epoll"):
from tornado.platform.epoll import EPollIOLoop
return EPollIOLoop
if hasattr(select, "kqueue"):
# Python 2.6+ on BSD or Mac
from tornado.platform.kqueue import KQueueIOLoop
return KQueueIOLoop
from tornado.platform.select import SelectIOLoop
return SelectIOLoop
在HTTPServer.py 中同樣繼承了TCPServer状知,進(jìn)而實(shí)現(xiàn)了HTTP協(xié)議秽五,代碼片段如下:
class HTTPServer(TCPServer, Configurable,
httputil.HTTPServerConnectionDelegate):
...
def initialize(self, request_callback, no_keep_alive=False, io_loop=None,
xheaders=False, ssl_options=None, protocol=None,
decompress_request=False,
chunk_size=None, max_header_size=None,
idle_connection_timeout=None, body_timeout=None,
max_body_size=None, max_buffer_size=None):
self.request_callback = request_callback
self.no_keep_alive = no_keep_alive
self.xheaders = xheaders
self.protocol = protocol
self.conn_params = HTTP1ConnectionParameters(
decompress=decompress_request,
chunk_size=chunk_size,
max_header_size=max_header_size,
header_timeout=idle_connection_timeout or 3600,
max_body_size=max_body_size,
body_timeout=body_timeout)
TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options,
max_buffer_size=max_buffer_size,
read_chunk_size=chunk_size)
self._connections = set()
...
Java網(wǎng)絡(luò)編程基礎(chǔ)
轉(zhuǎn)自并發(fā)編程網(wǎng)https://ifeve.com/
Java 網(wǎng)絡(luò)教程: 基礎(chǔ)
Java提供了非常易用的網(wǎng)絡(luò)API,調(diào)用這些API我們可以很方便的通過(guò)建立TCP/IP或UDP套接字饥悴,在網(wǎng)絡(luò)之間進(jìn)行相互通信坦喘,其中TCP要比UDP更加常用,但在本教程中我們對(duì)這兩種方式都有說(shuō)明西设。
在網(wǎng)站上還有其他三個(gè)與Java網(wǎng)絡(luò)相關(guān)的教程瓣铣,如下:
3.Java服務(wù)器多線程教程 (參與翻譯可以聯(lián)系我們)
盡管Java網(wǎng)絡(luò)API允許我們通過(guò)套接字(Socket)打開(kāi)或關(guān)閉網(wǎng)絡(luò)連接,但所有的網(wǎng)絡(luò)通信均是基于Java IO類 InputStream和OutputStream實(shí)現(xiàn)的贷揽。
此外棠笑,我們還可以使用Java NIO API中相關(guān)的網(wǎng)絡(luò)類,用法與Java網(wǎng)絡(luò)API基本類似禽绪,Java NIO API可以以非阻塞模式工作蓖救,在某些特定的場(chǎng)景中使用非阻塞模式可以獲得較大的性能提升。
Java TCP網(wǎng)絡(luò)基礎(chǔ)
通常情況下印屁,客戶端打開(kāi)一個(gè)連接到服務(wù)器端的TCP/IP連接循捺,然后客戶端開(kāi)始與服務(wù)器之間通信,當(dāng)通信結(jié)束后客戶端關(guān)閉連接库车,過(guò)程如下圖所示:
[圖片上傳失敗...(image-11fccb-1566521906361)]ClientServerOpen ConnectionSend RequestReceive ResponseClose Connection
?客戶端通過(guò)一個(gè)已打開(kāi)的連接可以發(fā)送不止一個(gè)請(qǐng)求巨柒。事實(shí)上在服務(wù)器處于接收狀態(tài)下,客戶端可以發(fā)送盡可能多的數(shù)據(jù)柠衍,服務(wù)器也可以主動(dòng)關(guān)閉連接洋满。
Java中Socket類和ServerSocket類
當(dāng)客戶端想要打開(kāi)一個(gè)連接到服務(wù)器的TCP/IP連接時(shí),就要使用到Java Socket類珍坊。socket類只需要被告知連接的IP地址和TCP端口牺勾,其余的都有Java實(shí)現(xiàn)。
假如我們想要打開(kāi)一個(gè)監(jiān)聽(tīng)服務(wù)阵漏,來(lái)監(jiān)聽(tīng)客戶端連接某些指定TCP端口的連接驻民,那就需要使用Java ServerSocket類。當(dāng)客戶端通過(guò)Socket連接服務(wù)器端的ServerSocket監(jiān)聽(tīng)時(shí)履怯,服務(wù)器端會(huì)指定這個(gè)連接的一個(gè)Socket回还,此時(shí)客戶端與服務(wù)器端間的通信就變成Socket與Socket之間的通信。
關(guān)于Socket類和ServerSocket類會(huì)在后面的文章中有詳細(xì)的介紹叹洲。
Java UDP網(wǎng)絡(luò)基礎(chǔ)
UDP的工作方式與TCP相比略有不同柠硕。使用UDP通信時(shí),在客戶端與服務(wù)器之間并沒(méi)有建立連接的概念,客戶端發(fā)送到服務(wù)器的數(shù)據(jù)蝗柔,服務(wù)器可能(也可能并沒(méi)有)收到這些數(shù)據(jù)闻葵,而且客戶端也并不知道這些數(shù)據(jù)是否被服務(wù)器成功接收。當(dāng)服務(wù)器向客戶端發(fā)送數(shù)據(jù)時(shí)也是如此癣丧。
正因?yàn)槭遣豢煽康臄?shù)據(jù)傳輸槽畔,UDP相比與TCP來(lái)說(shuō)少了很多的協(xié)議開(kāi)銷。
在某些場(chǎng)景中胁编,使用無(wú)連接的UDP要優(yōu)于TCP厢钧,這些在文章Java UDP DatagramSocket類介紹中會(huì)有更多介紹。
當(dāng)我們想要在Java中使用TCP/IP通過(guò)網(wǎng)絡(luò)連接到服務(wù)器時(shí)掏呼,就需要?jiǎng)?chuàng)建java.net.Socket對(duì)象并連接到服務(wù)器坏快。假如希望使用Java NIO,也可以創(chuàng)建Java NIO中的SocketChannel對(duì)象憎夷。
Java網(wǎng)絡(luò)教程之Socket
創(chuàng)建Socket
下面的示例代碼是連接到IP地址為78.64.84.171服務(wù)器上的80端口,這臺(tái)服務(wù)器就是我們的Web服務(wù)器(www.jenkov.com)昧旨,而80端口就是Web服務(wù)端口拾给。
Socket socket = new Socket("78.46.84.171", 80);
我們也可以像如下示例中使用域名代替IP地址:
Socket socket = new Socket("jenkov.com", 80);
Socket發(fā)送數(shù)據(jù)
要通過(guò)Socket發(fā)送數(shù)據(jù),我們需要獲取Socket的輸出流(OutputStream)兔沃,示例代碼如下:
Socket socket = new Socket("jenkov.com", 80);
OutputStream out = socket.getOutputStream();
out.write("some data".getBytes());
out.flush();
out.close();
socket.close();
代碼非常簡(jiǎn)單蒋得,但是想要通過(guò)網(wǎng)絡(luò)將數(shù)據(jù)發(fā)送到服務(wù)器端,一定不要忘記調(diào)用flush()方法乒疏。操作系統(tǒng)底層的TCP/IP實(shí)現(xiàn)會(huì)先將數(shù)據(jù)放入一個(gè)更大的數(shù)據(jù)緩存塊中额衙,而緩存塊的大小是與TCP/IP的數(shù)據(jù)包大小相適應(yīng)的。(譯者注:調(diào)用flush()方法只是將數(shù)據(jù)寫(xiě)入操作系統(tǒng)緩存中怕吴,并不保證數(shù)據(jù)會(huì)立即發(fā)送)
Socket讀取數(shù)據(jù)
從Socket中讀取數(shù)據(jù)窍侧,我們就需要獲取Socket的輸入流(InputStream),代碼如下:
Socket socket = new Socket("jenkov.com", 80);
InputStream in = socket.getInputStream();
int data = in.read();
//... read more data...
in.close();
socket.close();
代碼也并不復(fù)雜转绷,但需要注意的是伟件,從Socket的輸入流中讀取數(shù)據(jù)并不能讀取文件那樣,一直調(diào)用read()方法直到返回-1為止议经,因?yàn)閷?duì)Socket而言斧账,只有當(dāng)服務(wù)端關(guān)閉連接時(shí),Socket的輸入流才會(huì)返回-1煞肾,而是事實(shí)上服務(wù)器并不會(huì)不停地關(guān)閉連接咧织。假設(shè)我們想要通過(guò)一個(gè)連接發(fā)送多個(gè)請(qǐng)求,那么在這種情況下關(guān)閉連接就顯得非常愚蠢籍救。
因此习绢,從Socket的輸入流中讀取數(shù)據(jù)時(shí)我們必須要知道需要讀取的字節(jié)數(shù),這可以通過(guò)讓服務(wù)器在數(shù)據(jù)中告知發(fā)送了多少字節(jié)來(lái)實(shí)現(xiàn)钧忽,也可以采用在數(shù)據(jù)末尾設(shè)置特殊字符標(biāo)記的方式連實(shí)現(xiàn)毯炮。
關(guān)閉Socket
當(dāng)使用完Socket后我們必須將Socket關(guān)閉逼肯,斷開(kāi)與服務(wù)器之間的連接。關(guān)閉Socket只需要調(diào)用Socket.close()方法即可桃煎,代碼如下:
Socket socket = new Socket("jenkov.com", 80);
socket.close();
Java 網(wǎng)絡(luò)教程: ServerSocket
用java.net.ServerSocket實(shí)現(xiàn)java服務(wù)通過(guò)TCP/IP監(jiān)聽(tīng)客戶端連接篮幢,你也可以用Java NIO 來(lái)代替java網(wǎng)絡(luò)標(biāo)準(zhǔn)API,這時(shí)候需要用到 ServerSocketChannel为迈。
創(chuàng)建一個(gè) ServerSocket連接
以下是一個(gè)創(chuàng)建ServerSocket類來(lái)監(jiān)聽(tīng)9000端口的一個(gè)簡(jiǎn)單的代碼
ServerSocket serverSocket = new ServerSocket(9000);
監(jiān)聽(tīng)請(qǐng)求的連接
要獲取請(qǐng)求的連接需要用ServerSocket.accept()方法三椿。該方法返回一個(gè)Socket類,該類具有普通java Socket類的所有特性葫辐。代碼如下:
ServerSocket serverSocket = new ServerSocket(9000); boolean isStopped = false;while(!isStopped){ Socket clientSocket = serverSocket.accept(); //do something with clientSocket}
對(duì)每個(gè)調(diào)用了accept()方法的類都只獲得一個(gè)請(qǐng)求的連接搜锰。
另外,請(qǐng)求的連接也只能在線程運(yùn)行的server中調(diào)用了accept()方法之后才能夠接受請(qǐng)求耿战。線程運(yùn)行在server中其它所有的方法上的時(shí)候都不能接受客戶端的連接請(qǐng)求蛋叼。所以”接受”請(qǐng)求的線程通常都會(huì)把Socket的請(qǐng)求連接放入一個(gè)工作線程池中,然后再和客戶端連接剂陡。更多關(guān)于多線程服務(wù)端設(shè)計(jì)的文檔請(qǐng)參考 java多線程服務(wù)
關(guān)閉客戶端Socket
客戶端請(qǐng)求執(zhí)行完畢狈涮,并且不會(huì)再有該客戶端的其它請(qǐng)求發(fā)送過(guò)來(lái)的時(shí)候,就需要關(guān)閉Socket連接鸭栖,這和關(guān)閉一個(gè)普通的客戶端Socket連接一樣歌馍。如下代碼來(lái)執(zhí)行關(guān)閉:
socket.close();
關(guān)閉服務(wù)端Sockets
要關(guān)閉服務(wù)的時(shí)候需要關(guān)掉 ServerSocket連接。通過(guò)執(zhí)行如下代碼:
serverSocket.close();
Java網(wǎng)絡(luò)編程:UDP DatagramSocket
DatagramSocket類是java通過(guò)UDP通信的途徑晕鹊。UDP仍位于IP層的上面松却。 你可以用DatagramSocket類發(fā)送和接收UDP數(shù)據(jù)包。
UDP 和TCP
UDP工作方式和TCP有點(diǎn)不同溅话。當(dāng)你通過(guò)TCP發(fā)送數(shù)據(jù)時(shí)晓锻,你先要?jiǎng)?chuàng)建連接。一旦TCP連接建立了公荧,TCP會(huì)保證你的數(shù)據(jù)傳遞到對(duì)端带射,否則它將告訴你已發(fā)生的錯(cuò)誤。
僅僅用UDP來(lái)發(fā)送數(shù)據(jù)包(datagrams)到網(wǎng)絡(luò)間的某個(gè)IP地址循狰。你不能保證數(shù)據(jù)會(huì)不會(huì)到達(dá)窟社。你也不能保證UDP數(shù)據(jù)包到達(dá)接收方的指令。這意味著UDP比TCP有更少的協(xié)議開(kāi)銷(無(wú)完整檢查流)绪钥。
當(dāng)數(shù)據(jù)傳輸過(guò)程中不在乎數(shù)據(jù)包是否丟失時(shí)灿里,UDP就比較適合這樣的數(shù)據(jù)傳輸。比如程腹,網(wǎng)上的電視信號(hào)的傳輸匣吊。你希望信號(hào)到達(dá)客戶端時(shí)盡可能地接近直播。因此,如果丟失一兩個(gè)畫(huà)面色鸳,你一點(diǎn)都不在乎社痛。你不希望直播延遲,值想確保所有的畫(huà)面顯示在客戶端命雀。你寧可跳過(guò)丟失的畫(huà)面蒜哀,希望一直看到最新的畫(huà)面。
這種情況也會(huì)發(fā)生在網(wǎng)上攝像機(jī)直播節(jié)目中吏砂。誰(shuí)會(huì)關(guān)心過(guò)去發(fā)生的什么撵儿,你只想顯示當(dāng)前的畫(huà)面。你不希望比實(shí)際情況慢30s結(jié)束狐血,只因?yàn)槟阆肟吹綌z像機(jī)顯示給觀眾的所有畫(huà)面淀歇。這跟攝像機(jī)錄像有點(diǎn)不同。從攝像機(jī)錄制畫(huà)面到磁盤(pán)匈织,你不希望丟失一個(gè)畫(huà)面浪默。你可能還希望有點(diǎn)延遲,如果有重大的情況發(fā)生缀匕,就不需要倒回去檢查畫(huà)面浴鸿。
通過(guò)DatagramSocket發(fā)送數(shù)據(jù)
通過(guò)Java的DatagramSocket類發(fā)送數(shù)據(jù),首先需要?jiǎng)?chuàng)建DatagramPacket弦追。如下:
1 |
buffer = ``new byte``[``65508``];
|
---|
2 |
---|
3 |
InetAddress address = ``new DatagramPacket(buffer, buffer.length, address,``9000``);
|
---|
字節(jié)緩沖塊(字節(jié)數(shù)組)就是UDP數(shù)據(jù)包中用來(lái)發(fā)送的數(shù)據(jù)。緩沖塊上限長(zhǎng)度為65508字節(jié)花竞,是單一UDP數(shù)據(jù)包發(fā)送的最大的數(shù)據(jù)量劲件。
數(shù)據(jù)包構(gòu)造函數(shù)的長(zhǎng)度就是緩存塊中用于發(fā)送的數(shù)據(jù)的長(zhǎng)度。所有多于最大容量的數(shù)據(jù)都會(huì)被忽略约急。
包含節(jié)點(diǎn)(例如服務(wù)器)地址的InetAddress實(shí)例攜帶節(jié)點(diǎn)(如服務(wù)器)的地址發(fā)送的UDP數(shù)據(jù)包零远。InetAddress類表示一個(gè)ip地址(網(wǎng)絡(luò)地址)。getByName()方法返回帶有一個(gè)InetAddress實(shí)例厌蔽,該實(shí)例帶有匹配主機(jī)名的ip地址牵辣。
端口參數(shù)是UDP端口服務(wù)器用來(lái)接收正在監(jiān)聽(tīng)的數(shù)據(jù)。UDP端口和TCP端口是不一樣的奴饮。一臺(tái)電腦同時(shí)有不同的進(jìn)程監(jiān)聽(tīng)UDP和TCP 80端口纬向。
為了發(fā)送數(shù)據(jù)包,你需要?jiǎng)?chuàng)建DatagramSocket來(lái)發(fā)送數(shù)據(jù)戴卜。如下:
1 |
DatagramSocketdatagramSocket = ``new DatagramSocket();
|
---|
調(diào)用send()方法發(fā)送數(shù)據(jù)逾条,像這樣:
1 |
datagramSocket.send(packet); |
---|
完整示例:
1 |
DatagramSocket datagramSocket = ``new DatagramSocket();
|
---|
2 |
---|
3 |
byte``[] buffer = ``"0123456789"``.getBytes(); |
---|
4 |
---|
5 |
InetAddress receiverAddress = InetAddress.getLocalHost(); |
---|
6 |
---|
7 |
DataframPacket packet = ``new DatagramPacket( buffer, buffer.length, receiverAddress,``80``);
|
---|
8 |
datagramSocket.send(packet); |
---|
從DatagramSocket獲取數(shù)據(jù)
從DataframSocket獲取數(shù)據(jù)時(shí),首先創(chuàng)建DataframPacket ,然后通過(guò)DatagramSocket類的receive()方法接收數(shù)據(jù)投剥。例如:
1 |
DatagramSocket datagramSocket = ``new DatagramSocket(``80``);
|
---|
2 |
---|
3 |
byte``[] buffer = ``new byte``[``10``];
|
---|
4 |
---|
5 |
DatagramPacket packet = ``new DatagramPacket(buffer, buffer.length);
|
---|
6 |
---|
7 |
datagramSocket.receive(packet); |
---|
注意DatagramSocket是如何通過(guò)傳遞參數(shù)80到它的構(gòu)造器初始化的师脂。這個(gè)參數(shù)是UDP端口的DatagramSocket用來(lái)接收UDP數(shù)據(jù)包的。像之前提到的,TCP和UDP端口是不一樣的吃警,也不重疊糕篇。你可以有倆個(gè)不同的進(jìn)程同時(shí)在端口80監(jiān)聽(tīng)TCP和UDP,沒(méi)有任何沖突酌心。
第二拌消,字節(jié)緩存塊和DatagramPacket創(chuàng)建了。注意DatagramPacket是沒(méi)有關(guān)于節(jié)點(diǎn)如何發(fā)送數(shù)據(jù)的信息的谒府,當(dāng)創(chuàng)建一個(gè)方數(shù)據(jù)的DatagramPacket時(shí)拼坎,它會(huì)直到這個(gè)信息。這就是為什么我們會(huì)用DatagramPacket接收數(shù)據(jù)而不是發(fā)送數(shù)據(jù)完疫。因此沒(méi)有目標(biāo)地址是必須的泰鸡。
最后,調(diào)用DatagramSocket的receive()方法壳鹤。直到數(shù)據(jù)包接收到為止盛龄,這個(gè)方法都是阻塞的。
接收的數(shù)據(jù)位于DatagramPacket的字節(jié)緩沖塊芳誓。緩沖塊可以通過(guò)調(diào)用getData()獲得:
1 |
byte``[] buffer = packet.getData(); |
---|
緩沖塊接收了多少的數(shù)據(jù)需要你去找出來(lái)余舶。你用的協(xié)議應(yīng)該定義每個(gè)UDP包發(fā)多少數(shù)據(jù),活著定義一個(gè)你能找到的數(shù)據(jù)結(jié)束標(biāo)記锹淌。
一個(gè)真正的服務(wù)端程序可能會(huì)在一個(gè)loop中調(diào)用receive()方法匿值,傳送所有接收到的DatagramPacket到工作的線程池中,就像TCP服務(wù)器處理請(qǐng)求連接一樣(查看Java Multithreaded Servers獲取更多詳情)
Java網(wǎng)絡(luò)教程:URL + URLConnection
- HTTP GET和POST
- 從URLs到本地文件
在java.net包中包含兩個(gè)有趣的類:URL類和URLConnection類赂摆。這兩個(gè)類可以用來(lái)創(chuàng)建客戶端到web服務(wù)器(HTTP服務(wù)器)的連接挟憔。下面是一個(gè)簡(jiǎn)單的代碼例子:
1 |
URL url = ``new URL(``"[http://jenkov.com](https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fjenkov.com%2F)"``);
|
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
InputStream input = urlConnection.getInputStream(); |
---|
4 |
int data = input.read();
|
---|
5 |
while``(data != -``1``){ |
---|
6 |
System.out.print((``char``) data); |
---|
7 |
data = input.read(); |
---|
8 |
} |
---|
9 |
input.close(); |
---|
HTTP GET和POST
默認(rèn)情況下URLConnection發(fā)送一個(gè)HTTP GET請(qǐng)求到web服務(wù)器。如果你想發(fā)送一個(gè)HTTP POST請(qǐng)求烟号,要調(diào)用URLConnection.setDoOutput(true)方法绊谭,如下:
1 |
URL url = ``new URL(``"[http://jenkov.com](https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fjenkov.com%2F)"``);
|
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
urlConnection.setDoOutput(``true``); |
---|
一旦你調(diào)用了setDoOutput(true),你就可以打開(kāi)URLConnection的OutputStream汪拥,如下:
1 |
OutputStream output = urlConnection.getOutputStream(); |
---|
你可以使用這個(gè)OutputStream向相應(yīng)的HTTP請(qǐng)求中寫(xiě)任何數(shù)據(jù)达传,但你要記得將其轉(zhuǎn)換成URL編碼(關(guān)于URL編碼的解釋,自行Google)(譯者注:具體名字是:application/x-www-form-urlencoded MIME 格式編碼)迫筑。
當(dāng)你寫(xiě)完數(shù)據(jù)的時(shí)候要記得關(guān)閉OutputStream宪赶。
從URLs到本地文件
URL也被叫做統(tǒng)一資源定位符。如果你的代碼不關(guān)心文件是來(lái)自網(wǎng)絡(luò)還是來(lái)自本地文件系統(tǒng)铣焊,URL類是另外一種打開(kāi)文件的方式逊朽。
下面是一個(gè)如何使用URL類打開(kāi)一個(gè)本地文件系統(tǒng)文件的例子:
1 |
URL url = ``new URL(``"file:/c:/data/test.txt"``);
|
---|
2 |
URLConnection urlConnection = url.openConnection(); |
---|
3 |
InputStream input = urlConnection.getInputStream(); |
---|
4 |
int data = input.read();
|
---|
5 |
while``(data != -``1``){ |
---|
6 |
System.out.print((``char``) data); |
---|
7 |
data = input.read(); |
---|
8 |
} |
---|
9 |
input.close(); |
---|
注意:這和通過(guò)HTTP訪問(wèn)一個(gè)web服務(wù)器上的文件的唯一不同處就是URL:”file:/c:/data/test.txt”。
Java網(wǎng)絡(luò)教程:JarURLConnection
Java的JarURLConnection類用來(lái)連接Java Jar文件曲伊。一旦連接上叽讳,你可以獲取Jar文件的信息追他。一個(gè)簡(jiǎn)單的例子如下:
01 |
String urlString = ``"[http://butterfly.jenkov.com/](https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fbutterfly.jenkov.com%2F)" |
---|
02 |
+ ``"container/download/" |
---|
03 |
+ ``"jenkov-butterfly-container-2.9.9-beta.jar"``; |
---|
04 |
---|
05 |
URL jarUrl = ``new URL(urlString);
|
---|
06 |
JarURLConnection connection = ``new JarURLConnection(jarUrl);
|
---|
07 |
---|
08 |
Manifest manifest = connection.getManifest(); |
---|
09 |
---|
10 |
JarFile jarFile = connection.getJarFile(); |
---|
11 |
//do something with Jar file... |
---|
Java 網(wǎng)絡(luò)教程: InetAddress
- 創(chuàng)建一個(gè) InetAddress 實(shí)例
- InetAddress 的內(nèi)部方法
InetAddress 是 Java 對(duì) IP 地址的封裝。這個(gè)類的實(shí)例經(jīng)常和 UDP DatagramSockets 和 Socket岛蚤,ServerSocket 類一起使用邑狸。
創(chuàng)建一個(gè) InetAddress 實(shí)例
InetAddress 沒(méi)有公開(kāi)的構(gòu)造方法,因此你必須通過(guò)一系列靜態(tài)方法中的某一個(gè)來(lái)獲取它的實(shí)例涤妒。
<!–more–>
下面是為一個(gè)域名實(shí)例化 InetAddres 類的例子:
InetAddress address = InetAddress.getByName("jenkov.com");
當(dāng)然也會(huì)有為匹配某個(gè) IP 地址來(lái)實(shí)例化一個(gè) InetAddress:
InetAddress address = InetAddress.getByName("78.46.84.171");
另外单雾,它還有通過(guò)獲取本地 IP 地址的來(lái)獲取 InetAddress 的方法(正在運(yùn)行程序的那臺(tái)機(jī)器)
InetAddress address = InetAddress.getLocalHost();
InetAddress 內(nèi)部方法
InetAddress 類還擁有大量你可以調(diào)用的其它方法。例如:你可以通過(guò)調(diào)用getAddress()方法來(lái)獲取 IP 地址的 byte 數(shù)組她紫。如果要了解更多的方法硅堆,最簡(jiǎn)單的方式就是讀 JavaDoc 文檔中關(guān)于 InetAddress 類的部分。
Java網(wǎng)絡(luò)教程:Protocol Design
如果設(shè)計(jì)一個(gè)客戶端到服務(wù)器的系統(tǒng)贿讹,那么同時(shí)也需要設(shè)計(jì)客戶端和服務(wù)器之間的通信協(xié)議渐逃。當(dāng)然,有時(shí)候協(xié)議已經(jīng)為你決定好了民褂,比如HTTP茄菊、XML_RPC(http response 的 body 使用xml)、或者SOAP(也是http response 的 body 使用xml)赊堪。設(shè)計(jì)客戶端到服務(wù)端協(xié)議的時(shí)候面殖,一旦協(xié)議決定開(kāi)啟一會(huì)兒,來(lái)看一些你必須考慮的地方:
1. 客戶端到服務(wù)端的往返通訊
2.區(qū)分請(qǐng)求結(jié)束和響應(yīng)結(jié)束哭廉。
3.防火墻穿透
客戶端-服務(wù)端往返
當(dāng)客戶端和服務(wù)端通信脊僚,執(zhí)行操作時(shí),他們?cè)诮粨Q信息遵绰。比如吃挑,客戶端執(zhí)行一個(gè)服務(wù)請(qǐng)求,服務(wù)端嘗試完成這個(gè)請(qǐng)求街立,發(fā)回響應(yīng)告訴客戶端結(jié)果。這種客戶端和服務(wù)端的信息交換就叫做往返埠通。示意圖如下:
當(dāng)一個(gè)計(jì)算機(jī)(客戶端或者服務(wù)端)在網(wǎng)絡(luò)中發(fā)送數(shù)據(jù)到另一個(gè)計(jì)算機(jī)時(shí)赎离,從數(shù)據(jù)發(fā)送到另一端接收數(shù)據(jù)完會(huì)花費(fèi)一定時(shí)間。這就是數(shù)據(jù)在網(wǎng)絡(luò)間的傳送的時(shí)間花費(fèi)端辱。這個(gè)時(shí)間叫做延遲梁剔。
協(xié)議中含有越多的往返,協(xié)議變得越慢舞蔽,延遲特別高荣病。HTTP協(xié)議只包含一個(gè)單獨(dú)的響應(yīng)來(lái)執(zhí)行服務(wù)。換句話說(shuō)就是一個(gè)單獨(dú)的往返渗柿。另一方面个盆,在一封郵件發(fā)送前脖岛,SMTP協(xié)議包含了幾個(gè)客戶端和服務(wù)端的往返。
在協(xié)議中有多個(gè)往返的原因是:有大量的數(shù)據(jù)從客戶端發(fā)送到服務(wù)端颊亮。這種情況下你有2個(gè)選擇:
1.在分開(kāi)往返中發(fā)送頭信息柴梆;
2.將消息分成更小的數(shù)據(jù)塊。
如果服務(wù)端能完成頭信息的一些初始驗(yàn)證 终惑,那么分開(kāi)發(fā)送頭信息是很明智的绍在。如果頭信息是空白的,發(fā)送大量數(shù)據(jù)本身就是浪費(fèi)資源雹有。
在傳輸大量數(shù)據(jù)時(shí)偿渡,如果網(wǎng)絡(luò)連接失敗了,得從頭開(kāi)始重新發(fā)送數(shù)據(jù)霸奕。數(shù)據(jù)分割發(fā)送時(shí)溜宽,只需要在網(wǎng)絡(luò)連接失敗處重新發(fā)送數(shù)據(jù)塊。已經(jīng)發(fā)送成功的數(shù)據(jù)塊不需要重新發(fā)送铅祸。
區(qū)分請(qǐng)求結(jié)束和響應(yīng)結(jié)束
如果協(xié)議容許在同一個(gè)連接中發(fā)送多個(gè)請(qǐng)求坑质,需要一個(gè)讓服務(wù)端知道當(dāng)前請(qǐng)求何時(shí)結(jié)束、下一個(gè)請(qǐng)求何時(shí)開(kāi)始临梗∥卸螅客戶端也需要知道一個(gè)響應(yīng)何時(shí)結(jié)束了,下一個(gè)響應(yīng)何時(shí)開(kāi)始盟庞。
對(duì)于請(qǐng)求有2個(gè)方法區(qū)分結(jié)束:
1.在請(qǐng)求的開(kāi)始處發(fā)送請(qǐng)求的字長(zhǎng)
2.在請(qǐng)求數(shù)據(jù)的最后發(fā)送一個(gè)結(jié)束標(biāo)記吃沪。
HTTP用第一個(gè)機(jī)制。在請(qǐng)求頭中 發(fā)送了“Content-Length”什猖。請(qǐng)求頭會(huì)告訴服務(wù)端在頭文件后有多少字節(jié)是屬于請(qǐng)求的票彪。
這個(gè)模型的優(yōu)勢(shì)在于沒(méi)有請(qǐng)求結(jié)束標(biāo)志的開(kāi)銷。為了避免數(shù)據(jù)看上去像請(qǐng)求結(jié)束標(biāo)志不狮,也不需要對(duì)數(shù)據(jù)體進(jìn)行編碼降铸。
第一個(gè)方法的劣勢(shì):在數(shù)據(jù)傳輸前,發(fā)送者必須知道多少字節(jié)數(shù)將被傳輸摇零。如果數(shù)據(jù)時(shí)動(dòng)態(tài)生成的推掸,在發(fā)送前,首先你得緩存所有的數(shù)據(jù)驻仅,這樣才能計(jì)算出數(shù)據(jù)的字節(jié)數(shù)谅畅。
運(yùn)用請(qǐng)求結(jié)束標(biāo)志時(shí),不需要知道發(fā)送了多少字節(jié)數(shù)噪服。只需要知道請(qǐng)求結(jié)束標(biāo)志在數(shù)據(jù)的末尾毡泻。當(dāng)然,必須確認(rèn)已發(fā)送的數(shù)據(jù)中不包含會(huì)導(dǎo)致請(qǐng)求結(jié)束標(biāo)志錯(cuò)誤的數(shù)據(jù)粘优〕鹞叮可以這樣做:
可以說(shuō)請(qǐng)求結(jié)束標(biāo)志是字節(jié)值255呻顽。當(dāng)然數(shù)據(jù)可能包含值255。因此邪铲,對(duì)數(shù)據(jù)中包含值255的每一個(gè)字節(jié)添加一個(gè)額外的字節(jié)芬位,還有值255。結(jié)束請(qǐng)求標(biāo)志被從字節(jié)值255到255之后的值為0带到。如下編碼:
255 in data –>255昧碉, 255
end-of-request –> 255, 0
這種255,0的序列永遠(yuǎn)不會(huì)出現(xiàn)在數(shù)據(jù)中揽惹,因?yàn)槟惆阉械闹?55變成了255,255被饿。同時(shí),255,255,0也不會(huì)被錯(cuò)認(rèn)為255,0搪搏。255,255被理解成在一起的狭握,0是單獨(dú)的。
防火墻穿透
比起HTTP協(xié)議疯溺,大多數(shù)防火墻會(huì)攔截所有的其他通信论颅。因此把協(xié)議放在HTTP的上層是個(gè)好方法,像XML-RPC,SOAP和REST也可以這樣做囱嫩。
協(xié)議置于HTTP的上層恃疯,在客戶端和服務(wù)端的HTTP請(qǐng)求和響應(yīng)中可以來(lái)回發(fā)送數(shù)據(jù)。記住墨闲,HTTP請(qǐng)求和響應(yīng)不止包含text或者HTML今妄。也可以在里面發(fā)送二進(jìn)制數(shù)據(jù)。
將請(qǐng)求放置在HTTP協(xié)議上鸳碧,唯一有點(diǎn)奇怪的是:HTTP請(qǐng)求必須包含一個(gè)“主機(jī)”頭字段盾鳞。如果你在HTTP協(xié)議上設(shè)計(jì)P2P協(xié)議,同樣的人最可能不會(huì)運(yùn)行多個(gè)“主機(jī)”瞻离。在這種情況下需要頭字段是不必要的開(kāi)銷(但是個(gè)小開(kāi)銷)腾仅。