天天看點

談談遊戲伺服器的發送資料處理

發送資料處理模式的概念:

 相信每一個第一次寫遊戲伺服器的人都會在發送資料處理這裡卡主,因為相對于簡單易處理的接收消息處理,發送消息的時機和驅動更加難以把握。為什麼呢?我們看下套接字可讀的條件:

 1: 該套接字接收緩沖區中的資料位元組數大于接收低水位标記

 2: 該連接配接的讀關閉

 3: 該套接字是一個監聽套接字,并且有新的連接配接

 4: 該套接字上有錯誤處理

以上所有的條件,都可以通過注冊事件來完成,并且因為都是被動觸發,是以處理起來比較輕松。

那我們看看套接字可寫的條件:

 1: 該套接字發送緩沖區中可用空間大于發送高水位标記

 2: 該套接字的寫關閉

 3: 該套接字上有錯誤處理

看到套接字可寫的條件我們為難了,因為我們需要發送的時候,套接字并不一定可寫;而套接字可寫的時候,我們未必有資料要發送,這就造成了事件的浪費,也就造成了發送資料比接收資料更難。

前言: 我們在這裡是将網絡資料的發送和接收放在單獨的線程裡面處理,稱之為網絡io線程;而在其他的線程總處理玩家邏輯,稱之為玩家邏輯線程。

解決方案1: 定時發送。

定時發送算是一個比較通用的處理,也是用起來比較友善的方式。對于每一個來自用戶端的連接配接,我們隻是注冊可讀事件,而不會注冊可寫事件,對于發送處理,我們采用定時器觸發的模式,比如每一個連接配接上綁定一個30ms的定時器,每次定時器觸發的時候,也就是寫饑餓的時候,對每個連接配接都去做一次試圖發送的處理。

當大家看到思路的時候,我想麻煩也就跟着來了。因為是30ms的定時器,是以每條消息的延遲都是n*30ms;可能很多的套接字并沒有資料要發送,但是定時器到了,造成了很多浪費;因為定時器的觸發也是輪訓的模式,大家不是都說“輪訓就是強奸嗎”。

解決方案2: 按需注冊write事件

 如果我們真正了解write事件,就應該按需注冊write事件。每次發送玩家資料的時候,如果隻發送了部分資料,則把剩餘的資料存放到自定義緩沖區,并且注冊write寫事件;這個事件會在下一次select的時候觸發,觸發的時候發送剩餘的資料,如果發送完畢,就關閉write事件。

 這裡我們分開網絡io線程和遊戲邏輯線程,如果是網絡io線程内部發送資料,那麼很簡單,隻需要調用提供send函數就可以了。如果是遊戲邏輯線程裡發送資料,就會稍微麻煩一點。為了保證線程的安全性以及資料的完整性,在遊戲邏輯線程裡面調用發送資料的接口,實際的工作是在網絡線程裡面完成。簡單來說就是将這個函數或者task,投遞到網絡io線程裡面去執行。

我們可以參考下muduo的做法,如果是在非網絡io線程裡面發送資料,就将要發送的套接字和資料封裝成functor,投遞到網絡io線程中去,并且喚醒網絡io線程,去處理這些需要執行的functors(std::vector<functor>)。

我在修改自己的網絡發送模式的時候,也是這個思路,不過是用的是java為多線程提供的futuretask,如果是在非網絡io線程中發送資料,就将需要發送的連接配接和資料封裝成futuretask,投遞到網絡io線程中去,喚醒io線程,處理需要執行的futuretasks(vector<futuretask>)。

好吧,最後還是承認,其實在設計的時候,參考了一下mina的設計,不過看到muduo網絡庫居然和mina 的設計如此類似,相比muduo的設計也是參考了mina的設計。

可能很多人認為方案2的方案,會增加系統調用。因為我們不是總說要減少系統調用嗎。這也是定時器發送資料模式的依據,可是我們的tcp已經提供了negle算法,大部分send的函數,隻是将資料寫入到緩沖區,并沒有那麼大的消耗。大家可以測試看下!

繼續閱讀