• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            教父的告白
            一切都是紙老虎
            posts - 82,  comments - 7,  trackbacks - 0

            本文作者:sodme
            本文出處:http://blog.csdn.net/sodme
            聲明:本文可以不經(jīng)作者同意任意轉(zhuǎn)載、復(fù)制、傳播,但任何對(duì)本文的引用都請(qǐng)保留作者、出處及本聲明信息。謝謝!

            常見(jiàn)的網(wǎng)絡(luò)服務(wù)器,基本上是7*24小時(shí)運(yùn)轉(zhuǎn)的,對(duì)于網(wǎng)游來(lái)說(shuō),至少要求服務(wù)器要能連續(xù)工作一周以上的時(shí)間并保證不出現(xiàn)服務(wù)器崩潰這樣的災(zāi)難性事件。事 實(shí)上,要求一個(gè)服務(wù)器在連續(xù)的滿負(fù)荷運(yùn)轉(zhuǎn)下不出任何異常,要求它設(shè)計(jì)的近乎完美,這幾乎是不太現(xiàn)實(shí)的。服務(wù)器本身可以出異常(但要盡可能少得出),但是, 服務(wù)器本身應(yīng)該被設(shè)計(jì)得足以健壯,“小病小災(zāi)”打不垮它,這就要求服務(wù)器在異常處理方面要下很多功夫。

              服務(wù)器的異常處理包括的內(nèi)容非常廣泛,本文僅就在網(wǎng)絡(luò)封包方面出現(xiàn)的異常作一討論,希望能對(duì)正從事相關(guān)工作的朋友有所幫助。

              關(guān)于網(wǎng)絡(luò)封包方面的異常,總體來(lái)說(shuō),可以分為兩大類(lèi):一是封包格式出現(xiàn)異常;二是封包內(nèi)容(即封包數(shù)據(jù))出現(xiàn)異常。在封包格式的異常處理方面, 我們?cè)谧畹锥说木W(wǎng)絡(luò)數(shù)據(jù)包接收模塊便可以加以處理。而對(duì)于封包數(shù)據(jù)內(nèi)容出現(xiàn)的異常,只有依靠游戲本身的邏輯去加以判定和檢驗(yàn)。游戲邏輯方面的異常處理,是 隨每個(gè)游戲的不同而不同的,所以,本文隨后的內(nèi)容將重點(diǎn)闡述在網(wǎng)絡(luò)數(shù)據(jù)包接收模塊中的異常處理。

              為方便以下的討論,先明確兩個(gè)概念(這兩個(gè)概念是為了敘述方面,筆者自行取的,并無(wú)標(biāo)準(zhǔn)可言):
              1、邏輯包:指的是在應(yīng)用層提交的數(shù)據(jù)包,一個(gè)完整的邏輯包可以表示一個(gè)確切的邏輯意義。比如登錄包,它里面就可以含有用戶(hù)名字段和密碼字段。盡管它看上去也是一段緩沖區(qū)數(shù)據(jù),但這個(gè)緩沖區(qū)里的各個(gè)區(qū)間是代表一定的邏輯意義的。
              2、物理包:指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)從網(wǎng)絡(luò)底層接收到的數(shù)據(jù)包,這樣收到的一個(gè)數(shù)據(jù)包,能不能表示一個(gè)完整的邏輯意義,要取決于它是通過(guò)UDP類(lèi)的“數(shù)據(jù)報(bào)協(xié)議”發(fā)的包還是通過(guò)TCP類(lèi)的“流協(xié)議”發(fā)的包。

              我們知道,TCP是流協(xié)議,“流協(xié)議”與“數(shù)據(jù)報(bào)協(xié)議”的不同點(diǎn)在于:“數(shù)據(jù)報(bào)協(xié)議”中的一個(gè)網(wǎng)絡(luò)包本身就是一個(gè)完整的邏輯包,也就是說(shuō),在應(yīng) 用層使用sendto發(fā)送了一個(gè)邏輯包之后,在接收端通過(guò)recvfrom接收到的就是剛才使用sendto發(fā)送的那個(gè)邏輯包,這個(gè)包不會(huì)被分開(kāi)發(fā)送,也 不會(huì)與其它的包放在一起發(fā)送。但對(duì)于TCP而言,TCP會(huì)根據(jù)網(wǎng)絡(luò)狀況和neagle算法,或者將一個(gè)邏輯包單獨(dú)發(fā)送,或者將一個(gè)邏輯包分成若干次發(fā)送, 或者會(huì)將若干個(gè)邏輯包合在一起發(fā)送出去。正因?yàn)門(mén)CP在邏輯包處理方面的這種粘合性,要求我們?cè)谧骰赥CP的應(yīng)用時(shí),一般都要編寫(xiě)相應(yīng)的拼包、解包代 碼。

              因此,基于TCP的上層應(yīng)用,一般都要定義自己的包格式。TCP的封包定義中,除了具體的數(shù)據(jù)內(nèi)容所代表的邏輯意義之外,第一步就是要確定以何種方式表示當(dāng)前包的開(kāi)始和結(jié)束。通常情況下,表示一個(gè)TCP邏輯包的開(kāi)始和結(jié)束有兩種方式:
              1、以特殊的開(kāi)始和結(jié)束標(biāo)志表示,比如FF00表示開(kāi)始,00FF表示結(jié)束。
              2、直接以包長(zhǎng)度來(lái)表示。比如可以用第一個(gè)字節(jié)表示包總長(zhǎng)度,如果覺(jué)得這樣的話包比較小,也可以用兩個(gè)字節(jié)表示包長(zhǎng)度。

              下面將要給出的代碼是以第2種方式定義的數(shù)據(jù)包,包長(zhǎng)度以每個(gè)封包的前兩個(gè)字節(jié)表示。我將結(jié)合著代碼給出相關(guān)的解釋和說(shuō)明。

              函數(shù)中用到的變量說(shuō)明:

              CLIENT_BUFFER_SIZE:緩沖區(qū)的長(zhǎng)度,定義為:Const int CLIENT_BUFFER_SIZE=4096。
              m_ClientDataBuf:數(shù)據(jù)整理緩沖區(qū),每次收到的數(shù)據(jù),都會(huì)先被復(fù)制到這個(gè)緩沖區(qū)的末尾,然后由下面的整理函數(shù)對(duì)這個(gè)緩沖區(qū)進(jìn)行整理。它的定義是:char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。
              m_DataBufByteCount:數(shù)據(jù)整理緩沖區(qū)中當(dāng)前剩余的未整理字節(jié)數(shù)。
              GetPacketLen(const char*):函數(shù),可以根據(jù)傳入的緩沖區(qū)首址按照應(yīng)用層協(xié)議取出當(dāng)前邏輯包的長(zhǎng)度。
              GetGamePacket(const char*, int):函數(shù),可以根據(jù)傳入的緩沖區(qū)生成相應(yīng)的游戲邏輯數(shù)據(jù)包。
              AddToExeList(PBaseGamePacket):函數(shù),將指定的游戲邏輯數(shù)據(jù)包加入待處理的游戲邏輯數(shù)據(jù)包隊(duì)列中,等待邏輯處理線程對(duì)其進(jìn)行處理。
              DATA_POS:指的是除了包長(zhǎng)度、包類(lèi)型等這些標(biāo)志型字段之外,真正的數(shù)據(jù)包內(nèi)容的起始位置。

            Bool SplitFun(const char* pData,const int &len)
            {
                PBaseGamePacket pGamePacket=NULL;
                __int64 startPos=0, prePos=0, i=0;
                int packetLen=0;

              //先將本次收到的數(shù)據(jù)復(fù)制到整理緩沖區(qū)尾部
                startPos = m_DataBufByteCount;  
                memcpy( m_ClientDataBuf+startPos, pData, len );
                m_DataBufByteCount += len;   

                //當(dāng)整理緩沖區(qū)內(nèi)的字節(jié)數(shù)少于DATA_POS字節(jié)時(shí),取不到長(zhǎng)度信息則退出
             //注意:退出時(shí)并不置m_DataBufByteCount為0
                if (m_DataBufByteCount < DATA_POS+1)
                    return false; 

                //根據(jù)正常邏輯,下面的情況不可能出現(xiàn),為穩(wěn)妥起見(jiàn),還是加上
                if (m_DataBufByteCount >  2*CLIENT_BUFFER_SIZE)
                {
                    //設(shè)置m_DataBufByteCount為0,意味著丟棄緩沖區(qū)中的現(xiàn)有數(shù)據(jù)
                    m_DataBufByteCount = 0;

              //可以考慮開(kāi)放錯(cuò)誤格式數(shù)據(jù)包的處理接口,處理邏輯交給上層
              //OnPacketError()
                    return false;
                }

                 //還原起始指針
                 startPos = 0;

                 //只有當(dāng)m_ClientDataBuf中的字節(jié)個(gè)數(shù)大于最小包長(zhǎng)度時(shí)才能執(zhí)行此語(yǔ)句
                packetLen = GetPacketLen( pIOCPClient->m_ClientDataBuf );

                //當(dāng)邏輯層的包長(zhǎng)度不合法時(shí),則直接丟棄該包
                if ((packetLen < DATA_POS+1) || (packetLen > 2*CLIENT_BUFFER_SIZE))
                {
                    m_DataBufByteCount = 0;

              //OnPacketError()
                    return false;
                }

                //保留整理緩沖區(qū)的末尾指針
                __int64 oldlen = m_DataBufByteCount; 

                while ((packetLen <= m_DataBufByteCount) && (m_DataBufByteCount>0))
                {
                    //調(diào)用拼包邏輯,獲取該緩沖區(qū)數(shù)據(jù)對(duì)應(yīng)的數(shù)據(jù)包
                    pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen); 

                    if (pGamePacket!=NULL)
                    {
                        //將數(shù)據(jù)包加入執(zhí)行隊(duì)列
                        AddToExeList(pGamePacket);
                    }

                    pGamePacket = NULL;
             
              //整理緩沖區(qū)的剩余字節(jié)數(shù)和新邏輯包的起始位置進(jìn)行調(diào)整
                    m_DataBufByteCount -= packetLen;
                    startPos += packetLen; 

                    //殘留緩沖區(qū)的字節(jié)數(shù)少于一個(gè)正常包大小時(shí),只向前復(fù)制該包隨后退出
                    if (m_DataBufByteCount < DATA_POS+1)
                    {
                        for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
                            m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];

                        return true;
                    }

                    packetLen = GetPacketLen(m_ClientDataBuf + startPos );

                     //當(dāng)邏輯層的包長(zhǎng)度不合法時(shí),丟棄該包及緩沖區(qū)以后的包
                    if ((packetLen<DATA_POS+1) || (packetLen>2*CLIENT_BUFFER_SIZE))
                    {
                        m_DataBufByteCount = 0;

                  //OnPacketError()
                        return false;
                    }

                     if (startPos+packetLen>=oldlen)
                    {
                        for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
                            m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];          

                        return true;
                    }
                 }//取所有完整的包

                 return true;
            }

              以上便是數(shù)據(jù)接收模塊的處理函數(shù),下面是幾點(diǎn)簡(jiǎn)要說(shuō)明:

              1、用于拼包整理的緩沖區(qū)(m_ClientDataBuf)應(yīng)該比recv中指定的接收緩沖區(qū)(pData)長(zhǎng)度(CLIENT_BUFFER_SIZE)要大,通常前者是后者的2倍(2*CLIENT_BUFFER_SIZE)或更大。

              2、為避免因?yàn)槭S鄶?shù)據(jù)前移而導(dǎo)致的額外開(kāi)銷(xiāo),建議m_ClientDataBuf使用環(huán)形緩沖區(qū)實(shí)現(xiàn)。

            3、為了避免出現(xiàn)無(wú)法拼裝的包,我們約定每次發(fā)送的邏輯包,其單個(gè)邏輯包最大長(zhǎng)度不可以超過(guò)CLIENT_BUFFER_SIZE的2倍。因?yàn)槲覀兊恼? 理緩沖區(qū)只有2*CLIENT_BUFFER_SIZE這么長(zhǎng),更長(zhǎng)的數(shù)據(jù),我們將無(wú)法整理。這就要求在協(xié)議的設(shè)計(jì)上以及最終的發(fā)送函數(shù)的處理上要加上這 樣的異常處理機(jī)制。


              4、對(duì)于數(shù)據(jù)包過(guò)短或過(guò)長(zhǎng)的包,我們通常的情況是置m_DataBufByteCount為0,即舍棄當(dāng)前包的處理。如果此處不設(shè)置 m_DataBufByteCount為0也可,但該客戶(hù)端只要發(fā)了一次格式錯(cuò)誤的包,則其后繼發(fā)過(guò)來(lái)的包則也將連帶著產(chǎn)生格式錯(cuò)誤,如果設(shè)置 m_DataBufByteCount為0,則可以比較好的避免后繼的包受此包的格式錯(cuò)誤影響。更好的作法是,在此處開(kāi)放一個(gè)封包格式異常的處理接口 (OnPacketError),由上層邏輯決定對(duì)這種異常如何處置。比如上層邏輯可以對(duì)封包格式方面出現(xiàn)的異常進(jìn)行計(jì)數(shù),如果錯(cuò)誤的次數(shù)超過(guò)一定的值, 則可以斷開(kāi)該客戶(hù)端的連接。

              5、建議不要在recv或wsarecv的函數(shù)后,就緊接著作以上的處理。當(dāng)recv收到一段數(shù)據(jù)后,生成一個(gè)結(jié)構(gòu)體或?qū)ο?它主要含有 data和len兩個(gè)內(nèi)容,前者是數(shù)據(jù)緩沖區(qū),后者是數(shù)據(jù)長(zhǎng)度),將這樣的一個(gè)結(jié)構(gòu)體或?qū)ο蠓诺揭粋€(gè)隊(duì)列中由后面的線程對(duì)其使用SplitFun函數(shù)進(jìn)行 整理。這樣,可以最大限度地提高網(wǎng)絡(luò)數(shù)據(jù)的接收速度,不至因?yàn)閿?shù)據(jù)整理的原因而在此處浪費(fèi)時(shí)間。

              代碼中,我已經(jīng)作了比較詳細(xì)的注釋?zhuān)梢宰鳛槠窗瘮?shù)的參考,代碼是從偶的應(yīng)用中提取、修改而來(lái),本身只為演示之用,所以未作調(diào)試,應(yīng)用時(shí)需要你自己去完善。如有疑問(wèn),可以我的blog上留言提出。

            posted @ 2009-09-23 23:47 暗夜教父 閱讀(715) | 評(píng)論 (0)編輯 收藏
            本文作者:sodme 本文出處:http://blog.csdn.net/sodme
            版權(quán)聲明:本文可以不經(jīng)作者同意任意轉(zhuǎn)載,但轉(zhuǎn)載時(shí)煩請(qǐng)保留文章開(kāi)始前兩行的版權(quán)、作者及出處信息。

            提示:閱讀本文前,請(qǐng)先讀此文了解文章背景:http://data.gameres.com/message.asp?TopicID=27236

              讓無(wú)數(shù)中國(guó)玩家為之矚目的“魔獸世界”,隨著一系列內(nèi)測(cè)前期工作的逐步展開(kāi),正在一步步地走近中國(guó)玩家,但是,“魔獸”的服務(wù)器,卻著實(shí)讓我們?yōu)樗罅艘话押埂?br>
            造成一個(gè)網(wǎng)游服務(wù)器當(dāng)機(jī)的原因有很多,但主要有以下兩種:一,服務(wù)器在線人數(shù)達(dá)到上限,服務(wù)器處理效率嚴(yán)重遲緩,造成當(dāng)機(jī);二,由于外掛或其它游戲作弊 工具導(dǎo)致的非正常數(shù)據(jù)包的出錯(cuò),導(dǎo)致游戲服務(wù)器邏輯出現(xiàn)混亂,從而造成當(dāng)機(jī)。在這里,我主要想說(shuō)說(shuō)后者如何盡可能地避免。

              要避免以上 所說(shuō)到的第二種情況,我們就應(yīng)該遵循一個(gè)基本原則:在網(wǎng)游服務(wù)器的設(shè)計(jì)中,對(duì)于具有較強(qiáng)邏輯關(guān)系的處理單元,服務(wù)器端和客戶(hù)端應(yīng)該采用“互不信任原則”, 即:服務(wù)器端即使收到了客戶(hù)端的數(shù)據(jù)包,也并不是立刻就認(rèn)為客戶(hù)端已經(jīng)達(dá)到了某種功能或者狀態(tài),客戶(hù)端到達(dá)是否達(dá)到了某種功能或者狀態(tài),還必須依靠服務(wù)器 端上記載的該客戶(hù)端“以往狀態(tài)”來(lái)判定,也就是說(shuō):服務(wù)器端的邏輯執(zhí)行并不單純地以“當(dāng)前”的這一個(gè)客戶(hù)端封包來(lái)進(jìn)行,它還應(yīng)該廣泛參考當(dāng)前封包的上下文 環(huán)境,對(duì)執(zhí)行的邏輯作出更進(jìn)一步地判定,同時(shí),在單個(gè)封包的處理上,服務(wù)器端應(yīng)該廣泛考慮當(dāng)前客戶(hù)端封包所需要的“前置”封包,如果沒(méi)有收到該客戶(hù)端應(yīng)該 發(fā)過(guò)來(lái)的“前置”封包,則當(dāng)前的封包應(yīng)該不進(jìn)行處理或進(jìn)行異常處理(如果想要性能高,則可以直接忽略該封包;如果想讓服務(wù)器穩(wěn)定,可以進(jìn)行不同的異常處 理)。

              之所以采用“互不信任”原則設(shè)計(jì)網(wǎng)游服務(wù)器,一個(gè)很重要的考慮是:防外掛。對(duì)于一個(gè)網(wǎng)絡(luò)服務(wù)器(不僅僅是游戲服務(wù)器,泛指所有 服務(wù)器)而言,它所面對(duì)的對(duì)象既有屬于自己系統(tǒng)內(nèi)的合法的網(wǎng)絡(luò)客戶(hù)端,也有不屬于自己系統(tǒng)內(nèi)的非法客戶(hù)端訪問(wèn)。所以,我們?cè)诳紤]服務(wù)器向外開(kāi)放的接口時(shí), 就要同時(shí)考慮這兩種情況:合法客戶(hù)端訪問(wèn)時(shí)的邏輯走向以及非法客戶(hù)端訪問(wèn)時(shí)的邏輯走向。舉個(gè)簡(jiǎn)單的例子:一般情況下,玩家登錄邏輯中,都是先向服務(wù)器發(fā)送 用戶(hù)名和密碼,然后再向服務(wù)器發(fā)送進(jìn)入某組服務(wù)器的數(shù)據(jù)包;但在非法客戶(hù)端(如外掛)中,則這些客戶(hù)端則完全有可能先發(fā)進(jìn)入某組服務(wù)器的數(shù)據(jù)包。當(dāng)然,這 里僅僅是舉個(gè)例子,也許并不妥當(dāng),但基本的意思我已經(jīng)表達(dá)清楚了,即:你服務(wù)器端不要我客戶(hù)端發(fā)什么你就信什么,你還得進(jìn)行一系列的邏輯驗(yàn)證,以判定我當(dāng) 前執(zhí)行的操作是不是合法的。以這個(gè)例子中,服務(wù)器端可以通過(guò)以下邏輯執(zhí)行驗(yàn)證功能:只有當(dāng)客戶(hù)端的用戶(hù)名和密碼通過(guò)驗(yàn)證后,該客戶(hù)端才會(huì)進(jìn)入在線玩家列表 中。而只有在線玩家列表中的成員,才可以在登陸服務(wù)器的引導(dǎo)下進(jìn)入各分組服務(wù)器。

              總之,在從事網(wǎng)游服務(wù)器的設(shè)計(jì)過(guò)程中,要始終不移地 堅(jiān)持一個(gè)信念:我們的服務(wù)器,不僅僅有自己的游戲客戶(hù)端在訪問(wèn),還有其它很多他人寫(xiě)的游戲客戶(hù)端在訪問(wèn),所以,我們應(yīng)該確保我們的服務(wù)器是足夠強(qiáng)壯的,任 它風(fēng)吹雨打也不怕,更不會(huì)倒。如果在開(kāi)發(fā)實(shí)踐中,沒(méi)有很好地領(lǐng)會(huì)這一點(diǎn)或者未能將這一思路貫穿進(jìn)開(kāi)發(fā)之中,那么,你設(shè)計(jì)出來(lái)的服務(wù)器將是無(wú)比脆弱的。

            當(dāng)然,安全性和效率總是相互對(duì)立的。為了實(shí)現(xiàn)我們所說(shuō)的“互不信任”原則,難免的,就會(huì)在游戲邏輯中加入很多的異常檢測(cè)機(jī)制,但異常檢測(cè)又是比較耗時(shí) 的,這就需要我們?cè)谛屎桶踩苑矫孀鱾€(gè)取舍,對(duì)于特別重要的邏輯,我們應(yīng)該全面貫徹“互不信任”原則,一步扣一步,步步為營(yíng),不讓游戲邏輯出現(xiàn)一點(diǎn)漏 洞。而對(duì)于并非十分重要的場(chǎng)合,則完全可以采用“半信任”或者根本“不須信任”的原則進(jìn)行設(shè)計(jì),以盡可能地提高服務(wù)器效率。

              本文只是對(duì)自己長(zhǎng)期從事游戲服務(wù)器設(shè)計(jì)以來(lái)的感受加以總結(jié),也是對(duì)魔獸的服務(wù)器有感而發(fā)。歡迎有相同感受的朋友或從事相同工作的朋友一起討論。

            posted @ 2009-09-23 23:47 暗夜教父 閱讀(553) | 評(píng)論 (0)編輯 收藏

            本文作者:sodme 本文出處:http://blog.csdn.net/sodme
            版權(quán)聲明:本文可以不經(jīng)作者同意任意轉(zhuǎn)載,但轉(zhuǎn)載時(shí)煩請(qǐng)保留文章開(kāi)始前兩行的版權(quán)、作者及出處信息。

              QQ游戲于前幾日終于突破了百萬(wàn)人同時(shí)在線的關(guān)口,向著更為遠(yuǎn)大的目標(biāo)邁進(jìn),這讓其它眾多傳統(tǒng)的棋牌休閑游戲平臺(tái)黯然失色,相比之下,聯(lián)眾似乎 已經(jīng)根本不是QQ的對(duì)手,因?yàn)镼Q除了這100萬(wàn)的游戲在線人數(shù)外,它還擁有3億多的注冊(cè)量(當(dāng)然很多是重復(fù)注冊(cè)的)以及QQ聊天軟件900萬(wàn)的同時(shí)在線 率,我們已經(jīng)可以預(yù)見(jiàn)未來(lái)由QQ構(gòu)建起來(lái)的強(qiáng)大棋牌休閑游戲帝國(guó)。
            那么,在技術(shù)上,QQ游戲到底是如何實(shí)現(xiàn)百萬(wàn)人同時(shí)在線并保持游戲高效率的呢?
            事實(shí)上,針對(duì)于任何單一的網(wǎng)絡(luò)服務(wù)器程序,其可承受的同時(shí)連接數(shù)目是有理論峰值的,通過(guò)C++中對(duì)TSocket的定義類(lèi)型:word,我們可以判定 這個(gè)連接理論峰值是65535,也就是說(shuō),你的單個(gè)服務(wù)器程序,最多可以承受6萬(wàn)多的用戶(hù)同時(shí)連接。但是,在實(shí)際應(yīng)用中,能達(dá)到一萬(wàn)人的同時(shí)連接并能保證 正常的數(shù)據(jù)交換已經(jīng)是很不容易了,通常這個(gè)值都在2000到5000之間,據(jù)說(shuō)QQ的單臺(tái)服務(wù)器同時(shí)連接數(shù)目也就是在這個(gè)值這間。
            如果要實(shí)現(xiàn)2000到5000用戶(hù)的單服務(wù)器同時(shí)在線,是不難的。在windows下,比較成熟的技術(shù)是采用IOCP--完成端口。與完成端口相關(guān)的 資料在網(wǎng)上和CSDN論壇里有很多,感興趣的朋友可以自己搜索一下。只要運(yùn)用得當(dāng),一個(gè)完成端口服務(wù)器是完全可以達(dá)到2K到5K的同時(shí)在線量的。但,5K 這樣的數(shù)值離百萬(wàn)這樣的數(shù)值實(shí)在相差太大了,所以,百萬(wàn)人的同時(shí)在線是單臺(tái)服務(wù)器肯定無(wú)法實(shí)現(xiàn)的。
            要實(shí)現(xiàn)百萬(wàn)人同時(shí)在線,首先要實(shí)現(xiàn)一個(gè)比較完善的完成端口服務(wù)器模型,這個(gè)模型要求至少可以承載2K到5K的同時(shí)在線率(當(dāng)然,如果你MONEY多, 你也可以只開(kāi)發(fā)出最多允許100人在線的服務(wù)器)。在構(gòu)建好了基本的完成端口服務(wù)器之后,就是有關(guān)服務(wù)器組的架構(gòu)設(shè)計(jì)了。之所以說(shuō)這是一個(gè)服務(wù)器組,是因 為它絕不僅僅只是一臺(tái)服務(wù)器,也絕不僅僅是只有一種類(lèi)型的服務(wù)器。
            簡(jiǎn)單地說(shuō),實(shí)現(xiàn)百萬(wàn)人同時(shí)在線的服務(wù)器模型應(yīng)該是:登陸服務(wù)器+大廳服務(wù)器+房間服務(wù)器。當(dāng)然,也可以是其它的模型,但其基本的思想是一樣的。下面,我將逐一介紹這三類(lèi)服務(wù)器的各自作用。
            登陸服務(wù)器:一般情況下,我們會(huì)向玩家開(kāi)放若干個(gè)公開(kāi)的登陸服務(wù)器,就如QQ登陸時(shí)讓你選擇的從哪個(gè)QQ游戲服務(wù)器登陸一樣,QQ登陸時(shí)讓玩家選擇的 六個(gè)服務(wù)器入口實(shí)際上就是登陸服務(wù)器。登陸服務(wù)器主要完成負(fù)載平衡的作用。詳細(xì)點(diǎn)說(shuō)就是,在登陸服務(wù)器的背后,有N個(gè)大廳服務(wù)器,登陸服務(wù)器只是用于為當(dāng) 前的客戶(hù)端連接選擇其下一步應(yīng)該連接到哪個(gè)大廳服務(wù)器,當(dāng)?shù)顷懛?wù)器為當(dāng)前的客戶(hù)端連接選擇了一個(gè)合適的大廳服務(wù)器后,客戶(hù)端開(kāi)始根據(jù)登陸服務(wù)器提供的信 息連接到相應(yīng)的大廳上去,同時(shí)客戶(hù)端斷開(kāi)與登陸服務(wù)器的連接,為其他玩家客戶(hù)端連接登陸服務(wù)器騰出套接字資源。在設(shè)計(jì)登陸服務(wù)器時(shí),至少應(yīng)該有以下功 能:N個(gè)大廳服務(wù)器的每一個(gè)大廳服務(wù)器都要與所有的登陸服務(wù)器保持連接,并實(shí)時(shí)地把本大廳服務(wù)器當(dāng)前的同時(shí)在線人數(shù)通知給各個(gè)登陸服務(wù)器,這其中包括:用 戶(hù)進(jìn)入時(shí)的同時(shí)在線人數(shù)增加信息以及用戶(hù)退出時(shí)的同時(shí)在線人數(shù)減少信息。這里的各個(gè)大廳服務(wù)器同時(shí)在線人數(shù)信息就是登陸服務(wù)器為客戶(hù)端選擇某個(gè)大廳讓其登 陸的依據(jù)。舉例來(lái)說(shuō),玩家A通過(guò)登陸服務(wù)器1連接到登陸服務(wù)器,登陸服務(wù)器開(kāi)始為當(dāng)前玩家在眾多的大廳服務(wù)器中根據(jù)哪一個(gè)大廳服務(wù)器人數(shù)比較少來(lái)選擇一個(gè) 大廳,同時(shí)把這個(gè)大廳的連接IP和端口發(fā)給客戶(hù)端,客戶(hù)端收到這個(gè)IP和端口信息后,根據(jù)這個(gè)信息連接到此大廳,同時(shí),客戶(hù)端斷開(kāi)與登陸服務(wù)器之間的連 接,這便是用戶(hù)登陸過(guò)程中,在登陸服務(wù)器這一塊的處理流程。
            大廳服務(wù)器:大廳服務(wù)器,是普通玩家看不到的服務(wù)器,它的連接IP和端口信息是登陸服務(wù)器通知給客戶(hù)端的。也就是說(shuō),在QQ游戲的本地文件中,具體的 大廳服務(wù)器連接IP和端口信息是沒(méi)有保存的。大廳服務(wù)器的主要作用是向玩家發(fā)送游戲房間列表信息,這些信息包括:每個(gè)游戲房間的類(lèi)型,名稱(chēng),在線人數(shù),連 接地址以及其它如游戲幫助文件URL的信息。從界面上看的話,大廳服務(wù)器就是我們輸入用戶(hù)名和密碼并校驗(yàn)通過(guò)后進(jìn)入的游戲房間列表界面。大廳服務(wù)器,主要 有以下功能:一是向當(dāng)前玩家廣播各個(gè)游戲房間在線人數(shù)信息;二是提供游戲的版本以及下載地址信息;三是提供各個(gè)游戲房間服務(wù)器的連接IP和端口信息;四是 提供游戲幫助的URL信息;五是提供其它游戲輔助功能。但在這眾多的功能中,有一點(diǎn)是最為核心的,即:為玩家提供進(jìn)入具體的游戲房間的通道,讓玩家順利進(jìn) 入其欲進(jìn)入的游戲房間。玩家根據(jù)各個(gè)游戲房間在線人數(shù),判定自己進(jìn)入哪一個(gè)房間,然后雙擊服務(wù)器列表中的某個(gè)游戲房間后玩家開(kāi)始進(jìn)入游戲房間服務(wù)器。
            游戲房間服務(wù)器:游戲房間服務(wù)器,具體地說(shuō)就是如“斗地主1”,“斗地主2”這樣的游戲房間。游戲房間服務(wù)器才是具體的負(fù)責(zé)執(zhí)行游戲相關(guān)邏輯的服務(wù) 器。這樣的游戲邏輯分為兩大類(lèi):一類(lèi)是通用的游戲房間邏輯,如:進(jìn)入房間,離開(kāi)房間,進(jìn)入桌子,離開(kāi)桌子以及在房間內(nèi)說(shuō)話等;第二類(lèi)是游戲桌子邏輯,這個(gè) 就是各種不同類(lèi)型游戲的主要區(qū)別之處了,比如斗地主中的叫地主或不叫地主的邏輯等,當(dāng)然,游戲桌子邏輯里也包括有通用的各個(gè)游戲里都存在的游戲邏輯,比如 在桌子內(nèi)說(shuō)話等。總之,游戲房間服務(wù)器才是真正負(fù)責(zé)執(zhí)行游戲具體邏輯的服務(wù)器。
            這里提到的三類(lèi)服務(wù)器,我均采用的是完成端口模型,每個(gè)服務(wù)器最多連接數(shù)目是5000人,但是,我在游戲房間服務(wù)器上作了邏輯層的限定,最多只允許 300人同時(shí)在線。其他兩個(gè)服務(wù)器仍然允許最多5000人的同時(shí)在線。如果按照這樣的結(jié)構(gòu)來(lái)設(shè)計(jì),那么要實(shí)現(xiàn)百萬(wàn)人的同時(shí)在線就應(yīng)該是這樣:首先是大 廳,1000000/5000=200。也就是說(shuō),至少要200臺(tái)大廳服務(wù)器,但通常情況下,考慮到實(shí)際使用時(shí)服務(wù)器的處理能力和負(fù)載情況,應(yīng)該至少準(zhǔn)備 250臺(tái)左右的大廳服務(wù)器程序。另外,具體的各種類(lèi)型的游戲房間服務(wù)器需要多少,就要根據(jù)當(dāng)前玩各種類(lèi)型游戲的玩家數(shù)目分別計(jì)算了,比如斗地主最多是十萬(wàn) 人同時(shí)在線,每臺(tái)服務(wù)器最多允許300人同時(shí)在線,那么需要的斗地主服務(wù)器數(shù)目就應(yīng)該不少于:100000/300=333,準(zhǔn)備得充分一點(diǎn),就要準(zhǔn)備 350臺(tái)斗地主服務(wù)器。
            除正常的玩家連接外,還要考慮到:
            對(duì)于登陸服務(wù)器,會(huì)有250臺(tái)大廳服務(wù)器連接到每個(gè)登陸服務(wù)器上,這是始終都要保持的連接;
            而對(duì)于大廳服務(wù)器而言,如果僅僅有斗地主這一類(lèi)的服務(wù)器,就要有350多個(gè)連接與各個(gè)大廳服務(wù)器始終保持著。所以從這一點(diǎn)看,我的結(jié)構(gòu)在某些方面還存在著需要改進(jìn)的地方,但核心思想是:盡快地提供用戶(hù)登陸的速度,盡可能方便地讓玩家進(jìn)入游戲中。

            posted @ 2009-09-23 23:44 暗夜教父 閱讀(629) | 評(píng)論 (0)編輯 收藏

            本文作者:sodme
            本文出處:http://blog.csdn.net/sodme
            聲明:本文可以不經(jīng)作者同意任意轉(zhuǎn)載、復(fù)制、引用。但任何對(duì)本文的引用,均須注明本文的作者、出處以及本行聲明信息。

              之前,我分析過(guò)QQ游戲(特指QQ休閑平臺(tái),并非QQ堂,下同)的通信架構(gòu)(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx),分析過(guò)魔獸世界的通信架構(gòu)(http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx), 似乎網(wǎng)絡(luò)游戲的通信架構(gòu)也就是這些了,其實(shí)不然,在網(wǎng)絡(luò)游戲大家庭中,還有一種類(lèi)型的游戲我認(rèn)為有必要把它的通信架構(gòu)專(zhuān)門(mén)作個(gè)介紹,這便是如泡泡堂、QQ 堂類(lèi)的休閑類(lèi)競(jìng)技游戲。曾經(jīng)很多次,被網(wǎng)友們要求能抽時(shí)間看看泡泡堂之類(lèi)游戲的通信架構(gòu),這次由于被逼交作業(yè),所以今晚抽了一點(diǎn)的時(shí)間截了一下泡泡堂的 包,正巧昨日與網(wǎng)友就泡泡堂類(lèi)游戲的通信架構(gòu)有過(guò)一番討論,于是,將這兩天的討論、截包及思考總結(jié)于本文中,希望能對(duì)關(guān)心或者正在開(kāi)發(fā)此類(lèi)游戲的朋友有所 幫助,如果要討論具體的技術(shù)細(xì)節(jié),請(qǐng)到我的BLOG(http://blog.csdn.net/sodme)加我的MSN討論..

              總體來(lái)說(shuō),泡泡堂類(lèi)游戲(此下簡(jiǎn)稱(chēng)泡泡堂)在大廳到房間這一層的通信架構(gòu),其結(jié)構(gòu)與QQ游戲相當(dāng),甚至要比QQ游戲來(lái)得簡(jiǎn)單。所以,在房間這一層的通信架構(gòu)上,我不想過(guò)多討論,不清楚的朋友請(qǐng)參看我對(duì)QQ游戲通信架構(gòu)的分析文章(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx)。可以這么說(shuō),如果采用與QQ游戲相同的房間和大廳架構(gòu),是完全可以組建起一套可擴(kuò)展的支持百萬(wàn)人在線的游戲系統(tǒng)的。也就是說(shuō),通過(guò)負(fù)載均衡+大廳+游戲房間對(duì)游戲邏輯的分?jǐn)偅耆梢詫?shí)現(xiàn)一個(gè)可擴(kuò)展的百萬(wàn)人在線泡泡堂。

              但是,泡泡堂與斗地主的最大不同點(diǎn)在于:泡泡堂對(duì)于實(shí)時(shí)性要求特別高。那么,泡泡堂是如何解決實(shí)時(shí)性與網(wǎng)絡(luò)延遲以及大用戶(hù)量之間矛盾的呢?

              閱讀以下文字前,請(qǐng)確認(rèn)你已經(jīng)完全理解TCP與UDP之間的不同點(diǎn)。

              我們知道,TCP與UDP之間的最大不同點(diǎn)在于:TCP是可靠連接的,而UDP是無(wú)連接的。如果通信雙方使用TCP協(xié)議,那么他們之前必須事先 通過(guò)監(jiān)聽(tīng)+連接的方式將雙方的通信管道建立起來(lái);而如果通信雙方使用的是UDP通信,則雙方不用事先建立連接,發(fā)送方只管向目標(biāo)地址上的目標(biāo)端口發(fā)送 UDP包即可,不用管對(duì)方到底收沒(méi)收到。如果要說(shuō)形象點(diǎn),可以用這樣一句話概括:TCP是打電話,UDP是發(fā)電報(bào)。TCP通信,為了保持這樣的可靠連接, 在可靠性上下了很多功夫,所以導(dǎo)致了它的通信效率要比UDP差很多,所以,一般地,在地實(shí)時(shí)性要求非常高的場(chǎng)合,會(huì)選擇使用UDP協(xié)議,比如常見(jiàn)的動(dòng)作射 擊類(lèi)游戲。

              通過(guò)載包,我們發(fā)現(xiàn)泡泡堂中同時(shí)采用了TCP和UDP兩種通信協(xié)議。并且,具有以下特點(diǎn):
            1.當(dāng)玩家未進(jìn)入具體的游戲地圖時(shí),僅有TCP通信存在,而沒(méi)有UDP通信;
            2.進(jìn)入游戲地圖后,TCP的通信量遠(yuǎn)遠(yuǎn)小于UDP的通信量
            3.UDP的通信IP個(gè)數(shù),與房間內(nèi)的玩家成一一對(duì)應(yīng)關(guān)系(這一點(diǎn),應(yīng)網(wǎng)友疑惑而加,此前已經(jīng)證實(shí))

              以上是幾個(gè)表面現(xiàn)象,下面我們來(lái)分析它的本質(zhì)和內(nèi)在。^&^

              泡泡堂的游戲邏輯,簡(jiǎn)單地可以歸納為以下幾個(gè)方面:
            1.玩家移動(dòng)
            2.玩家埋地雷(如果你覺(jué)得這種叫法比較土,你也可以叫它:下泡泡,呵呵)
            3.地雷爆炸出道具或者地雷爆炸困住另一玩家
            4.玩家撿道具或者玩家消滅/解救一被困的玩家

              與MMORPG一樣,在上面的幾個(gè)邏輯中,廣播量最大的其實(shí)是玩家移動(dòng)。為了保持玩家畫(huà)面同步,其他玩家的每一步移動(dòng)消息都要即時(shí)地發(fā)給其它玩家。

              通常,網(wǎng)絡(luò)游戲的邏輯控制,絕大多數(shù)是在服務(wù)器端的。有時(shí),為了保證畫(huà)面的流暢性,我們會(huì)有意識(shí)地減少服務(wù)器端的邏輯判斷量和廣播量,當(dāng)然,這 個(gè)減少,是以“不危及游戲的安全運(yùn)行”為前提的。到底如何在效率、流暢性和安全性之間作取舍,很多時(shí)候是需要經(jīng)驗(yàn)積累的,效率提高的過(guò)程,就是邏輯不斷優(yōu) 化的過(guò)程。不過(guò),有一個(gè)原則是可以說(shuō)的,那就是:“關(guān)鍵邏輯”一定要放在服務(wù)器上來(lái)判斷。那么,什么是“關(guān)鍵邏輯”呢?

              拿泡泡堂來(lái)說(shuō),下面的這個(gè)邏輯,我認(rèn)為就是關(guān)鍵邏輯:玩家在某處埋下一顆地雷,地雷爆炸后到底能不能炸出道具以及炸出了哪些道具,這個(gè)信息,需要服務(wù)器來(lái)給。那么,什么又是“非關(guān)鍵邏輯”呢?

              “非關(guān)鍵邏輯”,在不同的游戲中,會(huì)有不同的概念。在通常的MMORPG中,玩家移動(dòng)邏輯的判斷,是算作關(guān)鍵邏輯的,否則,如果服務(wù)器端不對(duì)客 戶(hù)端發(fā)過(guò)來(lái)的移動(dòng)包進(jìn)行判斷那就很容易造成玩家的瞬移以及其它毀滅性的災(zāi)難。而在泡泡堂中,玩家移動(dòng)邏輯到底應(yīng)不應(yīng)該算作關(guān)鍵邏輯還是值得考慮的。泡泡堂 中的玩家可以取勝的方法,通常是確實(shí)因?yàn)榇虻煤枚A得勝利,不會(huì)因?yàn)樗惨贫A得勝利,因?yàn)槿绻鈷煲髋菖萏玫乃惨疲枰紤]的因素和判斷的邏輯太多 了,由于比賽進(jìn)程的瞬息萬(wàn)變,外掛的瞬移點(diǎn)判斷不一定就比真正的玩家來(lái)得準(zhǔn)確,所在,在玩家移動(dòng)這個(gè)邏輯上使用外掛,在泡泡堂這樣的游戲中通常是得不償失 的(當(dāng)然,那種特別變態(tài)的高智能的外掛除外)。從目前我查到的消息來(lái)看,泡泡堂的外掛多數(shù)是一些按鍵精靈腳本,它的本質(zhì)還不是完全的游戲機(jī)器人,并不是通 過(guò)純粹的協(xié)議接管實(shí)現(xiàn)的外掛功能。這也從反面驗(yàn)證了我以上的想法。

              說(shuō)到這里,也許你已經(jīng)明白了。是的!TCP通信負(fù)責(zé)“關(guān)鍵邏輯”,而UDP通信負(fù)責(zé)“非關(guān)鍵邏輯”,這里的“非關(guān)鍵邏輯”中就包含了玩家移動(dòng)。 在泡泡堂中,TCP通信用于本地玩家與服務(wù)器之間的通信,而UDP則用于本地玩家與同一地圖中的其他各玩家的通信。當(dāng)本地玩家要移動(dòng)時(shí),它會(huì)同時(shí)向同一地 圖內(nèi)的所有玩家廣播自己的移動(dòng)消息,其他玩家收到這個(gè)消息后會(huì)更新自己的游戲畫(huà)面以實(shí)現(xiàn)畫(huà)面同步。而當(dāng)本地玩家要在地圖上放置一個(gè)炸彈時(shí),本地玩家需要將 此消息同時(shí)通知同一地圖內(nèi)的其他玩家以及服務(wù)器,甚至這里,可以不把放置炸彈的消息通知給服務(wù)器,而僅僅通知其他玩家。當(dāng)炸彈爆炸后,要拾取物品時(shí)才向服 務(wù)器提交拾取物品的消息。

              那么,你可能會(huì)問(wèn),“地圖上某一點(diǎn)是否存在道具”這個(gè)消息,服務(wù)器是什么時(shí)候通知給客戶(hù)端的呢?這個(gè)問(wèn)題,可以有兩種解決方案:
            1.客戶(hù)端如果在放置炸彈時(shí),將放置炸彈的消息通知給服務(wù)器,服務(wù)器可以在收到這個(gè)消息后,告訴客戶(hù)端炸彈爆炸后會(huì)有哪些道具。但我覺(jué)得這種方案不好,因?yàn)檫@樣作會(huì)增加游戲運(yùn)行過(guò)程中的數(shù)據(jù)流量。
            2.而這第2種方案就是,客戶(hù)端進(jìn)入地圖后,游戲剛開(kāi)始時(shí),就由服務(wù)器將本地圖內(nèi)的各道具所在點(diǎn)的信息傳給各客戶(hù)端,這樣,可以省去兩方面的開(kāi) 銷(xiāo):a.客戶(hù)端放炸彈時(shí),可以不通知服務(wù)器而只通知其它玩家;b.服務(wù)器也不用在游戲運(yùn)行過(guò)程中再向客戶(hù)端傳遞有關(guān)某點(diǎn)有道具的信息。

            但是,不管采用哪種方案,服務(wù)器上都應(yīng)該保留一份本地圖內(nèi)道具所在點(diǎn)的信息。因?yàn)榉?wù)器要用它來(lái)驗(yàn)證一個(gè)關(guān)鍵邏輯:玩家拾取道具。當(dāng)玩家要在某點(diǎn)拾取道具時(shí),服務(wù)器必須要判定此點(diǎn)是否有道具,否則,外掛可以通過(guò)頻繁地發(fā)拾取道具的包而不斷取得道具。

              至于泡泡堂其它游戲邏輯的實(shí)現(xiàn)方法,我想,還是要依靠這個(gè)原則:首先判斷這個(gè)邏輯是關(guān)鍵邏輯嗎?如果不全是,那其中的哪部分是非關(guān)鍵邏輯呢?對(duì) 于非關(guān)鍵邏輯,都可以交由客戶(hù)端之間(UDP)去自行完成。而對(duì)于關(guān)鍵邏輯,則必須要有服務(wù)器(TCP)的校驗(yàn)和認(rèn)證。這便是我要說(shuō)的。

              以上僅僅是在理論上探討關(guān)于泡泡堂類(lèi)游戲在通信架構(gòu)上的可能作法,這些想法是沒(méi)有事實(shí)依據(jù)的,所有結(jié)論皆來(lái)源于對(duì)封包的分析以及個(gè)人經(jīng)驗(yàn),文章 的內(nèi)容和觀點(diǎn)可能跟真實(shí)的泡泡堂通信架構(gòu)實(shí)現(xiàn)有相當(dāng)大的差異,但我想,這并不是主要的,因?yàn)槲业哪康氖窍虼蠹医榻B這樣的TCP和UDP通信并存情況下,如 何對(duì)游戲邏輯的進(jìn)行取舍和劃分。無(wú)論是“關(guān)鍵邏輯”的定性,還是“玩家移動(dòng)”的具體實(shí)施,都需要開(kāi)發(fā)者在具體的實(shí)踐中進(jìn)行總結(jié)和優(yōu)化。此文全當(dāng)是一個(gè)引子 罷,如有疑問(wèn),請(qǐng)加Msn討論。

            posted @ 2009-09-23 23:44 暗夜教父 閱讀(795) | 評(píng)論 (0)編輯 收藏

            本文作者:sodme
            本文出處:http://blog.csdn.net/sodme
            聲明:本文可以不經(jīng)作者同意任意轉(zhuǎn)載,但任何對(duì)本文的引用都須注明作者、出處及此聲明信息。謝謝!!

              要了解此篇文章中引用的本人寫(xiě)的另一篇文章,請(qǐng)到以下地址:
            http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx
            以上的這篇文章是早在去年的時(shí)候?qū)懙牧耍?dāng)時(shí)正在作休閑平臺(tái),一直在想著如何實(shí)現(xiàn)一個(gè)可擴(kuò)充的支持百萬(wàn)人在線的游戲平臺(tái),后來(lái)思路有了,就寫(xiě)了那篇總結(jié)。文章的意思,重點(diǎn)在于闡述一個(gè)百萬(wàn)級(jí)在線的系統(tǒng)是如何實(shí)施的,倒沒(méi)真正認(rèn)真地考察過(guò)QQ游戲到底是不是那樣實(shí)現(xiàn)的。

              近日在與業(yè)內(nèi)人士討論時(shí),提到QQ游戲的實(shí)現(xiàn)方式并不是我原來(lái)所想的那樣,于是,今天又認(rèn)真抓了一下QQ游戲的包,結(jié)果確如這位兄弟所言,QQ 游戲的架構(gòu)與我當(dāng)初所設(shè)想的那個(gè)架構(gòu)相差確實(shí)不小。下面,我重新給出QQ百萬(wàn)級(jí)在線的技術(shù)實(shí)現(xiàn)方案,并以此展開(kāi),談?wù)劥笮驮诰€系統(tǒng)中的負(fù)載均衡機(jī)制的設(shè) 計(jì)。

              從QQ游戲的登錄及游戲過(guò)程來(lái)看,QQ游戲中,也至少分為三類(lèi)服務(wù)器。它們是:
            第一層:登陸/賬號(hào)服務(wù)器(Login Server),負(fù)責(zé)驗(yàn)證用戶(hù)身份、向客戶(hù)端傳送初始信息,從QQ聊天軟件的封包常識(shí)來(lái)看,這些初始信息可能包括“會(huì)話密鑰”此類(lèi)的信息,以后客戶(hù)端與后續(xù)服務(wù)器的通信就使用此會(huì)話密鑰進(jìn)行身份驗(yàn)證和信息加密;
            第二層:大廳服務(wù)器(估且這么叫吧, Game Hall Server),負(fù)責(zé)向客戶(hù)端傳遞當(dāng)前游戲中的所有房間信息,這些房間信息包括:各房間的連接IP,PORT,各房間的當(dāng)前在線人數(shù),房間名稱(chēng)等等。
            第三層:游戲邏輯服務(wù)器(Game Logic Server),負(fù)責(zé)處理房間邏輯及房間內(nèi)的桌子邏輯。

              從靜態(tài)的表述來(lái)看,以上的三層結(jié)構(gòu)似乎與我以前寫(xiě)的那篇文章相比并沒(méi)有太大的區(qū)別,事實(shí)上,重點(diǎn)是它的工作流程,QQ游戲的通信流程與我以前的設(shè)想可謂大相徑庭,其設(shè)計(jì)思想和技術(shù)水平確實(shí)非常優(yōu)秀。具體來(lái)說(shuō),QQ游戲的通信過(guò)程是這樣的:

              1.由Client向Login Server發(fā)送賬號(hào)及密碼等登錄消息,Login Server根據(jù)校驗(yàn)結(jié)果返回相應(yīng)信息。可以設(shè)想的是,如果Login Server通過(guò)了Client的驗(yàn)證,那么它會(huì)通知其它Game Hall Server或?qū)⑼ㄟ^(guò)驗(yàn)證的消息以及會(huì)話密鑰放在Game Hall Server也可以取到的地方。總之,Login Server與Game Hall Server之間是可以共享這個(gè)校驗(yàn)成功消息的。一旦Client收到了Login Server返回成功校驗(yàn)的消息后,Login Server會(huì)主動(dòng)斷開(kāi)與Client的連接,以騰出socket資源。Login Server的IP信息,是存放在QQGame\config\QQSvrInfo.ini里的。

              2.Client收到Login Server的校驗(yàn)成功等消息后,開(kāi)始根據(jù)事先選定的游戲大廳入口登錄游戲大廳,各個(gè)游戲大廳Game Hall Server的IP及Port信息,是存放在QQGame\Dirconfig.ini里的。Game Hall Server收到客戶(hù)端Client的登錄消息后,會(huì)根據(jù)一定的策略決定是否接受Client的登錄,如果當(dāng)前的Game Hall Server已經(jīng)到了上限或暫時(shí)不能處理當(dāng)前玩家登錄消息,則由Game Hall Server發(fā)消息給Client,以讓Client重定向到另外的Game Hall Server登錄。重定向的IP及端口信息,本地沒(méi)有保存,是通過(guò)數(shù)據(jù)包或一定的算法得到的。如果當(dāng)前的Game Hall Server接受了該玩家的登錄消息后,會(huì)向該Client發(fā)送房間目錄信息,這些信息的內(nèi)容我上面已經(jīng)提到。目錄等消息發(fā)送完畢后,Game Hall Server即斷開(kāi)與Client的連接,以騰出socket資源。在此后的時(shí)間里,Client每隔30分鐘會(huì)重新連接Game Hall Server并向其索要最新的房間目錄信息及在線人數(shù)信息。

              3.Client根據(jù)列出的房間列表,選擇某個(gè)房間進(jìn)入游戲。根據(jù)我的抓包結(jié)果分析,QQ游戲,并不是給每一個(gè)游戲房間都分配了一個(gè)單獨(dú)的端口 進(jìn)行處理。在QQ游戲里,有很多房間是共用的同一個(gè)IP和同一個(gè)端口。比如,在斗地主一區(qū),前50個(gè)房間,用的都是同一個(gè)IP和Port信息。這意味著, 這些房間,在QQ游戲的服務(wù)器上,事實(shí)上,可能是同一個(gè)程序在處理!!!QQ游戲房間的人數(shù)上限是400人,不難推算,QQ游戲單個(gè)服務(wù)器程序的用戶(hù)承載 量是2萬(wàn),即QQ的一個(gè)游戲邏輯服務(wù)器程序最多可同時(shí)與2萬(wàn)個(gè)玩家保持TCP連接并保證游戲效率和品質(zhì),更重要的是,這樣可以為騰訊省多少money 呀!!!哇哦!QQ確實(shí)很牛。以2萬(wàn)的在線數(shù)還能保持這么好的游戲品質(zhì),確實(shí)不容易!QQ游戲的單個(gè)服務(wù)器程序,管理的不再只是邏輯意義上的單個(gè)房間,而 可能是許多邏輯意義上的房間。其實(shí),對(duì)于服務(wù)器而言,它就是一個(gè)大區(qū)服務(wù)器或大區(qū)服務(wù)器的一部分,我們可以把它理解為一個(gè)龐大的游戲地圖,它實(shí)現(xiàn)的也是分 塊處理。而對(duì)于每一張桌子上的打牌邏輯,則是有一個(gè)統(tǒng)一的處理流程,50個(gè)房間的50*100張桌子全由這一個(gè)服務(wù)器程序進(jìn)行處理(我不知道QQ游戲的具 體打牌邏輯是如何設(shè)計(jì)的,我想很有可能也是分區(qū)域的,分塊的)。當(dāng)然,以上這些只是服務(wù)器作的事,針對(duì)于客戶(hù)端而言,客戶(hù)端只是在表現(xiàn)上,將一個(gè)個(gè)房間單 獨(dú)羅列了出來(lái),這樣作,是為便于玩家進(jìn)行游戲以及減少服務(wù)器的開(kāi)銷(xiāo),把這個(gè)大區(qū)中的每400人放在一個(gè)集合內(nèi)進(jìn)行處理(比如聊天信息,“向400人廣播” 和“向2萬(wàn)人廣播”,這是完全不同的兩個(gè)概念)。

              4.需要特別說(shuō)明的一點(diǎn)。進(jìn)入QQ游戲房間后,直到點(diǎn)擊某個(gè)位置坐下打開(kāi)另一個(gè)程序界面,客戶(hù)端的程序,沒(méi)有再創(chuàng)建新的socket,而仍然使 用原來(lái)大廳房間客戶(hù)端跟游戲邏輯服務(wù)器交互用的socket。也就是說(shuō),這是兩個(gè)進(jìn)程共用的同一個(gè)socket!不要小看這一點(diǎn)。如果你在創(chuàng)建桌子客戶(hù)端 程序后又新建了一個(gè)新的socket與游戲邏輯服務(wù)器進(jìn)行通信,那么由此帶來(lái)的玩家進(jìn)入、退出、逃跑等消息會(huì)帶來(lái)非常麻煩的數(shù)據(jù)同步問(wèn)題,俺在剛開(kāi)始的時(shí) 候就深受其害。而一旦共用了同一個(gè)socket后,你如果退出桌子,服務(wù)器不涉及釋放socket的問(wèn)題,所以,這里就少了很多的數(shù)據(jù)同步問(wèn)題。關(guān)于多個(gè) 進(jìn)程如何共享同一個(gè)socket的問(wèn)題,請(qǐng)去google以下內(nèi)容:WSADuplicateSocket。

              以上便是我根據(jù)最新的QQ游戲抓包結(jié)果分析得到的QQ游戲的通信流程,當(dāng)然,這個(gè)流程更多的是客戶(hù)端如何與服務(wù)器之間交互的,卻沒(méi)有涉及到服務(wù)器彼此之間是如何通信和作數(shù)據(jù)同步的。關(guān)于服務(wù)器之間的通信流程,我們只能基于自己的經(jīng)驗(yàn)和猜想,得出以下想法:

              1.Login Server與Game Hall Server之前的通信問(wèn)題。Login Server是負(fù)責(zé)用戶(hù)驗(yàn)證的,一旦驗(yàn)證通過(guò)之后,它要設(shè)法讓Game Hall Server知道這個(gè)消息。它們之前實(shí)現(xiàn)信息交流的途徑,我想可能有這樣幾條:a. Login Server將通過(guò)驗(yàn)證的用戶(hù)存放到臨時(shí)數(shù)據(jù)庫(kù)中;b. Login Server將驗(yàn)證通過(guò)的用戶(hù)存放在內(nèi)存中,當(dāng)然,這個(gè)信息,應(yīng)該是全局可訪問(wèn)的,就是說(shuō)所有QQ的Game Hall Server都可以通過(guò)服務(wù)器之間的數(shù)據(jù)包通信去獲得這樣的信息。

              2.Game Hall Server的最新房間目錄信息的取得。這個(gè)信息,是全局的,也就是整個(gè)游戲中,只保留一個(gè)目錄。它的信息來(lái)源,可以由底層的房間服務(wù)器逐級(jí)報(bào)上來(lái),報(bào)給誰(shuí)?我認(rèn)為就如保存的全局登錄列表一樣,它報(bào)給保存全局登錄列表的那個(gè)服務(wù)器或數(shù)據(jù)庫(kù)。

              3.在QQ游戲中,同一類(lèi)型的游戲,無(wú)法打開(kāi)兩上以上的游戲房間。這個(gè)信息的判定,可以根據(jù)全局信息來(lái)判定。

              以上關(guān)于服務(wù)器之間如何通信的內(nèi)容,均屬于個(gè)人猜想,QQ到底怎么作的,恐怕只有等大家中的某一位進(jìn)了騰訊之后才知道了。呵呵。不過(guò),有一點(diǎn)是 可以肯定的,在整個(gè)服務(wù)器架構(gòu)中,應(yīng)該有一個(gè)地方是專(zhuān)門(mén)保存了全局的登錄玩家列表,只有這樣才能保證玩家不會(huì)重復(fù)登錄以及進(jìn)入多個(gè)相同類(lèi)型的房間。

              在前面的描述中,我曾經(jīng)提到過(guò)一個(gè)問(wèn)題:當(dāng)?shù)卿洰?dāng)前Game Hall Server不成功時(shí),QQ游戲服務(wù)器會(huì)選擇讓客戶(hù)端重定向到另位的服務(wù)器去登錄,事實(shí)上,QQ聊天服務(wù)器和MSN服務(wù)器的登錄也是類(lèi)似的,它也存在登錄重定向問(wèn)題。

              那么,這就引出了另外的問(wèn)題,由誰(shuí)來(lái)作這個(gè)策略選擇?以及由誰(shuí)來(lái)提供這樣的選擇資源?這樣的處理,便是負(fù)責(zé)負(fù)載均衡的服務(wù)器的處理范圍了。由QQ游戲的通信過(guò)程分析派生出來(lái)的針對(duì)負(fù)責(zé)均衡及百萬(wàn)級(jí)在線系統(tǒng)的更進(jìn)一步討論,將在下篇文章中繼續(xù)。

              在此,特別感謝網(wǎng)友tilly及某位不便透露姓名的網(wǎng)友的討論,是你們讓我決定認(rèn)真再抓一次包探個(gè)究竟。

            posted @ 2009-09-23 23:43 暗夜教父 閱讀(655) | 評(píng)論 (0)編輯 收藏
            一直以來(lái),flash就是我非常喜愛(ài)的平臺(tái),
            因?yàn)樗?jiǎn)單,完整,但是功能強(qiáng)大,
            很適合游戲軟件的開(kāi)發(fā),
            只不過(guò)處理復(fù)雜的算法和海量數(shù)據(jù)的時(shí)候,
            速度慢了一些,
            但是這并不意味著flash不能做,
            我們需要變通的方法去讓flash做不善長(zhǎng)的事情,

            這個(gè)貼子用來(lái)專(zhuān)門(mén)討論用flash作為客戶(hù)端來(lái)開(kāi)發(fā)網(wǎng)絡(luò)游戲,
            持續(xù)時(shí)間也不會(huì)很長(zhǎng),在把服務(wù)器端的源代碼公開(kāi)完以后,
            就告一段落,
            注意,僅僅用flash作為客戶(hù)端,
            服務(wù)器端,我們使用vc6,
            我將陸續(xù)的公開(kāi)服務(wù)器端的源代碼和大家共享,
            并且將講解一些網(wǎng)絡(luò)游戲開(kāi)發(fā)的原理,
            希望對(duì)此感興趣的朋友能夠使用今后的資源或者理論開(kāi)發(fā)出完整的網(wǎng)絡(luò)游戲。
            我們從簡(jiǎn)單到復(fù)雜,
            從棋牌類(lèi)游戲到動(dòng)作類(lèi)的游戲,
            從2個(gè)人的游戲到10個(gè)人的游戲,
            因?yàn)楣ぷ髅Φ年P(guān)系,我所做的一切僅僅起到拋磚引玉的作用,
            希望大家能夠熱情的討論,為中國(guó)的flash事業(yè)墊上一塊磚,添上一片瓦。

            現(xiàn)在的大型網(wǎng)絡(luò)游戲(mmo game)都是基于server/client體系結(jié)構(gòu)的,
            server端用c(windows下我們使用vc.net+winsock)來(lái)編寫(xiě),
            客戶(hù)端就無(wú)所謂,
            在這里,我們討論用flash來(lái)作為客戶(hù)端的實(shí)現(xiàn),

            實(shí)踐證明,flash的xml socket完全可以勝任網(wǎng)絡(luò)傳輸部分,
            在別的貼子中,我看見(jiàn)有的朋友談?wù)搈sn中的flash game
            他使用msn內(nèi)部的網(wǎng)絡(luò)接口進(jìn)行傳輸,
            這種做法也是可以的,
            我找很久以前對(duì)于2d圖形編程的說(shuō)法,"給我一個(gè)打點(diǎn)函數(shù),我就能創(chuàng)造整個(gè)游戲世界",
            而在網(wǎng)絡(luò)游戲開(kāi)發(fā)過(guò)程中,"給我一個(gè)發(fā)送函數(shù)和一個(gè)接收函數(shù),我就能創(chuàng)造網(wǎng)絡(luò)游戲世界."

            我們抽象一個(gè)接口,就是網(wǎng)絡(luò)傳輸?shù)慕涌冢?br> 對(duì)于使用flash作為客戶(hù)端,要進(jìn)行網(wǎng)絡(luò)連接,
            一個(gè)網(wǎng)絡(luò)游戲的客戶(hù)端,
            可以簡(jiǎn)單的抽象為下面的流程
            1.與遠(yuǎn)程服務(wù)器建立一條長(zhǎng)連接
            2.用賬號(hào)密碼登陸
            3.循環(huán)
            接收消息
            發(fā)送消息
            4.關(guān)閉

            我們可以直接使用flash 的xml socket,也可以使用類(lèi)似msn的那種方式,
            這些我們先不管,我們先定義接口,
            Connect( "127.0.0.1", 20000 ); 連接遠(yuǎn)程服務(wù)器,建立一條長(zhǎng)連接
            Send( data, len ); 向服務(wù)器發(fā)送一條消息
            Recv( data, len ); 接收服務(wù)器傳來(lái)的消息

            項(xiàng)目開(kāi)發(fā)的基本硬件配置
            一臺(tái)普通的pc就可以了,
            安裝好windows 2000和vc6就可以了,
            然后連上網(wǎng),局域網(wǎng)和internet都可以,

            接下去的東西我都簡(jiǎn)化,不去用晦澀的術(shù)語(yǔ),

            既然是網(wǎng)絡(luò),我們就需要網(wǎng)絡(luò)編程接口,
            服務(wù)器端我們用的是winsock 1.1,使用tcp連接方式,

            [tcp和udp]
            tcp可以理解為一條連接兩個(gè)端子的隧道,提供可靠的數(shù)據(jù)傳輸服務(wù),
            只要發(fā)送信息的一方成功的調(diào)用了tcp的發(fā)送函數(shù)發(fā)送一段數(shù)據(jù),
            我們可以認(rèn)為接收方在若干時(shí)間以后一定會(huì)接收到完整正確的數(shù)據(jù),
            不需要去關(guān)心網(wǎng)絡(luò)傳輸上的細(xì)節(jié),
            而udp不保證這一點(diǎn),
            對(duì)于網(wǎng)絡(luò)游戲來(lái)說(shuō),tcp是普遍的選擇。

            [阻塞和非阻塞]
            在通過(guò)socket發(fā)送數(shù)據(jù)時(shí),如果直到數(shù)據(jù)發(fā)送完畢才返回的方式,也就是說(shuō)如果我們使用send( buffer, 100.....)這樣的函數(shù)發(fā)送100個(gè)字節(jié)給別人,我們要等待,直到100個(gè)自己發(fā)送完畢,程序才往下走,這樣就是阻塞的,
            而非阻塞的方式,當(dāng)你調(diào)用send(buffer,100....)以后,立即返回,此時(shí)send函數(shù)告訴你發(fā)送成功,并不意味著數(shù)據(jù)已經(jīng)向目的地發(fā)送完 畢,甚至有可能數(shù)據(jù)還沒(méi)有開(kāi)始發(fā)送,只被保留在系統(tǒng)的緩沖里面,等待被發(fā)送,但是你可以認(rèn)為數(shù)據(jù)在若干時(shí)間后,一定會(huì)被目的地完整正確的收到,我們要充分 的相信tcp。
            阻塞的方式會(huì)引起系統(tǒng)的停頓,一般網(wǎng)絡(luò)游戲里面使用的都是非阻塞的方式,


            [有狀態(tài)服務(wù)器和無(wú)狀態(tài)服務(wù)器]
            在c/s體系中,如果server不保存客戶(hù)端的狀態(tài),稱(chēng)之為無(wú)狀態(tài),反之為有狀態(tài),

            在這里要強(qiáng)調(diào)一點(diǎn),
            我們所說(shuō)的服務(wù)器不是一臺(tái)具體的機(jī)器,
            而是指服務(wù)器應(yīng)用程序,
            一臺(tái)具體的機(jī)器或者機(jī)器群組可以運(yùn)行一個(gè)或者多個(gè)服務(wù)器應(yīng)用程序,

            我們的網(wǎng)絡(luò)游戲使用的是有狀態(tài)服務(wù)器,
            保存所有玩家的數(shù)據(jù)和狀態(tài),


            一些有必要了解的理論和開(kāi)發(fā)工具

            [開(kāi)發(fā)語(yǔ)言]
            vc6
            我們首先要熟練的掌握一門(mén)開(kāi)發(fā)語(yǔ)言,
            學(xué)習(xí)c++是非常有必要的,
            而vc是windows下面的軟件開(kāi)發(fā)工具,
            為什么選擇vc,可能與我本身使用vc有關(guān),
            而且網(wǎng)上可以找到許多相關(guān)的資源和源代碼,

            [操作系統(tǒng)]
            我們使用windows2000作為服務(wù)器的運(yùn)行環(huán)境,
            所以我們有必要去了解windows是如何工作的,
            同時(shí)對(duì)它的編程原理應(yīng)該熟練的掌握

            [數(shù)據(jù)結(jié)構(gòu)和算法]
            要寫(xiě)出好的程序要先具有設(shè)計(jì)出好的數(shù)據(jù)結(jié)構(gòu)和算法的能力,
            好的算法未必是繁瑣的公式和復(fù)雜的代碼,
            我們要找到又好寫(xiě)有滿足需求的算法,
            有時(shí)候,最笨的方法同時(shí)也是很好的方法,
            很多程序員沉迷于追求精妙的算法而忽略了宏觀上的工程,
            花費(fèi)了大量的精力未必能夠取得好的效果,

            舉個(gè)例子,
            我當(dāng)年進(jìn)入游戲界工作,學(xué)習(xí)老師的代碼,
            發(fā)現(xiàn)有個(gè)函數(shù),要對(duì)畫(huà)面中的npc位置進(jìn)行排序,
            確定哪個(gè)先畫(huà),那個(gè)后畫(huà),
            他的方法太“笨”,
            任何人都會(huì)想到的冒泡,
            一個(gè)一個(gè)去比較,沒(méi)有任何的優(yōu)化,
            我當(dāng)時(shí)想到的算法就有很多,
            而且有一大堆優(yōu)化策略,
            可是,當(dāng)我花了很長(zhǎng)時(shí)間去實(shí)現(xiàn)我的算法時(shí),
            發(fā)現(xiàn)提升的那么一點(diǎn)效率對(duì)游戲整個(gè)運(yùn)行效率而言幾乎是沒(méi)起到什么作用,
            或者說(shuō)雖然算法本身快了幾倍,
            可是那是多余的,老師的算法雖然“笨”,
            可是他只花了幾十行代碼就搞定了,
            他的時(shí)間花在別的更需要的地方,
            這就是他可以獨(dú)自完成一個(gè)游戲,
            而我可以把一個(gè)函數(shù)優(yōu)化100倍也只能打雜的原因

            [tcp/ip的理論]
            推薦數(shù)據(jù)用tcp/ip進(jìn)行網(wǎng)際互連,tcp/ip詳解,
            這是兩套書(shū),共有6卷,
            都是國(guó)外的大師寫(xiě)的,
            可以說(shuō)是必讀的,


            網(wǎng)絡(luò)傳輸中的“消息”

            [消息]
            消息是個(gè)很常見(jiàn)的術(shù)語(yǔ),
            在windows中,消息機(jī)制是個(gè)十分重要的概念,
            我們?cè)诰W(wǎng)絡(luò)游戲中,也使用了消息這樣的機(jī)制,

            一般我們這么做,
            一個(gè)數(shù)據(jù)塊,頭4個(gè)字節(jié)是消息名,后面接2個(gè)字節(jié)的數(shù)據(jù)長(zhǎng)度,
            再后面就是實(shí)際的數(shù)據(jù)

            為什么使用消息??
            我們來(lái)看看例子,

            在游戲世界,
            一個(gè)玩家想要和別的玩家聊天,
            那么,他輸入好聊天信息,
            客戶(hù)端生成一條聊天消息,
            并把聊天的內(nèi)容打包到消息中,
            然后把聊天消息發(fā)送給服務(wù)器,
            請(qǐng)求服務(wù)器把聊天信息發(fā)送給另一個(gè)玩家,

            服務(wù)器接收到一條消息,
            此刻,服務(wù)器并不知道當(dāng)前的數(shù)據(jù)是什么東西,
            對(duì)于服務(wù)器來(lái)講,這段數(shù)據(jù)僅僅來(lái)自于網(wǎng)絡(luò)通訊的底層,
            不加以分析的話,沒(méi)有任何的信息,
            因?yàn)槲覀兊耐ㄓ嵤腔谙C(jī)制的,
            我們認(rèn)為服務(wù)器接收到的任何數(shù)據(jù)都是基于消息的數(shù)據(jù)方式組織的,
            4個(gè)字節(jié)消息名,2字節(jié)長(zhǎng)度,這個(gè)是不會(huì)變的,

            通過(guò)消息名,服務(wù)器發(fā)現(xiàn)當(dāng)前數(shù)據(jù)是一條聊天數(shù)據(jù),
            通過(guò)長(zhǎng)度把需要的數(shù)據(jù)還原,校驗(yàn),
            然后把這條消息發(fā)送給另一個(gè)玩家,

            大家注意,消息是變長(zhǎng)的,
            關(guān)于消息的解釋完全在于服務(wù)器和客戶(hù)端的應(yīng)用程序,
            可以認(rèn)為與網(wǎng)絡(luò)傳輸?shù)蛯訜o(wú)關(guān),
            比如一條私聊消息可能是這樣的,

            MsgID:4 byte
            Length:2 byte
            TargetPlayerID:2 byte
            String:anybyte < 256

            一條移動(dòng)消息可能是這樣的,
            MsgID:4 byte
            Length:2 byte
            TargetPlayerID:2 byte
            TargetPosition:4 byte (x,y)

            編程者可以自定義消息的內(nèi)容以滿足不同的需求


            隊(duì)列

            [隊(duì)列]
            隊(duì)列是一個(gè)很重要的數(shù)據(jù)結(jié)構(gòu),
            比如說(shuō)消息隊(duì)列,
            服務(wù)器或者客戶(hù)端,
            發(fā)送的消息不一定是立即發(fā)送的,
            而是等待一個(gè)適當(dāng)時(shí)間,
            或者系統(tǒng)規(guī)定的時(shí)間間隔以后才發(fā)送,
            這樣就需要?jiǎng)?chuàng)建一個(gè)消息隊(duì)列,以保存發(fā)送的消息,

            消息隊(duì)列的大小可以按照實(shí)際的需求創(chuàng)建,
            隊(duì)列又可能會(huì)滿,
            當(dāng)隊(duì)列滿了,可以直接丟棄消息,
            如果你覺(jué)得這樣不妥,
            也可以預(yù)先劃分一個(gè)足夠大的隊(duì)列,

            可以使用一個(gè)系統(tǒng)全局的大的消息隊(duì)列,
            也可以為每個(gè)對(duì)象創(chuàng)建一個(gè)消息隊(duì)列,


            這個(gè)我們的一個(gè)數(shù)據(jù)隊(duì)列的實(shí)現(xiàn),
            開(kāi)發(fā)工具vc.net,使用了C++的模板,
            關(guān)于隊(duì)列的算法和基礎(chǔ)知識(shí),我就不多說(shuō)了,

            DataBuffer.h

            #ifndef __DATABUFFER_H__
            #define __DATABUFFER_H__

            #include <windows.h>
            #include <assert.h>
            #include "g_assert.h"
            #include <stdio.h>

            #ifndef HAVE_BYTE
            typedef unsigned char byte;
            #endif // HAVE_BYTE

            //數(shù)據(jù)隊(duì)列管理類(lèi)
            template <const int _max_line, const int _max_size>
            class DataBufferTPL
            {
            public:

            bool Add( byte *data ) // 加入隊(duì)列數(shù)據(jù)
            {
            G_ASSERT_RET( data, false );
            m_ControlStatus = false;

            if( IsFull() )
            {
            //assert( false );
            return false;
            }

            memcpy( m_s_ptr, data, _max_size );

            NextSptr();
            m_NumData++;

            m_ControlStatus = true;
            return true;
            }


            bool Get( byte *data ) // 從隊(duì)列中取出數(shù)據(jù)
            {
            G_ASSERT_RET( data, false );
            m_ControlStatus = false;

            if( IsNull() )
            return false;

            memcpy( data, m_e_ptr, _max_size );

            NextEptr();
            m_NumData--;

            m_ControlStatus = true;
            return true;
            }


            bool CtrlStatus() // 獲取操作成功結(jié)果
            {
            return m_ControlStatus;
            }


            int GetNumber() // 獲得現(xiàn)在的數(shù)據(jù)大小
            {
            return m_NumData;
            }

            public:

            DataBufferTPL()
            {
            m_NumData = 0;
            m_start_ptr = m_DataTeam[0];
            m_end_ptr = m_DataTeam[_max_line-1];
            m_s_ptr = m_start_ptr;
            m_e_ptr = m_start_ptr;
            }
            ~DataBufferTPL()
            {
            m_NumData = 0;
            m_s_ptr = m_start_ptr;
            m_e_ptr = m_start_ptr;
            }

            private:

            bool IsFull() // 是否隊(duì)列滿
            {
            G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
            if( m_NumData == _max_line )
            return true;
            else
            return false;
            }
            bool IsNull() // 是否隊(duì)列空
            {
            G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
            if( m_NumData == 0 )
            return true;
            else
            return false;
            }
            void NextSptr() // 頭位置增加
            {
            assert(m_start_ptr);
            assert(m_end_ptr);
            assert(m_s_ptr);
            assert(m_e_ptr);
            m_s_ptr += _max_size;
            if( m_s_ptr > m_end_ptr )
            m_s_ptr = m_start_ptr;
            }
            void NextEptr() // 尾位置增加
            {
            assert(m_start_ptr);
            assert(m_end_ptr);
            assert(m_s_ptr);
            assert(m_e_ptr);
            m_e_ptr += _max_size;
            if( m_e_ptr > m_end_ptr )
            m_e_ptr = m_start_ptr;
            }

            private:

            byte m_DataTeam[_max_line][_max_size]; //數(shù)據(jù)緩沖
            int m_NumData; //數(shù)據(jù)個(gè)數(shù)
            bool m_ControlStatus; //操作結(jié)果

            byte *m_start_ptr; //起始位置
            byte *m_end_ptr; //結(jié)束位置
            byte *m_s_ptr; //排隊(duì)起始位置
            byte *m_e_ptr; //排隊(duì)結(jié)束位置
            };


            //////////////////////////////////////////////////////////////////////////
            // 放到這里了!

            //ID自動(dòng)補(bǔ)位列表模板,用于自動(dòng)列表,無(wú)間空順序列表。
            template <const int _max_count>
            class IDListTPL
            {
            public:
            // 清除重置
            void Reset()
            {
            for(int i=0;i<_max_count;i++)
            m_dwList[i] = G_ERROR;
            m_counter = 0;
            }

            int MaxSize() const { return _max_count; }
            int Count() const { return m_counter; }
            const DWORD operator[]( int iIndex ) {

            G_ASSERTN( iIndex >= 0 && iIndex < m_counter );

            return m_dwList[ iIndex ];
            }
            bool New( DWORD dwID )
            {
            G_ASSERT_RET( m_counter >= 0 && m_counter < _max_count, false );

            //ID 唯一性,不能存在相同ID
            if ( Find( dwID ) != -1 )
            return false;

            m_dwList[m_counter] = dwID;
            m_counter++;

            return true;
            }
            // 沒(méi)有Assert的加入ID功能
            bool Add( DWORD dwID )
            {
            if( m_counter <0 || m_counter >= _max_count )
            return false;

            //ID 唯一性,不能存在相同ID
            if ( Find( dwID ) != -1 )
            return false;

            m_dwList[m_counter] = dwID;
            m_counter++;
            return true;
            }
            bool Del( int iIndex )
            {
            G_ASSERT_RET( iIndex >=0 && iIndex < m_counter, false );

            for(int k=iIndex;k<m_counter-1;k++)
            {
            m_dwList[k] = m_dwList[k+1];
            }

            m_dwList[k] = G_ERROR;
            m_counter--;
            return true;
            }
            int Find( DWORD dwID )
            {
            for(int i=0;i<m_counter;i++)
            {
            if( m_dwList[i] == dwID )
            return i;
            }

            return -1;
            }

            IDListTPL():m_counter(0)
            {
            for(int i=0;i<_max_count;i++)
            m_dwList[i] = G_ERROR;
            }
            virtual ~IDListTPL()
            {}

            private:

            DWORD m_dwList[_max_count];
            int m_counter;

            };

            //////////////////////////////////////////////////////////////////////////


            #endif //__DATABUFFER_H__


            socket

            我們采用winsock作為網(wǎng)絡(luò)部分的編程接口,

            接下去編程者有必要學(xué)習(xí)一下socket的基本知識(shí),
            不過(guò)不懂也沒(méi)有關(guān)系,我提供的代碼已經(jīng)把那些麻煩的細(xì)節(jié)或者正確的系統(tǒng)設(shè)置給弄好了,
            編程者只需要按照規(guī)則編寫(xiě)游戲系統(tǒng)的處理代碼就可以了,

            這些代碼在vc6下編譯通過(guò),
            是通用的網(wǎng)絡(luò)傳輸?shù)讓樱?br> 這里是socket部分的代碼,

            我們需要安裝vc6才能夠編譯以下的代碼,
            因?yàn)榻酉氯ノ覀円佑|越來(lái)越多的c++,
            所以,大家還是去看看c++的書(shū)吧,

            // socket.h
            #ifndef _socket_h
            #define _socket_h
            #pragma once

            //定義最大連接用戶(hù)數(shù)目 ( 最大支持 512 個(gè)客戶(hù)連接 )
            #define MAX_CLIENTS 512
            //#define FD_SETSIZE MAX_CLIENTS

            #pragma comment( lib, "wsock32.lib" )

            #include <winsock.h>

            class CSocketCtrl
            {
            void SetDefaultOpt();
            public:
            CSocketCtrl(): m_sockfd(INVALID_SOCKET){}
            BOOL StartUp();
            BOOL ShutDown();
            BOOL IsIPsChange();

            BOOL CanWrite();
            BOOL HasData();
            int Recv( char* pBuffer, int nSize, int nFlag );
            int Send( char* pBuffer, int nSize, int nFlag );
            BOOL Create( UINT uPort );
            BOOL Create(void);
            BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
            void Close();

            BOOL Listen( int nBackLog );
            BOOL Accept( CSocketCtrl& sockCtrl );

            BOOL RecvMsg( char *sBuf );
            int SendMsg( char *sBuf,unsigned short stSize );
            SOCKET GetSockfd(){ return m_sockfd; }

            BOOL GetHostName( char szHostName[], int nNameLength );

            protected:
            SOCKET m_sockfd;

            static DWORD m_dwConnectOut;
            static DWORD m_dwReadOut;
            static DWORD m_dwWriteOut;
            static DWORD m_dwAcceptOut;
            static DWORD m_dwReadByte;
            static DWORD m_dwWriteByte;
            };


            #endif

            // socket.cpp

            #include <stdio.h>
            #include "msgdef.h"
            #include "socket.h"
            // 吊線時(shí)間
            #define ALL_TIMEOUT 120000
            DWORD CSocketCtrl::m_dwConnectOut = 60000;
            DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;
            DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;
            DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;
            DWORD CSocketCtrl::m_dwReadByte = 0;
            DWORD CSocketCtrl::m_dwWriteByte = 0;

            // 接收數(shù)據(jù)
            BOOL CSocketCtrl::RecvMsg( char *sBuf )
            {
            if( !HasData() )
            return FALSE;
            MsgHeader header;
            int nbRead = this->Recv( (char*)&header, sizeof( header ), MSG_PEEK );
            if( nbRead == SOCKET_ERROR )
            return FALSE;
            if( nbRead < sizeof( header ) )
            {
            this->Recv( (char*)&header, nbRead, 0 );
            printf( "\ninvalid msg, skip %ld bytes.", nbRead );
            return FALSE;
            }

            if( this->Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )
            return FALSE;

            return TRUE;
            }

            // 發(fā)送數(shù)據(jù)
            int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )
            {
            static char sSendBuf[ 4000 ];
            memcpy( sSendBuf,&stSize,sizeof(short) );
            memcpy( sSendBuf + sizeof(short),sBuf,stSize );

            if( (sizeof(short) + stSize) != this->Send( sSendBuf,stSize+sizeof(short),0 ) )
            return -1;
            return stSize;
            }


            // 啟動(dòng)winsock
            BOOL CSocketCtrl::StartUp()
            {
            WSADATA wsaData;
            WORD wVersionRequested = MAKEWORD( 1, 1 );

            int err = WSAStartup( wVersionRequested, &wsaData );
            if ( err != 0 )
            {
            return FALSE;
            }


            return TRUE;

            }
            // 關(guān)閉winsock
            BOOL CSocketCtrl::ShutDown()
            {
            WSACleanup();
            return TRUE;
            }

            // 得到主機(jī)名
            BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )
            {
            if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )
            return TRUE;
            return FALSE;
            }

            BOOL CSocketCtrl::IsIPsChange()
            {
            return FALSE;
            static int iIPNum = 0;
            char sHost[300];

            hostent *pHost;
            if( gethostname(sHost,299) != 0 )
            return FALSE;
            pHost = gethostbyname(sHost);
            int i;
            char *psHost;
            i = 0;
            do
            {
            psHost = pHost->h_addr_list[i++];
            if( psHost == 0 )
            break;

            }while(1);
            if( iIPNum != i )
            {
            iIPNum = i;
            return TRUE;
            }
            return FALSE;
            }

            // socket是否可以寫(xiě)
            BOOL CSocketCtrl::CanWrite()
            {
            int e;

            fd_set set;
            timeval tout;
            tout.tv_sec = 0;
            tout.tv_usec = 0;

            FD_ZERO(&set);
            FD_SET(m_sockfd,&set);
            e=::select(0,NULL,&set,NULL,&tout);
            if(e==SOCKET_ERROR) return FALSE;
            if(e>0) return TRUE;
            return FALSE;
            }

            // socket是否有數(shù)據(jù)
            BOOL CSocketCtrl::HasData()
            {
            int e;
            fd_set set;
            timeval tout;
            tout.tv_sec = 0;
            tout.tv_usec = 0;

            FD_ZERO(&set);
            FD_SET(m_sockfd,&set);
            e=::select(0,&set,NULL,NULL,&tout);
            if(e==SOCKET_ERROR) return FALSE;
            if(e>0) return TRUE;
            return FALSE;
            }

            int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )
            {
            return recv( m_sockfd, pBuffer, nSize, nFlag );
            }

            int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )
            {
            return send( m_sockfd, pBuffer, nSize, nFlag );
            }

            BOOL CSocketCtrl::Create( UINT uPort )
            {
            m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
            if(m_sockfd==INVALID_SOCKET) return FALSE;
            SOCKADDR_IN SockAddr;
            memset(&SockAddr,0,sizeof(SockAddr));
            SockAddr.sin_family = AF_INET;
            SockAddr.sin_addr.s_addr = INADDR_ANY;
            SockAddr.sin_port = ::htons( uPort );
            if(!::bind(m_sockfd,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
            {
            SetDefaultOpt();
            return TRUE;
            }
            Close();
            return FALSE;

            }

            void CSocketCtrl::Close()
            {
            ::closesocket( m_sockfd );
            m_sockfd = INVALID_SOCKET;
            }

            BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
            {
            if(m_sockfd==INVALID_SOCKET) return FALSE;

            SOCKADDR_IN sockAddr;

            memset(&sockAddr,0,sizeof(sockAddr));
            LPSTR lpszAscii=(LPSTR)lpszHostAddress;
            sockAddr.sin_family=AF_INET;
            sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);
            if(sockAddr.sin_addr.s_addr==INADDR_NONE)
            {
            HOSTENT * lphost;
            lphost = ::gethostbyname(lpszAscii);
            if(lphost!=NULL)
            sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost->h_addr)->s_addr;
            else return FALSE;
            }
            sockAddr.sin_port = htons((u_short)nHostPort);

            int r=::connect(m_sockfd,(SOCKADDR*)&sockAddr,sizeof(sockAddr));
            if(r!=SOCKET_ERROR) return TRUE;

            int e;
            e=::WSAGetLastError();
            if(e!=WSAEWOULDBLOCK) return FALSE;

            fd_set set;
            timeval tout;
            tout.tv_sec = 0;
            tout.tv_usec = 100000;

            UINT n=0;
            while( n< CSocketCtrl::m_dwConnectOut)
            {
            FD_ZERO(&set);
            FD_SET(m_sockfd,&set);
            e=::select(0,NULL,&set,NULL, &tout);

            if(e==SOCKET_ERROR) return FALSE;
            if(e>0) return TRUE;

            if( IsIPsChange() )
            return FALSE;
            n += 100;
            }

            return FALSE;

            }
            // 設(shè)置監(jiān)聽(tīng)socket
            BOOL CSocketCtrl::Listen( int nBackLog )
            {
            if( m_sockfd == INVALID_SOCKET ) return FALSE;
            if( !listen( m_sockfd, nBackLog) ) return TRUE;
            return FALSE;
            }

            // 接收一個(gè)新的客戶(hù)連接
            BOOL CSocketCtrl::Accept( CSocketCtrl& ms )
            {
            if( m_sockfd == INVALID_SOCKET ) return FALSE;
            if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;

            int e;
            fd_set set;
            timeval tout;
            tout.tv_sec = 0;
            tout.tv_usec = 100000;

            UINT n=0;
            while(n< CSocketCtrl::m_dwAcceptOut)
            {
            //if(stop) return FALSE;
            FD_ZERO(&set);
            FD_SET(m_sockfd,&set);
            e=::select(0,&set,NULL,NULL, &tout);
            if(e==SOCKET_ERROR) return FALSE;
            if(e==1) break;
            n += 100;
            }
            if( n>= CSocketCtrl::m_dwAcceptOut ) return FALSE;

            ms.m_sockfd=accept(m_sockfd,NULL,NULL);
            if(ms.m_sockfd==INVALID_SOCKET) return FALSE;
            ms.SetDefaultOpt();

            return TRUE;
            }

            BOOL CSocketCtrl::Create(void)
            {
            m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
            if(m_sockfd==INVALID_SOCKET) return FALSE;
            SOCKADDR_IN SockAddr;

            memset(&SockAddr,0,sizeof(SockAddr));
            SockAddr.sin_family = AF_INET;
            SockAddr.sin_addr.s_addr = INADDR_ANY;
            SockAddr.sin_port = ::htons(0);
            //if(!::bind(m_sock,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))
            {
            SetDefaultOpt();
            return TRUE;
            }
            Close();
            return FALSE;
            }

            // 設(shè)置正確的socket狀態(tài),
            // 主要是主要是設(shè)置非阻塞異步傳輸模式
            void CSocketCtrl::SetDefaultOpt()
            {
            struct linger ling;
            ling.l_onoff=1;
            ling.l_linger=0;
            setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
            setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);
            int bKeepAlive = 1;
            setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(int));
            BOOL bNoDelay = TRUE;
            setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&bNoDelay, sizeof(BOOL));
            unsigned long nonblock=1;
            ::ioctlsocket(m_sockfd,FIONBIO,&nonblock);
            }


            今天晚上寫(xiě)了一些測(cè)試代碼,
            想看看flash究竟能夠承受多大的網(wǎng)絡(luò)數(shù)據(jù)傳輸,

            我在flash登陸到服務(wù)器以后,
            每隔3毫秒就發(fā)送100次100個(gè)字符的串 "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" 給flash,
            然后在flash里面接收數(shù)據(jù)的函數(shù)里面統(tǒng)計(jì)數(shù)據(jù),


            var g_nTotalRecvByte = 0;
            var g_time = new Date();
            var g_nStartTime = g_time.getTime();
            var g_nCounter = 0;

            mySocket.onData=function(xmlDoc)
            {
            g_nTotalRecvByte += xmlDoc.length;
            // 每接收超過(guò)1k字節(jié)的數(shù)據(jù),輸出一次信息,
            if( g_nTotalRecvByte-g_nCounter > 1024 )
            {
            g_time = new Date();
            var nPassedTime = g_time.getTime()-g_nStartTime;
            trace( "花費(fèi)時(shí)間:"+nPassedTime+"毫秒" );
            g_nCounter = g_nTotalRecvByte;
            trace( "接收總數(shù):"+g_nTotalRecvByte+"字節(jié)" );
            trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字節(jié)/秒" );

            }
            結(jié)果十分令我意外,
            這是截取的一段調(diào)試信息,
            //
            花費(fèi)時(shí)間:6953毫秒
            接收總數(shù):343212字節(jié)
            接收速率:49361.7143678988字節(jié)/秒
            花費(fèi)時(shí)間:7109毫秒
            接收總數(shù):344323字節(jié)
            接收速率:48434.800956534字節(jié)/秒
            花費(fèi)時(shí)間:7109毫秒
            接收總數(shù):345434字節(jié)
            接收速率:48591.0817273878字節(jié)/秒
            。。。
            。。。
            。。。
            。。。
            花費(fèi)時(shí)間:8125毫秒
            接收總數(shù):400984字節(jié)
            接收速率:49351.8769230769字節(jié)/秒
            花費(fèi)時(shí)間:8125毫秒
            接收總數(shù):402095字節(jié)
            接收速率:49488.6153846154字節(jié)/秒
            花費(fèi)時(shí)間:8125毫秒
            接收總數(shù):403206字節(jié)
            接收速率:49625.3538461538字節(jié)/秒

            我檢查了幾遍源程序,沒(méi)有發(fā)現(xiàn)邏輯錯(cuò)誤,
            如果程序沒(méi)有問(wèn)題的話,
            那么我們得出的結(jié)論是,flash的xml socket每秒可以接收至少40K的數(shù)據(jù),
            這還沒(méi)有計(jì)算xmlSocket.onData事件的觸發(fā),調(diào)試代碼、信息輸出占用的時(shí)間。

            比我想象中快了一個(gè)數(shù)量級(jí),
            夠用了,
            flash網(wǎng)絡(luò)游戲我們可以繼續(xù)往下走了,


            有朋友問(wèn)到lag的問(wèn)題,
            問(wèn)得很好,不過(guò)也不要過(guò)于擔(dān)心,
            lag的產(chǎn)生有的是因?yàn)榫W(wǎng)絡(luò)延遲,
            有的是因?yàn)榉?wù)器負(fù)載過(guò)大,
            對(duì)于游戲的設(shè)計(jì)者和開(kāi)發(fā)者來(lái)說(shuō),
            首先要從設(shè)計(jì)的角度來(lái)避免或者減少lag產(chǎn)生的機(jī)會(huì),
            如果lag產(chǎn)生了,
            也不要緊,找到巧妙的辦法騙過(guò)玩家的眼睛,
            這也有很多成熟的方法了,
            比如航行預(yù)測(cè)法,路徑插值等等,
            都可以產(chǎn)生很好的效果,
            還有最后的絕招,就是提高服務(wù)器的配置和網(wǎng)絡(luò)帶寬,

            從我開(kāi)發(fā)網(wǎng)絡(luò)游戲這段時(shí)間的經(jīng)驗(yàn)來(lái)看,
            我們的服務(wù)器是vc開(kāi)發(fā)的,
            普通pc跑幾百個(gè)玩家,幾百個(gè)怪物是沒(méi)有問(wèn)題的,


            又作了一個(gè)flash發(fā)送的測(cè)試,

            網(wǎng)絡(luò)游戲的特點(diǎn)是,
            出去的信息比較少,
            進(jìn)來(lái)的信息比較多,

            這個(gè)很容易理解,
            人操作游戲的速度是很有限的,
            控制指令的產(chǎn)生也是隨機(jī)的,
            離散的,

            但是多人游戲的話,
            因?yàn)槿硕啵畔⒌牧髁恳簿蛥^(qū)域均勻分布了,

            在昨天接收數(shù)據(jù)的基礎(chǔ)上,
            我略加修改,
            這次,
            我在_root.enterFrame寫(xiě)了如下代碼,
            _root.onEnterFrame = function()
            {
            var i;
            for( i = 0; i < 10; i++ )
            mySocket.send( ConvertToMsg( "01234567890123456789012345678901234567890123456789" ) );
            return;
            }

            服務(wù)器端要做的是,
            把所有從flash客戶(hù)端收到的信息原封不動(dòng)的返回來(lái),

            這樣,我又可以通過(guò)昨天onData里面的統(tǒng)計(jì)算法來(lái)從側(cè)面估算出flash發(fā)送數(shù)據(jù)的能力,
            這里是輸出的數(shù)據(jù)
            //
            花費(fèi)時(shí)間:30531毫秒
            接收總數(shù):200236字節(jié)
            接收速率:6558.44878975468字節(jié)/秒
            花費(fèi)時(shí)間:30937毫秒
            接收總數(shù):201290字節(jié)
            接收速率:6506.44858906811字節(jié)/秒
            花費(fèi)時(shí)間:31140毫秒
            接收總數(shù):202344字節(jié)
            接收速率:6497.88053949904字節(jié)/秒
            花費(fèi)時(shí)間:31547毫秒
            接收總數(shù):203398字節(jié)
            接收速率:6447.45934637208字節(jié)/秒

            可以看出來(lái),發(fā)送+接收同時(shí)做,
            發(fā)送速率至少可以達(dá)到5k byte/s

            有一點(diǎn)要注意,要非常注意,
            不能讓flash的網(wǎng)絡(luò)傳輸滿載,
            所謂滿載就是flash在阻塞運(yùn)算的時(shí)候,
            不斷的有數(shù)據(jù)從網(wǎng)絡(luò)進(jìn)來(lái),
            而flash又無(wú)法在預(yù)計(jì)的時(shí)間內(nèi)處理我這些信息,
            或者flash發(fā)送數(shù)據(jù)過(guò)于頻繁,
            導(dǎo)致服務(wù)器端緩沖溢出導(dǎo)致錯(cuò)誤,

            對(duì)于5k的傳輸速率,
            已經(jīng)足夠了,
            因?yàn)槲乙蚕氩怀鰜?lái)有什么產(chǎn)生這么大的數(shù)據(jù)量,
            而且如果產(chǎn)生了這么大的數(shù)據(jù)量,
            也就意味著服務(wù)器每時(shí)每刻都要處理所有的玩家發(fā)出的海量數(shù)據(jù),
            還要把這些海量數(shù)據(jù)轉(zhuǎn)發(fā)給其他的玩家,
            已經(jīng)引起數(shù)據(jù)爆炸了,
            所以,5k的上傳從設(shè)計(jì)階段就要避免的,
            我想用flash做的網(wǎng)絡(luò)游戲,
            除了動(dòng)作類(lèi)游戲可能需要恒定1k以?xún)?nèi)的上傳速率,
            其他的200個(gè)字節(jié)/秒以?xún)?nèi)就可以了,


            使用于Flash的消息結(jié)構(gòu)定義

            我們以前討論過(guò),
            通過(guò)消息來(lái)傳遞信息,
            消息的結(jié)構(gòu)是
            struct msg
            {
            short nLength; // 2 byte
            DWORD dwId; // 4 byte

            ....
            data
            }

            但是在為flash開(kāi)發(fā)的消息中,
            不能采用這種結(jié)構(gòu),

            首先Flash xmlSocket只傳輸字符串,
            從xmlSocket的send,onData函數(shù)可以看出來(lái),
            發(fā)出去的,收進(jìn)來(lái)的都應(yīng)該是字符串,

            而在服務(wù)器端是使用vc,java等高級(jí)語(yǔ)言編寫(xiě)的,
            消息中使用的是二進(jìn)制數(shù)據(jù)塊,
            顯然,簡(jiǎn)單的使用字符串會(huì)帶來(lái)問(wèn)題,

            所以,我們需要制定一套協(xié)議,
            就是無(wú)論在客戶(hù)端還是服務(wù)器端,
            都用統(tǒng)一的字符串消息,
            通過(guò)解析字符串的方式來(lái)傳遞信息,

            我想這就是flash采用xml document來(lái)傳輸結(jié)構(gòu)化信息的理由之一,
            xml document描述了一個(gè)完整的數(shù)據(jù)結(jié)構(gòu),
            而且全部使用的是字符串,
            原來(lái)是這樣,怪不得叫做xml socket,
            本來(lái)socket和xml完全是不同的概念,
            flash偏偏出了個(gè)xml socket,
            一開(kāi)始令我費(fèi)解,
            現(xiàn)在,漸漸理解其中奧妙。


            Flash Msg結(jié)構(gòu)定義源代碼和相關(guān)函數(shù)

            在服務(wù)器端,我們?yōu)閒lash定義了一種msg結(jié)構(gòu),
            使用語(yǔ)言,vc6
            #define MSGMAXSIZE 512
            // 消息頭
            struct MsgHeader
            {
            short stLength;
            MsgHeader():stLength( 0 ){}

            };
            // 消息
            struct Msg
            {
            MsgHeader header;
            short GetLength(){ return header.stLength; }
            };
            // flash 消息
            struct MsgToFlashublic Msg
            {
            // 一個(gè)足夠大的緩沖,但是不會(huì)被整個(gè)發(fā)送,
            char szString[MSGMAXSIZE];
            // 計(jì)算設(shè)置好內(nèi)容后,內(nèi)部會(huì)計(jì)算將要發(fā)送部分的長(zhǎng)度,
            // 要發(fā)送的長(zhǎng)度=消息頭大小+字符串長(zhǎng)度+1
            void SetString( const char* pszChatString )
            {
            if( strlen( pszChatString ) < MSGMAXSIZE-1 )
            {
            strcpy( szString, pszChatString );
            header.stLength = sizeof( header )+
            (short)strlen( pszChatString )+1;
            }
            }

            };

            在發(fā)往flash的消息中,整個(gè)處理過(guò)后MsgToFlash結(jié)構(gòu)將被發(fā)送,
            實(shí)踐證明,在flash 客戶(hù)端的xmlSocket onData事件中,
            接收到了正確的消息,消息的內(nèi)容是MasToFlash的szString字段,
            是一個(gè)字符串,

            比如在服務(wù)器端,
            MsgToFlash msg;
            msg.SetString( "move player0 to 100 100" );
            SendMsg( msg,............. );
            那么,在我們的flash客戶(hù)端的onData( xmlDoc )中,
            我們trace( xmlDoc )
            結(jié)果是
            move player0 to 100 100


            然后是flash發(fā)送消息到服務(wù)器,
            我們強(qiáng)調(diào)flash只發(fā)送字符串,
            這個(gè)字符串無(wú)論是否內(nèi)部擁有有效數(shù)據(jù),
            服務(wù)器都應(yīng)該首先把消息收下來(lái),
            那就要保證發(fā)送給服務(wù)器的消息遵循統(tǒng)一的結(jié)構(gòu),
            在flash客戶(hù)端中,
            我們定義一個(gè)函數(shù),
            這個(gè)函數(shù)把一個(gè)字符串轉(zhuǎn)化為服務(wù)器可以識(shí)別的消息,

            補(bǔ)充:現(xiàn)在我們約定字符串長(zhǎng)度都不大于97個(gè)字節(jié)長(zhǎng)度,


            var num_table = new array( "0","1","2","3","4","5","6","7","8","9" );
            function ConvertToMsg( str )
            {
            var l = str.length+3;
            var t = "";
            if( l > 10 )
            t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+str;
            else
            t = num_table[0]+num_table[l]+str;
            return t;
            }

            比如
            var msg = ConvertToMsg( "client login" );
            我們trace( msg );
            看到的是
            15client login

            為什么是這個(gè)結(jié)果呢?
            15是消息的長(zhǎng)度,
            頭兩個(gè)字節(jié)是整個(gè)消息的長(zhǎng)度的asc碼,意思是整個(gè)消息有15個(gè)字節(jié)長(zhǎng),
            然后是信息client login,
            最后是一個(gè)0(c語(yǔ)言中的字符串結(jié)束符)

            當(dāng)服務(wù)器收到15client login,
            他首先把15給分析出來(lái),
            把"15"字符串轉(zhuǎn)化為15的數(shù)字,
            然后,根據(jù)15這個(gè)長(zhǎng)度把后面的client login讀出來(lái),
            這樣,網(wǎng)絡(luò)傳輸?shù)牡讓泳屯瓿闪耍?br> client login的處理就交給邏輯層,


            謝謝大家的支持,
            很感謝斑竹把這個(gè)貼子置頂,
            我寫(xiě)這文章的過(guò)程也是我自己摸索的過(guò)程,
            文章可以記錄我一段開(kāi)發(fā)的歷史,
            一個(gè)思考分析的歷程,
            有時(shí)候甚至作為日志來(lái)寫(xiě),

            由于我本身有雜務(wù)在身,
            所以貼子的更新有點(diǎn)慢,
            請(qǐng)大家見(jiàn)諒,

            我喜愛(ài)flash,
            雖然我在帝國(guó)中,但我并不能稱(chēng)之為閃客,
            因?yàn)槲抑谱鱢lash的水平實(shí)在很低,
            但是我想設(shè)計(jì)開(kāi)發(fā)出讓其他人能更好的使用flash的工具,

            前陣子我開(kāi)發(fā)了Match3D,
            一個(gè)可以把三維動(dòng)畫(huà)輸出成為swf的工具,
            而且實(shí)現(xiàn)了swf渲染的實(shí)時(shí)三維角色動(dòng)畫(huà),
            這可以說(shuō)是我真正推出的第一個(gè)flash第三方軟件,
            其實(shí)這以前,
            我曾經(jīng)開(kāi)發(fā)過(guò)幾個(gè)其他的flash第三方軟件,
            都中途停止了,
            因?yàn)椴粚?shí)用或者市場(chǎng)上有更好的同類(lèi)軟件,

            隨著互聯(lián)網(wǎng)的發(fā)展,
            flash的不斷升級(jí),
            我的flash第三方軟件目光漸漸的從美術(shù)開(kāi)發(fā)工具轉(zhuǎn)移到網(wǎng)絡(luò)互連,
            web應(yīng)用上面來(lái),
            如今已經(jīng)到了2004版本,
            flash的種種新特性讓我眼前發(fā)光,

            我最近在帝國(guó)的各個(gè)板塊看了很多貼子,
            分析里面潛在的用戶(hù)需求,
            總結(jié)了以下的幾個(gè)我認(rèn)為比較有意義的選題,
            可能很片面,

            flash源代碼保護(hù),主要是為了抵御asv之類(lèi)的軟件進(jìn)行反編譯和萃取
            flash與遠(yuǎn)端數(shù)據(jù)庫(kù)的配合,應(yīng)該出現(xiàn)一個(gè)能夠方便快捷的對(duì)遠(yuǎn)程數(shù)據(jù)庫(kù)進(jìn)行操作的方法或者控件,
            flash網(wǎng)際互連,我認(rèn)為flash網(wǎng)絡(luò)游戲是一塊金子,

            這里我想談?wù)刦lash網(wǎng)絡(luò)游戲,
            我要談的不僅僅是技術(shù),而是一個(gè)概念,
            用flash網(wǎng)絡(luò)游戲,
            我本身并不想把flash游戲做成rpg或者其他劇烈交互性的游戲,
            而是想讓flash實(shí)現(xiàn)那些節(jié)奏緩慢,玩法簡(jiǎn)單的游戲,
            把網(wǎng)絡(luò)的概念帶進(jìn)來(lái),

            你想玩游戲的時(shí)候,登上flash網(wǎng)絡(luò)游戲的網(wǎng)站,
            選擇你想玩的網(wǎng)絡(luò)游戲,
            因?yàn)楝F(xiàn)在幾乎所有上網(wǎng)的電腦都可以播放swf,
            所以,我們幾乎不用下載任何插件,
            輸入你的賬號(hào)和密碼,
            就可以開(kāi)始玩了,

            我覺(jué)得battle.net那種方式很適合flash,
            開(kāi)房間或者進(jìn)入別人開(kāi)的房間,
            然后2個(gè)人或者4個(gè)人就可以交戰(zhàn)了,

            這種游戲可以是棋類(lèi),這是最基本的,
            用戶(hù)很廣泛,
            我腦海中的那種是類(lèi)似與寵物飼養(yǎng)的,
            就像當(dāng)年的電子寵物,
            每個(gè)玩家都可以到服務(wù)器認(rèn)養(yǎng)寵物,
            然后在線養(yǎng)成寵物,
            還可以邀請(qǐng)別的玩家進(jìn)行寵物比武,
            看誰(shuí)的寵物厲害,

            就這樣簡(jiǎn)簡(jiǎn)單單的模式,
            配合清新可愛(ài)的畫(huà)面,
            趣味的玩法,
            加入網(wǎng)絡(luò)的要素,
            也許可以取得以想不到的效果,

            今天就說(shuō)到這里吧,
            想法那么多,要實(shí)現(xiàn)的話還有很多路要走,

            希望大家多多支持,積極參與,
            讓我們的想法不僅僅停留于紙上。
            大家好,
            非常抱歉,
            都很長(zhǎng)時(shí)間沒(méi)有回貼了,
            因?yàn)槭诸^項(xiàng)目的原因,
            幾乎沒(méi)有時(shí)間做flash multiplayer的研究,

            很感謝大家的支持,
            現(xiàn)在把整個(gè)flash networking的源代碼共享出來(lái),
            大家可以任意的使用,
            其實(shí)里面也沒(méi)有多少東西,
            相信感興趣的朋友還是可以從中找到一些有用的東西,

            這一次的源代碼做的事情很簡(jiǎn)單,
            服務(wù)器運(yùn)行,
            客戶(hù)端登陸到服務(wù)器,
            然后客戶(hù)端不斷的發(fā)送字符串給服務(wù)器,
            服務(wù)器收到后,在發(fā)還給客戶(hù)端,
            客戶(hù)端統(tǒng)計(jì)一些數(shù)據(jù),
            posted @ 2009-09-23 23:43 暗夜教父 閱讀(603) | 評(píng)論 (0)編輯 收藏
            原文:http://game.chinaitlab.com/freshmen/783449.html

                要想在修改游戲中做到百戰(zhàn)百勝,是需要相當(dāng)豐富的計(jì)算機(jī)知識(shí)的。有很多計(jì)算機(jī)高手就是從玩游戲,修改游戲中,逐步對(duì)計(jì)算機(jī)產(chǎn)生濃厚的興趣,逐步成長(zhǎng)起來(lái) 的。不要在羨慕別人能夠做到的,因?yàn)閯e人能夠做的你也能夠!我相信你們看了本教程后,會(huì)對(duì)游戲有一個(gè)全新的認(rèn)識(shí),呵呵,因?yàn)槲沂莻€(gè)好老師!(別拿雞蛋砸我 呀,救命啊!#¥%……*)

                不過(guò)要想從修改游戲中學(xué)到知識(shí),增加自己的計(jì)算機(jī)水平,可不能只是靠修改游戲呀! 要知道,修改游戲只是一個(gè)驗(yàn)證你對(duì)你所了解的某些計(jì)算機(jī)知識(shí)的理解程度的場(chǎng)所,只能給你一些發(fā)現(xiàn)問(wèn)題、解決問(wèn)題的機(jī)會(huì),只能起到幫助你提高學(xué)習(xí)計(jì)算機(jī)的興 趣的作用,而決不是學(xué)習(xí)計(jì)算機(jī)的捷徑。

                一:什么叫外掛?

                現(xiàn)在的網(wǎng)絡(luò)游戲多是基于Internet上客戶(hù)/服務(wù)器模式,服務(wù)端程序運(yùn)行在游戲服務(wù)器上,游戲的設(shè)計(jì)者在其中創(chuàng)造一個(gè)龐大的游戲空間,各地的玩家可以通過(guò)運(yùn)行客戶(hù)端程序同時(shí)登錄到游戲中。簡(jiǎn)單地說(shuō),網(wǎng)絡(luò)游戲?qū)嶋H上就是由游戲開(kāi)發(fā)商 提供一個(gè)游戲環(huán)境,而玩家們就是在這個(gè)環(huán)境中相對(duì)自由和開(kāi)放地進(jìn)行游戲操作。那么既然在網(wǎng)絡(luò)游戲中有了服務(wù)器這個(gè)概念,我們以前傳統(tǒng)的修改游戲方法就顯得 無(wú)能為力了。記得我們?cè)趩螜C(jī)版的游戲中,隨心所欲地通過(guò)內(nèi)存搜索來(lái)修改角色的各種屬性,這在網(wǎng)絡(luò)游戲中就沒(méi)有任何用處了。因?yàn)槲覀冊(cè)诰W(wǎng)絡(luò)游戲中所扮演角色 的各種屬性及各種重要資料都存放在服務(wù)器上,在我們自己機(jī)器上(客戶(hù)端)只是顯示角色的狀態(tài),所以通過(guò)修改客戶(hù)端內(nèi)存里有關(guān)角色的各種屬性是不切實(shí)際的。 那么是否我們就沒(méi)有辦法在網(wǎng)絡(luò)游戲中達(dá)到我們修改的目的?回答是"否".我們知道Internet客戶(hù)/服務(wù)器模式的通訊一般采用TCP/IP通信協(xié)議,數(shù)據(jù)交換是通過(guò)IP數(shù)據(jù)包的傳輸來(lái)實(shí)現(xiàn)的,一般來(lái)說(shuō)我們客戶(hù)端向服務(wù)器發(fā)出某些請(qǐng)求,比如移動(dòng)、戰(zhàn)斗等指令都是通過(guò)封包的形式和服務(wù)器交換數(shù) 據(jù)。那么我們把本地發(fā)出消息稱(chēng)為SEND,意思就是發(fā)送數(shù)據(jù),服務(wù)器收到我們SEND的消息后,會(huì)按照既定的程序把有關(guān)的信息反饋給客戶(hù)端,比如,移動(dòng)的 坐標(biāo),戰(zhàn)斗的類(lèi)型。那么我們把客戶(hù)端收到服務(wù)器發(fā)來(lái)的有關(guān)消息稱(chēng)為RECV.知道了這個(gè)道理,接下來(lái)我們要做的工作就是分析客戶(hù)端和服務(wù)器之間往來(lái)的數(shù)據(jù) (也就是封包),這樣我們就可以提取到對(duì)我們有用的數(shù)據(jù)進(jìn)行修改,然后模擬服務(wù)器發(fā)給客戶(hù)端,或者模擬客戶(hù)端發(fā)送給服務(wù)器,這樣就可以實(shí)現(xiàn)我們修改游戲的 目的了。

                目前除了修改游戲封包來(lái)實(shí)現(xiàn)修改游戲的目的,我們也可以修改客戶(hù)端的有關(guān)程序來(lái)達(dá)到我們的要求。我們知道目前各個(gè)服務(wù)器的運(yùn)算能力是有限的,特別在游戲 中,游戲服務(wù)器要計(jì)算游戲中所有玩家的狀況幾乎是不可能的,所以有一些運(yùn)算還是要依靠我們客戶(hù)端來(lái)完成,這樣又給了我們修改游戲提供了一些便利。比如我們 可以通過(guò)將客戶(hù)端程序脫殼來(lái)發(fā)現(xiàn)一些程序的判斷分支,通過(guò)跟蹤調(diào)試我們可以把一些對(duì)我們不利的判斷去掉,以此來(lái)滿足我們修改游戲的需求。 在下幾個(gè)章節(jié)中,我們將給大家講述封包的概念,和修改跟蹤客戶(hù)端的有關(guān)知識(shí)。大家準(zhǔn)備好了嗎?

                游戲數(shù)據(jù)格式和存儲(chǔ)

                在進(jìn)行我們的工作之前,我們需要掌握一些關(guān)于計(jì)算機(jī)中儲(chǔ)存數(shù)據(jù)方式的知識(shí)和游戲中儲(chǔ)存數(shù)據(jù)的特點(diǎn)。本章節(jié)是提供給菜鳥(niǎo)級(jí)的玩家看的,如果你是高手就可以跳 過(guò)了,如果,你想成為無(wú)堅(jiān)不摧的劍客,那么,這些東西就會(huì)花掉你一些時(shí)間;如果,你只想作個(gè)江湖的游客的話,那么這些東西,了解與否無(wú)關(guān)緊要。是作劍客, 還是作游客,你選擇吧!

                現(xiàn)在我們開(kāi)始!首先,你要知道游戲中儲(chǔ)存數(shù)據(jù)的幾種格式,這幾種格式是:字節(jié)(BYTE)、字(WORD)和雙字(DOUBLE WORD),或者說(shuō)是8位、16位和32位儲(chǔ)存方式。字節(jié)也就是8位方式能儲(chǔ)存0~255的數(shù)字;字或說(shuō)是16位儲(chǔ)存方式能儲(chǔ)存0~65535的數(shù);雙字 即32位方式能儲(chǔ)存0~4294967295的數(shù)。

                為何要了解這些知識(shí)呢?在游戲中各種參數(shù)的最大值是不同的,有些可能100左右就夠了,比如,金庸群俠傳中的角色的等級(jí)、隨機(jī)遇敵個(gè)數(shù)等等。而有些卻需要 大于255甚至大于65535,象金庸群俠傳中角色的金錢(qián)值可達(dá)到數(shù)百萬(wàn)。所以,在游戲中各種不同的數(shù)據(jù)的類(lèi)型是不一樣的。在我們修改游戲時(shí)需要尋找準(zhǔn)備 修改的數(shù)據(jù)的封包,在這種時(shí)候,正確判斷數(shù)據(jù)的類(lèi)型是迅速找到正確地址的重要條件。

                在計(jì)算機(jī)中數(shù)據(jù)以字節(jié)為基本的儲(chǔ)存單位,每個(gè)字節(jié)被賦予一個(gè)編號(hào),以確定各自的位置。這個(gè)編號(hào)我們就稱(chēng)為地址。

                在需要用到字或雙字時(shí),計(jì)算機(jī)用連續(xù)的兩個(gè)字節(jié)來(lái)組成一個(gè)字,連續(xù)的兩個(gè)字組成一個(gè)雙字。而一個(gè)字或雙字的地址就是它們的低位字節(jié)的地址。 現(xiàn)在我們常用的Windows 9x操作系統(tǒng)中,地址是用一個(gè)32位的二進(jìn)制數(shù)表示的。而在平時(shí)我們用到內(nèi)存地址時(shí),總是用一個(gè)8位的16進(jìn)制數(shù)來(lái)表示它。

                二進(jìn)制和十六進(jìn)制又是怎樣一回事呢?

                簡(jiǎn)單說(shuō)來(lái),二進(jìn)制數(shù)就是一種只有0和1兩個(gè)數(shù)碼,每滿2則進(jìn)一位的計(jì)數(shù)進(jìn)位法。同樣,16進(jìn)制就是每滿十六就進(jìn)一位的計(jì)數(shù)進(jìn)位法。16進(jìn)制有0——F十六 個(gè)數(shù)字,它為表示十到十五的數(shù)字采用了A、B、C、D、E、F六個(gè)數(shù)字,它們和十進(jìn)制的對(duì)應(yīng)關(guān)系是:A對(duì)應(yīng)于10,B對(duì)應(yīng)于11,C對(duì)應(yīng)于12,D對(duì)應(yīng)于 13,E對(duì)應(yīng)于14,F(xiàn)對(duì)應(yīng)于15.而且,16進(jìn)制數(shù)和二進(jìn)制數(shù)間有一個(gè)簡(jiǎn)單的對(duì)應(yīng)關(guān)系,那就是;四位二進(jìn)制數(shù)相當(dāng)于一位16進(jìn)制數(shù)。比如,一個(gè)四位的二 進(jìn)制數(shù)1111就相當(dāng)于16進(jìn)制的F,1010就相當(dāng)于A.了解這些基礎(chǔ)知識(shí)對(duì)修改游戲有著很大的幫助,下面我就要談到這個(gè)問(wèn)題。由于在計(jì)算機(jī)中數(shù)據(jù)是以 二進(jìn)制的方式儲(chǔ)存的,同時(shí)16進(jìn)制數(shù)和二進(jìn)制間的轉(zhuǎn)換關(guān)系十分簡(jiǎn)單,所以大部分的修改工具在顯示計(jì)算機(jī)中的數(shù)據(jù)時(shí)會(huì)顯示16進(jìn)制的代碼,而且在你修改時(shí)也 需要輸入16進(jìn)制的數(shù)字。你清楚了吧?

                在游戲中看到的數(shù)據(jù)可都是十進(jìn)制的,在要尋找并修改參數(shù)的值時(shí),可以使用Windows提供的計(jì)算器來(lái)進(jìn)行十進(jìn)制和16進(jìn)制的換算,我們可以在開(kāi)始菜單里的程序組中的附件中找到它。

                現(xiàn)在要了解的知識(shí)也差不多了!不過(guò),有個(gè)問(wèn)題在游戲修改中是需要注意的。在計(jì)算機(jī)中數(shù)據(jù)的儲(chǔ)存方式一般是低位數(shù)儲(chǔ)存在低位字節(jié),高位數(shù)儲(chǔ)存在高位字節(jié)。比如,十進(jìn)制數(shù)41715轉(zhuǎn)換為16進(jìn)制的數(shù)為A2F3,但在計(jì)算機(jī)中這個(gè)數(shù)被存為F3A2.

                看了以上內(nèi)容大家對(duì)數(shù)據(jù)的存貯和數(shù)據(jù)的對(duì)應(yīng)關(guān)系都了解了嗎? 好了,接下來(lái)我們要告訴大家在游戲中,封包到底是怎么一回事了,來(lái)!大家把袖口卷起來(lái),讓我們來(lái)干活吧!

                二:什么是封包?

                怎么截獲一個(gè)游戲的封包?怎么去檢查游戲服務(wù)器的ip地址和端口號(hào)? Internet用戶(hù)使用的各種信息服務(wù),其通訊的信息最終均可以歸結(jié)為以IP包為單位的信息傳送,IP包除了包括要傳送的數(shù)據(jù)信息外,還包含有信息要發(fā) 送到的目的IP地址、信息發(fā)送的源IP地址、以及一些相關(guān)的控制信息。當(dāng)一臺(tái)路由器收到一個(gè)IP數(shù)據(jù)包時(shí),它將根據(jù)數(shù)據(jù)包中的目的IP地址項(xiàng)查找路由表,根據(jù)查找的結(jié)果將此IP數(shù)據(jù)包送往對(duì)應(yīng)端口。下一臺(tái)IP路由器收到此數(shù)據(jù)包后繼續(xù)轉(zhuǎn)發(fā),直至發(fā)到目的地。路由器之間可以通過(guò)路由協(xié)議來(lái)進(jìn)行路由信息的交換,從而更新路由表。

                那么我們所關(guān)心的內(nèi)容只是IP包中的數(shù)據(jù)信息,我們可以使用許多監(jiān)聽(tīng)網(wǎng)絡(luò)的工具來(lái)截獲客戶(hù)端與服務(wù)器之間的交換數(shù)據(jù),下面就向你介紹其中的一種工具:WPE. WPE使用方法:執(zhí)行WPE會(huì)有下列幾項(xiàng)功能可選擇:

                SELECT GAME選擇目前在記憶體中您想攔截的程式,您只需雙擊該程式名稱(chēng)即可。

                TRACE追蹤功能。用來(lái)追蹤擷取程式送收的封包。WPE必須先完成點(diǎn)選欲追蹤的程式名稱(chēng),才可以使用此項(xiàng)目。 按下Play鍵開(kāi)始擷取程式收送的封包。您可以隨時(shí)按下 | | 暫停追蹤,想繼續(xù)時(shí)請(qǐng)?jiān)侔聪?| | .按下正方形可以停止擷取封包并且顯示所有已擷取封包內(nèi)容。若您沒(méi)按下正方形停止鍵,追蹤的動(dòng)作將依照OPTION里的設(shè)定值自動(dòng)停止。如果您沒(méi)有擷取到 資料,試試將OPTION里調(diào)整為Winsock Version 2.WPE 及 Trainers 是設(shè)定在顯示至少16 bits 顏色下才可執(zhí)行。

                FILTER過(guò)濾功能。用來(lái)分析所擷取到的封包,并且予以修改。

                SEND PACKET送出封包功能。能夠讓您送出假造的封包。

                TRAINER MAKER制作修改器。

                OPTIONS設(shè)定功能。讓您調(diào)整WPE的一些設(shè)定值。

                FILTER的詳細(xì)教學(xué)

                - 當(dāng)FILTER在啟動(dòng)狀態(tài)時(shí) ,ON的按鈕會(huì)呈現(xiàn)紅色。- 當(dāng)您啟動(dòng)FILTER時(shí),您隨時(shí)可以關(guān)閉這個(gè)視窗。FILTER將會(huì)留在原來(lái)的狀態(tài),直到您再按一次 on / off 鈕。- 只有FILTER啟用鈕在OFF的狀態(tài)下,才可以勾選Filter前的方框來(lái)編輯修改。- 當(dāng)您想編輯某個(gè)Filter,只要雙擊該Filter的名字即可。

                NORMAL MODE:

                范例:

                當(dāng)您在 Street Fighter Online ﹝快打旋風(fēng)線上版﹞游戲中,您使用了兩次火球而且擊中了對(duì)方,這時(shí)您會(huì)擷取到以下的封包:SEND-> 0000 08 14 21 06 01 04 SEND-> 0000 02 09 87 00 67 FF A4 AA 11 22 00 00 00 00 SEND-> 0000 03 84 11 09 11 09 SEND-> 0000 0A 09 C1 10 00 00 FF 52 44 SEND-> 0000 0A 09 C1 10 00 00 66 52 44您的第一個(gè)火球讓對(duì)方減了16滴﹝16 = 10h﹞的生命值,而您觀察到第4跟第5個(gè)封包的位置4有10h的值出現(xiàn),應(yīng)該就是這里了。

                您觀察10h前的0A 09 C1在兩個(gè)封包中都沒(méi)改變,可見(jiàn)得這3個(gè)數(shù)值是發(fā)出火球的關(guān)鍵。

                因此您將0A 09 C1 10填在搜尋列﹝SEARCH﹞,然后在修改列﹝MODIFY﹞的位置4填上FF.如此一來(lái),當(dāng)您再度發(fā)出火球時(shí),F(xiàn)F會(huì)取代之前的10,也就是攻擊力為255的火球了!

                ADVANCED MODE:范例: 當(dāng)您在一個(gè)游戲中,您不想要用真實(shí)姓名,您想用修改過(guò)的假名傳送給對(duì)方。在您使用TRACE后,您會(huì)發(fā)現(xiàn)有些封包里面有您的名字出現(xiàn)。假設(shè)您的名字是 Shadow,換算成16進(jìn)位則是﹝53 68 61 64 6F 77﹞;而您打算用moon﹝6D6F 6F 6E 20 20﹞來(lái)取代他。1) SEND-> 0000 08 14 21 06 01 042) SEND-> 0000 01 06 99 53 68 61 64 6F 77 00 01 05 3) SEND-> 0000 03 84 11 09 11 094) SEND-> 0000 0A 09 C1 10 00 53 68 61 64 6F 77 00 11 5) SEND-> 0000 0A 09 C1 10 00 00 66 52 44但是您仔細(xì)看,您的名字在每個(gè)封包中并不是出現(xiàn)在相同的位置上- 在第2個(gè)封包里,名字是出現(xiàn)在第4個(gè)位置上- 在第4個(gè)封包里,名字是出現(xiàn)在第6個(gè)位置上在這種情況下,您就需要使用ADVANCED MODE- 您在搜尋列﹝SEARCH﹞填上:53 68 61 64 6F 77 ﹝請(qǐng)務(wù)必從位置1開(kāi)始填﹞- 您想要從原來(lái)名字Shadow的第一個(gè)字母開(kāi)始置換新名字,因此您要選擇從數(shù)值被發(fā)現(xiàn)的位置開(kāi)始替代連續(xù)數(shù)值﹝from the position of the chain found﹞.- 現(xiàn)在,在修改列﹝MODIFY﹞000的位置填上:6D 6F 6F 6E 20 20 ﹝此為相對(duì)應(yīng)位置,也就是從原來(lái)搜尋欄的+001位置開(kāi)始遞換﹞- 如果您想從封包的第一個(gè)位置就修改數(shù)值,請(qǐng)選擇﹝from the beginning of the packet﹞了解一點(diǎn)TCP/IP協(xié)議常識(shí)的人都知道,互聯(lián)網(wǎng)是 將信息數(shù)據(jù)打包之后再傳送出去的。每個(gè)數(shù)據(jù)包分為頭部信息和數(shù)據(jù)信息兩部分。頭部信息包括數(shù)據(jù)包的發(fā)送地址和到達(dá)地址等。數(shù)據(jù)信息包括我們?cè)谟螒蛑邢嚓P(guān)操 作的各項(xiàng)信息。那么在做截獲封包的過(guò)程之前我們先要知道游戲服務(wù)器的IP地址和端口號(hào)等各種信息,實(shí)際上最簡(jiǎn)單的是看看我們游戲目錄下,是否有一個(gè) SERVER.INI的配置文件,這個(gè)文件里你可以查看到個(gè)游戲服務(wù)器的IP地址,比如金庸群俠傳就是如此,那么除了這個(gè)我們還可以在DOS下使用 NETSTAT這個(gè)命令, NETSTAT命令的功能是顯示網(wǎng)絡(luò)連接、路由表和網(wǎng)絡(luò)接口信息,可以讓用戶(hù)得知目前都有哪些網(wǎng)絡(luò)連接正在運(yùn)作。或者你可以使用木馬客星等工具來(lái)查看網(wǎng)絡(luò) 連接。工具是很多的,看你喜歡用哪一種了。

                NETSTAT命令的一般格式為:NETSTAT [選項(xiàng)]命令中各選項(xiàng)的含義如下:-a 顯示所有socket,包括正在監(jiān)聽(tīng)的。-c 每隔1秒就重新顯示一遍,直到用戶(hù)中斷它。

                -i 顯示所有網(wǎng)絡(luò)接口的信息。-n 以網(wǎng)絡(luò)IP地址代替名稱(chēng),顯示出網(wǎng)絡(luò)連接情形。-r 顯示核心路由表,格式同"route -e".-t 顯示TCP協(xié)議的連接情況。-u 顯示UDP協(xié)議的連接情況。-v 顯示正在進(jìn)行的工作。

                三:怎么來(lái)分析我們截獲的封包?

                首先我們將WPE截獲的封包保存為文本文件,然后打開(kāi)它,這時(shí)會(huì)看到如下的數(shù)據(jù)(這里我們以金庸群俠傳里PK店小二客戶(hù)端發(fā)送的數(shù)據(jù)為例來(lái)講解):第一個(gè) 文件:SEND-> 0000 E6 56 0D 22 7E 6B E4 17 13 13 12 13 12 13 67 1BSEND-> 0010 17 12 DD 34 12 12 12 12 17 12 0E 12 12 12 9BSEND-> 0000 E6 56 1E F1 29 06 17 12 3B 0E 17 1ASEND-> 0000 E6 56 1B C0 68 12 12 12 5ASEND-> 0000 E6 56 02 C8 13 C9 7E 6B E4 17 10 35 27 13 12 12SEND-> 0000 E6 56 17 C9 12第二個(gè)文件:SEND-> 0000 83 33 68 47 1B 0E 81 72 76 76 77 76 77 76 02 7ESEND-> 0010 72 77 07 1C 77 77 77 77 72 77 72 77 77 77 6DSEND-> 0000 83 33 7B 94 4C 63 72 77 5E 6B 72 F3SEND-> 0000 83 33 7E A5 21 77 77 77 3FSEND-> 0000 83 33 67 AD 76 CF 1B 0E 81 72 75 50 42 76 77 77SEND-> 0000 83 33 72 AC 77我們發(fā)現(xiàn)兩次PK店小二的數(shù)據(jù)格式一樣,但是內(nèi)容卻不相同,我們是PK的同一個(gè)NPC,為什么會(huì)不同呢? 原來(lái)金庸群俠傳的封包是經(jīng)過(guò)了加密運(yùn)算才在網(wǎng)路上傳輸?shù)模敲次覀兠媾R的問(wèn)題就是如何將密文解密成明文再分析了。

                因?yàn)橐话愕臄?shù)據(jù)包加密都是異或運(yùn)算,所以這里先講一下什么是異或。 簡(jiǎn)單的說(shuō),異或就是"相同為0,不同為1"(這是針對(duì)二進(jìn)制按位來(lái)講的),舉個(gè)例子,0001和0010異或,我們按位對(duì)比,得到異或結(jié)果是0011,計(jì) 算的方法是:0001的第4位為0,0010的第4位為0,它們相同,則異或結(jié)果的第4位按照"相同為0,不同為1"的原則得到0,0001的第3位為 0,0010的第3位為0,則異或結(jié)果的第3位得到0,0001的第2位為0,0010的第2位為1,則異或結(jié)果的第2位得到1,0001的第1位為 1,0010的第1位為0,則異或結(jié)果的第1位得到1,組合起來(lái)就是0011.異或運(yùn)算今后會(huì)遇到很多,大家可以先熟悉熟悉,熟練了對(duì)分析很有幫助的。

                下面我們繼續(xù)看看上面的兩個(gè)文件,按照常理,數(shù)據(jù)包的數(shù)據(jù)不會(huì)全部都有值的,游戲開(kāi)發(fā)時(shí)會(huì)預(yù)留一些字節(jié)空間來(lái)便于日后的擴(kuò)充,也就是說(shuō)數(shù)據(jù)包里會(huì)存在一些"00"的字節(jié),觀察上面的文件,我們會(huì)發(fā)現(xiàn)文件一里很多"12",文件二里很多"77",那么這是不是代表我們說(shuō)的"00"呢?推理到這里,我們就開(kāi)始行動(dòng)吧!

                我們把文件一與"12"異或,文件二與"77"異或,當(dāng)然用手算很費(fèi)事,我們使用"M2M 1.0 加密封包分析工具"來(lái)計(jì)算就方便多了。得到下面的結(jié)果:第一個(gè)文件:1 SEND-> 0000 F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 CF 26 00 00 00 00 05 00 1C 00 00 00 892 SEND-> 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 083 SEND-> 0000 F4 44 09 D2 7A 00 00 00 484 SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 005 SEND-> 0000 F4 44 05 DB 00第二個(gè)文件:1 SEND-> 0000 F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 70 6B 00 00 00 00 05 00 05 00 00 00 1A2 SEND-> 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 843 SEND-> 0000 F4 44 09 D2 56 00 00 00 484 SEND-> 0000 F4 44 10 DA 01 B8 6C 79 F6 05 02 27 35 01 00 005 SEND-> 0000 F4 44 05 DB 00哈,這一下兩個(gè)文件大部分都一樣啦,說(shuō)明我們的推理是正確的,上面就是我們需要的明文!

                接下來(lái)就是搞清楚一些關(guān)鍵的字節(jié)所代表的含義,這就需要截獲大量的數(shù)據(jù)來(lái)分析。

                首先我們會(huì)發(fā)現(xiàn)每個(gè)數(shù)據(jù)包都是"F4 44"開(kāi)頭,第3個(gè)字節(jié)是變化的,但是變化很有規(guī)律。我們來(lái)看看各個(gè)包的長(zhǎng)度,發(fā)現(xiàn)什么沒(méi)有?對(duì)了,第3個(gè)字節(jié)就是包的長(zhǎng)度! 通過(guò)截獲大量的數(shù)據(jù)包,我們判斷第4個(gè)字節(jié)代表指令,也就是說(shuō)客戶(hù)端告訴服務(wù)器進(jìn)行的是什么操作。例如向服務(wù)器請(qǐng)求戰(zhàn)斗指令為"30",戰(zhàn)斗中移動(dòng)指令 為"D4"等。 接下來(lái),我們就需要分析一下上面第一個(gè)包"F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09 05 00 CF 26 00 00 00 00 05 00 1C 00 00 00 89",在這個(gè)包里包含什么信息呢?應(yīng)該有通知服務(wù)器你PK的哪個(gè)NPC吧,我們就先來(lái)找找這個(gè)店小二的代碼在什么地方。 我們?cè)貾K一個(gè)小嘍羅(就是大理客棧外的那個(gè)咯):SEND-> 0000 F4 44 1F 30 D4 75 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 8A 19 00 00 00 00 11 00 02 00 00 00 C0 我們根據(jù)常理分析,游戲里的NPC種類(lèi)雖然不會(huì)超過(guò)65535(FFFF),但開(kāi)發(fā)時(shí)不會(huì)把自己限制在字的范圍,那樣不利于游戲的擴(kuò)充,所以我們?cè)陔p字里 看看。通過(guò)"店小二"和"小嘍羅"兩個(gè)包的對(duì)比,我們把目標(biāo)放在"6C 79 F6 05"和"CF 26 00 00"上。(對(duì)比一下很容易的,但你不能太遲鈍咯,呵呵)我們?cè)倏纯春竺娴陌诤竺娴陌飸?yīng)該還會(huì)出現(xiàn)NPC的代碼,比如移動(dòng)的包,游戲允許觀戰(zhàn),服務(wù) 器必然需要知道NPC的移動(dòng)坐標(biāo),再?gòu)V播給觀戰(zhàn)的其他玩家。在后面第4個(gè)包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00"里我們又看到了"6C 79 F6 05",初步斷定店小二的代碼就是它了!(這分析里邊包含了很多工作的,大家可以用WPE截下數(shù)據(jù)來(lái)自己分析分析)

                第一個(gè)包的分析暫時(shí)就到這里(里面還有的信息我們暫時(shí)不需要完全清楚了)

                我們看看第4個(gè)包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00",再截獲PK黃狗的包,(狗會(huì)出來(lái)2只哦)看看包的格式:SEND-> 0000 F4 44 1A DA 02 0B 4B 7D F6 05 02 27 35 01 00 00SEND-> 0010 EB 03 F8 05 02 27 36 01 00 00根據(jù)上面的分析,黃狗的代碼為"4B 7D F6 05"(100040011),不過(guò)兩只黃狗服務(wù)器怎樣分辨呢?看看"EB 03 F8 05"(100140011),是上一個(gè)代碼加上100000,呵呵,這樣服務(wù)器就可以認(rèn)出兩只黃狗了。我們?cè)偻ㄟ^(guò)野外遇敵截獲的數(shù)據(jù)包來(lái)證實(shí),果然如 此。

                那么,這個(gè)包的格式應(yīng)該比較清楚了:第3個(gè)字節(jié)為包的長(zhǎng)度,"DA"為指令,第5個(gè)字節(jié)為NPC個(gè)數(shù),從第7個(gè)字節(jié)開(kāi)始的10個(gè)字節(jié)代表一個(gè)NPC的信息,多一個(gè)NPC就多10個(gè)字節(jié)來(lái)表示。

                大家如果玩過(guò)網(wǎng)金,必然知道隨機(jī)遇敵有時(shí)會(huì)出現(xiàn)增援,我們就利用游戲這個(gè)增援來(lái)讓每次戰(zhàn)斗都會(huì)出現(xiàn)增援的NPC吧。

                通過(guò)在戰(zhàn)斗中出現(xiàn)增援截獲的數(shù)據(jù)包,我們會(huì)發(fā)現(xiàn)服務(wù)器端發(fā)送了這樣一個(gè)包:F4 44 12 E9 EB 03 F8 05 02 00 00 03 00 00 00 00 00 00 第5-第8個(gè)字節(jié)為增援NPC的代碼(這里我們就簡(jiǎn)單的以黃狗的代碼來(lái)舉例)。 那么,我們就利用單機(jī)代理技術(shù)來(lái)同時(shí)欺騙客戶(hù)端和服務(wù)器吧!

                好了,呼叫NPC的工作到這里算是完成了一小半,接下來(lái)的事情,怎樣修改封包和發(fā)送封包,我們下節(jié)繼續(xù)講解吧。

                四:怎么冒充"客戶(hù)端"向"服務(wù)器"發(fā)我們需要的封包?

                這里我們需要使用一個(gè)工具,它位于客戶(hù)端和服務(wù)器端之間,它的工作就是進(jìn)行數(shù)據(jù)包的接收和轉(zhuǎn)發(fā),這個(gè)工具我們稱(chēng)為代理。如果代理的工作單純就是接收和轉(zhuǎn)發(fā) 的話,這就毫無(wú)意義了,但是請(qǐng)注意:所有的數(shù)據(jù)包都要通過(guò)它來(lái)傳輸,這里的意義就重大了。我們可以分析接收到的數(shù)據(jù)包,或者直接轉(zhuǎn)發(fā),或者修改后轉(zhuǎn)發(fā),或 者壓住不轉(zhuǎn)發(fā),甚至偽造我們需要的封包來(lái)發(fā)送。

                下面我們繼續(xù)講怎樣來(lái)同時(shí)欺騙服務(wù)器和客戶(hù)端,也就是修改封包和偽造封包。 通過(guò)我們上節(jié)的分析,我們已經(jīng)知道了打多個(gè)NPC的封包格式,那么我們就動(dòng)手吧!

                首先我們要查找客戶(hù)端發(fā)送的包,找到戰(zhàn)斗的特征,就是請(qǐng)求戰(zhàn)斗的第1個(gè)包,我們找"F4 44 1F 30"這個(gè)特征,這是不會(huì)改變的,當(dāng)然是要解密后來(lái)查找哦。 找到后,表示客戶(hù)端在向服務(wù)器請(qǐng)求戰(zhàn)斗,我們不動(dòng)這個(gè)包,轉(zhuǎn)發(fā)。 繼續(xù)向下查找,這時(shí)需要查找的特征碼不太好辦,我們先查找"DA",這是客戶(hù)端發(fā)送NPC信息的數(shù)據(jù)包的指令,那么可能其他包也有"DA",沒(méi)關(guān)系,我們 看前3個(gè)字節(jié)有沒(méi)有"F4 44"就行了。找到后,我們的工作就開(kāi)始了!

                我們確定要打的NPC數(shù)量。這個(gè)數(shù)量不能很大,原因在于網(wǎng)金的封包長(zhǎng)度用一個(gè)字節(jié)表示,那么一個(gè)包可以有255個(gè)字節(jié),我們上面分析過(guò),增加一個(gè)NPC要增加10個(gè)字節(jié),所以大家算算就知道,打20個(gè)NPC比較合適。

                然后我們要把客戶(hù)端原來(lái)的NPC代碼分析計(jì)算出來(lái),因?yàn)樵黾拥腘PC代碼要加上100000哦。再把我們?cè)黾拥腘PC代碼計(jì)算出來(lái),并且組合成新的封包,注意代表包長(zhǎng)度的字節(jié)要修改啊,然后轉(zhuǎn)發(fā)到服務(wù)器,這一步在編寫(xiě)程序的時(shí)候要注意算法,不要造成較大延遲。

                上面我們欺騙服務(wù)器端完成了,欺騙客戶(hù)端就簡(jiǎn)單了。

                發(fā)送了上面的封包后,我們根據(jù)新增NPC代碼構(gòu)造封包馬上發(fā)給客戶(hù)端,格式就是"F4 44 12 E9 NPC代碼 02 00 00 03 00 00 00 00 00 00",把每個(gè)新增的NPC都構(gòu)造這樣一個(gè)包,按順序連在一起發(fā)送給客戶(hù)端,客戶(hù)端也就被我們騙過(guò)了,很簡(jiǎn)單吧。

                以后戰(zhàn)斗中其他的事我們就不管了,盡情地開(kāi)打吧。

                游戲外掛基本原理及實(shí)現(xiàn)

                解釋游戲外掛的基本原理和實(shí)現(xiàn)方法

                游戲外掛已經(jīng)深深地影響著眾多網(wǎng)絡(luò)游戲玩家,今天在網(wǎng)上看到了一些關(guān)于游戲外掛編寫(xiě)的技術(shù),于是轉(zhuǎn)載上供大家參考

                1、游戲外掛的原理

                外掛現(xiàn)在分為好多種,比如模擬鍵盤(pán)的,鼠標(biāo)的,修改數(shù)據(jù)包的,還有修改本地內(nèi)存的,但好像沒(méi)有修改服務(wù)器內(nèi)存的哦,呵呵。其實(shí)修改服務(wù)器也是有辦法的,只是技術(shù)太高一般人沒(méi)有辦法入手而已。(比如請(qǐng)GM去夜總會(huì)、送禮、收黑錢(qián)等等辦法都可以修改服務(wù)器數(shù)據(jù),哈哈)

                修改游戲無(wú)非是修改一下本地內(nèi)存的數(shù)據(jù),或者截獲API函數(shù)等等。這里我把所能想到的方法都作一個(gè)介紹,希望大家能做出很好的外掛來(lái)使游戲廠商更好的完善 自己的技術(shù)。我見(jiàn)到一篇文章是講魔力寶貝的理論分析,寫(xiě)得不錯(cuò),大概是那個(gè)樣子。下來(lái)我就講解一下技術(shù)方面的東西,以作引玉之用。


               2 技術(shù)分析部分

                2.1 模擬鍵盤(pán)或鼠標(biāo)的響應(yīng)

                我們一般使用:

            UINT SendInput(
                UINT nInputs,   // count of input events
               PINPUT pInputs, // array of input events
                int cbSize    // size of structure
              );
                API函數(shù)。第一個(gè)參數(shù)是說(shuō)明第二個(gè)參數(shù)的矩陣的維數(shù)的,第二個(gè)參數(shù)包含了響應(yīng)事件,這個(gè)自己填充就可以,最后是這個(gè)結(jié)構(gòu)的大小,非常簡(jiǎn)單,這是最簡(jiǎn)單的方法模擬鍵盤(pán)鼠標(biāo)了,呵呵。注意,這個(gè)函數(shù)還有個(gè)替代函數(shù):

            VOID keybd_event(
                BYTE bVk,       // 虛擬鍵碼
                BYTE bScan,      // 掃描碼
                DWORD dwFlags,
                ULONG_PTR dwExtraInfo // 附加鍵狀態(tài)
              );
              與
              VOID mouse_event(
                DWORD dwFlags,      // motion and click options
                DWORD dx,         // horizontal position or change
                DWORD dy,        // vertical position or change
                DWORD dwData,      // wheel movement
                ULONG_PTR dwExtraInfo  // application-defined information
              );
                這兩個(gè)函數(shù)非常簡(jiǎn)單了,我想那些按鍵精靈就是用的這個(gè)吧。上面的是模擬鍵盤(pán),下面的是模擬鼠標(biāo)的。這個(gè)僅僅是模擬部分,要和游戲聯(lián)系起來(lái)我們還需要找到游 戲的窗口才行,或者包含快捷鍵,就象按鍵精靈的那個(gè)激活鍵一樣,我們可以用GetWindow函數(shù)來(lái)枚舉窗口,也可以用Findwindow函數(shù)來(lái)查找制 定的窗口(注意,還有一個(gè)FindWindowEx),F(xiàn)indwindowEx可以找到窗口的子窗口,比如按鈕,等什么東西。當(dāng)游戲切換場(chǎng)景的時(shí)候我們 可以用FindWindowEx來(lái)確定一些當(dāng)前窗口的特征,從而判斷是否還在這個(gè)場(chǎng)景,方法很多了,比如可以GetWindowInfo來(lái)確定一些東西, 比如當(dāng)查找不到某個(gè)按鈕的時(shí)候就說(shuō)明游戲場(chǎng)景已經(jīng)切換了,等等辦法。有的游戲沒(méi)有控件在里面,這是對(duì)圖像做坐標(biāo)變換的話,這種方法就要受到限制了。這就需 要我們用別的辦法來(lái)輔助分析了。

                至于快捷鍵我們要用動(dòng)態(tài)連接庫(kù)實(shí)現(xiàn)了,里面要用到hook技術(shù)了,這個(gè)也非常簡(jiǎn)單。大家可能都會(huì)了,其實(shí)就是一個(gè)全局的hook對(duì)象然后 SetWindowHook就可以了,回調(diào)函數(shù)都是現(xiàn)成的,而且現(xiàn)在網(wǎng)上的例子多如牛毛。這個(gè)實(shí)現(xiàn)在外掛中已經(jīng)很普遍了。如果還有誰(shuí)不明白,那就去看看 MSDN查找SetWindowHook就可以了。

                不要低估了這個(gè)動(dòng)態(tài)連接庫(kù)的作用,它可以切入所有的進(jìn)程空間,也就是可以加載到所有的游戲里面哦,只要用對(duì),你會(huì)發(fā)現(xiàn)很有用途的。這個(gè)需要你復(fù)習(xí)一下Win32編程的基礎(chǔ)知識(shí)了。呵呵,趕快去看書(shū)吧。

                2.2 截獲消息

                有些游戲的響應(yīng)機(jī)制比較簡(jiǎn)單,是基于消息的,或者用什么定時(shí)器的東西。這個(gè)時(shí)候你就可以用攔截消息來(lái)實(shí)現(xiàn)一些有趣的功能了。

                我們攔截消息使用的也是hook技術(shù),里面包括了鍵盤(pán)消息,鼠標(biāo)消息,系統(tǒng)消息,日志等,別的對(duì)我們沒(méi)有什么大的用處,我們只用攔截消息的回調(diào)函數(shù)就可以 了,這個(gè)不會(huì)讓我寫(xiě)例子吧。其實(shí)這個(gè)和上面的一樣,都是用SetWindowHook來(lái)寫(xiě)的,看看就明白了很簡(jiǎn)單的。

                至于攔截了以后做什么就是你的事情了,比如在每個(gè)定時(shí)器消息里面處理一些我們的數(shù)據(jù)判斷,或者在定時(shí)器里面在模擬一次定時(shí)器,那么有些數(shù)據(jù)就會(huì)處理兩次, 呵呵。后果嘛,不一定是好事情哦,呵呵,不過(guò)如果數(shù)據(jù)計(jì)算放在客戶(hù)端的游戲就可以真的改變數(shù)據(jù)了,呵呵,試試看吧。用途還有很多,自己想也可以想出來(lái)的, 呵呵。

                2.3 攔截Socket包

                這個(gè)技術(shù)難度要比原來(lái)的高很多。

                首先我們要替換WinSock.DLL或者WinSock32.DLL,我們寫(xiě)的替換函數(shù)要和原來(lái)的函數(shù)一致才行,就是說(shuō)它的函數(shù)輸出什么樣的,我們也要 輸出什么樣子的函數(shù),而且參數(shù),參數(shù)順序都要一樣才行,然后在我們的函數(shù)里面調(diào)用真正的WinSock32.DLL里面的函數(shù)就可以了。

                首先:我們可以替換動(dòng)態(tài)庫(kù)到系統(tǒng)路徑。

                其次:我們應(yīng)用程序啟動(dòng)的時(shí)候可以加載原有的動(dòng)態(tài)庫(kù),用這個(gè)函數(shù)LoadLibary然后定位函數(shù)入口用GetProcAddress函數(shù)獲得每個(gè)真正Socket函數(shù)的入口地址。

                當(dāng)游戲進(jìn)行的時(shí)候它會(huì)調(diào)用我們的動(dòng)態(tài)庫(kù),然后從我們的動(dòng)態(tài)庫(kù)中處理完畢后才跳轉(zhuǎn)到真正動(dòng)態(tài)庫(kù)的函數(shù)地址,這樣我們就可以在里面處理自己的數(shù)據(jù)了,應(yīng)該是一 切數(shù)據(jù)。呵呵,興奮吧,攔截了數(shù)據(jù)包我們還要分析之后才能進(jìn)行正確的應(yīng)答,不要以為這樣工作就完成了,還早呢。等分析完畢以后我們還要仿真應(yīng)答機(jī)制來(lái)和服 務(wù)器通信,一個(gè)不小心就會(huì)被封號(hào)。

                分析數(shù)據(jù)才是工作量的來(lái)源呢,游戲每次升級(jí)有可能加密方式會(huì)有所改變,因此我們寫(xiě)外掛的人都是亡命之徒啊,被人愚弄了還不知道。

                2.4 截獲API

                上面的技術(shù)如果可以靈活運(yùn)用的話我們就不用截獲API函數(shù)了,其實(shí)這種技術(shù)是一種補(bǔ)充技術(shù)。比如我們需要截獲Socket以外的函數(shù)作為我們的用途,我們就要用這個(gè)技術(shù)了,其實(shí)我們也可以用它直接攔截在Socket中的函數(shù),這樣更直接。

                現(xiàn)在攔截API的教程到處都是,我就不列舉了,我用的比較習(xí)慣的方法是根據(jù)輸入節(jié)進(jìn)行攔截的,這個(gè)方法可以用到任何一種操作系統(tǒng)上,比如Windows 98/2000等,有些方法不是跨平臺(tái)的,我不建議使用。這個(gè)技術(shù)大家可以參考《Windows核心編程》里面的545頁(yè)開(kāi)始的內(nèi)容來(lái)學(xué)習(xí),如果是 Win98系統(tǒng)可以用“Windows系統(tǒng)奧秘”那個(gè)最后一章來(lái)學(xué)習(xí)。

                網(wǎng)絡(luò)游戲通訊模型初探

                [文章導(dǎo)讀]本文就將圍繞三個(gè)主題來(lái)給大家講述一下網(wǎng)絡(luò)游戲的網(wǎng)絡(luò)互連實(shí)現(xiàn)方法

                序言

                網(wǎng)絡(luò)游戲,作為游戲與網(wǎng)絡(luò)有機(jī)結(jié)合的產(chǎn)物,把玩家?guī)肓诵碌膴蕵?lè)領(lǐng)域。網(wǎng)絡(luò)游戲在中國(guó)開(kāi)始發(fā)展至今也僅有3,4年的歷史,跟已經(jīng)擁有幾十年開(kāi)發(fā)歷史的單機(jī)游戲相比,網(wǎng)絡(luò)游戲還是非常年輕的。當(dāng)然,它的形成也是根據(jù)歷史變化而產(chǎn)生的可以說(shuō)沒(méi)有互聯(lián)網(wǎng)的 興起,也就沒(méi)有網(wǎng)絡(luò)游戲的誕生。作為新興產(chǎn)物,網(wǎng)絡(luò)游戲的開(kāi)發(fā)對(duì)廣大開(kāi)發(fā)者來(lái)說(shuō)更加神秘,對(duì)于一個(gè)未知領(lǐng)域,開(kāi)發(fā)者可能更需要了解的是網(wǎng)絡(luò)游戲與普通單機(jī) 游戲有何區(qū)別,網(wǎng)絡(luò)游戲如何將玩家們連接起來(lái),以及如何為玩家提供一個(gè)互動(dòng)的娛樂(lè)環(huán)境。本文就將圍繞這三個(gè)主題來(lái)給大家講述一下網(wǎng)絡(luò)游戲的網(wǎng)絡(luò)互連實(shí)現(xiàn)方 法。

                網(wǎng)絡(luò)游戲與單機(jī)游戲

                說(shuō)到網(wǎng)絡(luò)游戲,不得不讓人聯(lián)想到單機(jī)游戲,實(shí)際上網(wǎng)絡(luò)游戲的實(shí)質(zhì)脫離不了單機(jī)游戲的制作思想,網(wǎng)絡(luò)游戲和單機(jī)游戲的差別大家可以很直接的想到:不就是可以 多人連線嗎?沒(méi)錯(cuò),但如何實(shí)現(xiàn)這些功能,如何把網(wǎng)絡(luò)連線合理的融合進(jìn)單機(jī)游戲,就是我們下面要討論的內(nèi)容。在了解網(wǎng)絡(luò)互連具體實(shí)現(xiàn)之前,我們先來(lái)了解一下 單機(jī)與網(wǎng)絡(luò)它們各自的運(yùn)行流程,只有了解這些,你才能深入網(wǎng)絡(luò)游戲開(kāi)發(fā)的核心。

                現(xiàn)在先讓我們來(lái)看一下普通單機(jī)游戲的簡(jiǎn)化執(zhí)行流程:

            Initialize() // 初始化模塊
            {
             初始化游戲數(shù)據(jù);
            }
            Game() // 游戲循環(huán)部分
            {
             繪制游戲場(chǎng)景、人物以及其它元素;
             獲取用戶(hù)操作輸入;
             switch( 用戶(hù)輸入數(shù)據(jù))
             {
              case 移動(dòng):
              {
               處理人物移動(dòng);
              }
              break;
              case 攻擊:
              {
               處理攻擊邏輯:
              }
              break;
              ...
              其它處理響應(yīng);
              ...
              default:
               break;
             }
             游戲的NPC等邏輯AI處理;
            }
            Exit() // 游戲結(jié)束
            {
             釋放游戲數(shù)據(jù);
             離開(kāi)游戲;
            }

                我們來(lái)說(shuō)明一下上面單機(jī)游戲的流程。首先,不管是游戲軟件還是其他應(yīng)用軟件,初始化部分必不可少,這里需要對(duì)游戲的數(shù)據(jù)進(jìn)行初始化,包括圖像、聲音以及一 些必備的數(shù)據(jù)。接下來(lái),我們的游戲?qū)?chǎng)景、人物以及其他元素進(jìn)行循環(huán)繪制,把游戲世界展現(xiàn)給玩家,同時(shí)接收玩家的輸入操作,并根據(jù)操作來(lái)做出響應(yīng),此外, 游戲還需要對(duì)NPC以及一些邏輯AI進(jìn)行處理。最后,游戲數(shù)據(jù)被釋放,游戲結(jié)束。

                網(wǎng)絡(luò)游戲與單機(jī)游戲有一個(gè)很顯著的差別,就是網(wǎng)絡(luò)游戲除了一個(gè)供操作游戲的用戶(hù)界面平臺(tái)(如單機(jī)游戲)外,還需要一個(gè)用于連接所有用戶(hù),并為所有用戶(hù)提供數(shù)據(jù)服務(wù)的服務(wù)器,從某些角度來(lái)看,游戲服務(wù)器就像一個(gè)大型的數(shù)據(jù)庫(kù),

                提供數(shù)據(jù)以及數(shù)據(jù)邏輯交互的功能。讓我們來(lái)看看一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)游戲模型執(zhí)行流程:

                客戶(hù)機(jī):

            Login()// 登入模塊
            {
             初始化游戲數(shù)據(jù);
             獲取用戶(hù)輸入的用戶(hù)和密碼;
             與服務(wù)器創(chuàng)建網(wǎng)絡(luò)連接;
             發(fā)送至服務(wù)器進(jìn)行用戶(hù)驗(yàn)證;
             ...
             等待服務(wù)器確認(rèn)消息;
             ...
             獲得服務(wù)器反饋的登入消息;
             if( 成立 )
              進(jìn)入游戲;
             else
              提示用戶(hù)登入錯(cuò)誤并重新接受用戶(hù)登入;
            }
            Game()// 游戲循環(huán)部分
            {
             繪制游戲場(chǎng)景、人物以及其它元素;
             獲取用戶(hù)操作輸入;
             將用戶(hù)的操作發(fā)送至服務(wù)器;
             ...
             等待服務(wù)器的消息;
             ...
             接收服務(wù)器的反饋信息;
             switch( 服務(wù)器反饋的消息數(shù)據(jù) )
             {
              case 本地玩家移動(dòng)的消息:
              {
               if( 允許本地玩家移動(dòng) )
                客戶(hù)機(jī)處理人物移動(dòng);
               else
                客戶(hù)機(jī)保持原有狀態(tài);
              }
               break;
              case 其他玩家/NPC的移動(dòng)消息:
              {
               根據(jù)服務(wù)器的反饋信息進(jìn)行其他玩家或者NPC的移動(dòng)處理;
              }
              break;
              case 新玩家加入游戲:
              {
               在客戶(hù)機(jī)中添加顯示此玩家;
              }
               break;
              case 玩家離開(kāi)游戲:
              {
               在客戶(hù)機(jī)中銷(xiāo)毀此玩家數(shù)據(jù);
              }
               break;
              ...
              其它消息類(lèi)型處理;
              ... 
              default:
               break;
             }
            }
            Exit()// 游戲結(jié)束
            {
             發(fā)送離開(kāi)消息給服務(wù)器;
             ...
             等待服務(wù)器確認(rèn);
             ...
             得到服務(wù)器確認(rèn)消息;
             與服務(wù)器斷開(kāi)連接;
             釋放游戲數(shù)據(jù);
             離開(kāi)游戲;
            }


              服務(wù)器:

            Listen()  // 游戲服務(wù)器等待玩家連接模塊
            {
             ...
             等待用戶(hù)的登入信息;
             ...
             接收到用戶(hù)登入信息;
             分析用戶(hù)名和密碼是否符合;
             if( 符合 )
             {
              發(fā)送確認(rèn)允許進(jìn)入游戲消息給客戶(hù)機(jī); 
              把此玩家進(jìn)入游戲的消息發(fā)布給場(chǎng)景中所有玩家;
              把此玩家添加到服務(wù)器場(chǎng)景中;
             }
             else
             {
              斷開(kāi)與客戶(hù)機(jī)的連接;
             }
            }
            Game() // 游戲服務(wù)器循環(huán)部分
            {
             ...
             等待場(chǎng)景中玩家的操作輸入;
             ...
             接收到某玩家的移動(dòng)輸入或NPC的移動(dòng)邏輯輸入;
             // 此處只以移動(dòng)為例
             進(jìn)行此玩家/NPC在地圖場(chǎng)景是否可移動(dòng)的邏輯判斷;

             if( 可移動(dòng) )
             {
              對(duì)此玩家/NPC進(jìn)行服務(wù)器移動(dòng)處理;
              發(fā)送移動(dòng)消息給客戶(hù)機(jī);
              發(fā)送此玩家的移動(dòng)消息給場(chǎng)景上所有玩家;
             }
             else
              發(fā)送不可移動(dòng)消息給客戶(hù)機(jī);
            }
            Exit()  // 游戲服務(wù)=器結(jié)束
            {
             接收到玩家離開(kāi)消息;
             將此消息發(fā)送給場(chǎng)景中所有玩家;
             發(fā)送允許離開(kāi)的信息;
             將玩家數(shù)據(jù)存入數(shù)據(jù)庫(kù);
             注銷(xiāo)此玩家在服務(wù)器內(nèi)存中的數(shù)據(jù);
            }
            }


                讓我們來(lái)說(shuō)明一下上面簡(jiǎn)單網(wǎng)絡(luò)游戲模型的運(yùn)行機(jī)制。先來(lái)講講服務(wù)器端,這里服務(wù)器端分為三個(gè)部分(實(shí)際上一個(gè)完整的網(wǎng)絡(luò)游戲遠(yuǎn)不止這些):登入模塊、游戲 模塊和登出模塊。登入模塊用于監(jiān)聽(tīng)網(wǎng)絡(luò)游戲客戶(hù)端發(fā)送過(guò)來(lái)的網(wǎng)絡(luò)連接消息,并且驗(yàn)證其合法性,然后在服務(wù)器中創(chuàng)建這個(gè)玩家并且把玩家?guī)ьI(lǐng)到游戲模塊中; 游戲模塊則提供給玩家用戶(hù)實(shí)際的應(yīng)用服務(wù),我們?cè)诤竺鏁?huì)詳細(xì)介紹這個(gè)部分; 在得到玩家要離開(kāi)游戲的消息后,登出模塊則會(huì)把玩家從服務(wù)器中刪除,并且把玩家的屬性數(shù)據(jù)保存到服務(wù)器數(shù)據(jù)庫(kù)中,如: 經(jīng)驗(yàn)值、等級(jí)、生命值等。

                接下來(lái)讓我們看看網(wǎng)絡(luò)游戲的客戶(hù)端。這時(shí)候,客戶(hù)端不再像單機(jī)游戲一樣,初始化數(shù)據(jù)后直接進(jìn)入游戲,而是在與服務(wù)器創(chuàng)建連接,并且獲得許可的前提下才進(jìn)入 游戲。除此之外,網(wǎng)絡(luò)游戲的客戶(hù)端游戲進(jìn)程需要不斷與服務(wù)器進(jìn)行通訊,通過(guò)與服務(wù)器交換數(shù)據(jù)來(lái)確定當(dāng)前游戲的狀態(tài),例如其他玩家的位置變化、物品掉落情 況。同樣,在離開(kāi)游戲時(shí),客戶(hù)端會(huì)向服務(wù)器告知此玩家用戶(hù)離開(kāi),以便于服務(wù)器做出相應(yīng)處理。

                以上用簡(jiǎn)單的偽代碼給大家闡述了單機(jī)游戲與網(wǎng)絡(luò)游戲的執(zhí)行流程,大家應(yīng)該可以清楚看出兩者的差別,以及兩者間相互的關(guān)系。我們可以換個(gè)角度考慮,網(wǎng)絡(luò)游戲 就是把單機(jī)游戲的邏輯運(yùn)算部分搬移到游戲服務(wù)器中進(jìn)行處理,然后把處理結(jié)果(包括其他玩家數(shù)據(jù))通過(guò)游戲服務(wù)器返回給連接的玩家。

                網(wǎng)絡(luò)互連

                在了解了網(wǎng)絡(luò)游戲基本形態(tài)之后,讓我們進(jìn)入真正的實(shí)際應(yīng)用部分。首先,作為網(wǎng)絡(luò)游戲,除了常規(guī)的單機(jī)游戲所必需的東西之外,我們還需要增加一個(gè)網(wǎng)絡(luò)通訊模塊,當(dāng)然,這也是網(wǎng)絡(luò)游戲較為主要的部分,我們來(lái)討論一下如何實(shí)現(xiàn)網(wǎng)絡(luò)的通訊模塊。

                一個(gè)完善的網(wǎng)絡(luò)通訊模塊涉及面相當(dāng)廣,本文僅對(duì)較為基本的處理方式進(jìn)行討論。網(wǎng)絡(luò)游戲是由客戶(hù)端和服務(wù)器組成,相應(yīng)也需要兩種不同的網(wǎng)絡(luò)通訊處理方式,不 過(guò)也有相同之處,我們先就它們的共同點(diǎn)來(lái)進(jìn)行介紹。我們這里以Microsoft Windows 2000 [2000 Server]作為開(kāi)發(fā)平臺(tái),并且使用Winsock作為網(wǎng)絡(luò)接口(可能一些朋友會(huì)考慮使用DirectPlay來(lái)進(jìn)行網(wǎng)絡(luò)通訊,不過(guò)對(duì)于當(dāng)前在線游 戲,DirectPlay并不適合,具體原因這里就不做討論了)。

                確定好平臺(tái)與接口后,我們開(kāi)始進(jìn)行網(wǎng)絡(luò)連接創(chuàng)建之前的一些必要的初始化工作,這部分無(wú)論是客戶(hù)端或者服務(wù)器都需要進(jìn)行。讓我們看看下面的代碼片段:

            WORD wVersionRequested;
            WSADATAwsaData;
            wVersionRequested MAKEWORD(1, 1);
            if( WSAStartup( wVersionRequested, &wsaData ) !0 )
            {
             Failed( WinSock Version Error!" );
            }
              上面通過(guò)調(diào)用Windows的socket API函數(shù)來(lái)初始化網(wǎng)絡(luò)設(shè)備,接下來(lái)進(jìn)行網(wǎng)絡(luò)Socket的創(chuàng)建,代碼片段如下:

            SOCKET sSocket socket( AF_INET, m_lProtocol, 0 );
            if( sSocket == INVALID_SOCKET )
            {
             Failed( "WinSocket Create Error!" );
            }

                這里需要說(shuō)明,客戶(hù)端和服務(wù)端所需要的Socket連接數(shù)量是不同的,客戶(hù)端只需要一個(gè)Socket連接足以滿足游戲的需要,而服務(wù)端必須為每個(gè)玩家用戶(hù) 創(chuàng)建一個(gè)用于通訊的Socket連接。當(dāng)然,并不是說(shuō)如果服務(wù)器上沒(méi)有玩家那就不需要?jiǎng)?chuàng)建Socket連接,服務(wù)器端在啟動(dòng)之時(shí)會(huì)生成一個(gè)特殊的 Socket用來(lái)對(duì)玩家創(chuàng)建與服務(wù)器連接的請(qǐng)求進(jìn)行響應(yīng),等介紹網(wǎng)絡(luò)監(jiān)聽(tīng)部分后會(huì)有更詳細(xì)說(shuō)明。

                有初始化與創(chuàng)建必然就有釋放與刪除,讓我們看看下面的釋放部分:

            if( sSocket != INVALID_SOCKET )
            {
             closesocket( sSocket );
            }
            if( WSACleanup() != 0 )
            {
             Warning( "Can't release Winsocket" );
            }

                這里兩個(gè)步驟分別對(duì)前面所作的創(chuàng)建初始化進(jìn)行了相應(yīng)釋放。

                接下來(lái)看看服務(wù)器端的一個(gè)網(wǎng)絡(luò)執(zhí)行處理,這里我們假設(shè)服務(wù)器端已經(jīng)創(chuàng)建好一個(gè)Socket供使用,我們要做的就是讓這個(gè)Socket變成監(jiān)聽(tīng)網(wǎng)絡(luò)連接請(qǐng)求的專(zhuān)用接口,看看下面代碼片段:

            SOCKADDR_IN addr;
            memset( &addr, 0, sizeof(addr) );
            addr.sin_family = AF_INET;
            addr.sin_addr.s_addr = htonl( INADDR_ANY );
            addr.sin_port = htons( Port );  // Port為要監(jiān)聽(tīng)的端口號(hào)
            // 綁定socket
            if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR )
            {
             Failed( "WinSocket Bind Error!");
            }
            // 進(jìn)行監(jiān)聽(tīng)
            if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )
            {
             Failed( "WinSocket Listen Error!");
            }

                這里使用的是阻塞式通訊處理,此時(shí)程序?qū)⑻幱诘却婕矣脩?hù)連接的狀態(tài),倘若這時(shí)候有客戶(hù)端連接進(jìn)來(lái),則通過(guò)accept()來(lái)創(chuàng)建針對(duì)此玩家用戶(hù)的Socket連接,代碼片段如下:

            sockaddraddrServer;
            int nLen sizeof( addrServer );
            SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen );
            if( sPlayerSocket == INVALID_SOCKET )
            {
             Failed( WinSocket Accept Error!");
            }

                這里我們創(chuàng)建了sPlayerSocket連接,此后游戲服務(wù)器與這個(gè)玩家用戶(hù)的通訊全部通過(guò)此Socket進(jìn)行,到這里為止,我們服務(wù)器已經(jīng)有了接受玩家用戶(hù)連接的功能,現(xiàn)在讓我們來(lái)看看游戲客戶(hù)端是如何連接到游戲服務(wù)器上,代碼片段如下:

            SOCKADDR_IN addr;
            memset( &addr, 0, sizeof(addr) );
            addr.sin_family = AF_INET;// 要連接的游戲服務(wù)器端口號(hào)
            addr.sin_addr.s_addr = inet_addr( IP );// 要連接的游戲服務(wù)器IP地址,
            addr.sin_port = htons( Port );//到此,客戶(hù)端和服務(wù)器已經(jīng)有了通訊的橋梁,
            //接下來(lái)就是進(jìn)行數(shù)據(jù)的發(fā)送和接收:
            connect( sSocket, (SOCKADDR*)&addr, sizeof(addr) );
            if( send( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
            {
             Failed( "WinSocket Send Error!");
            }


                這里的pBuffer為要發(fā)送的數(shù)據(jù)緩沖指針,lLength為需要發(fā)送的數(shù)據(jù)長(zhǎng)度,通過(guò)這支Socket API函數(shù),我們無(wú)論在客戶(hù)端或者服務(wù)端都可以進(jìn)行數(shù)據(jù)的發(fā)送工作,同時(shí),我們可以通過(guò)recv()這支Socket API函數(shù)來(lái)進(jìn)行數(shù)據(jù)接收:

            lLength, 0 ) == SOCKET_ERROR )
            {
             Failed( "WinSocket Recv Error!");
            }

                其中pBuffer用來(lái)存儲(chǔ)獲取的網(wǎng)絡(luò)數(shù)據(jù)緩沖,lLength則為需要獲取的數(shù)據(jù)長(zhǎng)度。


                現(xiàn)在,我們已經(jīng)了解了一些網(wǎng)絡(luò)互連的基本知識(shí),但作為網(wǎng)絡(luò)游戲,如此簡(jiǎn)單的連接方式是無(wú)法滿足網(wǎng)絡(luò)游戲中百人千人同時(shí)在線的,我們需要更合理容錯(cuò)性更強(qiáng)的網(wǎng)絡(luò)通訊處理方式,當(dāng)然,我們需要先了解一下網(wǎng)絡(luò)游戲?qū)W(wǎng)絡(luò)通訊的需求是怎樣的。

                大家知道,游戲需要不斷循環(huán)處理游戲中的邏輯并進(jìn)行游戲世界的繪制,上面所介紹的Winsock處理方式均是以阻塞方式進(jìn)行,這樣就違背了游戲的執(zhí)行本 質(zhì),可以想象,在客戶(hù)端連接到服務(wù)器的過(guò)程中,你的游戲不能得到控制,這時(shí)如果玩家想取消連接或者做其他處理,甚至顯示一個(gè)最基本的動(dòng)態(tài)連接提示都不行。

                所以我們需要用其他方式來(lái)處理網(wǎng)絡(luò)通訊,使其不會(huì)與游戲主線相沖突,可能大家都會(huì)想到: 創(chuàng)建一個(gè)網(wǎng)絡(luò)線程來(lái)處理不就可以了?沒(méi)錯(cuò),我們可以創(chuàng)建一個(gè)專(zhuān)門(mén)用于網(wǎng)絡(luò)通訊的子線程來(lái)解決這個(gè)問(wèn)題。當(dāng)然,我們游戲中多了一個(gè)線程,我們就需要做更多的 考慮,讓我們來(lái)看看如何創(chuàng)建網(wǎng)絡(luò)通訊線程。

                在Windows系統(tǒng)中,我們可以通過(guò)CreateThread()函數(shù)來(lái)進(jìn)行線程的創(chuàng)建,看看下面的代碼片段:

            DWORD dwThreadID;
            HANDLE hThread = CreateThread( NULL, 0, NetThread/*網(wǎng)絡(luò)線程函式*/, sSocket, 0, &dwThreadID );
            if( hThread == NULL )
            {
             Failed( "WinSocket Thread Create Error!");
            }
              這里我們創(chuàng)建了一個(gè)線程,同時(shí)將我們的Socket傳入線程函數(shù):

            DWORD WINAPINetThread(LPVOID lParam)

            {
             SOCKET sSocket (SOCKET)lParam;
             ...
             return 0;
            }

                NetThread就是我們將來(lái)用于處理網(wǎng)絡(luò)通訊的網(wǎng)絡(luò)線程。那么,我們又如何把Socket的處理引入線程中?

                看看下面的代碼片段:

            HANDLE hEvent;
            hEvent = CreateEvent(NULL,0,0,0);
            // 設(shè)置異步通訊
            if( WSAEventSelect( sSocket, hEvent,
            FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR )
            {
             Failed( "WinSocket EventSelect Error!");
            }

                通過(guò)上面的設(shè)置之后,WinSock API函數(shù)均會(huì)以非阻塞方式運(yùn)行,也就是函數(shù)執(zhí)行后會(huì)立即返回,這時(shí)網(wǎng)絡(luò)通訊會(huì)以事件方式存儲(chǔ)于hEvent,而不會(huì)停頓整支程式。

                完成了上面的步驟之后,我們需要對(duì)事件進(jìn)行響應(yīng)與處理,讓我們看看如何在網(wǎng)絡(luò)線程中獲得網(wǎng)絡(luò)通訊所產(chǎn)生的事件消息:

            WSAEnumNetworkEvents( sSocket, hEvent, &SocketEvents );
            if( SocketEvents.lNetworkEvents != 0 )
            {
             switch( SocketEvents.lNetworkEvents )
             {
              case FD_ACCEPT:
               WSANETWORKEVENTS SocketEvents;
               break;
              case FD_CONNECT:
              {
               if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0)
               // 連接成功  
               {
               // 連接成功后通知主線程(游戲線程)進(jìn)行處理
               }
              }
               break;
              case FD_READ:
              // 獲取網(wǎng)絡(luò)數(shù)據(jù)
              {
               if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR )
               {
                Failed( "WinSocket Recv Error!");
               }
              }
               break;
              case FD_WRITE:
               break;
              case FD_CLOSE:
               // 通知主線程(游戲線程), 網(wǎng)絡(luò)已經(jīng)斷開(kāi)
               break;
              default:
            }
            }


                這里僅對(duì)網(wǎng)絡(luò)連接(FD_CONNECT) 和讀取數(shù)據(jù)(FD_READ) 進(jìn)行了簡(jiǎn)單模擬操作,但實(shí)際中網(wǎng)絡(luò)線程接收到事件消息后,會(huì)對(duì)數(shù)據(jù)進(jìn)行組織整理,然后再將數(shù)據(jù)回傳給我們的游戲主線程使用,游戲主線程再將處理過(guò)的數(shù)據(jù)發(fā) 送出去,這樣一個(gè)往返就構(gòu)成了我們網(wǎng)絡(luò)游戲中的數(shù)據(jù)通訊,是讓網(wǎng)絡(luò)游戲動(dòng)起來(lái)的最基本要素。

                最后,我們來(lái)談?wù)勱P(guān)于網(wǎng)絡(luò)數(shù)據(jù)包(數(shù)據(jù)封包)的組織,網(wǎng)絡(luò)游戲的數(shù)據(jù)包是游戲數(shù)據(jù)通訊的最基本單位,網(wǎng)絡(luò)游戲一般不會(huì)用字節(jié)流的方式來(lái)進(jìn)行數(shù)據(jù)傳輸,一個(gè) 數(shù)據(jù)封包也可以看作是一條消息指令,在游戲進(jìn)行中,服務(wù)器和客戶(hù)端會(huì)不停的發(fā)送和接收這些消息包,然后將消息包解析轉(zhuǎn)換為真正所要表達(dá)的指令意義并執(zhí)行。

                互動(dòng)與管理

                說(shuō)到互動(dòng),對(duì)于玩家來(lái)說(shuō)是與其他玩家的交流,但對(duì)于計(jì)算機(jī)而言,實(shí)現(xiàn)互動(dòng)也就是實(shí)現(xiàn)數(shù)據(jù)消息的相互傳遞。前面我們已經(jīng)了解過(guò)網(wǎng)絡(luò)通訊的基本概念,它構(gòu)成了 互動(dòng)的最基本條件,接下來(lái)我們需要在這個(gè)網(wǎng)絡(luò)層面上進(jìn)行數(shù)據(jù)的通訊。遺憾的是,計(jì)算機(jī)并不懂得如何表達(dá)玩家之間的交流,因此我們需要提供一套可讓計(jì)算機(jī)了 解的指令組織和解析機(jī)制,也就是對(duì)我們上面簡(jiǎn)單提到的網(wǎng)絡(luò)數(shù)據(jù)包(數(shù)據(jù)封包)的處理機(jī)制。

                為了能夠更簡(jiǎn)單的給大家闡述網(wǎng)絡(luò)數(shù)據(jù)包的組織形式,我們以一個(gè)聊天處理模塊來(lái)進(jìn)行討論,看看下面的代碼結(jié)構(gòu):

            struct tagMessage{
             long lType;
             long lPlayerID;
            };
            // 消息指令
            // 指令相關(guān)的玩家標(biāo)識(shí)
            char strTalk[256]; // 消息內(nèi)容

                上面是抽象出來(lái)的一個(gè)極為簡(jiǎn)單的消息包結(jié)構(gòu),我們先來(lái)談?wù)勂涓鱾€(gè)數(shù)據(jù)域的用途:首先,lType 是消息指令的類(lèi)型,這是最為基本的消息標(biāo)識(shí),這個(gè)標(biāo)識(shí)用來(lái)告訴服務(wù)器或客戶(hù)端這條指令的具體用途,以便于服務(wù)器或客戶(hù)端做出相應(yīng)處理。lPlayerID 被作為玩家的標(biāo)識(shí)。大家知道,一個(gè)玩家在機(jī)器內(nèi)部實(shí)際上也就是一堆數(shù)據(jù),特別是在游戲服務(wù)器中,可能有成千上萬(wàn)個(gè)玩家,這時(shí)候我們需要一個(gè)標(biāo)記來(lái)區(qū)分玩 家,這樣就可以迅速找到特定玩家,并將通訊數(shù)據(jù)應(yīng)用于其上。

                strTalk 是我們要傳遞的聊天數(shù)據(jù),這部分才是真正的數(shù)據(jù)實(shí)體,前面的參數(shù)只是數(shù)據(jù)實(shí)體應(yīng)用范圍的限定。

                在組織完數(shù)據(jù)之后,緊接著就是把這個(gè)結(jié)構(gòu)體數(shù)據(jù)通過(guò)Socket 連接發(fā)送出去和接收進(jìn)來(lái)。這里我們要了解,網(wǎng)絡(luò)在進(jìn)行數(shù)據(jù)傳輸過(guò)程中,它并不關(guān)心數(shù)據(jù)采用的數(shù)據(jù)結(jié)構(gòu),這就需要我們把數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為二進(jìn)制數(shù)據(jù)碼進(jìn)行發(fā) 送,在接收方,我們?cè)賹⑦@些二進(jìn)制數(shù)據(jù)碼轉(zhuǎn)換回程序使用的相應(yīng)數(shù)據(jù)結(jié)構(gòu)。讓我們來(lái)看看如何實(shí)現(xiàn):

            tagMessageMsg;
            Msg.lTypeMSG_CHAT;
            Msg.lPlayerID 1000;
            strcpy( &Msg.strTalk, "聊天信息" );

                首先,我們假設(shè)已經(jīng)組織好一個(gè)數(shù)據(jù)包,這里MSG_CHAT 是我們自行定義的標(biāo)識(shí)符,當(dāng)然,這個(gè)標(biāo)識(shí)符在服務(wù)器和客戶(hù)端要統(tǒng)一。玩家的ID 則根據(jù)游戲需要來(lái)進(jìn)行設(shè)置,這里1000 只作為假設(shè),現(xiàn)在繼續(xù):

            char* p = (char*)&Msg;
            long lLength = sizeof( tagMessage );
            send( sSocket, p, lLength );
            // 獲取數(shù)據(jù)結(jié)構(gòu)的長(zhǎng)度

                我們通過(guò)強(qiáng)行轉(zhuǎn)換把結(jié)構(gòu)體轉(zhuǎn)變?yōu)閏har 類(lèi)型的數(shù)據(jù)指針,這樣就可以通過(guò)這個(gè)指針來(lái)進(jìn)行流式數(shù)據(jù)處理,這里通過(guò)

                sizeof() 獲得結(jié)構(gòu)體長(zhǎng)度,然后用WinSock 的Send() 函數(shù)將數(shù)據(jù)發(fā)送出去。

                接下來(lái)看看如何接收數(shù)據(jù):

            long lLength = sizeof( tagMessage );
            char* Buffer = new char[lLength];
            recv( sSocket, Buffer, lLength );
            tagMessage* p = (tagMessage*)Buffer;
            // 獲取數(shù)據(jù)

                在通過(guò)WinSock 的recv() 函數(shù)獲取網(wǎng)絡(luò)數(shù)據(jù)之后,我們同樣通過(guò)強(qiáng)行轉(zhuǎn)換把獲取出來(lái)的緩沖數(shù)據(jù)轉(zhuǎn)換為相應(yīng)結(jié)構(gòu)體,這樣就可以方便地對(duì)數(shù)據(jù)進(jìn)行訪問(wèn)。(注:強(qiáng)行轉(zhuǎn)換僅僅作為數(shù)據(jù)轉(zhuǎn)換的 一種手段,實(shí)際應(yīng)用中有更多可選方式,這里只為簡(jiǎn)潔地說(shuō)明邏輯)談到此處,不得不提到服務(wù)器/ 客戶(hù)端如何去篩選處理各種消息以及如何對(duì)通訊數(shù)據(jù)包進(jìn)行管理。無(wú)論是服務(wù)器還是客戶(hù)端,在收到網(wǎng)絡(luò)消息的時(shí)候,通過(guò)上面的數(shù)據(jù)解析之后,還必須對(duì)消息類(lèi)型 進(jìn)行一次篩選和派分,簡(jiǎn)單來(lái)說(shuō)就是類(lèi)似Windows 的消息循環(huán),不同消息進(jìn)行不同處理。這可以通過(guò)一個(gè)switch 語(yǔ)句(熟悉Windows 消息循環(huán)的朋友相信已經(jīng)明白此意),基于消

                息封包里的lType 信息,對(duì)消息進(jìn)行區(qū)分處理,考慮如下代碼片段:

            switch( p->lType ) // 這里的p->lType為我們解析出來(lái)的消息類(lèi)型標(biāo)識(shí)
            {
             case MSG_CHAT: // 聊天消息
              break;
             case MSG_MOVE: // 玩家移動(dòng)消息
              break;
             case MSG_EXIT: // 玩家離開(kāi)消息
              break;
             default:
              break;
            }

                面片段中的MSG_MOVE 和MSG_EXIT 都是我們虛擬的消息標(biāo)識(shí)(一個(gè)真實(shí)游戲中的標(biāo)識(shí)可能會(huì)有上百個(gè),這就需要考慮優(yōu)化和優(yōu)先消息處理問(wèn)題)。此外,一個(gè)網(wǎng)絡(luò)游戲服務(wù)器面對(duì)的是成百上千的連接 用戶(hù),我們還需要一些合理的數(shù)據(jù)組織管理方式來(lái)進(jìn)行相關(guān)處理。普通的單體游戲服務(wù)器,可能會(huì)因?yàn)楫?dāng)機(jī)或者用戶(hù)過(guò)多而導(dǎo)致整個(gè)游戲網(wǎng)絡(luò)癱瘓,而這也就引入分 組服務(wù)器機(jī)制,我們把服務(wù)器分開(kāi)進(jìn)行數(shù)據(jù)的分布式處理。

                我們把每個(gè)模塊提取出來(lái),做成專(zhuān)用的服務(wù)器系統(tǒng),然后建立一個(gè)連接所有服務(wù)器的數(shù)據(jù)中心來(lái)進(jìn)行數(shù)據(jù)交互,這里每個(gè)模塊均與數(shù)據(jù)中心創(chuàng)建了連接,保證了每個(gè) 模塊的相關(guān)性,同時(shí)玩家轉(zhuǎn)變?yōu)榕c當(dāng)前提供服務(wù)的服務(wù)器進(jìn)行連接通訊,這樣就可以緩解單獨(dú)一臺(tái)服務(wù)器所承受的負(fù)擔(dān),把壓力分散到多臺(tái)服務(wù)器上,同時(shí)保證了數(shù) 據(jù)的統(tǒng)一,而且就算某臺(tái)服務(wù)因?yàn)楫惓6?dāng)機(jī)也不會(huì)影響其他模塊的游戲玩家,從而提高了整體穩(wěn)定性。分組式服務(wù)器緩解了服務(wù)器的壓力,但也帶來(lái)了服務(wù)器調(diào)度 問(wèn)題,分組式服務(wù)器需要對(duì)服務(wù)器跳轉(zhuǎn)進(jìn)行處理,就以一個(gè)玩家進(jìn)行游戲場(chǎng)景跳轉(zhuǎn)作為討論基礎(chǔ):假設(shè)有一玩家處于游戲場(chǎng)景A,他想從場(chǎng)景A 跳轉(zhuǎn)到場(chǎng)景B,在游戲中,我們稱(chēng)之場(chǎng)景切換,這時(shí)玩家就會(huì)觸發(fā)跳轉(zhuǎn)需求,比如走到了場(chǎng)景中的切換點(diǎn),這樣服務(wù)器就把玩家數(shù)據(jù)從"游戲場(chǎng)景A 服務(wù)器"刪除,同時(shí)在"游戲場(chǎng)景B 服務(wù)器"中把玩家建立起來(lái)。

                這里描述了場(chǎng)景切換的簡(jiǎn)單模型,當(dāng)中處理還有很多步驟,不過(guò)通過(guò)這樣的思考相信大家可以派生出很多應(yīng)用技巧。

                不過(guò)需要注意的是,在場(chǎng)景切換或者說(shuō)模塊間切換的時(shí)候,需要切實(shí)考慮好數(shù)據(jù)的傳輸安全以及邏輯合理性,否則切換很可能會(huì)成為將來(lái)玩家復(fù)制物品的橋梁。

                總結(jié)

                本篇講述的都是通過(guò)一些簡(jiǎn)單的過(guò)程來(lái)進(jìn)行網(wǎng)絡(luò)游戲通訊,提供了一個(gè)制作的思路,雖然具體實(shí)現(xiàn)起來(lái)還有許多要做 ,但只要順著這個(gè)思路去擴(kuò)展、去完善,相信大家很快就能夠編寫(xiě)出自己的網(wǎng)絡(luò)通訊模塊。由于時(shí)間倉(cāng)促,本文在很多細(xì)節(jié)方面都有省略,文中若有錯(cuò)誤之處也望大 家見(jiàn)諒



            posted @ 2009-09-23 23:39 暗夜教父 閱讀(1453) | 評(píng)論 (0)編輯 收藏
            原文:http://hi.baidu.com/huangyunjun999/blog/item/7396b8c2378e4a3ce4dd3bda.html

            接觸了一段時(shí)間的網(wǎng)游封包設(shè)計(jì),有了一些初步的思路,想借這篇文章總結(jié)一下,同時(shí)也作個(gè)記錄,以利于以后更新自己的思路。
            網(wǎng)絡(luò)游戲的技術(shù)研發(fā),分為三個(gè)主要的方面:服務(wù)器設(shè)計(jì),客戶(hù)端設(shè)計(jì),數(shù)據(jù)庫(kù)設(shè)計(jì)。而在服務(wù)器和客戶(hù)端之間實(shí)現(xiàn)游戲邏輯的中介則是游戲數(shù)據(jù)包,服務(wù)器和 客戶(hù)端通過(guò)交換游戲數(shù)據(jù)包并根據(jù)分析得到的數(shù)據(jù)包來(lái)驅(qū)動(dòng)游戲邏輯。網(wǎng)絡(luò)游戲的實(shí)質(zhì)是互動(dòng),而互動(dòng)的控制則由服務(wù)器和客戶(hù)端協(xié)同完成,協(xié)同就必然要依靠數(shù)據(jù) 來(lái)完成。
            當(dāng)前網(wǎng)絡(luò)游戲中的封包,其定義形式是各種各樣的,但歸納起來(lái),一般都具有如下要素:封包長(zhǎng)度,封包類(lèi)型,封包參數(shù),校驗(yàn)碼等。
            封包長(zhǎng)度用于確定當(dāng)前游戲數(shù)據(jù)包的長(zhǎng)度,之所以提供這個(gè)數(shù)據(jù),是因?yàn)樵诘讓拥腡CP網(wǎng)絡(luò)傳輸中,出于傳輸效率的考慮,傳輸有時(shí)會(huì)把若干個(gè)小的數(shù)據(jù)包合 并成一個(gè)大的數(shù)據(jù)包發(fā)送出去,而在合并的過(guò)程中,并不是把每一個(gè)邏輯上完整的數(shù)據(jù)包全部合并到一起,有時(shí)可能因?yàn)檫@種合并而將一個(gè)在邏輯上具有完整意義的 游戲數(shù)據(jù)包分在了兩次發(fā)送過(guò)程中進(jìn)行發(fā)送,這樣,當(dāng)前一次的數(shù)據(jù)發(fā)送到接受方之后,其尾部的數(shù)據(jù)包必然造成了“斷尾”現(xiàn)象,為了判定這種斷尾的情況以及斷 尾的具體內(nèi)容,游戲數(shù)據(jù)包在設(shè)計(jì)時(shí)一般都會(huì)提供封包長(zhǎng)度這個(gè)信息,根據(jù)這個(gè)信息接受方就知道收到的包是否有斷尾,如果有斷尾,則把斷尾的數(shù)據(jù)包與下次發(fā)過(guò) 來(lái)的數(shù)據(jù)包進(jìn)行拼接生成原本在邏輯意義上完整的數(shù)據(jù)包。
            封包類(lèi)型用于標(biāo)識(shí)當(dāng)前封包是何種類(lèi)型的封包,表示的是什么含義。
            封包參數(shù)則是對(duì)封包類(lèi)型更具體的描述,它里面指定了這種類(lèi)型封包說(shuō)明里所必須的參數(shù)。比如說(shuō)話封包,它的封包類(lèi)型,可以用一個(gè)數(shù)值進(jìn)行表示,而具體的說(shuō)話內(nèi)容和發(fā)言的人則作為封包參數(shù)。
            校驗(yàn)碼的作用是對(duì)前述封包內(nèi)容進(jìn)行校驗(yàn),以確保封包在傳遞過(guò)程中內(nèi)容不致被改變,同時(shí)根據(jù)校驗(yàn)碼也可以確定這個(gè)封包在格式上是不是一個(gè)合法的封包。以 校驗(yàn)碼作為提高封包安全性的方法,已經(jīng)是目前網(wǎng)游普遍采用的方式。封包設(shè)計(jì),一般是先確定封包的總體結(jié)構(gòu),然后再來(lái)具體細(xì)分有哪些封包,每個(gè)封包中應(yīng)該含 有哪些內(nèi)容,最后再詳細(xì)寫(xiě)出封包中各部分內(nèi)容具體占有的字節(jié)數(shù)及含義。
            數(shù)據(jù)包的具體設(shè)計(jì),一般來(lái)說(shuō)是根據(jù)游戲功能進(jìn)行劃分和圈定的。比如游戲中有聊天功能,那么就得設(shè)計(jì)客戶(hù)端與服務(wù)器的聊天數(shù)據(jù)包,客戶(hù)端要有一個(gè)描述發(fā) 言?xún)?nèi)容與發(fā)言人信息的數(shù)據(jù)包,而在服務(wù)器端要有一個(gè)包含用戶(hù)發(fā)言?xún)?nèi)容及發(fā)言人信息的廣播數(shù)據(jù)包,通過(guò)它,服務(wù)器端可以向其他附近玩家廣播發(fā)送當(dāng)前玩家的發(fā) 言?xún)?nèi)容。再比如游戲中要有交易功能,那么與這個(gè)功能相對(duì)應(yīng)的就可能會(huì)有以下數(shù)據(jù)包:申請(qǐng)交易包,申請(qǐng)交易的信息包,允許或拒絕交易包,允許或拒絕交易的信 息包,提交交易物品包,提交交易物品的信息包,確認(rèn)交易包,取消交易包,取消交易的信息包,交易成功或失敗的信息包。需要注意的是,在這些封包中,有的是 一方使用而另一方不使用的,而有的則是雙方都使用的包。比如申請(qǐng)交易包,只可能是一方使用,而另一方會(huì)得到一個(gè)申請(qǐng)交易的信息包;而確認(rèn)交易包和提交交易 物品這樣的數(shù)據(jù)包,都是雙方在確定要進(jìn)行交易時(shí)要同時(shí)使用的。封包的設(shè)計(jì)也遵從由上到下的設(shè)計(jì)原則,即先確定有哪些功能的封包,再確定封包中應(yīng)該含有的信 息,最后確定這些信息應(yīng)該占有的位置及長(zhǎng)度。一層層的分析與定義,最終形成一個(gè)完善的封包定義方案。在實(shí)際的封包設(shè)計(jì)過(guò)程中,回溯的情況是經(jīng)常出現(xiàn)的。由 于初期設(shè)計(jì)時(shí)的考慮不周或其它原因,可能造成封包設(shè)計(jì)方案的修改或增刪,這時(shí)候一個(gè)重要的問(wèn)題是要記得及時(shí)更新你的設(shè)計(jì)文檔。在我的封包設(shè)計(jì)中,我采用的 是以下的封包描述表格進(jìn)行描述:
            封包編號(hào)   功能描述  對(duì)應(yīng)的類(lèi)或結(jié)構(gòu)體名  類(lèi)型命令字  命令參數(shù)結(jié)構(gòu)體及含義 
            根據(jù)游戲的功能,我們可以大致圈定封包的大致結(jié)構(gòu)及所含的大致內(nèi)容。但是,封包設(shè)計(jì)還包含有其它更多的內(nèi)容,如何在保證封包邏輯簡(jiǎn)潔的前提下縮短封包 的設(shè)計(jì)長(zhǎng)度提高封包的傳輸速度和游戲的運(yùn)行速度,這也是我們應(yīng)該考慮的一個(gè)重要問(wèn)題。一般情況下,設(shè)計(jì)封包時(shí),應(yīng)該盡量避免產(chǎn)生一百字節(jié)以上的封包,多數(shù) 封包的定義控制在100字節(jié)以?xún)?nèi),甚至20-50字節(jié)以?xún)?nèi)。在我所定義的封包中,多數(shù)在20字節(jié)以?xún)?nèi),對(duì)于諸如傳遞服務(wù)器列表和用戶(hù)列表這樣的封包可能會(huì) 大一點(diǎn)。總之一句話,應(yīng)該用盡可能短的內(nèi)容盡可能簡(jiǎn)潔清晰地描述封包功能和含義。
            在封包結(jié)構(gòu)設(shè)計(jì)方面,我有另一種可擴(kuò)展的思路:對(duì)封包中各元素的位置進(jìn)行動(dòng)態(tài)定義。這樣,當(dāng)換成其它游戲或想更換當(dāng)前游戲的封包結(jié)構(gòu)時(shí),只要改變這些 元素的動(dòng)態(tài)定義即可,而不需要完全重新設(shè)計(jì)封包結(jié)構(gòu)。比如我們對(duì)封包編號(hào),封包類(lèi)型,封包參數(shù),校驗(yàn)碼這些信息的開(kāi)始位置和長(zhǎng)度進(jìn)行定義,這樣就可以形成 一個(gè)動(dòng)態(tài)定義的封包結(jié)構(gòu),對(duì)以后的封包移植將會(huì)有很大幫助,一個(gè)可能動(dòng)態(tài)改變封包結(jié)構(gòu)的游戲數(shù)據(jù)包,在相當(dāng)程度上增加了外掛分析封包結(jié)構(gòu)的難度。
            在進(jìn)行封包設(shè)計(jì)時(shí),最好根據(jù)封包客戶(hù)端和服務(wù)器端的不同來(lái)分類(lèi)進(jìn)行設(shè)計(jì)。比如大廳與游戲服務(wù)器的封包及游戲服務(wù)器與游戲客戶(hù)端的封包分開(kāi)來(lái)進(jìn)行設(shè)計(jì),在包的編號(hào)上表示出他們的不同(以不同的開(kāi)頭單詞表示),這樣在封包的總體結(jié)構(gòu)上就會(huì)更清晰。

            posted @ 2009-09-23 23:36 暗夜教父 閱讀(462) | 評(píng)論 (0)編輯 收藏

            先要這樣
            apt-get install build-essential  
            apt-get install libncurses5-dev  
            apt-get install m4  
            apt-get install libssl-dev 


            然后要用新立得裝如下庫(kù):
            libc6
            unixodbc
            unixodbc-dev
            gcj

            freeglut3-dev
            libwxgtk2.8-dev
            g++


            然后下載源代碼
            tar -xvf otp-src-R12B-0.tar.gz  
            cd otp-src-R12B-0 
            sudo ./configure --prefix=/otp/erlang  
            sudo make  
            sudo make install 


            安裝完畢,可以rm -fr opt-src-R12B-0刪除源代碼

            然后改改/etc/profile
            export PATH=/opt/erlang/bin:$PATH
            alias ls='ls -color=auto'
            alias ll='ll -lht'

            可以source /etc/profile一下,及時(shí)修改PATH

            posted @ 2009-09-18 19:12 暗夜教父 閱讀(848) | 評(píng)論 (0)編輯 收藏
            前提:
            需要下載as3corelib來(lái)為ActionScript3處理JSON codec

            server.erl
            -module(server).   
            -export([start/0,start/1,process/1]).   
            -define(defPort, 8888).   
              
            start() 
            -> start(?defPort).   
              
            start(Port) 
            ->   
              
            case gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]) of   
                {ok, LSock} 
            -> server_loop(LSock);   
                {error, Reason} 
            -> exit({Port,Reason})   
              end.   
              
            %% main server loop - wait for next connection, spawn child to process it   
            server_loop(LSock) 
            ->   
              
            case gen_tcp:accept(LSock) of   
                {ok, Sock} 
            ->   
                  spawn(
            ?MODULE,process,[Sock]),   
                  server_loop(LSock);   
                {error, Reason} 
            ->   
                  exit({accept,Reason})   
              end.   
              
            %% process current connection   
            process(Sock) 
            ->   
              Req 
            = do_recv(Sock),   
              io:format(
            "~p~n", [Req]),   
              {ok, D, []} 
            = rfc4627:decode(Req),   
              {obj, [{
            "name", _Name}, {"age", Age}]} = D,   
              Name 
            = binary_to_list(_Name),   
              io:format(
            "Name: ~p, Age: ~p~n", [Name, Age]),   
              Resp 
            = rfc4627:encode({obj, [{"name"'Hideto2'}, {"age"24}]}),   
              do_send(Sock,Resp),   
              gen_tcp:close(Sock).   
              
            %% send a line of text to the socket   
            do_send(Sock,Msg) 
            ->   
              
            case gen_tcp:send(Sock, Msg) of   
                ok 
            -> ok;   
                {error, Reason} 
            -> exit(Reason)   
              end.   
              
            %% receive data from the socket   
            do_recv(Sock) 
            ->   
              
            case gen_tcp:recv(Sock, 0) of   
                {ok, Bin} 
            -> binary_to_list(Bin);   
                {error, closed} 
            -> exit(closed);   
                {error, Reason} 
            -> exit(Reason)   
              end.  

            Person.as
            package  
            {   
                
            public class Person   
                {   
                    
            public var name:String;   
                    
            public var age:int;   
                    
            public function Person()   
                    {   
                    }   
                }   
            }  

            Client.as
            package {   
                
            import com.adobe.serialization.json.JSON;   
                   
                
            import flash.display.Sprite;   
                
            import flash.events.*;   
                
            import flash.net.Socket;   
                
            import flash.text.*;   
                   
                
            public class Client extends Sprite   
                {   
                    
            private var socket:Socket;   
                    
            private var myField:TextField;   
                    
            private var send_data:Person;   
                    
            public function Client()   
                    {   
                        socket 
            = new Socket();   
                        myField 
            = new TextField();   
                        send_data 
            = new Person();   
                        send_data.name 
            = "Hideto";   
                        send_data.age 
            = 23;   
                        socket.addEventListener(ProgressEvent.SOCKET_DATA, onSocketData);   
                        socket.connect(
            "localhost"8888);   
                        socket.writeUTFBytes(JSON.encode(send_data));   
                        socket.flush();   
                        myField.x 
            = 20;   
                        myField.y 
            = 30;   
                        myField.text 
            = "test";   
                        myField.autoSize 
            = TextFieldAutoSize.LEFT;   
                        addChild(myField);   
                    }   
                    
            private function onSocketData(event:ProgressEvent):void {   
                        
            while(socket.bytesAvailable) {   
                            var recv_data:
            * = JSON.decode(socket.readUTFBytes(socket.bytesAvailable));   
                            myField.text 
            = "Name: " + recv_data.name + ", age: " + recv_data.age.toString();   
                        }   
                    }   
                }   

            運(yùn)行Erlang服務(wù)器端:
            Eshell> c(server).   
            Eshell
            > server:start().   
            "{\"name\":\"Hideto\",\"age\":23}"  
            Name: 
            "Hideto", Age: 23 

            這里打印出了Erlang Socket Server接收到的AS3 Client發(fā)過(guò)來(lái)的JSON decode過(guò)的一個(gè)person對(duì)象

            運(yùn)行AS3客戶(hù)端:
            client.html上首先顯示“test”,然后異步處理完Socket消息發(fā)送和接受后,decode Erlang Server端發(fā)過(guò)來(lái)的person對(duì)象,將頁(yè)面上的TextField替換為“Name: Hideto2, age: 24”
            posted @ 2009-09-18 16:07 暗夜教父 閱讀(786) | 評(píng)論 (0)編輯 收藏
            僅列出標(biāo)題
            共9頁(yè): 1 2 3 4 5 6 7 8 9 

            <2009年9月>
            303112345
            6789101112
            13141516171819
            20212223242526
            27282930123
            45678910

            常用鏈接

            留言簿(2)

            隨筆分類(lèi)

            隨筆檔案

            文章分類(lèi)

            文章檔案

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            久久精品亚洲乱码伦伦中文| 精品熟女少妇AV免费久久| 亚洲中文字幕无码一久久区| 国产69精品久久久久777| 久久久久国产精品| 久久99国产精品久久99小说| 久久A级毛片免费观看| 久久综合精品国产一区二区三区| 久久发布国产伦子伦精品| 亚洲国产精品热久久| 99久久久精品| 女人高潮久久久叫人喷水| 久久久WWW成人免费精品| 一本色综合网久久| 久久综合伊人77777麻豆| 久久se精品一区精品二区| 国产亚洲欧美成人久久片| 日本精品一区二区久久久| 久久精品国产WWW456C0M| 久久精品亚洲中文字幕无码麻豆 | 99久久这里只精品国产免费| 漂亮人妻被黑人久久精品| 久久精品国产亚洲αv忘忧草| 热久久国产精品| www.久久热| 国产精品免费福利久久| 精品久久久久久亚洲精品| 久久久久亚洲av综合波多野结衣| 精品久久久久一区二区三区 | 久久乐国产综合亚洲精品| 日韩精品久久久久久| 久久国产精品99久久久久久老狼 | 99久久99久久久精品齐齐| 无码日韩人妻精品久久蜜桃| 99久久99久久精品国产片果冻| 狠狠人妻久久久久久综合蜜桃| 国内精品久久久久影院免费| 久久91亚洲人成电影网站| 99国产精品久久| 国内精品久久久久久中文字幕| 国产亚洲精午夜久久久久久|