老曹眼中的網(wǎng)絡(luò)編程基礎(chǔ)
我們是幸運(yùn)的,因?yàn)槲覀儞碛芯W(wǎng)絡(luò)。網(wǎng)絡(luò)是一個(gè)神奇的東西,它改變了你和我的生活方式,改變了整個(gè)世界。 然而,網(wǎng)絡(luò)的無標(biāo)度和小世界特性使得它又是復(fù)雜的,無所不在,無所不能,以致于我們無法區(qū)分甚至無法描述。
對(duì)于一個(gè)碼農(nóng)而言,了解網(wǎng)絡(luò)的基礎(chǔ)知識(shí)可能還是從了解定義開始,認(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)中,由若干元件組成的用來使電信號(hào)按一定要求傳輸?shù)碾娐坊蜻@種電路的部分,叫網(wǎng)絡(luò)。 |
作為一名從事過TMN開發(fā)的通信專業(yè)畢業(yè)生,固執(zhí)地認(rèn)為網(wǎng)絡(luò)是從通信系統(tǒng)中誕生的。通信是人與人之間通過某種媒介進(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)行分類...... 滿滿的回憶,還是留在書架上吧。
網(wǎng)絡(luò)的概念外延被不斷的放大著,抽象的思維能力是人們創(chuàng)新乃至創(chuàng)造的根源。網(wǎng)絡(luò)用來表示諸多對(duì)象及其相互聯(lián)系,數(shù)學(xué)上的圖,物理學(xué)上的模型,交通網(wǎng)絡(luò),人際網(wǎng)絡(luò),城市網(wǎng)絡(luò)等等,總之,網(wǎng)絡(luò)被總結(jié)成從同類問題中抽象出來用數(shù)學(xué)中的圖論科學(xué)來表達(dá)并研究的一種模型。
很多伙伴認(rèn)為,了解這些之后呢,然并卵。我們關(guān)心的只是計(jì)算機(jī)網(wǎng)絡(luò),計(jì)算機(jī)網(wǎng)絡(luò)是用通信線路和設(shè)備將分布在不同地點(diǎn)的多臺(tái)計(jì)算機(jī)系統(tǒng)互相連接起來,按照網(wǎng)絡(luò)協(xié)議,分享軟硬件功能,最終實(shí)現(xiàn)資源共享的系統(tǒng)。特別的,我們談到的網(wǎng)絡(luò)只是互聯(lián)網(wǎng)——Internet,或者移動(dòng)互聯(lián)網(wǎng),需要的是寫互連網(wǎng)應(yīng)用程序。但是,一位工作了五六年的編程高手曾對(duì)我說,現(xiàn)在終于了解到基礎(chǔ)知識(shí)有多重要,技術(shù)在不斷演進(jìn),而相對(duì)不變的就是那些原理和編程模型了。
老碼農(nóng)深以為然,編程實(shí)踐就是從具體到抽象,再到具體,循環(huán)往復(fù),螺旋式上升的過程。了解前世今生,只是為了可能觸摸到“勢(shì)”?;A(chǔ)越扎實(shí),建筑就會(huì)越有想象的空間。 對(duì)于網(wǎng)絡(luò)編程的基礎(chǔ),大概要從OSI的七層協(xié)議模型開始了。
七層模型
七層模型(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ù)、斷開物理連接。
- 數(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 有四層模型和五層模型之說,區(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è)不同的概念,這里沒有涉及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是一種無連接的傳輸層協(xié)議,提供的是簡(jiǎn)單不可靠的信息傳輸。協(xié)議結(jié)構(gòu)相對(duì)簡(jiǎn)單,包括源和目標(biāo)的端口號(hào),長(zhǎng)度以及校驗(yàn)和?;赥CP和UDP的數(shù)據(jù)封裝及解析示例如下:
還是然并卵么?一個(gè)數(shù)據(jù)包的大小了解了,會(huì)發(fā)現(xiàn)什么呢?PayLoad到底是多少?在設(shè)計(jì)協(xié)議通信的時(shí)候,這些都為我們提供了粒度定義的依據(jù)。進(jìn)一步,通過一個(gè)例子看看吧。
模型解讀示例
FTP是一個(gè)比較好的例子。為了方便起見,假設(shè)兩條計(jì)算機(jī)分別是A 和 B,將使用FTP 將A上的一個(gè)文件X傳輸?shù)紹上。
首先,計(jì)算機(jī)A和B之間要有物理層的連接,可以是有線比如同軸電纜或者雙絞線通過RJ-45的電路接口連接,也可以是無線連接例如WIFI。先簡(jiǎn)化一下,考慮局域網(wǎng),暫不討論路由器和交換機(jī)以及WIFI熱點(diǎn)。這些物理層的連接建立了比特流的原始傳輸通路。
接下來,數(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ù)鏈路呢?這一過程就是物理尋址,A要在眾多的物理連接中找到B,依賴的是計(jì)算機(jī)的物理地址即MAC地址,對(duì)就是網(wǎng)卡上的MAC地址。以太網(wǎng)采用CSMA/CD方式來傳輸數(shù)據(jù),數(shù)據(jù)在以太網(wǎng)的局域網(wǎng)中都是以廣播方式傳輸?shù)?,整個(gè)局域網(wǎng)中的所有節(jié)點(diǎn)都會(huì)收到該幀,只有目標(biāo)MAC地址與自己的MAC地址相同的幀才會(huì)被接收。A通過差錯(cuò)控制和接入控制找到了B的網(wǎng)卡,建立可靠的數(shù)據(jù)通路。
那IP地址呢? 數(shù)據(jù)鏈路建立起來了,還需要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)系起來的。所以,IP是用來在網(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上無法正常閱讀或使用。所以需要一個(gè)可靠的連接,能夠確保傳輸過程的完整性,這就是傳輸層的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ì)話層用來建立、維護(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ǔ)言就可以耍了么?剛開始上手嘗試還是可以的,如果要進(jìn)一步,老碼農(nóng)覺得還是看看底層實(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ò)讀寫作為IO操作,就像讀寫文件那樣,對(duì)外提供出來的編程接口就是Socket。所以,socket(套接字)是通信的基石,是支持TCP/IP協(xié)議網(wǎng)絡(luò)通信的基本操作單元。socket實(shí)質(zhì)上提供了進(jìn)程通信的端點(diǎn)。進(jìn)程通信之前,雙方首先必須各自創(chuàng)建一個(gè)端點(diǎn),否則是沒有辦法建立聯(lián)系并相互通信的。一個(gè)完整的socket有一個(gè)本地唯一的socket號(hào),這是由操作系統(tǒng)分配的。
從設(shè)計(jì)模式的角度看, Socket其實(shí)是一個(gè)外觀模式,它把復(fù)雜的TCP/IP協(xié)議棧隱藏在Socket接口后面,對(duì)用戶來說,一組簡(jiǎn)單的Socket接口就是全部。當(dāng)應(yīng)用程序創(chuàng)建一個(gè)socket時(shí),操作系統(tǒng)就返回一個(gè)整數(shù)作為描述符(descriptor)來標(biāo)識(shí)這個(gè)套接字。然后,應(yīng)用程序以該描述符為傳遞參數(shù),通過調(diào)用函數(shù)來完成某種操作(例如通過網(wǎng)絡(luò)傳送數(shù)據(jù)或接收輸入的數(shù)據(jù))。以TCP 為例,典型的Socket 使用如下:
在許多操作系統(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)程打開一個(gè)文件時(shí),系統(tǒng)把一個(gè)指向此文件內(nèi)部數(shù)據(jù)結(jié)構(gòu)的指針寫入文件描述符表,并把該表的索引值返回給調(diào)用者 。
既然Socket和操作系統(tǒng)的IO操作相關(guān),那么各操作系統(tǒng)IO實(shí)現(xiàn)上的差異會(huì)導(dǎo)致Socket編程上的些許不同??纯次襇ac上的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)有文件描述符的文件讀寫操作完成以及發(fā)生異?;蛘叱瑫r(shí),該調(diào)用會(huì)返回這些文件描述符。select 需要遍歷所有的文件描述符,就遍歷操作而言,復(fù)雜度是 O(N)。
epoll相關(guān)系統(tǒng)調(diào)用是在Linux 2.5 后的某個(gè)版本開始引入的。該系統(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)用開始時(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ù),真正的編寫網(wǎng)絡(luò)程序代碼并不復(fù)雜,例如,用Erlang 中 gen_tcp 用于編寫一個(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.
然而,寫出漂亮的服務(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_sizechunk_size=chunk_size,
- max_header_sizemax_header_size=max_header_size,
- header_timeout=idle_connection_timeout or 3600,
- max_body_sizemax_body_size=max_body_size,
- body_timeoutbody_timeout=body_timeout)
- TCPServer.__init__(self, io_loopio_loop=io_loop, ssl_optionsssl_options=ssl_options,
- max_buffer_sizemax_buffer_size=max_buffer_size,
- read_chunk_size=chunk_size)
- self._connections = set()
- ...
或許,老碼農(nóng)說的都是錯(cuò)的,了解了所謂的網(wǎng)絡(luò)基礎(chǔ),也不一定寫出漂亮的代碼,不了解所謂的網(wǎng)絡(luò)基礎(chǔ),也不一定寫不出漂亮的代碼,全當(dāng)他自言自語(yǔ)吧。
【本文來自51CTO專欄作者“老曹”的原創(chuàng)文章,作者微信公眾號(hào):喔家ArchiSelf,id:wrieless-com】