IP協議的抽象簡單概念與IP協議底層傳輸的複雜真實實現形成鮮明對比。
這裡不需要任何連線的概念,資料包從一個電腦傳輸到另外一個電腦,就像你在上課的教室裡面傳紙條一樣。你只要給出一個最終的目的地,包自然會傳到那個終端,當然中間還會經過好多層的路由遞送。
你完全不能確定自己的包最終會不會到達目的地,只能希望它到達。如果你想知道包最終是否到達,那你就必須讓接收者收到以後做出回覆。
事實上,整個過程可能還要更復雜一些,由於沒有一臺機器能夠預知包可以最快到達的完整路由途徑,所以有時候IP協議會將包複製多份發出,這些包會走不同的路由,因此他們一般也會在不同的時刻到達。
你必須瞭解因特網路由問題的設計原則是自主組織路徑,自主修復路徑。所以當你思考問題的底層是如何實現時那是相當帶勁的,當然你可以在相關的教科書上找到你想要的內容或者wiki。
UDP
我們已經有一種可以像寫檔案一樣穩定傳輸資料的方法了,如果我們還想要一種可以自由的收發包的方法,我們可以怎麼做呢?
這裡我們可以用到UDP。UDP解釋為“user datagram protocol”(使用者自定義資料協議)。它和TCP類似也是建立在IP協議上的,不過他相比起TCP來在IP協議上只做了薄薄的一層協議。
我們可以使用UDP協議直接對指定的IP和埠發包,比如 1.0.0.127:21(本機的FTP埠)。這個包會從傳送者自己路由到接收者手中,當然也有可能半路丟失掉。
在接收端,我們只要偵聽相關埠就可以了,當有包從任何電腦發來的時候(這裡沒有連線的概念),我們在得到包的資料的同時,也得到了包的傳送者的IP和埠資料,傳送包的大小。
UDP是不穩定可靠的傳輸協議,事實上大部分的包是會被送達的,但是你一般會有1%到5%的丟包率。甚至你會發現有段時間,你連一個包都收不到。路由路徑上的那些機器出了點啥問題,誰知道呢?
有時候收包的順序和發包的順序也是不同的,可能你發包的時候是1,2,3,4,5的順序發。收到的包就變成1,3,4,5,2的順序了。當然大部分時候這個順序是不會亂的。但是這個誰都不敢打保票。
UDP只能保證你一件事,那就是包的完整性。你如果發一個256bytes的包,那麼對方收到的肯定也是一個256bytes的包。他不會只收到半個包之類的。當然這其實也是UDP唯一能保證你的事情。其他事情都要靠你自己去訂製。
TCP vs UDP
我們現在要做一個選擇了。開發遊戲到底是用UDP好呢?還是用TCP好呢?
首先我們連列一下他們的有缺點:
TCP:
1. 基於連線的協議。
2. 可靠性和資料包的序列性是有保證的。
3. 自動為你的資料封包。
4. 確保包的流量不會超出你的網路連結的負載上限。
5. 簡單易用,你只需要像寫檔案一樣去讀寫就萬事大吉了。
UDP:
1. 沒有連線的概念,如果你想要,自己去實現去。
2. 沒有關於可靠性和包序列性的保證,包可能會丟失,重複,亂序。
3. 你必須自己去封包。
4. 你必須自己確保自己的資料包不會流量過大從而導致超過連結負載上限。
5. 你必須自己處理包的丟失,重複,亂序的情況,如果你不想他們對你的程式造成麻煩,必須要自己實現程式碼來做出應對。
這樣一比,我們顯然應該用那個TCP協議了。它完成了所有我們想要的特性,實在是太完美了,不是嗎?
TCP的真實工作情況
TCP和UDP都是基於IP協議的,但是他們的本質確實截然不同的。UDP和它底層的IP協議類似,TCP卻將很多東西進行了抽象,它使你在使用它的時候感覺就像讀寫一個檔案一樣,事實上他對你隱藏了很多複雜功能的實現細節。
它到底是如何實現這些細節的呢?
首先,TCP是流性質的協議,你將資料一個位元一個位元的寫入流,然後TCP來確保他們會最終到底目的地。我們必須明確:TCP協議是基於IP協議的,IP協議是基於資料包的。這裡TCP其實是把我們的資料流在底層進行了打包。事實上有一些TCP協議中的程式碼就是把我們的資料流進行排隊,然後依次寫入包中,當包寫入了足夠多的資料以後,它就會把包發走。
這裡就會導致一些問題,因為你不能控制底層的打包和傳輸,所以當很小的使用者輸入資料寫入資料流的時候,TCP可能會湊滿一定量的資料(比如100bytes),然後再打包傳送。這樣的話,在實時性上面就會很差,因為也許你會需要這些包越快到達越好。這些小延遲也許就會大大的影響你的遊戲性。
TCP中其實有一個TCP_NODELAY的選項,使用這個選項以後,你的資料就會跳過TCP的佇列打包過程,直接傳送。
但是即使你使用的這個技術,你在多人網路遊戲中還是會遇到不小的問題。
這些問題源自於TCP實現可靠傳輸的機制。
TCP是如何實現可靠傳輸的
基本上來說,TCP將資料流中的資料做成封包,然後將他們通過不可靠的IP協議傳送,然後在接收端重組這些包成為資料流。
但是當一個包丟失的時候TCP會做些什麼呢?當包重複和亂序的時候TCP又做了些什麼呢?
這裡我不想做太深入的介紹,有興趣的讀者可以在wiki上找到他們需要的細節。大致來說,TCP發現丟包的時候,會要求傳送者重發,重複的包會被丟棄,亂序包會被排序,事實上他就是這麼保證傳輸的可靠性的。[1]
這裡的丟包處理對遊戲來說就很糟糕了。TCP中,如果你發現丟包了,必須等待傳送者進行重發,在重發的包到來以前,即使有新包來,你也只能讓他們在佇列裡等著,不能讀取,這個等待的時間大概是ping值的1.5倍,如果重發的包再次丟失的話那就是3倍的時間。假設你的ping值是125ms,丟包一次就會延遲200ms左右,如果連續丟包就是400ms,這樣的情況在大多數即時類遊戲中是不能忍受的。
為何你不能選用TCP作為遊戲開發的網路協議
通過上面的論述,其實已經很明白了。在注重即時性的遊戲中,對於延遲的要求是很苛刻的,我們可以丟包,但是如果我們收到了比丟掉的包更新的包的話,我們完全可以不管丟掉的包。我們只關注當前資料。
這裡我們來假設一個最簡單的多人遊戲的模型。比如一個FPS遊戲,你在客戶段每次將輸入的資料(比如前進,跳躍,開火)傳送到伺服器端,然後伺服器端將玩家當前的位置和情況發回給客戶端來做顯示。
在這個最簡單的模型中,只要有一個包丟失了,所有的東西都必須停下來等包的重發,任何操作都得停掉,你不能移動也不能射擊。等到這個包到達的時候,你總算能繼續操作了。但是可能你會發現還有一堆等等待重發的包在排隊,於是你只好繼續等,而且可能你收到的這個重發包對遊戲來說已經失去時效性,完全沒意義了。這樣的遊戲你能忍嗎?
不幸的是你對TCP協議的這個行為完全無能為力。這是TCP協議的本性,就是它保證了TCP協議的可靠性的。
我們不需要這樣無法訂製的可靠性協議。我們需要自己進行訂製丟包時的處理原則。這也是我們在開發遊戲時,避免使用TCP協議的原因。[2]
是否可以混合使用TCP和UDP協議呢?
上面的結論中,我們可以明確知道,一些類似玩家操作,玩家位置的時效性相關資料,我們必須不能使用TCP協議。但是有些資料確是必須保證可靠性的,那我們是否可以使用TCP協議來做同步呢?
這個想法是很好的,我們可以在玩家操作等即時性很強的資料上使用UDP協議,在玩家AI,資料載入,玩家對話等序列性很強的資料上使用TCP。如果你願意的話,甚至可以為不同的AI建立不同的TCP連線,以免一個AI的丟包會影響的另外一個AI的即時性。
看上去這是一個不錯的思路。但是這僅僅是看上去而已。由於TCP和UDP都是基於IP協議的,事實上他們在底層會互相干擾。關於干擾的細節我這裡就不詳細論述了,想了解的讀者可以閱讀這篇文章。
結論
我的建議是在遊戲中僅僅使用UDP作為網路協議,即使要使用TCP也是自己在UDP的基礎上實現一種類TCP的協議。這也是現代遊戲中流行的網路架構。
接下來的文章中我會介紹如何實現這套架構。下一篇會講的比較實際點,講關於如何使用UDP收發包。盡請期待。 [3]
SCUTWANG附註:
[1]準對tcp丟包的問題,主要採用回退N步(GBN)或者選擇重傳(SR)的技術,保證資料的可靠傳輸。
[2]TCP的擁塞控制機制,也是影響遊戲實時性的重要原因之一。
[3]關於作者的這些文章,大家可以訪問博主的部落格http://blog.csdn.net/rellikt/article/details/5833233