【C#】游戲網(wǎng)絡(luò)模塊的設(shè)計(jì)與實(shí)現(xiàn)

游戲的網(wǎng)絡(luò)模塊其實(shí)有很多可以深挖的技術(shù)灌闺、細(xì)節(jié)供大家討論,本文僅作為筆者的經(jīng)驗(yàn)總結(jié)與記錄坏瞄,對(duì)在Unity中如何開發(fā)一個(gè)快速可用的網(wǎng)絡(luò)模塊進(jìn)行介紹與記錄桂对。我希望能在需要使用時(shí)參考這篇文章就能快速搭建一個(gè)簡(jiǎn)易基礎(chǔ)的網(wǎng)絡(luò)模塊,如果能幫到讀者就算這篇文章有了大作用了鸠匀。

游戲的網(wǎng)絡(luò)通常分為了短鏈接與長(zhǎng)鏈接兩種蕉斜。

短鏈接:以HTTP通訊為基礎(chǔ),由客戶端發(fā)起向服務(wù)器請(qǐng)求數(shù)據(jù)缀棍。

長(zhǎng)鏈接:大多情況下以TCP通訊為基礎(chǔ),在客戶端與服務(wù)器之間構(gòu)建穩(wěn)定鏈接爬范,以便于相互傳輸數(shù)據(jù)父腕;具體通訊協(xié)議依據(jù)需求可以選擇可靠UDP等其他通訊協(xié)議。

在Unity的實(shí)現(xiàn)

注意事項(xiàng)

因?yàn)槲覀兪褂肬nity開發(fā)游戲或應(yīng)用青瀑,都不希望因網(wǎng)絡(luò)發(fā)送或接收數(shù)據(jù)導(dǎo)致界面出現(xiàn)卡頓璧亮,所以常見的在主線程調(diào)用同步發(fā)送方式就不屬于我們的選擇范圍萧诫。

我們需要盡可能采取各種異步的形式,既能夠向服務(wù)器發(fā)送或請(qǐng)求數(shù)據(jù)枝嘶,又不影響主線程的運(yùn)行帘饶。

短鏈接

Unity中實(shí)現(xiàn)短鏈接是非常簡(jiǎn)單的,因?yàn)閁nity內(nèi)部封裝了UnityWebRequest方法躬络,極大簡(jiǎn)化了我們發(fā)送HTTP請(qǐng)求的復(fù)雜度尖奔。

客戶端通過HTTP向服務(wù)器請(qǐng)求數(shù)據(jù),一般分為GETPOST兩種方式穷当。對(duì)應(yīng)這兩種方式,Unity提供了UnityWebRequest.PostUnityWebRequest.Get靜態(tài)方法淹禾,以創(chuàng)建UnityWebRequest實(shí)例馁菜,或者也可以依據(jù)自身需求,通過調(diào)用UnityWebRequest的構(gòu)造方法來創(chuàng)建實(shí)例铃岔。

接下來我將著重講述POST數(shù)據(jù)通過UnityWebRequest如何實(shí)現(xiàn)(因?yàn)?code>GET參數(shù)外露汪疮,太容易被破解了,所以項(xiàng)目中也很少再使用它)毁习。

首先是UnityWebRequest的構(gòu)建與數(shù)據(jù)發(fā)送:

UnityWebRequest webRequest = new UnityWebRequest(url, "POST"); // 初始化使用POST的UnityWebRequest智嚷,并附上目標(biāo)地址

webRequest.SetRequestHeader("", ""); // 設(shè)置HTTP的頭信息

// 初始化UnityWebRequest的downloadHandler與uploadHandler
webRequest.downloadHandler = (DownloadHandler) new DownloadHandlerBuffer();
webRequest.uploadHandler = (UploadHandler) new UploadHandlerRaw(data);
webRequest.uploadHandler.contentType = "application/x-www-form-urlencoded";

var asyncOperation = webRequest.SendWebRequest(); // 數(shù)據(jù)發(fā)送

