基於TCP流協議的數據包通訊

                                                                                                                     Fanxiushu   2016-02-04,引用或轉載請註明原始作者.

TCP通訊是流協議,它不像UDP那樣基於包爲邊界的通訊方式,

TCP流式協議,舉個簡單例子,一端用send 分別發送 100,123,120字節的數據,
另一端用recv可以一下子接收到 100+123+120=343字節的數據,或者先接收 3個字節的數據,再接收餘下的340字節,
不管另一端怎麼接收,最終是要接收到343字節的數據。
而且TCP保證數據的完整性和順序,也就是兩端是數據同步的,出現任何一點的數據不一致,都會造成TCP連接的失效。
UDP則跟TCP大不一樣,他是基於包邊界的。所謂的包邊界,就是一端分別發送 100, 123, 120字節的數據,
另一端接收到也應該分別是 100,123,120字節數據的三個包,
不會出現一端發送100字節的一個數據包,另一端只接收到小於100字節的數據包,或者收到大於100字節的數據包。
UDP同時也不保證穩定和順序,如發送端發送100,123,120三個包,接收端可能接收到3個包,也可能只接收到2個包,
也可能一個包也收不到,收到的順序不一定是100,123,120,可能是100,120,123,或者123,100,120等。
這些TCP和UDP的屬性,大家稍微查查資料就該很清楚。
UDP的這種特殊通訊方式其實跟網絡底層鏈路層的通訊方式很接近。
鏈路層的數據是一個數據包一個數據包的傳輸,並不保證數據能否達到對方,或者按照順序到達對方。
UDP只是簡單把鏈路層和IP層的數據加了一層封裝,加了端口用於識別同一個機器的不同進程,
UDP數據包的收發方式,只是組合成UDP包之後,簡單的發送到底層網絡了事,
至於底層網卡有沒有發成功或者接收成功,它是一概不聞不問的。
他的底層處理方式比起 TCP協議來說簡單太多了。

正是因爲UDP的簡單和直接,所以在某些場合他是非常實用的。
比如定時報告狀態,只需要很少數據量的包,也不必擔心包丟失問題,反正都是定時發送。
再比如某些遊戲,尤其是實時對戰遊戲,UDP簡直可以說是爲他們量身定製的。
因爲這類注重即時性的遊戲,對延遲要求比較高,使用UDP收發少量的對戰類數據包,簡直是最佳的選擇。
UDP還有一個好處,就是編程的IO模型簡單,
在服務端你可以簡單到開啓一個讀線程和一個寫線程,就能接收和發送數據到所有的客戶端。
而TCP的IO模型往往要複雜得多。

既然UDP這麼好,編程又簡單,可現在網絡中大部分都在使用TCP,
一個非常重要的原因就是TCP提供的是可靠傳輸,TCP有一套複雜的底層算法來保證數據的完整和可靠,
有這個理由就已經足夠讓TCP在大部分場所比UDP好使了。
因爲大部分時候,我們在開發網絡通信程序,都希望能隨意的接收和發送任意大小和完整的數據,
如果使用UDP,還得自己寫算法來保證數據的順序和完整,整個處理過程就等於實現一個小型的TCP協議。
一些特殊場所,比如P2P,各種使用P2P的下載軟件如迅雷等,
這些軟件和傳統的服務端客戶端模式不大一樣,每個運行軟件的機器既是客戶端也是服務端,
而用戶的每個機器可能處於不同的網絡環境中,最典型的就是大部分機器處於NAT中,
這樣的環境下,採用UDP是最佳選擇,因爲TCP的NAT穿透能力差。
當然這些軟件使用UDP,他們也必須實現一套算法來保證UDP傳輸的完整和順序。

我們在開發TCP程序時候,最先想到的就是 請求-應答模式:
就是客戶端發起一個請求,然後服務端接收到請求,進行處理,接着向客戶端應答這個請求。
最典型和常用的就是 HTTP協議,我們瀏覽的所有網頁,以及各種玲琅滿目的網站,
這些都是HTTP的功勞,HTTP協議是建立在TCP上的應用層協議,採用就是 請求-應答方式。
瀏覽器首先發起一個網頁請求的TCP連接,web服務器通過這個TCP連接應答這個網頁,並把網頁內容傳輸給瀏覽器。
然後瀏覽器可能關閉這個TCP連接,或者也可能利用這個TCP連接發起另外一個網頁請求。
這個請求-應答模式,也是我在使用TCP開發私有協議時候,使用的最多的模式,
多得來以至於都忘記其他模式需求的存在了。

