1. 程式人生 > >全棧必備 網路程式設計基礎

全棧必備 網路程式設計基礎

我們是幸運的,因為我們擁有網路。網路是一個神奇的東西,它改變了你和我的生活方式,改變了整個世界。 然而,網路的無標度和小世界特性使得它又是複雜的,無所不在,無所不能,以致於我們無法區分甚至無法描述。

對於一個碼農而言,瞭解網路的基礎知識可能還是從瞭解定義開始,認識OSI的七層協議模型,深入Socket內部,進而熟練地進行網路程式設計。

關於網路

關於網路,在詞典中的定義是這樣的:

在電的系統中,由若干元件組成的用來使電訊號按一定要求傳輸的電路或這種電路的部分,叫網路。

作為一名從事過TMN開發的通訊專業畢業生,固執地認為網路是從通訊系統中誕生的。通訊是人與人之間通過某種媒介進行的資訊交流與傳遞。傳統的通訊網路(即電話網路)是由傳輸、交換和終端三大部分組成,通訊網路是指將各個孤立的裝置進行物理連線,實現資訊交換的鏈路,從而達到資源共享和通訊的目的。通訊網路可以從覆蓋範圍,拓撲結構,交換方式等諸多視角進行分類…… 滿滿的回憶,還是留在書架上吧。

網路的概念外延被不斷的放大著,抽象的思維能力是人們創新乃至創造的根源。網路用來表示諸多物件及其相互聯絡,數學上的圖,物理學上的模型,交通網路,人際網路,城市網路等等,總之,網路被總結成從同類問題中抽象出來用數學中的圖論科學來表達並研究的一種模型。

很多夥伴認為,瞭解這些之後呢,然並卵。我們關心的只是計算機網路,算機網路是用通訊線路和裝置將分佈在不同地點的多臺計算機系統互相連線起來,按照網路協議,分享軟硬體功能,最終實現資源共享的系統。特別的,我們談到的網路只是網際網路——Internet,或者移動網際網路,需要的是寫互連網應用程式。但是,一位工作了五六年的程式設計高手曾對我說,現在終於瞭解到基礎知識有多重要,技術在不斷演進,而相對不變的就是那些原理和程式設計模型了。

老碼農深以為然,程式設計實踐就是從具體到抽象,再到具體,迴圈往復,螺旋式上升的過程。瞭解前世今生,只是為了可能觸控到“勢”。基礎越紮實,建築就會越有想象的空間。 對於網路程式設計的基礎,大概要從OSI的七層協議模型開始了。

七層模型

七層模型(OSI,Open System Interconnection參考模型),是參考是國際標準化組織制定的一個用於計算機或通訊系統間互聯的標準體系。它是一個七層抽象的模型,不僅包括一系列抽象的術語和概念,也包括具體的協議。 經典的描述如下:

這裡寫圖片描述

簡述每一層的含義:

  1. 物理層(Physical Layer):建立、維護、斷開物理連線。
  2. 資料鏈路層 (Link):邏輯連線、進行硬體地址定址、差錯校驗等。
  3. 網路層 (Network):進行邏輯定址,實現不同網路之間的路徑選擇。
  4. 傳輸層 (Transport):定義傳輸資料的協議埠號,及流控和差錯校驗。
  5. 會話層(Session Layer):建立、管理、終止會話。
  6. 表示層(Presentation Layer):資料的表示、安全、壓縮。
  7. 應用層 (Application):網路服務與終端使用者的一個介面

每一層利用下一層提供的服務與對等層通訊,每一層使用自己的協議。瞭解了這些,然並卵。但是,這一模型確實是絕大多數網路程式設計的基礎,作為抽象類存在的,而TCP/IP協議棧只是這一模型的一個具體實現。

TCP/IP 協議模型

TCP/IP是Internet的基礎,是一組協議的代名詞,包括許多協議,組成了TCP/IP協議棧。TCP/IP 有四層模型和五層模型之說,區別在於資料鏈路層是否作為獨立的一層存在。個人傾向於5層模型,這樣2層和3層的交換裝置更容易弄明白。當談到網路的2層或3層交換機的時候,就知道指的是那些協議。

這裡寫圖片描述