在調(diào)用了UnityWebRequest.SendWebRequest之后,會(huì)得到一個(gè)UnityWebRequestAsyncOperation類型的實(shí)例纺且。作為繼承自AsyncOperation的類盏道,UnityWebRequestAsyncOperation除了能表示請(qǐng)求的進(jìn)度以及是否結(jié)束外,還附帶有創(chuàng)建了本次請(qǐng)求的UnityWebRequest的實(shí)例的引用载碌。后續(xù)數(shù)據(jù)的接收方案都基于此猜嘱。

短鏈接的接收方案1——completed回調(diào)

當(dāng)調(diào)用了UnityWebRequest.SendWebRequest之后,可以對(duì)得到的UnityWebRequestAsyncOperation類型實(shí)例內(nèi)的completed事件添加行為嫁艇,以期在收到服務(wù)器返回消息后能夠立即處理接收到的數(shù)據(jù)朗伶。

asyncOperation.completed += (a) =>
{
    // 將事件傳入的 AsyncOperation 轉(zhuǎn)換為 UnityWebRequestAsyncOperation,
    // 并從webRequest的downloadHandler中獲取下載的數(shù)據(jù)
    Do(((UnityWebRequestAsyncOperation)a).webRequest.downloadHandler.data);
};

短鏈接的接收方案2——Update

當(dāng)調(diào)用UnityWebRequest.SendWebRequest獲取到UnityWebRequestAsyncOperation類型的實(shí)例后步咪,可以將該實(shí)例加入一個(gè)隊(duì)列论皆。依托網(wǎng)絡(luò)模塊的心跳(Update方法,這里的網(wǎng)絡(luò)模塊指你自己實(shí)現(xiàn)的網(wǎng)絡(luò)架構(gòu)猾漫,如何讓它的Update執(zhí)行起來由你說了算)点晴,在每次心跳時(shí)檢測(cè)隊(duì)列內(nèi)的請(qǐng)求是否完成,并對(duì)完成的請(qǐng)求執(zhí)行后續(xù)操作静袖。

// 存放 UnityWebRequestAsyncOperation 的列表
private List<UnityWebRequestAsyncOperation> _asyncOperations = new List<UnityWebRequestAsyncOperation>();

public void Update()
{
    for (int i = 0; i < _asyncOperations.Count; i++)
    {
        var asyncOperation = _asyncOperations[i];
        if (!asyncOperation.isDone)
        {
            // 請(qǐng)求未完成觉鼻,則略過后續(xù)處理邏輯
            continue;
        }

        // 從webRequest的downloadHandler中獲取下載的數(shù)據(jù)并處理
        Do(asyncOperation.webRequest.downloadHandler.data);
        asyncOperation.Dispose();
        // 移除已處理完的請(qǐng)求
        _asyncOperations.RemoveAt(i--);
    }
}

需要說明的是,處理完下載數(shù)據(jù)后一定要對(duì)asyncOperation執(zhí)行Dispose操作队橙,否則Unity很可能會(huì)有Native數(shù)據(jù)泄露的報(bào)錯(cuò)坠陈。初步判定這是Unity對(duì)UnityWebRequest管理導(dǎo)致的報(bào)錯(cuò)萨惑。

UnityWebRequest的結(jié)果

上述代碼并沒有加入處理網(wǎng)絡(luò)錯(cuò)誤的內(nèi)容。

UnityWebRequest對(duì)于錯(cuò)誤的處理仇矾,主要是識(shí)別UnityWebRequest.result的值庸蔼。其本質(zhì)是一個(gè)枚舉,具體如下:

枚舉值 含義 解釋
InProgress 請(qǐng)求尚未結(jié)束 -
Success 請(qǐng)求成功 -
ConnectionError 與服務(wù)器通信失敗 例如贮匕,請(qǐng)求無(wú)法連接或無(wú)法建立安全通道
ProtocolError 服務(wù)器返回一個(gè)錯(cuò)誤響應(yīng) 請(qǐng)求成功地與服務(wù)器進(jìn)行了通信姐仅,但收到了連接協(xié)議定義的錯(cuò)誤
DataProcessingError 數(shù)據(jù)處理錯(cuò)誤。 請(qǐng)求成功地與服務(wù)器通信刻盐,但在處理接收到的數(shù)據(jù)時(shí)遇到了錯(cuò)誤掏膏。例如,數(shù)據(jù)已損壞或格式不正確