現在我們來看另一個通訊情況:windows遠程桌面。
使用遠程桌面可以遠程控制另一臺windows機器,可以在遠程桌面裏做任何本地桌面上的操作,
比如刪除,複製文件,可以把本地文件複製到遠程機器裏,在複製的同時還能執行其他操作,
遠程機器的桌面變化實時更新到本地,等等。
但是仔細研究會發現,遠程桌面只使用了一條 TCP連接,連接到被控制機器的 3389 端口。
也就是在一條TCP通訊連接裏,傳輸各種請求數據和接收各種應答數據。
遠程桌面使用的是 RDP協議,我們這裏不討論RDP的細節,
只討論如何在一條TCP連接中,如何做到遠程桌面的各種操作。
如果我們還是按照請求-應答的模式來解釋遠程桌面的通訊協議,顯然會有很多無法處理的問題。
比如舉個簡單例子:
我們在遠程桌面客戶端點擊鼠標操作,這個操作會通過3389的TCP連接發送到被控制端,如果按照請求-應答模式來工作,
則必須在被控制端接收到這個鼠標操作,執行這個動作,然後回答給客戶端已經執行了這個操作。
如果這期間,被控制機器的桌面界面內容發生變化,則無法通知給客戶端,
因爲一切通訊都是按照客戶端發起請求,然後服務端應答的方式通訊的。
即使我們使用請求-應答的方式,通過輪詢定時查詢被控制機器的界面內容變化情況,也無法做到實時,
而且輪詢慢了會嚴重影響視覺效果,輪詢快了會嚴重浪費資源。

於是,我們改換一種解決問題的辦法,從 UDP 通訊的特點:(按照包模式通訊)入手去解決上邊的問題。
假定我們在遠程桌面的TCP通訊中,一切通訊的數據都定義成一個一個的單獨的數據包在同一條TCP連接中傳輸,
數據包的接收和發送分開進行,就是在同一個TCP連接中,一個線程專門接收數據包,一個線程專門發送數據包。
這是可以的,因爲現在的網卡都是工作在全雙工狀態下。所謂全雙工,就是接收和發送使用各自的通道,能獨立進行數據傳輸。
大致僞代碼如下:

int tcp_socket = 客戶端連接到服務端的socket或者服務端接收到客戶端連接的socket。
receive_thread() //負責接收數據包的線程
{
      tcp_packet = recv_packet (tcp_socket );
      ////TCP 是流協議,因此,我們必須至少定義一個表示包大小的頭+包內容,才能保證TCP數據傳輸的同步。
     
       //處理 tcp_packet 包,爲了不阻塞讀線程,一般是把tcp_packet交給別的線程處理。
}
send_thread()//負責發送數據包的線程
{
     while(loop){
          從發送隊列取出一個包 tcp_packet,(發送隊列,是別的線程生成的需要發送的數據包。)
          send_packet( tcp_socket, tcp_packet ); //發送這個數據包。
     }
}

再回到上邊的問題,
遠程桌面控制端(客戶端)和被控制端(服務端),分別開啓兩個線程,一個負責接收數據包,一個負責發送數據包。
當我們在遠程桌面客戶端點擊鼠標等操作,生成一個鼠標的數據包投遞到發送線程,
發送線程再把它傳輸到被控制端,被控制端接收到這個數據包,然後執行,
他如果要回復這個鼠標的執行結果,則再生成一個結果包投遞到發送線程,發送線程再把這個包傳輸給客戶端。
同時如果被控制端的界面發生改變,則生成一個界面內容改變的數據包,投遞到發送線程,發送線程再傳輸給客戶端。
客戶端的接收線程接收到界面內容改變的數據包,顯示新的被控制端的界面內容。
客戶端接收到鼠標執行結果的包,知道鼠標操作是失敗還是成功。
按照包的方式通訊,就能在遠程桌面中傳遞各種複雜的動作,每個動作都封裝成一個一個的數據包進行傳輸。
接收包和發送包分開獨立進行,互相不干擾,每個包是否需要應答包,根據每個包的需求決定,不是必須的。
這又回到了 UDP通訊方式。那爲何不乾脆使用UDP代替呢?
還是上邊提到的原因,TCP保證穩定和順序,這點在遠程桌面等這類要求數據必須準確的地方,是十分必要的。