資料是如何傳遞呢?這就要了解網路層和傳輸層的協議,我們熟知的IP包結構是這樣的:
這裡寫圖片描述
IP協議和IP地址是兩個不同的概念,這裡沒有涉及IPV6的。不關注網路安全的話,對這些結構不必耳熟能詳的。傳輸層使用這樣的資料包進行傳輸,傳輸層又分為面向連線的可靠傳輸TCP和資料報UDP。TCP的包結構:
這裡寫圖片描述
TCP 連線建立的三次握手肯定是必知必會,在系統調優的時候,核心中關於網路的相關引數與這個圖息息相關。UDP是一種無連線的傳輸層協議,提供的是簡單不可靠的資訊傳輸。協議結構相對簡單,包括源和目標的埠號,長度以及校驗和。基於TCP和UDP的資料封裝及解析示例如下:
這裡寫圖片描述

還是然並卵麼?一個數據包的大小了解了,會發現什麼?PayLoad到底是多少?在設計協議通訊的時候,這些都為我們提供了粒度定義的依據。進一步,通過一個例子看看吧。

模型解讀示例

FTP是一個比較好的例子。為了方便起見,假設兩條計算機分別是A 和 B,將使用FTP 將A上的一個檔案X傳輸到B上。

首先,計算機A和B之間要有物理層的連線,可以是有線比如同軸電纜或者雙絞線通過RJ-45的電路介面連線,也可以是無線連線例如WIFI。先簡化一下,考慮區域網,暫不討論路由器和交換機以及WIFI熱點。這些物理層的連線建立了位元流的原始傳輸通路。

接下來,資料鏈路層登場,建立兩臺計算機的資料鏈路。如果A和B所在的網路上同時連線著計算機C,D,E等等,A和B之間如何建立的資料鏈路呢?這一過程就是物理定址,A要在眾多的物理連線中找到B,依賴的是計算機的實體地址即MAC地址,對就是網絡卡上的MAC地址。乙太網採用CSMA/CD方式來傳輸資料,資料在乙太網的區域網中都是以廣播方式傳輸的,整個區域網中的所有節點都會收到該幀,只有目標MAC地址與自己的MAC地址相同的幀才會被接收。A通過差錯控制和接入控制找到了B的網絡卡,建立可靠的資料通路。

那IP地址呢? 資料鏈路建立起來了,還需要IP地址麼?我們FTP 命令中制定的是IP地址而不是MAC地址呀?IP地址是邏輯地址,包括網路地址和主機地址。如果A和B在不同的區域網中,中間有著多個路由器,A需要對B進行邏輯定址才可以的。實體地址用於底層的硬體的通訊,邏輯地址用於上層的協議間的通訊。在乙太網中:邏輯地址就是IP地址,實體地址就是MAC 地址。在使用中,兩種地址是用一定的演算法將他們兩個聯絡起來的。所以,IP是用來在網路上選擇路由的,在FTP的命令中,IP中的原地址就是A的IP地址,目標地址就是B的IP地址。這應該就是網路層,負責將分組資料從源端傳輸到目的端。

A向B傳輸一個檔案時,如果檔案中有部分資料丟失,就可能會造成在B上無法正常閱讀或使用。所以需要一個可靠的連線,能夠確保傳輸過程的完整性,這就是傳輸層的TCP協議,FTP就是建立在TCP之上的。TCP的三次握手確定了雙方資料包的序號、最大接受資料的大小(window)以及MSS(Maximum Segment Size)。TCP利用IP完成定址,TCP中的提供了埠號,FTP中目的埠號一般是21。傳輸層的埠號對應主機程序,指本地主機與遠端主機正在進行的會話。

會話層用來建立、維護、管理應用程式之間的會話,主要功能是對話控制和同步,程式設計中所涉及的session是會話層的具體體現。表示層完成資料的解編碼,加解密,壓縮解壓縮等,例如FTP中bin命令,代表了二進位制傳輸,即所傳輸層資料的格式。 HTTP協議裡body中的Json,XML等都可以認為是表示層。應用層就是具體應用的本身了,FTP中的PUT,GET等命令都是應用的具體功能特性。

簡單地,物理層到電纜連線,資料鏈路層到網絡卡,網路層路由到主機,傳輸層到埠,會話層維持會話,表示層表達資料格式,應用層就是具體FTP中的各種命令功能了。

Socket

瞭解了7層模型就可以程式設計了麼,拿起程式語言就可以耍了麼?剛開始上手嘗試還是可以的,如果要進一步,老碼農覺得還是看看底層實現的好,因為一切歸根到底都會歸結為系統呼叫。到了作業系統層面如何看網路呢?Socket登場了。