長(zhǎng)鏈接

大多數(shù)情況下敦锌,我們游戲開發(fā)的長(zhǎng)鏈接都是建立在TCP協(xié)議之上的馒疹,所以在Unity的實(shí)現(xiàn)中,主要用到的就是Socket乙墙。

基于異步的需求颖变,我們大體有兩種方案實(shí)現(xiàn):

  1. 使用C#的SocketBeginEnd方法實(shí)現(xiàn)連接、發(fā)送听想、接收腥刹,如BeginConnect/EndConnectBeginSend/EndSend汉买、BeginReceive/EndReceive衔峰;
  2. 利用多線程,開啟單獨(dú)的線程進(jìn)行Socket的連接录别、發(fā)送朽色、接收功能,主線程與網(wǎng)絡(luò)線程之間采用環(huán)形Buffer實(shí)現(xiàn)數(shù)據(jù)交互组题。

方案1——Begin/End方法對(duì)

對(duì)這些方法對(duì)的解釋葫男,建議直接查看MSDN上的解釋:Socket 類 (System.Net.Sockets) | Microsoft Docs。下面著重講數(shù)據(jù)發(fā)送與接收的方案崔列。

數(shù)據(jù)發(fā)送梢褐,因?yàn)椴捎昧水惒椒桨福哉w難度不大赵讯,只是在EndSend調(diào)用時(shí)需要判斷返回的已發(fā)送的字節(jié)數(shù)是否與BeginSend發(fā)送的一致盈咳,以及會(huì)否try-catch到異常。一旦發(fā)現(xiàn)數(shù)據(jù)發(fā)送異常(實(shí)際發(fā)送數(shù)量小于開始發(fā)送的數(shù)量边翼,或捕獲到了異常)鱼响,就需要考慮做斷線處理。

public void Send(byte[] data)
{
    _socket.BeginSend(data, 0, data.Length, SocketFlags.None, OnSend, _socket);
}

private void OnSend(IAsyncResult result)
{
    try
    {
        Socket socket = result.AsyncState as Socket;
        int sendLen = socket.EndSend(result);
        // 驗(yàn)證發(fā)送數(shù)據(jù)長(zhǎng)度
    }
    catch (Exception e)
    {
        // 關(guān)閉Socket
    }
}

數(shù)據(jù)接收的方案有很多组底,我所才用的方案需要與底層數(shù)據(jù)協(xié)議相關(guān)聯(lián)丈积。

底層數(shù)據(jù)協(xié)議

字節(jié)數(shù) 4 byte n byte
含義: 數(shù)據(jù)長(zhǎng)度 數(shù)據(jù)體
詳情: 存儲(chǔ)數(shù)據(jù)體的長(zhǎng)度n筐骇,因占用4字節(jié)而存儲(chǔ)為Int型 具體的數(shù)據(jù)體,可以是一個(gè)PB協(xié)議序列化后的內(nèi)容江滨,也可以是PB類型id與PB數(shù)據(jù)的組合

基于上面的協(xié)議铛纬,就可以給出代碼:

// 數(shù)據(jù)緩存buffer
byte[] buffer = new byte[1024];

void BeginReceive()
{
    // 開始接收數(shù)據(jù)長(zhǎng)度
    _socket.BeginReceive(buffer, 0, 4, SocketFlags.None, OnReceiveHead, _socket);
}

void OnReceiveHead(IAsyncResult result)
{
    try
    {
        Socket socket = result.AsyncState as Socket;
        int sendLen = socket.EndReceive(result);
        // 將接收到的數(shù)據(jù)長(zhǎng)度轉(zhuǎn)化為int值
        int bodyLen = BitConverter.ToInt32(buffer);
        // 開始接收數(shù)據(jù)體
        socket.BeginReceive(buffer, 0, bodyLen, SocketFlags.None, OnReceiveHead, _socket);
    }
    catch (Exception e)
    {
        // 關(guān)閉Socket
    }
}

