1. 程式人生 > >【Java TCP/IP Socket】深入剖析socket——資料傳輸的底層實現

【Java TCP/IP Socket】深入剖析socket——資料傳輸的底層實現

    底層資料結構

    如果不理解套接字的具體實現所關聯的資料結構和底層協議的工作細節,就很難抓住網路程式設計的精妙之處,對於TCP套接字來說,更是如此。套接字所關聯的底層的資料結構集包含了特定Socket例項所關聯的資訊。比附,套接字結構除其他資訊外還包含:

    1、該套接字所關聯的本地和遠端網際網路地址和埠號。

    2、一個FIFO(First Im First Out)佇列,用於存放接收到的等待分配的資料,以及一個用於存放等待傳輸的資料的佇列。

    3、對於TCP套接字,還包含了與開啟和關閉TCP握手相關的額定協議狀態資訊。


    瞭解這些資料結構,以及底層協議如何對其進行影響是非常有用的,因為它們控制了各種Socket物件行為的各個方面。例如,由於TCP提供了一種可信賴的位元組流服務,任何寫入Socket和OutpitStream的資料副本都必須保留,直到連線的另一端將這些資料成功接收。向輸出流寫資料並不意味著資料實際上已經被髮送——它們只是被複制到了本地緩衝區,就算在Socket的OutputStream上進行flush()操作,也不能保證資料能夠立即傳送到通道。此外,位元組流服務的自身屬性決定了其無法保留輸入流中訊息的邊界資訊。

    資料傳輸的底層實現

    在使用TCP套接字時,需要記住的最重要的一點是:不能假設在連線的一端將資料寫入輸出流和在另一端從輸入流讀出資料之間有任何的一致性。尤其是在傳送端由單個輸出流的write()方法傳輸的資料,可能會通過另一端的多個輸入流的read()方法獲取,而一個read()方法可能會返回多個write()方法傳輸的資料。

    一般來講,我們可以認為TCP連線上傳送的所有位元組序列在某一瞬間被分成了3個FIFO佇列:

    1、SendQ:在傳送端底層實現中快取的位元組,這些位元組已經寫入輸出流,但還沒在接收端成功接收。它佔用大約37KB記憶體。

    2、RecvQ:在接收端底層實現中快取的位元組,這些位元組等待分配到接收程式——即從輸入流中讀取。它佔用大約25KB記憶體。

    3、Delivered:接收者從輸入流已經讀取到的位元組。

當我們呼叫OutputStream的write()方法時,將向SendQ追加位元組。

    TCP協議負責將位元組按順序從SendQ移動到RecvQ。這裡有重要的一點需要明確:這個轉移過程無法由使用者程式控制或直接觀察到,並且在塊中發生,這些塊的大小在一定程度上獨立於傳遞給write()方法的緩衝區大小。

    接收程式從Socket的InputStream讀取資料時,位元組就從RecvQ移動到Delivered中,而轉移的塊的大小依賴於RecvQ中的資料量和傳遞給read()方法的緩衝區的大小。


   示例分析

     為了展示這種情況,考慮如下程式:


     其中,圓點代表了設定緩衝區資料的程式碼,但不包含對out.write()方法的呼叫。這個TCP連線向接收端傳輸8000位元組,在連線的接收端,這8000位元組的分組方式取決於連線兩端的out.write()方法和in.read()方法的呼叫時間差,以及提供給in.read()方法的緩衝區的大小。

    下圖展示了3次呼叫out.write()方法後,另一端呼叫in.read()方法前,以上3個佇列的一種可能狀態。不同的陰影效果分別代表了上文中3次呼叫write()方法傳輸的不同資料:


     現在假設接收者呼叫read()方法時使用的緩衝區陣列大小為2000位元組,read()呼叫則將把RecvQ中的1500位元組全部移動到陣列中,返回值為1500。注意,這些資料中包含了第一次和第二次呼叫write()方法時傳輸的位元組,再過一段時間,當TCP連線傳完更多資料後,這三部分的狀態可能如下圖所示:


     如果接收者現在呼叫read()方法時使用4000位元組的緩衝區陣列,將有很多位元組從RecvQ佇列轉移到Delivered佇列中,這包括第二次呼叫write()方法時剩下的1500位元組加上第三次呼叫write()方法的錢2500位元組。此時,佇列的狀態如下圖:


     下次呼叫read()方法返回的位元組數,取決於緩衝區陣列的大小,亦及傳送方套接字通過網路向接收方實現傳輸資料的時機。資料從sendQ到RecvQ緩衝區的移動過程對應用程式協議的設計有重要的指導性。