在Linux世界,“一切皆檔案”,作業系統把網路讀寫作為IO操作,就像讀寫檔案那樣,對外提供出來的程式設計介面就是Socket。所以,socket(套接字)是通訊的基石,是支援TCP/IP協議網路通訊的基本操作單元。socket實質上提供了程序通訊的端點。程序通訊之前,雙方首先必須各自建立一個端點,否則是沒有辦法建立聯絡並相互通訊的。一個完整的socket有一個本地唯一的socket號,這是由作業系統分配的。

從設計模式的角度看, Socket其實是一個外觀模式,它把複雜的TCP/IP協議棧隱藏在Socket介面後面,對使用者來說,一組簡單的Socket介面就是全部。當應用程式建立一個socket時,作業系統就返回一個整數作為描述符(descriptor)來標識這個套接字。然後,應用程式以該描述符為傳遞引數,通過呼叫函式來完成某種操作(例如通過網路傳送資料或接收輸入的資料)。以TCP 為例,典型的Socket 使用如下:
這裡寫圖片描述

在許多作業系統中,Socket描述符和其他I/O描述符是整合在一起的,作業系統把socket描述符實現為一個指標陣列,這些指標指向內部資料結構。進一步看,作業系統為每個執行的程序維護一張單獨的檔案描述符表。當程序開啟一個檔案時,系統把一個指向此檔案內部資料結構的指標寫入檔案描述符表,並把該表的索引值返回給呼叫者 。

既然Socket和作業系統的IO操作相關,那麼各作業系統IO實現上的差異會導致Socket程式設計上的些許不同。看看我Mac上的Socket.so 會發現和CentOS上的還是些不同的。
這裡寫圖片描述

程序進行Socket操作時,也有著多種處理方式,如阻塞式IO,非阻塞式IO,多路複用(select/poll/epoll),AIO等等。

多路複用往往在提升效能方面有著重要的作用。select系統呼叫的功能是對多個檔案描述符進行監視,當有檔案描述符的檔案讀寫操作完成以及發生異常或者超時,該呼叫會返回這些檔案描述符。select 需要遍歷所有的檔案描述符,就遍歷操作而言,複雜度是 O(N)。

epoll相關係統呼叫是在Linux 2.5 後的某個版本開始引入的。該系統呼叫針對傳統的select/poll不足,設計上作了很大的改動。select/poll 的缺點在於:

  1. 每次呼叫時要重複地從使用者模式讀入引數,並重復地掃描檔案描述符。
  2. 每次在呼叫開始時,要把當前程序放入各個檔案描述符的等待佇列。在呼叫結束後,又把程序從各個等待佇列中刪除。

epoll 是把 select/poll 單個的操作拆分為 1 個 epoll_create,多個 epoll_ctrl和一個 wait。此外,作業系統核心針對 epoll 操作添加了一個檔案系統,每一個或者多個要監視的檔案描述符都有一個對應的inode 節點,主要資訊儲存在 eventpoll 結構中。而被監視的檔案的重要資訊則儲存在 epitem 結構中,是一對多的關係。由於在執行 epoll_create 和 epoll_ctrl 時,已經把使用者模式的資訊儲存到核心了, 所以之後即便反覆地呼叫 epoll_wait,也不會重複地拷貝引數,不會重複掃描檔案描述符,也不反覆地把當前程序放入/拿出等待佇列。

所以,當前主流的Server側Socket實現大都採用了epoll的方式,例如Nginx, 在配置檔案可以顯式地看到 use epoll

網路程式設計

瞭解了7層協議模型和作業系統層面的Socket實現,可以方便我們理解網路程式設計。

在系統架構的時候,有重要的一環就是拓撲架構,這裡涉及了網路等基礎設施,那麼7層協議下四層就會有助於我們對業務系統網路結構的觀察和判斷。在系統設計的時候,往往採用面向介面的設計,而介面也往往是基於HTTP協議的Restful API。 那介面的粒度就可以將data segment作為一個約束了,同時可以關注到移動網際網路中的弱網環境。

不同的程式語言,有著不同的框架和庫,真正的編寫網路程式程式碼並不複雜,例如,用Erlang 中 gen_tcp 用於編寫一個簡單的Echo伺服器:

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.

然而,寫出漂亮的伺服器程式仍然是一件非常吃功夫的事情,例如,個人非常喜歡的python Tornado 程式碼, 在ioloop.py 中有對多路複用的選擇:

@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,進而實現了HTTP協議,程式碼片段如下:

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()
        ...

或許,老碼農說的都是錯的,瞭解了所謂的網路基礎,也不一定寫出漂亮的程式碼,不瞭解所謂的網路基礎,也不一定寫不出漂亮的程式碼,全當他自言自語吧。