void OnReceiveBody(IAsyncResult result)
{
    try
    {
        Socket socket = result.AsyncState as Socket;
        int sendLen = socket.EndReceive(result);
        // 對(duì)接收到的數(shù)據(jù)進(jìn)行處理
        BeginReceive(); // 繼續(xù)開始接收下一條數(shù)據(jù)
    }
    catch (Exception e)
    {
        // 關(guān)閉Socket
    }
}

方案的具體執(zhí)行在上面的代碼中已給出,雖然缺少了很多異常的驗(yàn)證以及拼包的操作唬滑,但是上面的代碼已經(jīng)能夠說明我們方案的思路告唆。

這個(gè)方案就是借助了Socket的異步操作可以指定接收數(shù)據(jù)長(zhǎng)度的特性,將每次數(shù)據(jù)接收分為了兩部分:

  1. 接收數(shù)據(jù)的長(zhǎng)度信息
  2. 依據(jù)已經(jīng)接收到的數(shù)據(jù)長(zhǎng)度信息晶密,繼續(xù)接收指定長(zhǎng)度的數(shù)據(jù)體

這兩部分構(gòu)成了一條完整數(shù)據(jù)的接收流程擒悬,每一步完成后再繼續(xù)執(zhí)行下一步,直到socket主動(dòng)關(guān)閉或收到異常信息(可能是服務(wù)器主動(dòng)斷開連接)稻艰。

方案2——多線程

除了利用Socket所提供的異步方法茄螃,我們還可以將它的同步方法用在非主線程中。

網(wǎng)絡(luò)上關(guān)于Socket的同步方法實(shí)現(xiàn)發(fā)送與接收的文章已經(jīng)很多了连锯,這里不做贅述,只是提醒這一部分我們需要仔細(xì)設(shè)計(jì)的是環(huán)形隊(duì)列的部分用狱。

PS:大多數(shù)使用多線程的網(wǎng)絡(luò)方案运怖,主要是為了減輕主線程的計(jì)算、序列化夏伊、反序列化的壓力摇展。一旦要考慮這些,就說明當(dāng)前的使用場(chǎng)景是一個(gè)與服務(wù)器進(jìn)行高頻互動(dòng)的場(chǎng)景溺忧。

環(huán)形隊(duì)列本身可以參考linux中的實(shí)現(xiàn)咏连,只是需要我們依據(jù)項(xiàng)目需求去設(shè)定隊(duì)列的長(zhǎng)度,只有有了合適的長(zhǎng)度才能去使用無(wú)鎖環(huán)形隊(duì)列以幫助我們提升效率鲁森。

發(fā)送祟滴、接收分別采用兩個(gè)環(huán)形隊(duì)列。

因?yàn)槎嗑€程收到的數(shù)據(jù)都會(huì)直接放到接收隊(duì)列中歌溉,所以需要網(wǎng)絡(luò)模塊的心跳介入垄懂,每一幀都去處理接收隊(duì)列中的數(shù)據(jù)(雖然我們可以采用委托的形式,綁定Action以便于收到數(shù)據(jù)后立即執(zhí)行痛垛,但是因?yàn)槎嗑€程中牽扯到主線程內(nèi)Unity相關(guān)的運(yùn)算大多是不支持的草慧,所以就需要在主線程內(nèi)讀取數(shù)據(jù)并處理,最合適的地方就是網(wǎng)絡(luò)模塊的心跳方法)匙头。

C#中socket的異步用法與注意事項(xiàng)

我的項(xiàng)目中的長(zhǎng)鏈接使用了方案1漫谷,所以有一些開發(fā)當(dāng)中遇到的坑,在此記錄并警醒蹂析。

異步操作在多線程內(nèi)執(zhí)行

C#中socketBegin方法的回調(diào)舔示,是在一根單獨(dú)的線程內(nèi)執(zhí)行的碟婆。這就引入了一些與多線程相關(guān)的問題