再看看即時通訊,大家最熟悉的莫過於QQ了,還有已經進入歷史的MSN。
QQ的通訊是混合模式,UDP和TCP都在使用,任何的通訊軟件,UDP和TCP的混合使用有時是不可避免的。
即時通訊軟件發送接收的聊天message,本身就是一個個的消息包,定義成數據包在TCP中傳輸,最合適不過了。
我們只討論TCP連接的服務器轉發聊天消息的情況。
假設用戶A和B聊天。A只有一條TCP連接到服務端,同樣B也只有一條TCP連接到服務端。
按照TCP的包方式進行通訊,
當A生成一條message,客戶端程序組合成一個聊天包,發送到服務器端, 服務端找到這個包需要轉發的B用戶,
然後把這個包投遞到B的發送線程隊列,發送隊列把數據包發送給B 。
B的接收線程接收到這個包,顯示出來,於是B用戶就看到A發來的消息,同時B也可能在發消息給A,
同樣的道理,A也會收到B的消息。在A和B聊天期間,有些狀態信息,比如 對方是否掉線,對方是否正在打字等等,
這些信息都組合成一個一個的數據包,在服務器和客戶端之間接收和發送。
試想下,如果採用請求-應答的方式,根本無法處理即時聊天通訊。

我們再看看網絡遊戲通訊,各種大型的還是小型的,只要牽涉到網絡通信的,
當然比較簡單的只用HTTP通訊的遊戲除外。
 遊戲通訊中,基於數據包的方式是非常普遍的做法。

TCP是流協議,要怎樣做,才能保證傳輸的是一個一個的單獨的數據包,並且不破壞客戶端和服務端之間的TCP連接的同步性呢?
其實是挺簡單的:每個數據包定義成 ”包大小+包內容“,比如4個字節表示包的大小,然後是包數據。
發送的時候,“包大小+包內容”組合到一起發送,接收的時候,先接收固定的4個字節,獲取到包的size,
然後再接收size字節的數據,這樣一個包就接收完成。大致僞代碼如下:
send_packet(tcp_socket, packet, packet_size) //發送數據包
{
     int32 size = pakcet_size; ///應該採用網絡序
     send(tcp_socket, &size, 4..);
     send(tcp_socket, packet, packet_size);
}
recv_packet(tcp_socket)
{
       int32 size;
       recv(tcp_socket, &size, 4,...);
      char* packet = malloc(size);
      recv( tcp_socket, packet, size, ...);
     return packet;
}

當然,實際的通訊協議中,不會這麼簡單。是根據需求來定義包格式。

CSDN上的鏈接,提供的是本人實現的TCP按照包模式通訊的服務端框架代碼。

框架來源於做的一個手遊服務端項目,可惜項目沒完成就中斷了。
當然有很多現成的遊戲通訊框架可以借用,當初堅持自己實現,是習慣了自己造輪子。

實現起來也不算難,而且出了問題更容易調試BUG 。


框架實現了可以同時偵聽多個端口,
每個數據包既可以不壓縮傳輸,也能支持zlib壓縮和blowfish加密傳輸。
服務端提供三種線程池來進行tcp連接處理,
一類是接收線程池,接收線程池獲取每個socket傳輸來的數據包,
同時保證每個socket的包按照到來的順序進行處理,
二類是工作線程池,由接收線程池把接收到的數據包投遞到工作線程池,
工作線程池專門處理這些接收到的數據包。
三類是發送線程池,當工作線程池處理完這些數據包,確定需要發送處理結果數據包到客戶端,
或者其他線程需要發送數據包到客戶端,他們首先把數據包投遞到發送線程池,
發送線程池專門負責數據包的發送。
框架同時提供了每個客戶端的定時器功能,在服務端內部各個socket之間數據通信等。
框架支持 Linux和windows平臺。


代碼CSDN下載地址:

http://download.csdn.net/detail/fanxiushu/9427108



發佈了76 篇原創文章 · 獲贊 101 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章