比如:當(dāng)我們將異步接收的數(shù)據(jù)放入隊(duì)列斩郎,而Unity又在心跳內(nèi)從隊(duì)列中拿取數(shù)據(jù)脑融,這就牽扯到了多線程對(duì)隊(duì)列資源的爭(zhēng)搶,需要考慮線程安全問題與解決方案缩宜。

再比如:當(dāng)我們?cè)谶B接成功的回調(diào)內(nèi)調(diào)用函數(shù)X時(shí)肘迎,這個(gè)X可能也會(huì)在主線程內(nèi)調(diào)用,這時(shí)就凸顯出函數(shù)X內(nèi)線程安全的重要性锻煌。我們項(xiàng)目中的一個(gè)bug就與此相關(guān)妓布,就是因?yàn)槲覀冊(cè)谶@個(gè)X函數(shù)中使用了一個(gè)公共的緩存數(shù)組,導(dǎo)致這個(gè)函數(shù)X被Unity主線程與socket的回調(diào)線程同時(shí)調(diào)用宋梧,出現(xiàn)了意想不到的結(jié)果匣沼。

關(guān)閉socket與異步操作的時(shí)序

當(dāng)關(guān)閉socket時(shí),我們依此調(diào)用了socket的Shutdown捂龄、Disconnect释涛、Close方法,而此時(shí)我們的BeginReceive還處于阻塞狀態(tài)倦沧。因?yàn)?code>BeginReceive多線程實(shí)現(xiàn)的原因唇撬,我們的Close方法執(zhí)行過程內(nèi)并不會(huì)立即執(zhí)行BeginReceive的終止操作,這就導(dǎo)致我們?cè)?code>Close調(diào)用后的邏輯可能會(huì)先于BeginReceive的中止執(zhí)行展融。也是因?yàn)檫@個(gè)原因窖认,當(dāng)我們?cè)谡{(diào)用Close之后立即執(zhí)行SocketConnect以期建立一個(gè)新鏈接時(shí),卻受到了鏈接斷開的結(jié)果(因?yàn)槲覀兊拇a中當(dāng)BeginReceive終止時(shí)執(zhí)行斷開鏈接操作)告希。


以上扑浸,就是我對(duì)于一個(gè)簡(jiǎn)易而又基礎(chǔ)的網(wǎng)絡(luò)模塊的整理與總結(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末燕偶,一起剝皮案震驚了整個(gè)濱河市喝噪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杭跪,老刑警劉巖仙逻,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異涧尿,居然都是意外死亡系奉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門姑廉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缺亮,“玉大人,你說我怎么就攤上這事∶弱猓” “怎么了葵礼?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)并鸵。 經(jīng)常有香客問我鸳粉,道長(zhǎng),這世上最難降的妖魔是什么园担? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任届谈,我火速辦了婚禮,結(jié)果婚禮上弯汰,老公的妹妹穿的比我還像新娘艰山。我一直安慰自己,他們只是感情好咏闪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布曙搬。 她就那樣靜靜地躺著,像睡著了一般鸽嫂。 火紅的嫁衣襯著肌膚如雪纵装。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天据某,我揣著相機(jī)與錄音搂擦,去河邊找鬼。 笑死哗脖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的扳还。 我是一名探鬼主播才避,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼氨距!你這毒婦竟也來了桑逝?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤俏让,失蹤者是張志新(化名)和其女友劉穎楞遏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體首昔,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寡喝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勒奇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片预鬓。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赊颠,靈堂內(nèi)的尸體忽然破棺而出格二,到底是詐尸還是另有隱情劈彪,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布顶猜,位于F島的核電站沧奴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏长窄。R本人自食惡果不足惜滔吠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望抄淑。 院中可真熱鬧屠凶,春花似錦、人聲如沸肆资。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)郑原。三九已至唉韭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間犯犁,已是汗流浹背属愤。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留酸役,地道東北人住诸。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像涣澡,于是被迫代替她去往敵國(guó)和親贱呐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容