網遊服務器

本文作者:sodme
本文出處:http://blog.csdn.net/sodme
聲明:本文可以不經作者同意任意轉載、複製、傳播,但任何對本文的引用都請保留作者、出處及本聲明信息。謝謝!

常見的網絡服務器,基本上是7*24小時運轉的,對於網遊來說,至少要求服務器要能連續工作一週以上的時間並保證不出現服務器崩潰這樣的災難性事件。事 實上,要求一個服務器在連續的滿負荷運轉下不出任何異常,要求它設計的近乎完美,這幾乎是不太現實的。服務器本身可以出異常(但要儘可能少得出),但是, 服務器本身應該被設計得足以健壯,“小病小災”打不垮它,這就要求服務器在異常處理方面要下很多功夫。

  服務器的異常處理包括的內容非常廣泛,本文僅就在網絡封包方面出現的異常作一討論,希望能對正從事相關工作的朋友有所幫助。

  關於網絡封包方面的異常,總體來說,可以分爲兩大類:一是封包格式出現異常;二是封包內容(即封包數據)出現異常。在封包格式的異常處理方面, 我們在最底端的網絡數據包接收模塊便可以加以處理。而對於封包數據內容出現的異常,只有依靠遊戲本身的邏輯去加以判定和檢驗。遊戲邏輯方面的異常處理,是 隨每個遊戲的不同而不同的,所以,本文隨後的內容將重點闡述在網絡數據包接收模塊中的異常處理。

  爲方便以下的討論,先明確兩個概念(這兩個概念是爲了敘述方面,筆者自行取的,並無標準可言):
  1、邏輯包:指的是在應用層提交的數據包,一個完整的邏輯包可以表示一個確切的邏輯意義。比如登錄包,它裏面就可以含有用戶名字段和密碼字段。儘管它看上去也是一段緩衝區數據,但這個緩衝區裏的各個區間是代表一定的邏輯意義的。
  2、物理包:指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)從網絡底層接收到的數據包,這樣收到的一個數據包,能不能表示一個完整的邏輯意義,要取決於它是通過UDP類的“數據報協議”發的包還是通過TCP類的“流協議”發的包。

  我們知道,TCP是流協議,“流協議”與“數據報協議”的不同點在於:“數據報協議”中的一個網絡包本身就是一個完整的邏輯包,也就是說,在應 用層使用sendto發送了一個邏輯包之後,在接收端通過recvfrom接收到的就是剛纔使用sendto發送的那個邏輯包,這個包不會被分開發送,也 不會與其它的包放在一起發送。但對於TCP而言,TCP會根據網絡狀況和neagle算法,或者將一個邏輯包單獨發送,或者將一個邏輯包分成若干次發送, 或者會將若干個邏輯包合在一起發送出去。正因爲TCP在邏輯包處理方面的這種粘合性,要求我們在作基於TCP的應用時,一般都要編寫相應的拼包、解包代 碼。

  因此,基於TCP的上層應用,一般都要定義自己的包格式。TCP的封包定義中,除了具體的數據內容所代表的邏輯意義之外,第一步就是要確定以何種方式表示當前包的開始和結束。通常情況下,表示一個TCP邏輯包的開始和結束有兩種方式:
  1、以特殊的開始和結束標誌表示,比如FF00表示開始,00FF表示結束。
  2、直接以包長度來表示。比如可以用第一個字節表示包總長度,如果覺得這樣的話包比較小,也可以用兩個字節表示包長度。

  下面將要給出的代碼是以第2種方式定義的數據包,包長度以每個封包的前兩個字節表示。我將結合着代碼給出相關的解釋和說明。

  函數中用到的變量說明:

  CLIENT_BUFFER_SIZE:緩衝區的長度,定義爲:Const int CLIENT_BUFFER_SIZE=4096。
  m_ClientDataBuf:數據整理緩衝區,每次收到的數據,都會先被複制到這個緩衝區的末尾,然後由下面的整理函數對這個緩衝區進行整理。它的定義是:char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。
  m_DataBufByteCount:數據整理緩衝區中當前剩餘的未整理字節數。
  GetPacketLen(const char*):函數,可以根據傳入的緩衝區首址按照應用層協議取出當前邏輯包的長度。
  GetGamePacket(const char*, int):函數,可以根據傳入的緩衝區生成相應的遊戲邏輯數據包。
  AddToExeList(PBaseGamePacket):函數,將指定的遊戲邏輯數據包加入待處理的遊戲邏輯數據包隊列中,等待邏輯處理線程對其進行處理。
  DATA_POS:指的是除了包長度、包類型等這些標誌型字段之外,真正的數據包內容的起始位置。

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

  //先將本次收到的數據複製到整理緩衝區尾部
    startPos = m_DataBufByteCount;  
    memcpy( m_ClientDataBuf+startPos, pData, len );
    m_DataBufByteCount += len;   

    //當整理緩衝區內的字節數少於DATA_POS字節時,取不到長度信息則退出
 //注意:退出時並不置m_DataBufByteCount爲0
    if (m_DataBufByteCount < DATA_POS+1)
        return false; 

    //根據正常邏輯,下面的情況不可能出現,爲穩妥起見,還是加上
    if (m_DataBufByteCount >  2*CLIENT_BUFFER_SIZE)
    {
        //設置m_DataBufByteCount爲0,意味着丟棄緩衝區中的現有數據
        m_DataBufByteCount = 0;

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

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

     //只有當m_ClientDataBuf中的字節個數大於最小包長度時才能執行此語句
    packetLen = GetPacketLen( pIOCPClient->m_ClientDataBuf );

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

  //OnPacketError()
        return false;
    }

    //保留整理緩衝區的末尾指針
    __int64 oldlen = m_DataBufByteCount; 

    while ((packetLen <= m_DataBufByteCount) && (m_DataBufByteCount>0))
    {
        //調用拼包邏輯,獲取該緩衝區數據對應的數據包
        pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen); 

        if (pGamePacket!=NULL)
        {
            //將數據包加入執行隊列
            AddToExeList(pGamePacket);
        }

        pGamePacket = NULL;
 
  //整理緩衝區的剩餘字節數和新邏輯包的起始位置進行調整
        m_DataBufByteCount -= packetLen;
        startPos += packetLen; 

        //殘留緩衝區的字節數少於一個正常包大小時,只向前複製該包隨後退出
        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 );

         //當邏輯層的包長度不合法時,丟棄該包及緩衝區以後的包
        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;
}

  以上便是數據接收模塊的處理函數,下面是幾點簡要說明:

  1、用於拼包整理的緩衝區(m_ClientDataBuf)應該比recv中指定的接收緩衝區(pData)長度(CLIENT_BUFFER_SIZE)要大,通常前者是後者的2倍(2*CLIENT_BUFFER_SIZE)或更大。

  2、爲避免因爲剩餘數據前移而導致的額外開銷,建議m_ClientDataBuf使用環形緩衝區實現。

3、爲了避免出現無法拼裝的包,我們約定每次發送的邏輯包,其單個邏輯包最大長度不可以超過CLIENT_BUFFER_SIZE的2倍。因爲我們的整 理緩衝區只有2*CLIENT_BUFFER_SIZE這麼長,更長的數據,我們將無法整理。這就要求在協議的設計上以及最終的發送函數的處理上要加上這 樣的異常處理機制。


  4、對於數據包過短或過長的包,我們通常的情況是置m_DataBufByteCount爲0,即捨棄當前包的處理。如果此處不設置 m_DataBufByteCount爲0也可,但該客戶端只要發了一次格式錯誤的包,則其後繼發過來的包則也將連帶着產生格式錯誤,如果設置 m_DataBufByteCount爲0,則可以比較好的避免後繼的包受此包的格式錯誤影響。更好的作法是,在此處開放一個封包格式異常的處理接口 (OnPacketError),由上層邏輯決定對這種異常如何處置。比如上層邏輯可以對封包格式方面出現的異常進行計數,如果錯誤的次數超過一定的值, 則可以斷開該客戶端的連接。

  5、建議不要在recv或wsarecv的函數後,就緊接着作以上的處理。當recv收到一段數據後,生成一個結構體或對象(它主要含有 data和len兩個內容,前者是數據緩衝區,後者是數據長度),將這樣的一個結構體或對象放到一個隊列中由後面的線程對其使用SplitFun函數進行 整理。這樣,可以最大限度地提高網絡數據的接收速度,不至因爲數據整理的原因而在此處浪費時間。

  代碼中,我已經作了比較詳細的註釋,可以作爲拼包函數的參考,代碼是從偶的應用中提取、修改而來,本身只爲演示之用,所以未作調試,應用時需要你自己去完善。如有疑問,可以我的blog上留言提出。

posted @ 2009-09-23 23:47 暗夜教父 閱讀(97) | 評論 (0) | 編輯 收藏
本文作者:sodme 本文出處:http://blog.csdn.net/sodme
版權聲明:本文可以不經作者同意任意轉載,但轉載時煩請保留文章開始前兩行的版權、作者及出處信息。

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

  讓無數中國玩家爲之矚目的“魔獸世界”,隨着一系列內測前期工作的逐步展開,正在一步步地走近中國玩家,但是,“魔獸”的服務器,卻着實讓我們爲它捏了一把汗。

造成一個網遊服務器當機的原因有很多,但主要有以下兩種:一,服務器在線人數達到上限,服務器處理效率嚴重遲緩,造成當機;二,由於外掛或其它遊戲作弊 工具導致的非正常數據包的出錯,導致遊戲服務器邏輯出現混亂,從而造成當機。在這裏,我主要想說說後者如何儘可能地避免。

  要避免以上 所說到的第二種情況,我們就應該遵循一個基本原則:在網遊服務器的設計中,對於具有較強邏輯關係的處理單元,服務器端和客戶端應該採用“互不信任原則”, 即:服務器端即使收到了客戶端的數據包,也並不是立刻就認爲客戶端已經達到了某種功能或者狀態,客戶端到達是否達到了某種功能或者狀態,還必須依靠服務器 端上記載的該客戶端“以往狀態”來判定,也就是說:服務器端的邏輯執行並不單純地以“當前”的這一個客戶端封包來進行,它還應該廣泛參考當前封包的上下文 環境,對執行的邏輯作出更進一步地判定,同時,在單個封包的處理上,服務器端應該廣泛考慮當前客戶端封包所需要的“前置”封包,如果沒有收到該客戶端應該 發過來的“前置”封包,則當前的封包應該不進行處理或進行異常處理(如果想要性能高,則可以直接忽略該封包;如果想讓服務器穩定,可以進行不同的異常處 理)。

  之所以採用“互不信任”原則設計網遊服務器,一個很重要的考慮是:防外掛。對於一個網絡服務器(不僅僅是遊戲服務器,泛指所有 服務器)而言,它所面對的對象既有屬於自己系統內的合法的網絡客戶端,也有不屬於自己系統內的非法客戶端訪問。所以,我們在考慮服務器向外開放的接口時, 就要同時考慮這兩種情況:合法客戶端訪問時的邏輯走向以及非法客戶端訪問時的邏輯走向。舉個簡單的例子:一般情況下,玩家登錄邏輯中,都是先向服務器發送 用戶名和密碼,然後再向服務器發送進入某組服務器的數據包;但在非法客戶端(如外掛)中,則這些客戶端則完全有可能先發進入某組服務器的數據包。當然,這 裏僅僅是舉個例子,也許並不妥當,但基本的意思我已經表達清楚了,即:你服務器端不要我客戶端發什麼你就信什麼,你還得進行一系列的邏輯驗證,以判定我當 前執行的操作是不是合法的。以這個例子中,服務器端可以通過以下邏輯執行驗證功能:只有當客戶端的用戶名和密碼通過驗證後,該客戶端纔會進入在線玩家列表 中。而只有在線玩家列表中的成員,纔可以在登陸服務器的引導下進入各分組服務器。

  總之,在從事網遊服務器的設計過程中,要始終不移地 堅持一個信念:我們的服務器,不僅僅有自己的遊戲客戶端在訪問,還有其它很多他人寫的遊戲客戶端在訪問,所以,我們應該確保我們的服務器是足夠強壯的,任 它風吹雨打也不怕,更不會倒。如果在開發實踐中,沒有很好地領會這一點或者未能將這一思路貫穿進開發之中,那麼,你設計出來的服務器將是無比脆弱的。

當然,安全性和效率總是相互對立的。爲了實現我們所說的“互不信任”原則,難免的,就會在遊戲邏輯中加入很多的異常檢測機制,但異常檢測又是比較耗時 的,這就需要我們在效率和安全性方面作個取捨,對於特別重要的邏輯,我們應該全面貫徹“互不信任”原則,一步扣一步,步步爲營,不讓遊戲邏輯出現一點漏 洞。而對於並非十分重要的場合,則完全可以採用“半信任”或者根本“不須信任”的原則進行設計,以儘可能地提高服務器效率。

  本文只是對自己長期從事遊戲服務器設計以來的感受加以總結,也是對魔獸的服務器有感而發。歡迎有相同感受的朋友或從事相同工作的朋友一起討論。

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

本文作者:sodme 本文出處:http://blog.csdn.net/sodme
版權聲明:本文可以不經作者同意任意轉載,但轉載時煩請保留文章開始前兩行的版權、作者及出處信息。

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

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

本文作者:sodme
本文出處:http://blog.csdn.net/sodme
聲明:本文可以不經作者同意任意轉載、複製、引用。但任何對本文的引用,均須註明本文的作者、出處以及本行聲明信息。

  之前,我分析過QQ遊戲(特指QQ休閒平臺,並非QQ堂,下同)的通信架構(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx),分析過魔獸世界的通信架構(http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx), 似乎網絡遊戲的通信架構也就是這些了,其實不然,在網絡遊戲大家庭中,還有一種類型的遊戲我認爲有必要把它的通信架構專門作個介紹,這便是如泡泡堂、QQ 堂類的休閒類競技遊戲。曾經很多次,被網友們要求能抽時間看看泡泡堂之類遊戲的通信架構,這次由於被逼交作業,所以今晚抽了一點的時間截了一下泡泡堂的 包,正巧昨日與網友就泡泡堂類遊戲的通信架構有過一番討論,於是,將這兩天的討論、截包及思考總結於本文中,希望能對關心或者正在開發此類遊戲的朋友有所 幫助,如果要討論具體的技術細節,請到我的BLOG(http://blog.csdn.net/sodme)加我的MSN討論..

  總體來說,泡泡堂類遊戲(此下簡稱泡泡堂)在大廳到房間這一層的通信架構,其結構與QQ遊戲相當,甚至要比QQ遊戲來得簡單。所以,在房間這一層的通信架構上,我不想過多討論,不清楚的朋友請參看我對QQ遊戲通信架構的分析文章(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx)。可以這麼說,如果採用與QQ遊戲相同的房間和大廳架構,是完全可以組建起一套可擴展的支持百萬人在線的遊戲系統的。也就是說,通過負載均衡+大廳+遊戲房間對遊戲邏輯的分攤,完全可以實現一個可擴展的百萬人在線泡泡堂。

  但是,泡泡堂與鬥地主的最大不同點在於:泡泡堂對於實時性要求特別高。那麼,泡泡堂是如何解決實時性與網絡延遲以及大用戶量之間矛盾的呢?

  閱讀以下文字前,請確認你已經完全理解TCP與UDP之間的不同點。

  我們知道,TCP與UDP之間的最大不同點在於:TCP是可靠連接的,而UDP是無連接的。如果通信雙方使用TCP協議,那麼他們之前必須事先 通過監聽+連接的方式將雙方的通信管道建立起來;而如果通信雙方使用的是UDP通信,則雙方不用事先建立連接,發送方只管向目標地址上的目標端口發送 UDP包即可,不用管對方到底收沒收到。如果要說形象點,可以用這樣一句話概括:TCP是打電話,UDP是發電報。TCP通信,爲了保持這樣的可靠連接, 在可靠性上下了很多功夫,所以導致了它的通信效率要比UDP差很多,所以,一般地,在地實時性要求非常高的場合,會選擇使用UDP協議,比如常見的動作射 擊類遊戲。

  通過載包,我們發現泡泡堂中同時採用了TCP和UDP兩種通信協議。並且,具有以下特點:
1.當玩家未進入具體的遊戲地圖時,僅有TCP通信存在,而沒有UDP通信;
2.進入遊戲地圖後,TCP的通信量遠遠小於UDP的通信量
3.UDP的通信IP個數,與房間內的玩家成一一對應關係(這一點,應網友疑惑而加,此前已經證實)

  以上是幾個表面現象,下面我們來分析它的本質和內在。^&^

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

  與MMORPG一樣,在上面的幾個邏輯中,廣播量最大的其實是玩家移動。爲了保持玩家畫面同步,其他玩家的每一步移動消息都要即時地發給其它玩家。

  通常,網絡遊戲的邏輯控制,絕大多數是在服務器端的。有時,爲了保證畫面的流暢性,我們會有意識地減少服務器端的邏輯判斷量和廣播量,當然,這 個減少,是以“不危及遊戲的安全運行”爲前提的。到底如何在效率、流暢性和安全性之間作取捨,很多時候是需要經驗積累的,效率提高的過程,就是邏輯不斷優 化的過程。不過,有一個原則是可以說的,那就是:“關鍵邏輯”一定要放在服務器上來判斷。那麼,什麼是“關鍵邏輯”呢?

  拿泡泡堂來說,下面的這個邏輯,我認爲就是關鍵邏輯:玩家在某處埋下一顆地雷,地雷爆炸後到底能不能炸出道具以及炸出了哪些道具,這個信息,需要服務器來給。那麼,什麼又是“非關鍵邏輯”呢?

  “非關鍵邏輯”,在不同的遊戲中,會有不同的概念。在通常的MMORPG中,玩家移動邏輯的判斷,是算作關鍵邏輯的,否則,如果服務器端不對客 戶端發過來的移動包進行判斷那就很容易造成玩家的瞬移以及其它毀滅性的災難。而在泡泡堂中,玩家移動邏輯到底應不應該算作關鍵邏輯還是值得考慮的。泡泡堂 中的玩家可以取勝的方法,通常是確實因爲打得好而贏得勝利,不會因爲瞬移而贏得勝利,因爲如果外掛要作泡泡堂的瞬移,它需要考慮的因素和判斷的邏輯太多 了,由於比賽進程的瞬息萬變,外掛的瞬移點判斷不一定就比真正的玩家來得準確,所在,在玩家移動這個邏輯上使用外掛,在泡泡堂這樣的遊戲中通常是得不償失 的(當然,那種特別變態的高智能的外掛除外)。從目前我查到的消息來看,泡泡堂的外掛多數是一些按鍵精靈腳本,它的本質還不是完全的遊戲機器人,並不是通 過純粹的協議接管實現的外掛功能。這也從反面驗證了我以上的想法。

  說到這裏,也許你已經明白了。是的!TCP通信負責“關鍵邏輯”,而UDP通信負責“非關鍵邏輯”,這裏的“非關鍵邏輯”中就包含了玩家移動。 在泡泡堂中,TCP通信用於本地玩家與服務器之間的通信,而UDP則用於本地玩家與同一地圖中的其他各玩家的通信。當本地玩家要移動時,它會同時向同一地 圖內的所有玩家廣播自己的移動消息,其他玩家收到這個消息後會更新自己的遊戲畫面以實現畫面同步。而當本地玩家要在地圖上放置一個炸彈時,本地玩家需要將 此消息同時通知同一地圖內的其他玩家以及服務器,甚至這裏,可以不把放置炸彈的消息通知給服務器,而僅僅通知其他玩家。當炸彈爆炸後,要拾取物品時才向服 務器提交拾取物品的消息。

  那麼,你可能會問,“地圖上某一點是否存在道具”這個消息,服務器是什麼時候通知給客戶端的呢?這個問題,可以有兩種解決方案:
1.客戶端如果在放置炸彈時,將放置炸彈的消息通知給服務器,服務器可以在收到這個消息後,告訴客戶端炸彈爆炸後會有哪些道具。但我覺得這種方案不好,因爲這樣作會增加遊戲運行過程中的數據流量。
2.而這第2種方案就是,客戶端進入地圖後,遊戲剛開始時,就由服務器將本地圖內的各道具所在點的信息傳給各客戶端,這樣,可以省去兩方面的開 銷:a.客戶端放炸彈時,可以不通知服務器而只通知其它玩家;b.服務器也不用在遊戲運行過程中再向客戶端傳遞有關某點有道具的信息。

但是,不管採用哪種方案,服務器上都應該保留一份本地圖內道具所在點的信息。因爲服務器要用它來驗證一個關鍵邏輯:玩家拾取道具。當玩家要在某點拾取道具時,服務器必須要判定此點是否有道具,否則,外掛可以通過頻繁地發拾取道具的包而不斷取得道具。

  至於泡泡堂其它遊戲邏輯的實現方法,我想,還是要依靠這個原則:首先判斷這個邏輯是關鍵邏輯嗎?如果不全是,那其中的哪部分是非關鍵邏輯呢?對 於非關鍵邏輯,都可以交由客戶端之間(UDP)去自行完成。而對於關鍵邏輯,則必須要有服務器(TCP)的校驗和認證。這便是我要說的。

  以上僅僅是在理論上探討關於泡泡堂類遊戲在通信架構上的可能作法,這些想法是沒有事實依據的,所有結論皆來源於對封包的分析以及個人經驗,文章 的內容和觀點可能跟真實的泡泡堂通信架構實現有相當大的差異,但我想,這並不是主要的,因爲我的目的是向大家介紹這樣的TCP和UDP通信並存情況下,如 何對遊戲邏輯的進行取捨和劃分。無論是“關鍵邏輯”的定性,還是“玩家移動”的具體實施,都需要開發者在具體的實踐中進行總結和優化。此文全當是一個引子 罷,如有疑問,請加Msn討論。

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

本文作者:sodme
本文出處:http://blog.csdn.net/sodme
聲明:本文可以不經作者同意任意轉載,但任何對本文的引用都須註明作者、出處及此聲明信息。謝謝!!

  要了解此篇文章中引用的本人寫的另一篇文章,請到以下地址:
http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx
以上的這篇文章是早在去年的時候寫的了,當時正在作休閒平臺,一直在想着如何實現一個可擴充的支持百萬人在線的遊戲平臺,後來思路有了,就寫了那篇總結。文章的意思,重點在於闡述一個百萬級在線的系統是如何實施的,倒沒真正認真地考察過QQ遊戲到底是不是那樣實現的。

  近日在與業內人士討論時,提到QQ遊戲的實現方式並不是我原來所想的那樣,於是,今天又認真抓了一下QQ遊戲的包,結果確如這位兄弟所言,QQ 遊戲的架構與我當初所設想的那個架構相差確實不小。下面,我重新給出QQ百萬級在線的技術實現方案,並以此展開,談談大型在線系統中的負載均衡機制的設 計。

  從QQ遊戲的登錄及遊戲過程來看,QQ遊戲中,也至少分爲三類服務器。它們是:
第一層:登陸/賬號服務器(Login Server),負責驗證用戶身份、向客戶端傳送初始信息,從QQ聊天軟件的封包常識來看,這些初始信息可能包括“會話密鑰”此類的信息,以後客戶端與後續服務器的通信就使用此會話密鑰進行身份驗證和信息加密;
第二層:大廳服務器(估且這麼叫吧, Game Hall Server),負責向客戶端傳遞當前遊戲中的所有房間信息,這些房間信息包括:各房間的連接IP,PORT,各房間的當前在線人數,房間名稱等等。
第三層:遊戲邏輯服務器(Game Logic Server),負責處理房間邏輯及房間內的桌子邏輯。

  從靜態的表述來看,以上的三層結構似乎與我以前寫的那篇文章相比並沒有太大的區別,事實上,重點是它的工作流程,QQ遊戲的通信流程與我以前的設想可謂大相徑庭,其設計思想和技術水平確實非常優秀。具體來說,QQ遊戲的通信過程是這樣的:

  1.由Client向Login Server發送賬號及密碼等登錄消息,Login Server根據校驗結果返回相應信息。可以設想的是,如果Login Server通過了Client的驗證,那麼它會通知其它Game Hall Server或將通過驗證的消息以及會話密鑰放在Game Hall Server也可以取到的地方。總之,Login Server與Game Hall Server之間是可以共享這個校驗成功消息的。一旦Client收到了Login Server返回成功校驗的消息後,Login Server會主動斷開與Client的連接,以騰出socket資源。Login Server的IP信息,是存放在QQGame\config\QQSvrInfo.ini裏的。

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

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

  4.需要特別說明的一點。進入QQ遊戲房間後,直到點擊某個位置坐下打開另一個程序界面,客戶端的程序,沒有再創建新的socket,而仍然使 用原來大廳房間客戶端跟遊戲邏輯服務器交互用的socket。也就是說,這是兩個進程共用的同一個socket!不要小看這一點。如果你在創建桌子客戶端 程序後又新建了一個新的socket與遊戲邏輯服務器進行通信,那麼由此帶來的玩家進入、退出、逃跑等消息會帶來非常麻煩的數據同步問題,俺在剛開始的時 候就深受其害。而一旦共用了同一個socket後,你如果退出桌子,服務器不涉及釋放socket的問題,所以,這裏就少了很多的數據同步問題。關於多個 進程如何共享同一個socket的問題,請去google以下內容:WSADuplicateSocket。

  以上便是我根據最新的QQ遊戲抓包結果分析得到的QQ遊戲的通信流程,當然,這個流程更多的是客戶端如何與服務器之間交互的,卻沒有涉及到服務器彼此之間是如何通信和作數據同步的。關於服務器之間的通信流程,我們只能基於自己的經驗和猜想,得出以下想法:

  1.Login Server與Game Hall Server之前的通信問題。Login Server是負責用戶驗證的,一旦驗證通過之後,它要設法讓Game Hall Server知道這個消息。它們之前實現信息交流的途徑,我想可能有這樣幾條:a. Login Server將通過驗證的用戶存放到臨時數據庫中;b. Login Server將驗證通過的用戶存放在內存中,當然,這個信息,應該是全局可訪問的,就是說所有QQ的Game Hall Server都可以通過服務器之間的數據包通信去獲得這樣的信息。

  2.Game Hall Server的最新房間目錄信息的取得。這個信息,是全局的,也就是整個遊戲中,只保留一個目錄。它的信息來源,可以由底層的房間服務器逐級報上來,報給誰?我認爲就如保存的全局登錄列表一樣,它報給保存全局登錄列表的那個服務器或數據庫。

  3.在QQ遊戲中,同一類型的遊戲,無法打開兩上以上的遊戲房間。這個信息的判定,可以根據全局信息來判定。

  以上關於服務器之間如何通信的內容,均屬於個人猜想,QQ到底怎麼作的,恐怕只有等大家中的某一位進了騰訊之後才知道了。呵呵。不過,有一點是 可以肯定的,在整個服務器架構中,應該有一個地方是專門保存了全局的登錄玩家列表,只有這樣才能保證玩家不會重複登錄以及進入多個相同類型的房間。

  在前面的描述中,我曾經提到過一個問題:當登錄當前Game Hall Server不成功時,QQ遊戲服務器會選擇讓客戶端重定向到另位的服務器去登錄,事實上,QQ聊天服務器和MSN服務器的登錄也是類似的,它也存在登錄重定向問題。

  那麼,這就引出了另外的問題,由誰來作這個策略選擇?以及由誰來提供這樣的選擇資源?這樣的處理,便是負責負載均衡的服務器的處理範圍了。由QQ遊戲的通信過程分析派生出來的針對負責均衡及百萬級在線系統的更進一步討論,將在下篇文章中繼續。

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

posted @ 2009-09-23 23:43 暗夜教父 閱讀(116) | 評論 (0) | 編輯 收藏
一直以來,flash就是我非常喜愛的平臺,
因爲他簡單,完整,但是功能強大,
很適合遊戲軟件的開發,
只不過處理複雜的算法和海量數據的時候,
速度慢了一些,
但是這並不意味着flash不能做,
我們需要變通的方法去讓flash做不善長的事情,

這個貼子用來專門討論用flash作爲客戶端來開發網絡遊戲,
持續時間也不會很長,在把服務器端的源代碼公開完以後,
就告一段落,
注意,僅僅用flash作爲客戶端,
服務器端,我們使用vc6,
我將陸續的公開服務器端的源代碼和大家共享,
並且將講解一些網絡遊戲開發的原理,
希望對此感興趣的朋友能夠使用今後的資源或者理論開發出完整的網絡遊戲。
我們從簡單到複雜,
從棋牌類遊戲到動作類的遊戲,
從2個人的遊戲到10個人的遊戲,
因爲工作忙的關係,我所做的一切僅僅起到拋磚引玉的作用,
希望大家能夠熱情的討論,爲中國的flash事業墊上一塊磚,添上一片瓦。

現在的大型網絡遊戲(mmo game)都是基於server/client體系結構的,
server端用c(windows下我們使用vc.net+winsock)來編寫,
客戶端就無所謂,
在這裏,我們討論用flash來作爲客戶端的實現,

實踐證明,flash的xml socket完全可以勝任網絡傳輸部分,
在別的貼子中,我看見有的朋友談論msn中的flash game
他使用msn內部的網絡接口進行傳輸,
這種做法也是可以的,
我找很久以前對於2d圖形編程的說法,"給我一個打點函數,我就能創造整個遊戲世界",
而在網絡遊戲開發過程中,"給我一個發送函數和一個接收函數,我就能創造網絡遊戲世界."

我們抽象一個接口,就是網絡傳輸的接口,
對於使用flash作爲客戶端,要進行網絡連接,
一個網絡遊戲的客戶端,
可以簡單的抽象爲下面的流程
1.與遠程服務器建立一條長連接
2.用賬號密碼登陸
3.循環
接收消息
發送消息
4.關閉

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

項目開發的基本硬件配置
一臺普通的pc就可以了,
安裝好windows 2000和vc6就可以了,
然後連上網,局域網和internet都可以,

接下去的東西我都簡化,不去用晦澀的術語,

既然是網絡,我們就需要網絡編程接口,
服務器端我們用的是winsock 1.1,使用tcp連接方式,

[tcp和udp]
tcp可以理解爲一條連接兩個端子的隧道,提供可靠的數據傳輸服務,
只要發送信息的一方成功的調用了tcp的發送函數發送一段數據,
我們可以認爲接收方在若干時間以後一定會接收到完整正確的數據,
不需要去關心網絡傳輸上的細節,
而udp不保證這一點,
對於網絡遊戲來說,tcp是普遍的選擇。

[阻塞和非阻塞]
在通過socket發送數據時,如果直到數據發送完畢才返回的方式,也就是說如果我們使用send( buffer, 100.....)這樣的函數發送100個字節給別人,我們要等待,直到100個自己發送完畢,程序才往下走,這樣就是阻塞的,
而非阻塞的方式,當你調用send(buffer,100....)以後,立即返回,此時send函數告訴你發送成功,並不意味着數據已經向目的地發送完 畢,甚至有可能數據還沒有開始發送,只被保留在系統的緩衝裏面,等待被髮送,但是你可以認爲數據在若干時間後,一定會被目的地完整正確的收到,我們要充分 的相信tcp。
阻塞的方式會引起系統的停頓,一般網絡遊戲裏面使用的都是非阻塞的方式,


[有狀態服務器和無狀態服務器]
在c/s體系中,如果server不保存客戶端的狀態,稱之爲無狀態,反之爲有狀態,

在這裏要強調一點,
我們所說的服務器不是一臺具體的機器,
而是指服務器應用程序,
一臺具體的機器或者機器羣組可以運行一個或者多個服務器應用程序,

我們的網絡遊戲使用的是有狀態服務器,
保存所有玩家的數據和狀態,


一些有必要了解的理論和開發工具

[開發語言]
vc6
我們首先要熟練的掌握一門開發語言,
學習c++是非常有必要的,
而vc是windows下面的軟件開發工具,
爲什麼選擇vc,可能與我本身使用vc有關,
而且網上可以找到許多相關的資源和源代碼,

[操作系統]
我們使用windows2000作爲服務器的運行環境,
所以我們有必要去了解windows是如何工作的,
同時對它的編程原理應該熟練的掌握

[數據結構和算法]
要寫出好的程序要先具有設計出好的數據結構和算法的能力,
好的算法未必是繁瑣的公式和複雜的代碼,
我們要找到又好寫有滿足需求的算法,
有時候,最笨的方法同時也是很好的方法,
很多程序員沉迷於追求精妙的算法而忽略了宏觀上的工程,
花費了大量的精力未必能夠取得好的效果,

舉個例子,
我當年進入遊戲界工作,學習老師的代碼,
發現有個函數,要對畫面中的npc位置進行排序,
確定哪個先畫,那個後畫,
他的方法太“笨”,
任何人都會想到的冒泡,
一個一個去比較,沒有任何的優化,
我當時想到的算法就有很多,
而且有一大堆優化策略,
可是,當我花了很長時間去實現我的算法時,
發現提升的那麼一點效率對遊戲整個運行效率而言幾乎是沒起到什麼作用,
或者說雖然算法本身快了幾倍,
可是那是多餘的,老師的算法雖然“笨”,
可是他只花了幾十行代碼就搞定了,
他的時間花在別的更需要的地方,
這就是他可以獨自完成一個遊戲,
而我可以把一個函數優化100倍也只能打雜的原因

[tcp/ip的理論]
推薦數據用tcp/ip進行網際互連,tcp/ip詳解,
這是兩套書,共有6卷,
都是國外的大師寫的,
可以說是必讀的,


網絡傳輸中的“消息”

[消息]
消息是個很常見的術語,
在windows中,消息機制是個十分重要的概念,
我們在網絡遊戲中,也使用了消息這樣的機制,

一般我們這麼做,
一個數據塊,頭4個字節是消息名,後面接2個字節的數據長度,
再後面就是實際的數據

爲什麼使用消息??
我們來看看例子,

在遊戲世界,
一個玩家想要和別的玩家聊天,
那麼,他輸入好聊天信息,
客戶端生成一條聊天消息,
並把聊天的內容打包到消息中,
然後把聊天消息發送給服務器,
請求服務器把聊天信息發送給另一個玩家,

服務器接收到一條消息,
此刻,服務器並不知道當前的數據是什麼東西,
對於服務器來講,這段數據僅僅來自於網絡通訊的底層,
不加以分析的話,沒有任何的信息,
因爲我們的通訊是基於消息機制的,
我們認爲服務器接收到的任何數據都是基於消息的數據方式組織的,
4個字節消息名,2字節長度,這個是不會變的,

通過消息名,服務器發現當前數據是一條聊天數據,
通過長度把需要的數據還原,校驗,
然後把這條消息發送給另一個玩家,

大家注意,消息是變長的,
關於消息的解釋完全在於服務器和客戶端的應用程序,
可以認爲與網絡傳輸低層無關,
比如一條私聊消息可能是這樣的,

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

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

編程者可以自定義消息的內容以滿足不同的需求


隊列

[隊列]
隊列是一個很重要的數據結構,
比如說消息隊列,
服務器或者客戶端,
發送的消息不一定是立即發送的,
而是等待一個適當時間,
或者系統規定的時間間隔以後才發送,
這樣就需要創建一個消息隊列,以保存發送的消息,

消息隊列的大小可以按照實際的需求創建,
隊列又可能會滿,
當隊列滿了,可以直接丟棄消息,
如果你覺得這樣不妥,
也可以預先劃分一個足夠大的隊列,

可以使用一個系統全局的大的消息隊列,
也可以爲每個對象創建一個消息隊列,


這個我們的一個數據隊列的實現,
開發工具vc.net,使用了C++的模板,
關於隊列的算法和基礎知識,我就不多說了,

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

//數據隊列管理類
template <const int _max_line, const int _max_size>
class DataBufferTPL
{
public:

bool Add( byte *data ) // 加入隊列數據
{
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 ) // 從隊列中取出數據
{
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() // 獲取操作成功結果
{
return m_ControlStatus;
}


int GetNumber() // 獲得現在的數據大小
{
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() // 是否隊列滿
{
G_ASSERT_RET( m_NumData >=0 && m_NumData <= _max_line, false );
if( m_NumData == _max_line ) 
return true; 
else 
return false;
}
bool IsNull() // 是否隊列空
{
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]; //數據緩衝
int m_NumData; //數據個數
bool m_ControlStatus; //操作結果

byte *m_start_ptr; //起始位置
byte *m_end_ptr; //結束位置
byte *m_s_ptr; //排隊起始位置
byte *m_e_ptr; //排隊結束位置
};


//////////////////////////////////////////////////////////////////////////
// 放到這裏了!

//ID自動補位列表模板,用於自動列表,無間空順序列表。
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;
}
// 沒有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作爲網絡部分的編程接口,

接下去編程者有必要學習一下socket的基本知識,
不過不懂也沒有關係,我提供的代碼已經把那些麻煩的細節或者正確的系統設置給弄好了,
編程者只需要按照規則編寫遊戲系統的處理代碼就可以了,

這些代碼在vc6下編譯通過,
是通用的網絡傳輸底層,
這裏是socket部分的代碼,

我們需要安裝vc6才能夠編譯以下的代碼,
因爲接下去我們要接觸越來越多的c++,
所以,大家還是去看看c++的書吧,

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

//定義最大連接用戶數目 ( 最大支持 512 個客戶連接 )
#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"
// 吊線時間
#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;

// 接收數據
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;
}

// 發送數據
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;
}


// 啓動winsock
BOOL CSocketCtrl::StartUp()
{
WSADATA wsaData;
WORD wVersionRequested = MAKEWORD( 1, 1 );

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


return TRUE;

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

// 得到主機名
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是否可以寫
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是否有數據
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;

}
// 設置監聽socket
BOOL CSocketCtrl::Listen( int nBackLog )
{
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( !listen( m_sockfd, nBackLog) ) return TRUE;
return FALSE;
}

// 接收一個新的客戶連接
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;
}

// 設置正確的socket狀態,
// 主要是主要是設置非阻塞異步傳輸模式
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);
}


今天晚上寫了一些測試代碼,
想看看flash究竟能夠承受多大的網絡數據傳輸,

我在flash登陸到服務器以後,
每隔3毫秒就發送100次100個字符的串 "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" 給flash,
然後在flash裏面接收數據的函數裏面統計數據,


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;
// 每接收超過1k字節的數據,輸出一次信息,
if( g_nTotalRecvByte-g_nCounter > 1024 )
{
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartTime;
trace( "花費時間:"+nPassedTime+"毫秒" );
g_nCounter = g_nTotalRecvByte;
trace( "接收總數:"+g_nTotalRecvByte+"字節" );
trace( "接收速率:"+g_nTotalRecvByte*1000/nPassedTime+"字節/秒" );

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

我檢查了幾遍源程序,沒有發現邏輯錯誤,
如果程序沒有問題的話,
那麼我們得出的結論是,flash的xml socket每秒可以接收至少40K的數據,
這還沒有計算xmlSocket.onData事件的觸發,調試代碼、信息輸出佔用的時間。

比我想象中快了一個數量級,
夠用了,
flash網絡遊戲我們可以繼續往下走了,


有朋友問到lag的問題,
問得很好,不過也不要過於擔心,
lag的產生有的是因爲網絡延遲,
有的是因爲服務器負載過大,
對於遊戲的設計者和開發者來說,
首先要從設計的角度來避免或者減少lag產生的機會,
如果lag產生了,
也不要緊,找到巧妙的辦法騙過玩家的眼睛,
這也有很多成熟的方法了,
比如航行預測法,路徑插值等等,
都可以產生很好的效果,
還有最後的絕招,就是提高服務器的配置和網絡帶寬,

從我開發網絡遊戲這段時間的經驗來看,
我們的服務器是vc開發的,
普通pc跑幾百個玩家,幾百個怪物是沒有問題的,


又作了一個flash發送的測試,

網絡遊戲的特點是,
出去的信息比較少,
進來的信息比較多,

這個很容易理解,
人操作遊戲的速度是很有限的,
控制指令的產生也是隨機的,
離散的,

但是多人遊戲的話,
因爲人多,信息的流量也就區域均勻分佈了,

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

服務器端要做的是,
把所有從flash客戶端收到的信息原封不動的返回來,

這樣,我又可以通過昨天onData裏面的統計算法來從側面估算出flash發送數據的能力,
這裏是輸出的數據
//
花費時間:30531毫秒
接收總數:200236字節
接收速率:6558.44878975468字節/秒
花費時間:30937毫秒
接收總數:201290字節
接收速率:6506.44858906811字節/秒
花費時間:31140毫秒
接收總數:202344字節
接收速率:6497.88053949904字節/秒
花費時間:31547毫秒
接收總數:203398字節
接收速率:6447.45934637208字節/秒

可以看出來,發送+接收同時做,
發送速率至少可以達到5k byte/s

有一點要注意,要非常注意,
不能讓flash的網絡傳輸滿載,
所謂滿載就是flash在阻塞運算的時候,
不斷的有數據從網絡進來,
而flash又無法在預計的時間內處理我這些信息,
或者flash發送數據過於頻繁,
導致服務器端緩衝溢出導致錯誤,

對於5k的傳輸速率,
已經足夠了,
因爲我也想不出來有什麼產生這麼大的數據量,
而且如果產生了這麼大的數據量,
也就意味着服務器每時每刻都要處理所有的玩家發出的海量數據,
還要把這些海量數據轉發給其他的玩家,
已經引起數據爆炸了,
所以,5k的上傳從設計階段就要避免的,
我想用flash做的網絡遊戲,
除了動作類遊戲可能需要恆定1k以內的上傳速率,
其他的200個字節/秒以內就可以了,


使用於Flash的消息結構定義

我們以前討論過,
通過消息來傳遞信息,
消息的結構是
struct msg
{
short nLength; // 2 byte
DWORD dwId; // 4 byte

....
data
}

但是在爲flash開發的消息中,
不能採用這種結構,

首先Flash xmlSocket只傳輸字符串,
從xmlSocket的send,onData函數可以看出來,
發出去的,收進來的都應該是字符串,

而在服務器端是使用vc,java等高級語言編寫的,
消息中使用的是二進制數據塊,
顯然,簡單的使用字符串會帶來問題,

所以,我們需要制定一套協議,
就是無論在客戶端還是服務器端,
都用統一的字符串消息,
通過解析字符串的方式來傳遞信息,

我想這就是flash採用xml document來傳輸結構化信息的理由之一,
xml document描述了一個完整的數據結構,
而且全部使用的是字符串,
原來是這樣,怪不得叫做xml socket,
本來socket和xml完全是不同的概念,
flash偏偏出了個xml socket,
一開始令我費解,
現在,漸漸理解其中奧妙。


Flash Msg結構定義源代碼和相關函數

在服務器端,我們爲flash定義了一種msg結構,
使用語言,vc6
#define MSGMAXSIZE 512
// 消息頭
struct MsgHeader
{
short stLength;
MsgHeader():stLength( 0 ){}

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

};

在發往flash的消息中,整個處理過後MsgToFlash結構將被髮送,
實踐證明,在flash 客戶端的xmlSocket onData事件中,
接收到了正確的消息,消息的內容是MasToFlash的szString字段,
是一個字符串,

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


然後是flash發送消息到服務器,
我們強調flash只發送字符串,
這個字符串無論是否內部擁有有效數據,
服務器都應該首先把消息收下來,
那就要保證發送給服務器的消息遵循統一的結構,
在flash客戶端中,
我們定義一個函數,
這個函數把一個字符串轉化爲服務器可以識別的消息,

補充:現在我們約定字符串長度都不大於97個字節長度,


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

爲什麼是這個結果呢?
15是消息的長度,
頭兩個字節是整個消息的長度的asc碼,意思是整個消息有15個字節長,
然後是信息client login,
最後是一個0(c語言中的字符串結束符)

當服務器收到15client login,
他首先把15給分析出來,
把"15"字符串轉化爲15的數字,
然後,根據15這個長度把後面的client login讀出來,
這樣,網絡傳輸的底層就完成了,
client login的處理就交給邏輯層,


謝謝大家的支持,
很感謝斑竹把這個貼子置頂,
我寫這文章的過程也是我自己摸索的過程,
文章可以記錄我一段開發的歷史,
一個思考分析的歷程,
有時候甚至作爲日誌來寫,

由於我本身有雜務在身,
所以貼子的更新有點慢,
請大家見諒,

我喜愛flash,
雖然我在帝國中,但我並不能稱之爲閃客,
因爲我製作flash的水平實在很低,
但是我想設計開發出讓其他人能更好的使用flash的工具,

前陣子我開發了Match3D,
一個可以把三維動畫輸出成爲swf的工具,
而且實現了swf渲染的實時三維角色動畫,
這可以說是我真正推出的第一個flash第三方軟件,
其實這以前,
我曾經開發過幾個其他的flash第三方軟件,
都中途停止了,
因爲不實用或者市場上有更好的同類軟件,

隨着互聯網的發展,
flash的不斷升級,
我的flash第三方軟件目光漸漸的從美術開發工具轉移到網絡互連,
web應用上面來,
如今已經到了2004版本,
flash的種種新特性讓我眼前發光,

我最近在帝國的各個板塊看了很多貼子,
分析裏面潛在的用戶需求,
總結了以下的幾個我認爲比較有意義的選題,
可能很片面,

flash源代碼保護,主要是爲了抵禦asv之類的軟件進行反編譯和萃取
flash與遠端數據庫的配合,應該出現一個能夠方便快捷的對遠程數據庫進行操作的方法或者控件,
flash網際互連,我認爲flash網絡遊戲是一塊金子,

這裏我想談談flash網絡遊戲,
我要談的不僅僅是技術,而是一個概念,
用flash網絡遊戲,
我本身並不想把flash遊戲做成rpg或者其他劇烈交互性的遊戲,
而是想讓flash實現那些節奏緩慢,玩法簡單的遊戲,
把網絡的概念帶進來,

你想玩遊戲的時候,登上flash網絡遊戲的網站,
選擇你想玩的網絡遊戲,
因爲現在幾乎所有上網的電腦都可以播放swf,
所以,我們幾乎不用下載任何插件,
輸入你的賬號和密碼,
就可以開始玩了,

我覺得battle.net那種方式很適合flash,
開房間或者進入別人開的房間,
然後2個人或者4個人就可以交戰了,

這種遊戲可以是棋類,這是最基本的,
用戶很廣泛,
我腦海中的那種是類似與寵物飼養的,
就像當年的電子寵物,
每個玩家都可以到服務器認養寵物,
然後在線養成寵物,
還可以邀請別的玩家進行寵物比武,
看誰的寵物厲害,

就這樣簡簡單單的模式,
配合清新可愛的畫面,
趣味的玩法,
加入網絡的要素,
也許可以取得以想不到的效果,

今天就說到這裏吧,
想法那麼多,要實現的話還有很多路要走,

希望大家多多支持,積極參與,
讓我們的想法不僅僅停留於紙上。
大家好,
非常抱歉,
都很長時間沒有回貼了,
因爲手頭項目的原因,
幾乎沒有時間做flash multiplayer的研究,

很感謝大家的支持,
現在把整個flash networking的源代碼共享出來,
大家可以任意的使用,
其實裏面也沒有多少東西,
相信感興趣的朋友還是可以從中找到一些有用的東西,

這一次的源代碼做的事情很簡單,
服務器運行,
客戶端登陸到服務器,
然後客戶端不斷的發送字符串給服務器,
服務器收到後,在發還給客戶端,
客戶端統計一些數據,
posted @ 2009-09-23 23:43 暗夜教父 閱讀(160) | 評論 (0) | 編輯 收藏
原文:http://game.chinaitlab.com/freshmen/783449.html

    要想在修改遊戲中做到百戰百勝,是需要相當豐富的計算機知識的。有很多計算機高手就是從玩遊戲,修改遊戲中,逐步對計算機產生濃厚的興趣,逐步成長起來 的。不要在羨慕別人能夠做到的,因爲別人能夠做的你也能夠!我相信你們看了本教程後,會對遊戲有一個全新的認識,呵呵,因爲我是個好老師!(別拿雞蛋砸我 呀,救命啊!#¥%……*)

    不過要想從修改遊戲中學到知識,增加自己的計算機水平,可不能只是靠修改遊戲呀! 要知道,修改遊戲只是一個驗證你對你所瞭解的某些計算機知識的理解程度的場所,只能給你一些發現問題、解決問題的機會,只能起到幫助你提高學習計算機的興 趣的作用,而決不是學習計算機的捷徑。

    一:什麼叫外掛?

    現在的網絡遊戲多是基於Internet上客戶/服務器模式,服務端程序運行在遊戲服務器上,遊戲的設計者在其中創造一個龐大的遊戲空間,各地的玩家可以通過運行客戶端程序同時登錄到遊戲中。簡單地說,網絡遊戲實際上就是由遊戲開發商 提供一個遊戲環境,而玩家們就是在這個環境中相對自由和開放地進行遊戲操作。那麼既然在網絡遊戲中有了服務器這個概念,我們以前傳統的修改遊戲方法就顯得 無能爲力了。記得我們在單機版的遊戲中,隨心所欲地通過內存搜索來修改角色的各種屬性,這在網絡遊戲中就沒有任何用處了。因爲我們在網絡遊戲中所扮演角色 的各種屬性及各種重要資料都存放在服務器上,在我們自己機器上(客戶端)只是顯示角色的狀態,所以通過修改客戶端內存裏有關角色的各種屬性是不切實際的。 那麼是否我們就沒有辦法在網絡遊戲中達到我們修改的目的?回答是"否".我們知道Internet客戶/服務器模式的通訊一般採用TCP/IP通信協議,數據交換是通過IP數據包的傳輸來實現的,一般來說我們客戶端向服務器發出某些請求,比如移動、戰鬥等指令都是通過封包的形式和服務器交換數 據。那麼我們把本地發出消息稱爲SEND,意思就是發送數據,服務器收到我們SEND的消息後,會按照既定的程序把有關的信息反饋給客戶端,比如,移動的 座標,戰鬥的類型。那麼我們把客戶端收到服務器發來的有關消息稱爲RECV.知道了這個道理,接下來我們要做的工作就是分析客戶端和服務器之間往來的數據 (也就是封包),這樣我們就可以提取到對我們有用的數據進行修改,然後模擬服務器發給客戶端,或者模擬客戶端發送給服務器,這樣就可以實現我們修改遊戲的 目的了。

    目前除了修改遊戲封包來實現修改遊戲的目的,我們也可以修改客戶端的有關程序來達到我們的要求。我們知道目前各個服務器的運算能力是有限的,特別在遊戲 中,遊戲服務器要計算遊戲中所有玩家的狀況幾乎是不可能的,所以有一些運算還是要依靠我們客戶端來完成,這樣又給了我們修改遊戲提供了一些便利。比如我們 可以通過將客戶端程序脫殼來發現一些程序的判斷分支,通過跟蹤調試我們可以把一些對我們不利的判斷去掉,以此來滿足我們修改遊戲的需求。 在下幾個章節中,我們將給大家講述封包的概念,和修改跟蹤客戶端的有關知識。大家準備好了嗎?

    遊戲數據格式和存儲

    在進行我們的工作之前,我們需要掌握一些關於計算機中儲存數據方式的知識和遊戲中儲存數據的特點。本章節是提供給菜鳥級的玩家看的,如果你是高手就可以跳 過了,如果,你想成爲無堅不摧的劍客,那麼,這些東西就會花掉你一些時間;如果,你只想作個江湖的遊客的話,那麼這些東西,瞭解與否無關緊要。是作劍客, 還是作遊客,你選擇吧!

    現在我們開始!首先,你要知道遊戲中儲存數據的幾種格式,這幾種格式是:字節(BYTE)、字(WORD)和雙字(DOUBLE WORD),或者說是8位、16位和32位儲存方式。字節也就是8位方式能儲存0~255的數字;字或說是16位儲存方式能儲存0~65535的數;雙字 即32位方式能儲存0~4294967295的數。

    爲何要了解這些知識呢?在遊戲中各種參數的最大值是不同的,有些可能100左右就夠了,比如,金庸羣俠傳中的角色的等級、隨機遇敵個數等等。而有些卻需要 大於255甚至大於65535,象金庸羣俠傳中角色的金錢值可達到數百萬。所以,在遊戲中各種不同的數據的類型是不一樣的。在我們修改遊戲時需要尋找準備 修改的數據的封包,在這種時候,正確判斷數據的類型是迅速找到正確地址的重要條件。

    在計算機中數據以字節爲基本的儲存單位,每個字節被賦予一個編號,以確定各自的位置。這個編號我們就稱爲地址。

    在需要用到字或雙字時,計算機用連續的兩個字節來組成一個字,連續的兩個字組成一個雙字。而一個字或雙字的地址就是它們的低位字節的地址。 現在我們常用的Windows 9x操作系統中,地址是用一個32位的二進制數表示的。而在平時我們用到內存地址時,總是用一個8位的16進制數來表示它。

    二進制和十六進制又是怎樣一回事呢?

    簡單說來,二進制數就是一種只有0和1兩個數碼,每滿2則進一位的計數進位法。同樣,16進制就是每滿十六就進一位的計數進位法。16進制有0——F十六 個數字,它爲表示十到十五的數字採用了A、B、C、D、E、F六個數字,它們和十進制的對應關係是:A對應於10,B對應於11,C對應於12,D對應於 13,E對應於14,F對應於15.而且,16進制數和二進制數間有一個簡單的對應關係,那就是;四位二進制數相當於一位16進制數。比如,一個四位的二 進制數1111就相當於16進制的F,1010就相當於A.瞭解這些基礎知識對修改遊戲有着很大的幫助,下面我就要談到這個問題。由於在計算機中數據是以 二進制的方式儲存的,同時16進制數和二進制間的轉換關係十分簡單,所以大部分的修改工具在顯示計算機中的數據時會顯示16進制的代碼,而且在你修改時也 需要輸入16進制的數字。你清楚了吧?

    在遊戲中看到的數據可都是十進制的,在要尋找並修改參數的值時,可以使用Windows提供的計算器來進行十進制和16進制的換算,我們可以在開始菜單裏的程序組中的附件中找到它。

    現在要了解的知識也差不多了!不過,有個問題在遊戲修改中是需要注意的。在計算機中數據的儲存方式一般是低位數儲存在低位字節,高位數儲存在高位字節。比如,十進制數41715轉換爲16進制的數爲A2F3,但在計算機中這個數被存爲F3A2.

    看了以上內容大家對數據的存貯和數據的對應關係都瞭解了嗎? 好了,接下來我們要告訴大家在遊戲中,封包到底是怎麼一回事了,來!大家把袖口捲起來,讓我們來幹活吧!

    二:什麼是封包?

    怎麼截獲一個遊戲的封包?怎麼去檢查遊戲服務器的ip地址和端口號? Internet用戶使用的各種信息服務,其通訊的信息最終均可以歸結爲以IP包爲單位的信息傳送,IP包除了包括要傳送的數據信息外,還包含有信息要發 送到的目的IP地址、信息發送的源IP地址、以及一些相關的控制信息。當一臺路由器收到一個IP數據包時,它將根據數據包中的目的IP地址項查找路由表,根據查找的結果將此IP數據包送往對應端口。下一臺IP路由器收到此數據包後繼續轉發,直至發到目的地。路由器之間可以通過路由協議來進行路由信息的交換,從而更新路由表。

    那麼我們所關心的內容只是IP包中的數據信息,我們可以使用許多監聽網絡的工具來截獲客戶端與服務器之間的交換數據,下面就向你介紹其中的一種工具:WPE. WPE使用方法:執行WPE會有下列幾項功能可選擇:

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

    TRACE追蹤功能。用來追蹤擷取程式送收的封包。WPE必須先完成點選欲追蹤的程式名稱,纔可以使用此項目。 按下Play鍵開始擷取程式收送的封包。您可以隨時按下 | | 暫停追蹤,想繼續時請再按下 | | .按下正方形可以停止擷取封包並且顯示所有已擷取封包內容。若您沒按下正方形停止鍵,追蹤的動作將依照OPTION裏的設定值自動停止。如果您沒有擷取到 資料,試試將OPTION裏調整爲Winsock Version 2.WPE 及 Trainers 是設定在顯示至少16 bits 顏色下才可執行。

    FILTER過濾功能。用來分析所擷取到的封包,並且予以修改。

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

    TRAINER MAKER製作修改器。

    OPTIONS設定功能。讓您調整WPE的一些設定值。

    FILTER的詳細教學

    - 當FILTER在啓動狀態時 ,ON的按鈕會呈現紅色。- 當您啓動FILTER時,您隨時可以關閉這個視窗。FILTER將會留在原來的狀態,直到您再按一次 on / off 鈕。- 只有FILTER啓用鈕在OFF的狀態下,纔可以勾選Filter前的方框來編輯修改。- 當您想編輯某個Filter,只要雙擊該Filter的名字即可。

    NORMAL MODE:

    範例:

    當您在 Street Fighter Online ﹝快打旋風線上版﹞遊戲中,您使用了兩次火球而且擊中了對方,這時您會擷取到以下的封包: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您的第一個火球讓對方減了16滴﹝16 = 10h﹞的生命值,而您觀察到第4跟第5個封包的位置4有10h的值出現,應該就是這裏了。

    您觀察10h前的0A 09 C1在兩個封包中都沒改變,可見得這3個數值是發出火球的關鍵。

    因此您將0A 09 C1 10填在搜尋列﹝SEARCH﹞,然後在修改列﹝MODIFY﹞的位置4填上FF.如此一來,當您再度發出火球時,FF會取代之前的10,也就是攻擊力爲255的火球了!

    ADVANCED MODE:範例: 當您在一個遊戲中,您不想要用真實姓名,您想用修改過的假名傳送給對方。在您使用TRACE後,您會發現有些封包裏面有您的名字出現。假設您的名字是 Shadow,換算成16進位則是﹝53 68 61 64 6F 77﹞;而您打算用moon﹝6D6F 6F 6E 20 20﹞來取代他。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但是您仔細看,您的名字在每個封包中並不是出現在相同的位置上- 在第2個封包裏,名字是出現在第4個位置上- 在第4個封包裏,名字是出現在第6個位置上在這種情況下,您就需要使用ADVANCED MODE- 您在搜尋列﹝SEARCH﹞填上:53 68 61 64 6F 77 ﹝請務必從位置1開始填﹞- 您想要從原來名字Shadow的第一個字母開始置換新名字,因此您要選擇從數值被發現的位置開始替代連續數值﹝from the position of the chain found﹞.- 現在,在修改列﹝MODIFY﹞000的位置填上:6D 6F 6F 6E 20 20 ﹝此爲相對應位置,也就是從原來搜尋欄的+001位置開始遞換﹞- 如果您想從封包的第一個位置就修改數值,請選擇﹝from the beginning of the packet﹞瞭解一點TCP/IP協議常識的人都知道,互聯網是 將信息數據打包之後再傳送出去的。每個數據包分爲頭部信息和數據信息兩部分。頭部信息包括數據包的發送地址和到達地址等。數據信息包括我們在遊戲中相關操 作的各項信息。那麼在做截獲封包的過程之前我們先要知道遊戲服務器的IP地址和端口號等各種信息,實際上最簡單的是看看我們遊戲目錄下,是否有一個 SERVER.INI的配置文件,這個文件裏你可以查看到個遊戲服務器的IP地址,比如金庸羣俠傳就是如此,那麼除了這個我們還可以在DOS下使用 NETSTAT這個命令, NETSTAT命令的功能是顯示網絡連接、路由表和網絡接口信息,可以讓用戶得知目前都有哪些網絡連接正在運作。或者你可以使用木馬客星等工具來查看網絡 連接。工具是很多的,看你喜歡用哪一種了。

    NETSTAT命令的一般格式爲:NETSTAT [選項]命令中各選項的含義如下:-a 顯示所有socket,包括正在監聽的。-c 每隔1秒就重新顯示一遍,直到用戶中斷它。

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

    三:怎麼來分析我們截獲的封包?

    首先我們將WPE截獲的封包保存爲文本文件,然後打開它,這時會看到如下的數據(這裏我們以金庸羣俠傳裏PK店小二客戶端發送的數據爲例來講解):第一個 文件: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第二個文件: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我們發現兩次PK店小二的數據格式一樣,但是內容卻不相同,我們是PK的同一個NPC,爲什麼會不同呢? 原來金庸羣俠傳的封包是經過了加密運算纔在網路上傳輸的,那麼我們面臨的問題就是如何將密文解密成明文再分析了。

    因爲一般的數據包加密都是異或運算,所以這裏先講一下什麼是異或。 簡單的說,異或就是"相同爲0,不同爲1"(這是針對二進制按位來講的),舉個例子,0001和0010異或,我們按位對比,得到異或結果是0011,計 算的方法是:0001的第4位爲0,0010的第4位爲0,它們相同,則異或結果的第4位按照"相同爲0,不同爲1"的原則得到0,0001的第3位爲 0,0010的第3位爲0,則異或結果的第3位得到0,0001的第2位爲0,0010的第2位爲1,則異或結果的第2位得到1,0001的第1位爲 1,0010的第1位爲0,則異或結果的第1位得到1,組合起來就是0011.異或運算今後會遇到很多,大家可以先熟悉熟悉,熟練了對分析很有幫助的。

    下面我們繼續看看上面的兩個文件,按照常理,數據包的數據不會全部都有值的,遊戲開發時會預留一些字節空間來便於日後的擴充,也就是說數據包裏會存在一些"00"的字節,觀察上面的文件,我們會發現文件一里很多"12",文件二里很多"77",那麼這是不是代表我們說的"00"呢?推理到這裏,我們就開始行動吧!

    我們把文件一與"12"異或,文件二與"77"異或,當然用手算很費事,我們使用"M2M 1.0 加密封包分析工具"來計算就方便多了。得到下面的結果:第一個文件: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第二個文件: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哈,這一下兩個文件大部分都一樣啦,說明我們的推理是正確的,上面就是我們需要的明文!

    接下來就是搞清楚一些關鍵的字節所代表的含義,這就需要截獲大量的數據來分析。

    首先我們會發現每個數據包都是"F4 44"開頭,第3個字節是變化的,但是變化很有規律。我們來看看各個包的長度,發現什麼沒有?對了,第3個字節就是包的長度! 通過截獲大量的數據包,我們判斷第4個字節代表指令,也就是說客戶端告訴服務器進行的是什麼操作。例如向服務器請求戰鬥指令爲"30",戰鬥中移動指令 爲"D4"等。 接下來,我們就需要分析一下上面第一個包"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",在這個包裏包含什麼信息呢?應該有通知服務器你PK的哪個NPC吧,我們就先來找找這個店小二的代碼在什麼地方。 我們再PK一個小嘍羅(就是大理客棧外的那個咯):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 我們根據常理分析,遊戲裏的NPC種類雖然不會超過65535(FFFF),但開發時不會把自己限制在字的範圍,那樣不利於遊戲的擴充,所以我們在雙字裏 看看。通過"店小二"和"小嘍羅"兩個包的對比,我們把目標放在"6C 79 F6 05"和"CF 26 00 00"上。(對比一下很容易的,但你不能太遲鈍咯,呵呵)我們再看看後面的包,在後面的包裏應該還會出現NPC的代碼,比如移動的包,遊戲允許觀戰,服務 器必然需要知道NPC的移動座標,再廣播給觀戰的其他玩家。在後面第4個包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00"裏我們又看到了"6C 79 F6 05",初步斷定店小二的代碼就是它了!(這分析裏邊包含了很多工作的,大家可以用WPE截下數據來自己分析分析)

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

    我們看看第4個包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00",再截獲PK黃狗的包,(狗會出來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根據上面的分析,黃狗的代碼爲"4B 7D F6 05"(100040011),不過兩隻黃狗服務器怎樣分辨呢?看看"EB 03 F8 05"(100140011),是上一個代碼加上100000,呵呵,這樣服務器就可以認出兩隻黃狗了。我們再通過野外遇敵截獲的數據包來證實,果然如 此。

    那麼,這個包的格式應該比較清楚了:第3個字節爲包的長度,"DA"爲指令,第5個字節爲NPC個數,從第7個字節開始的10個字節代表一個NPC的信息,多一個NPC就多10個字節來表示。

    大家如果玩過網金,必然知道隨機遇敵有時會出現增援,我們就利用遊戲這個增援來讓每次戰鬥都會出現增援的NPC吧。

    通過在戰鬥中出現增援截獲的數據包,我們會發現服務器端發送了這樣一個包:F4 44 12 E9 EB 03 F8 05 02 00 00 03 00 00 00 00 00 00 第5-第8個字節爲增援NPC的代碼(這裏我們就簡單的以黃狗的代碼來舉例)。 那麼,我們就利用單機代理技術來同時欺騙客戶端和服務器吧!

    好了,呼叫NPC的工作到這裏算是完成了一小半,接下來的事情,怎樣修改封包和發送封包,我們下節繼續講解吧。

    四:怎麼冒充"客戶端"向"服務器"發我們需要的封包?

    這裏我們需要使用一個工具,它位於客戶端和服務器端之間,它的工作就是進行數據包的接收和轉發,這個工具我們稱爲代理。如果代理的工作單純就是接收和轉發 的話,這就毫無意義了,但是請注意:所有的數據包都要通過它來傳輸,這裏的意義就重大了。我們可以分析接收到的數據包,或者直接轉發,或者修改後轉發,或 者壓住不轉發,甚至僞造我們需要的封包來發送。

    下面我們繼續講怎樣來同時欺騙服務器和客戶端,也就是修改封包和僞造封包。 通過我們上節的分析,我們已經知道了打多個NPC的封包格式,那麼我們就動手吧!

    首先我們要查找客戶端發送的包,找到戰鬥的特徵,就是請求戰鬥的第1個包,我們找"F4 44 1F 30"這個特徵,這是不會改變的,當然是要解密後來查找哦。 找到後,表示客戶端在向服務器請求戰鬥,我們不動這個包,轉發。 繼續向下查找,這時需要查找的特徵碼不太好辦,我們先查找"DA",這是客戶端發送NPC信息的數據包的指令,那麼可能其他包也有"DA",沒關係,我們 看前3個字節有沒有"F4 44"就行了。找到後,我們的工作就開始了!

    我們確定要打的NPC數量。這個數量不能很大,原因在於網金的封包長度用一個字節表示,那麼一個包可以有255個字節,我們上面分析過,增加一個NPC要增加10個字節,所以大家算算就知道,打20個NPC比較合適。

    然後我們要把客戶端原來的NPC代碼分析計算出來,因爲增加的NPC代碼要加上100000哦。再把我們增加的NPC代碼計算出來,並且組合成新的封包,注意代表包長度的字節要修改啊,然後轉發到服務器,這一步在編寫程序的時候要注意算法,不要造成較大延遲。

    上面我們欺騙服務器端完成了,欺騙客戶端就簡單了。

    發送了上面的封包後,我們根據新增NPC代碼構造封包馬上發給客戶端,格式就是"F4 44 12 E9 NPC代碼 02 00 00 03 00 00 00 00 00 00",把每個新增的NPC都構造這樣一個包,按順序連在一起發送給客戶端,客戶端也就被我們騙過了,很簡單吧。

    以後戰鬥中其他的事我們就不管了,盡情地開打吧。

    遊戲外掛基本原理及實現

    解釋遊戲外掛的基本原理和實現方法

    遊戲外掛已經深深地影響着衆多網絡遊戲玩家,今天在網上看到了一些關於遊戲外掛編寫的技術,於是轉載上供大家參考

    1、遊戲外掛的原理

    外掛現在分爲好多種,比如模擬鍵盤的,鼠標的,修改數據包的,還有修改本地內存的,但好像沒有修改服務器內存的哦,呵呵。其實修改服務器也是有辦法的,只是技術太高一般人沒有辦法入手而已。(比如請GM去夜總會、送禮、收黑錢等等辦法都可以修改服務器數據,哈哈)

    修改遊戲無非是修改一下本地內存的數據,或者截獲API函數等等。這裏我把所能想到的方法都作一個介紹,希望大家能做出很好的外掛來使遊戲廠商更好的完善 自己的技術。我見到一篇文章是講魔力寶貝的理論分析,寫得不錯,大概是那個樣子。下來我就講解一下技術方面的東西,以作引玉之用。


   2 技術分析部分

    2.1 模擬鍵盤或鼠標的響應

    我們一般使用:

UINT SendInput(
    UINT nInputs,   // count of input events
   PINPUT pInputs, // array of input events
    int cbSize    // size of structure
  );
    API函數。第一個參數是說明第二個參數的矩陣的維數的,第二個參數包含了響應事件,這個自己填充就可以,最後是這個結構的大小,非常簡單,這是最簡單的方法模擬鍵盤鼠標了,呵呵。注意,這個函數還有個替代函數:

VOID keybd_event(
    BYTE bVk,       // 虛擬鍵碼
    BYTE bScan,      // 掃描碼
    DWORD dwFlags,
    ULONG_PTR dwExtraInfo // 附加鍵狀態
  );
  與
  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
  );
    這兩個函數非常簡單了,我想那些按鍵精靈就是用的這個吧。上面的是模擬鍵盤,下面的是模擬鼠標的。這個僅僅是模擬部分,要和遊戲聯繫起來我們還需要找到遊 戲的窗口才行,或者包含快捷鍵,就象按鍵精靈的那個激活鍵一樣,我們可以用GetWindow函數來枚舉窗口,也可以用Findwindow函數來查找制 定的窗口(注意,還有一個FindWindowEx),FindwindowEx可以找到窗口的子窗口,比如按鈕,等什麼東西。當遊戲切換場景的時候我們 可以用FindWindowEx來確定一些當前窗口的特徵,從而判斷是否還在這個場景,方法很多了,比如可以GetWindowInfo來確定一些東西, 比如當查找不到某個按鈕的時候就說明遊戲場景已經切換了,等等辦法。有的遊戲沒有控件在裏面,這是對圖像做座標變換的話,這種方法就要受到限制了。這就需 要我們用別的辦法來輔助分析了。

    至於快捷鍵我們要用動態連接庫實現了,裏面要用到hook技術了,這個也非常簡單。大家可能都會了,其實就是一個全局的hook對象然後 SetWindowHook就可以了,回調函數都是現成的,而且現在網上的例子多如牛毛。這個實現在外掛中已經很普遍了。如果還有誰不明白,那就去看看 MSDN查找SetWindowHook就可以了。

    不要低估了這個動態連接庫的作用,它可以切入所有的進程空間,也就是可以加載到所有的遊戲裏面哦,只要用對,你會發現很有用途的。這個需要你複習一下Win32編程的基礎知識了。呵呵,趕快去看書吧。

    2.2 截獲消息

    有些遊戲的響應機制比較簡單,是基於消息的,或者用什麼定時器的東西。這個時候你就可以用攔截消息來實現一些有趣的功能了。

    我們攔截消息使用的也是hook技術,裏面包括了鍵盤消息,鼠標消息,系統消息,日誌等,別的對我們沒有什麼大的用處,我們只用攔截消息的回調函數就可以 了,這個不會讓我寫例子吧。其實這個和上面的一樣,都是用SetWindowHook來寫的,看看就明白了很簡單的。

    至於攔截了以後做什麼就是你的事情了,比如在每個定時器消息裏面處理一些我們的數據判斷,或者在定時器裏面在模擬一次定時器,那麼有些數據就會處理兩次, 呵呵。後果嘛,不一定是好事情哦,呵呵,不過如果數據計算放在客戶端的遊戲就可以真的改變數據了,呵呵,試試看吧。用途還有很多,自己想也可以想出來的, 呵呵。

    2.3 攔截Socket包

    這個技術難度要比原來的高很多。

    首先我們要替換WinSock.DLL或者WinSock32.DLL,我們寫的替換函數要和原來的函數一致纔行,就是說它的函數輸出什麼樣的,我們也要 輸出什麼樣子的函數,而且參數,參數順序都要一樣纔行,然後在我們的函數裏面調用真正的WinSock32.DLL裏面的函數就可以了。

    首先:我們可以替換動態庫到系統路徑。

    其次:我們應用程序啓動的時候可以加載原有的動態庫,用這個函數LoadLibary然後定位函數入口用GetProcAddress函數獲得每個真正Socket函數的入口地址。

    當遊戲進行的時候它會調用我們的動態庫,然後從我們的動態庫中處理完畢後才跳轉到真正動態庫的函數地址,這樣我們就可以在裏面處理自己的數據了,應該是一 切數據。呵呵,興奮吧,攔截了數據包我們還要分析之後才能進行正確的應答,不要以爲這樣工作就完成了,還早呢。等分析完畢以後我們還要仿真應答機制來和服 務器通信,一個不小心就會被封號。

    分析數據纔是工作量的來源呢,遊戲每次升級有可能加密方式會有所改變,因此我們寫外掛的人都是亡命之徒啊,被人愚弄了還不知道。

    2.4 截獲API

    上面的技術如果可以靈活運用的話我們就不用截獲API函數了,其實這種技術是一種補充技術。比如我們需要截獲Socket以外的函數作爲我們的用途,我們就要用這個技術了,其實我們也可以用它直接攔截在Socket中的函數,這樣更直接。

    現在攔截API的教程到處都是,我就不列舉了,我用的比較習慣的方法是根據輸入節進行攔截的,這個方法可以用到任何一種操作系統上,比如Windows 98/2000等,有些方法不是跨平臺的,我不建議使用。這個技術大家可以參考《Windows核心編程》裏面的545頁開始的內容來學習,如果是 Win98系統可以用“Windows系統奧祕”那個最後一章來學習。

    網絡遊戲通訊模型初探

    [文章導讀]本文就將圍繞三個主題來給大家講述一下網絡遊戲的網絡互連實現方法

    序言

    網絡遊戲,作爲遊戲與網絡有機結合的產物,把玩家帶入了新的娛樂領域。網絡遊戲在中國開始發展至今也僅有3,4年的歷史,跟已經擁有幾十年開發歷史的單機遊戲相比,網絡遊戲還是非常年輕的。當然,它的形成也是根據歷史變化而產生的可以說沒有互聯網的 興起,也就沒有網絡遊戲的誕生。作爲新興產物,網絡遊戲的開發對廣大開發者來說更加神祕,對於一個未知領域,開發者可能更需要了解的是網絡遊戲與普通單機 遊戲有何區別,網絡遊戲如何將玩家們連接起來,以及如何爲玩家提供一個互動的娛樂環境。本文就將圍繞這三個主題來給大家講述一下網絡遊戲的網絡互連實現方 法。

    網絡遊戲與單機遊戲

    說到網絡遊戲,不得不讓人聯想到單機遊戲,實際上網絡遊戲的實質脫離不了單機遊戲的製作思想,網絡遊戲和單機遊戲的差別大家可以很直接的想到:不就是可以 多人連線嗎?沒錯,但如何實現這些功能,如何把網絡連線合理的融合進單機遊戲,就是我們下面要討論的內容。在瞭解網絡互連具體實現之前,我們先來了解一下 單機與網絡它們各自的運行流程,只有瞭解這些,你才能深入網絡遊戲開發的核心。

    現在先讓我們來看一下普通單機遊戲的簡化執行流程:

Initialize() // 初始化模塊
{
 初始化遊戲數據;
}
Game() // 遊戲循環部分
{
 繪製遊戲場景、人物以及其它元素;
 獲取用戶操作輸入;
 switch( 用戶輸入數據)
 {
  case 移動:
  {
   處理人物移動;
  }
  break;
  case 攻擊:
  {
   處理攻擊邏輯:
  }
  break;
  ...
  其它處理響應;
  ...
  default:
   break;
 }
 遊戲的NPC等邏輯AI處理;
}
Exit() // 遊戲結束
{
 釋放遊戲數據;
 離開遊戲;
}

    我們來說明一下上面單機遊戲的流程。首先,不管是遊戲軟件還是其他應用軟件,初始化部分必不可少,這裏需要對遊戲的數據進行初始化,包括圖像、聲音以及一 些必備的數據。接下來,我們的遊戲對場景、人物以及其他元素進行循環繪製,把遊戲世界展現給玩家,同時接收玩家的輸入操作,並根據操作來做出響應,此外, 遊戲還需要對NPC以及一些邏輯AI進行處理。最後,遊戲數據被釋放,遊戲結束。

    網絡遊戲與單機遊戲有一個很顯著的差別,就是網絡遊戲除了一個供操作遊戲的用戶界面平臺(如單機遊戲)外,還需要一個用於連接所有用戶,併爲所有用戶提供數據服務的服務器,從某些角度來看,遊戲服務器就像一個大型的數據庫,

    提供數據以及數據邏輯交互的功能。讓我們來看看一個簡單的網絡遊戲模型執行流程:

    客戶機:

Login()// 登入模塊
{
 初始化遊戲數據;
 獲取用戶輸入的用戶和密碼;
 與服務器創建網絡連接;
 發送至服務器進行用戶驗證;
 ...
 等待服務器確認消息;
 ...
 獲得服務器反饋的登入消息;
 if( 成立 )
  進入遊戲;
 else
  提示用戶登入錯誤並重新接受用戶登入;
}
Game()// 遊戲循環部分
{
 繪製遊戲場景、人物以及其它元素;
 獲取用戶操作輸入;
 將用戶的操作發送至服務器;
 ...
 等待服務器的消息;
 ...
 接收服務器的反饋信息;
 switch( 服務器反饋的消息數據 )
 {
  case 本地玩家移動的消息:
  {
   if( 允許本地玩家移動 )
    客戶機處理人物移動;
   else
    客戶機保持原有狀態;
  }
   break;
  case 其他玩家/NPC的移動消息:
  {
   根據服務器的反饋信息進行其他玩家或者NPC的移動處理;
  }
  break;
  case 新玩家加入遊戲:
  {
   在客戶機中添加顯示此玩家;
  }
   break;
  case 玩家離開遊戲:
  {
   在客戶機中銷燬此玩家數據;
  }
   break;
  ...
  其它消息類型處理;
  ... 
  default:
   break;
 }
}
Exit()// 遊戲結束
{
 發送離開消息給服務器;
 ...
 等待服務器確認;
 ...
 得到服務器確認消息;
 與服務器斷開連接;
 釋放遊戲數據;
 離開遊戲;
}


  服務器:

Listen()  // 遊戲服務器等待玩家連接模塊
{
 ...
 等待用戶的登入信息;
 ...
 接收到用戶登入信息;
 分析用戶名和密碼是否符合;
 if( 符合 )
 {
  發送確認允許進入遊戲消息給客戶機; 
  把此玩家進入遊戲的消息發佈給場景中所有玩家;
  把此玩家添加到服務器場景中;
 }
 else
 {
  斷開與客戶機的連接;
 }
}
Game() // 遊戲服務器循環部分
{
 ...
 等待場景中玩家的操作輸入;
 ...
 接收到某玩家的移動輸入或NPC的移動邏輯輸入;
 // 此處只以移動爲例
 進行此玩家/NPC在地圖場景是否可移動的邏輯判斷;

 if( 可移動 )
 {
  對此玩家/NPC進行服務器移動處理;
  發送移動消息給客戶機;
  發送此玩家的移動消息給場景上所有玩家;
 }
 else
  發送不可移動消息給客戶機;
}
Exit()  // 遊戲服務=器結束
{
 接收到玩家離開消息;
 將此消息發送給場景中所有玩家;
 發送允許離開的信息;
 將玩家數據存入數據庫;
 註銷此玩家在服務器內存中的數據;
}
}


    讓我們來說明一下上面簡單網絡遊戲模型的運行機制。先來講講服務器端,這裏服務器端分爲三個部分(實際上一個完整的網絡遊戲遠不止這些):登入模塊、遊戲 模塊和登出模塊。登入模塊用於監聽網絡遊戲客戶端發送過來的網絡連接消息,並且驗證其合法性,然後在服務器中創建這個玩家並且把玩家帶領到遊戲模塊中; 遊戲模塊則提供給玩家用戶實際的應用服務,我們在後面會詳細介紹這個部分; 在得到玩家要離開遊戲的消息後,登出模塊則會把玩家從服務器中刪除,並且把玩家的屬性數據保存到服務器數據庫中,如: 經驗值、等級、生命值等。

    接下來讓我們看看網絡遊戲的客戶端。這時候,客戶端不再像單機遊戲一樣,初始化數據後直接進入遊戲,而是在與服務器創建連接,並且獲得許可的前提下才進入 遊戲。除此之外,網絡遊戲的客戶端遊戲進程需要不斷與服務器進行通訊,通過與服務器交換數據來確定當前遊戲的狀態,例如其他玩家的位置變化、物品掉落情 況。同樣,在離開遊戲時,客戶端會向服務器告知此玩家用戶離開,以便於服務器做出相應處理。

    以上用簡單的僞代碼給大家闡述了單機遊戲與網絡遊戲的執行流程,大家應該可以清楚看出兩者的差別,以及兩者間相互的關係。我們可以換個角度考慮,網絡遊戲 就是把單機遊戲的邏輯運算部分搬移到遊戲服務器中進行處理,然後把處理結果(包括其他玩家數據)通過遊戲服務器返回給連接的玩家。

    網絡互連

    在瞭解了網絡遊戲基本形態之後,讓我們進入真正的實際應用部分。首先,作爲網絡遊戲,除了常規的單機遊戲所必需的東西之外,我們還需要增加一個網絡通訊模塊,當然,這也是網絡遊戲較爲主要的部分,我們來討論一下如何實現網絡的通訊模塊。

    一個完善的網絡通訊模塊涉及面相當廣,本文僅對較爲基本的處理方式進行討論。網絡遊戲是由客戶端和服務器組成,相應也需要兩種不同的網絡通訊處理方式,不 過也有相同之處,我們先就它們的共同點來進行介紹。我們這裏以Microsoft Windows 2000 [2000 Server]作爲開發平臺,並且使用Winsock作爲網絡接口(可能一些朋友會考慮使用DirectPlay來進行網絡通訊,不過對於當前在線遊 戲,DirectPlay並不適合,具體原因這裏就不做討論了)。

    確定好平臺與接口後,我們開始進行網絡連接創建之前的一些必要的初始化工作,這部分無論是客戶端或者服務器都需要進行。讓我們看看下面的代碼片段:

WORD wVersionRequested;
WSADATAwsaData;
wVersionRequested MAKEWORD(1, 1);
if( WSAStartup( wVersionRequested, &wsaData ) !0 )
{
 Failed( WinSock Version Error!" );
}
  上面通過調用Windows的socket API函數來初始化網絡設備,接下來進行網絡Socket的創建,代碼片段如下:

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

    這裏需要說明,客戶端和服務端所需要的Socket連接數量是不同的,客戶端只需要一個Socket連接足以滿足遊戲的需要,而服務端必須爲每個玩家用戶 創建一個用於通訊的Socket連接。當然,並不是說如果服務器上沒有玩家那就不需要創建Socket連接,服務器端在啓動之時會生成一個特殊的 Socket用來對玩家創建與服務器連接的請求進行響應,等介紹網絡監聽部分後會有更詳細說明。

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

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

    這裏兩個步驟分別對前面所作的創建初始化進行了相應釋放。

    接下來看看服務器端的一個網絡執行處理,這裏我們假設服務器端已經創建好一個Socket供使用,我們要做的就是讓這個Socket變成監聽網絡連接請求的專用接口,看看下面代碼片段:

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爲要監聽的端口號
// 綁定socket
if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR )
{
 Failed( "WinSocket Bind Error!");
}
// 進行監聽
if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )
{
 Failed( "WinSocket Listen Error!");
}

    這裏使用的是阻塞式通訊處理,此時程序將處於等待玩家用戶連接的狀態,倘若這時候有客戶端連接進來,則通過accept()來創建針對此玩家用戶的Socket連接,代碼片段如下:

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

    這裏我們創建了sPlayerSocket連接,此後遊戲服務器與這個玩家用戶的通訊全部通過此Socket進行,到這裏爲止,我們服務器已經有了接受玩家用戶連接的功能,現在讓我們來看看遊戲客戶端是如何連接到遊戲服務器上,代碼片段如下:

SOCKADDR_IN addr;
memset( &addr, 0, sizeof(addr) );
addr.sin_family = AF_INET;// 要連接的遊戲服務器端口號
addr.sin_addr.s_addr = inet_addr( IP );// 要連接的遊戲服務器IP地址,
addr.sin_port = htons( Port );//到此,客戶端和服務器已經有了通訊的橋樑,
//接下來就是進行數據的發送和接收:
connect( sSocket, (SOCKADDR*)&addr, sizeof(addr) );
if( send( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
{
 Failed( "WinSocket Send Error!");
}


    這裏的pBuffer爲要發送的數據緩衝指針,lLength爲需要發送的數據長度,通過這支Socket API函數,我們無論在客戶端或者服務端都可以進行數據的發送工作,同時,我們可以通過recv()這支Socket API函數來進行數據接收:

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

    其中pBuffer用來存儲獲取的網絡數據緩衝,lLength則爲需要獲取的數據長度。


    現在,我們已經瞭解了一些網絡互連的基本知識,但作爲網絡遊戲,如此簡單的連接方式是無法滿足網絡遊戲中百人千人同時在線的,我們需要更合理容錯性更強的網絡通訊處理方式,當然,我們需要先了解一下網絡遊戲對網絡通訊的需求是怎樣的。

    大家知道,遊戲需要不斷循環處理遊戲中的邏輯並進行遊戲世界的繪製,上面所介紹的Winsock處理方式均是以阻塞方式進行,這樣就違背了遊戲的執行本 質,可以想象,在客戶端連接到服務器的過程中,你的遊戲不能得到控制,這時如果玩家想取消連接或者做其他處理,甚至顯示一個最基本的動態連接提示都不行。

    所以我們需要用其他方式來處理網絡通訊,使其不會與遊戲主線相沖突,可能大家都會想到: 創建一個網絡線程來處理不就可以了?沒錯,我們可以創建一個專門用於網絡通訊的子線程來解決這個問題。當然,我們遊戲中多了一個線程,我們就需要做更多的 考慮,讓我們來看看如何創建網絡通訊線程。

    在Windows系統中,我們可以通過CreateThread()函數來進行線程的創建,看看下面的代碼片段:

DWORD dwThreadID;
HANDLE hThread = CreateThread( NULL, 0, NetThread/*網絡線程函式*/, sSocket, 0, &dwThreadID );
if( hThread == NULL )
{
 Failed( "WinSocket Thread Create Error!");
}
  這裏我們創建了一個線程,同時將我們的Socket傳入線程函數:

DWORD WINAPINetThread(LPVOID lParam)

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

    NetThread就是我們將來用於處理網絡通訊的網絡線程。那麼,我們又如何把Socket的處理引入線程中?

    看看下面的代碼片段:

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

    通過上面的設置之後,WinSock API函數均會以非阻塞方式運行,也就是函數執行後會立即返回,這時網絡通訊會以事件方式存儲於hEvent,而不會停頓整支程式。

    完成了上面的步驟之後,我們需要對事件進行響應與處理,讓我們看看如何在網絡線程中獲得網絡通訊所產生的事件消息:

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)
   // 連接成功  
   {
   // 連接成功後通知主線程(遊戲線程)進行處理
   }
  }
   break;
  case FD_READ:
  // 獲取網絡數據
  {
   if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR )
   {
    Failed( "WinSocket Recv Error!");
   }
  }
   break;
  case FD_WRITE:
   break;
  case FD_CLOSE:
   // 通知主線程(遊戲線程), 網絡已經斷開
   break;
  default:
}
}


    這裏僅對網絡連接(FD_CONNECT) 和讀取數據(FD_READ) 進行了簡單模擬操作,但實際中網絡線程接收到事件消息後,會對數據進行組織整理,然後再將數據回傳給我們的遊戲主線程使用,遊戲主線程再將處理過的數據發 送出去,這樣一個往返就構成了我們網絡遊戲中的數據通訊,是讓網絡遊戲動起來的最基本要素。

    最後,我們來談談關於網絡數據包(數據封包)的組織,網絡遊戲的數據包是遊戲數據通訊的最基本單位,網絡遊戲一般不會用字節流的方式來進行數據傳輸,一個 數據封包也可以看作是一條消息指令,在遊戲進行中,服務器和客戶端會不停的發送和接收這些消息包,然後將消息包解析轉換爲真正所要表達的指令意義並執行。

    互動與管理

    說到互動,對於玩家來說是與其他玩家的交流,但對於計算機而言,實現互動也就是實現數據消息的相互傳遞。前面我們已經瞭解過網絡通訊的基本概念,它構成了 互動的最基本條件,接下來我們需要在這個網絡層面上進行數據的通訊。遺憾的是,計算機並不懂得如何表達玩家之間的交流,因此我們需要提供一套可讓計算機了 解的指令組織和解析機制,也就是對我們上面簡單提到的網絡數據包(數據封包)的處理機制。

    爲了能夠更簡單的給大家闡述網絡數據包的組織形式,我們以一個聊天處理模塊來進行討論,看看下面的代碼結構:

struct tagMessage{
 long lType;
 long lPlayerID;
};
// 消息指令
// 指令相關的玩家標識
char strTalk[256]; // 消息內容

    上面是抽象出來的一個極爲簡單的消息包結構,我們先來談談其各個數據域的用途:首先,lType 是消息指令的類型,這是最爲基本的消息標識,這個標識用來告訴服務器或客戶端這條指令的具體用途,以便於服務器或客戶端做出相應處理。lPlayerID 被作爲玩家的標識。大家知道,一個玩家在機器內部實際上也就是一堆數據,特別是在遊戲服務器中,可能有成千上萬個玩家,這時候我們需要一個標記來區分玩 家,這樣就可以迅速找到特定玩家,並將通訊數據應用於其上。

    strTalk 是我們要傳遞的聊天數據,這部分纔是真正的數據實體,前面的參數只是數據實體應用範圍的限定。

    在組織完數據之後,緊接着就是把這個結構體數據通過Socket 連接發送出去和接收進來。這裏我們要了解,網絡在進行數據傳輸過程中,它並不關心數據採用的數據結構,這就需要我們把數據結構轉換爲二進制數據碼進行發 送,在接收方,我們再將這些二進制數據碼轉換回程序使用的相應數據結構。讓我們來看看如何實現:

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

    首先,我們假設已經組織好一個數據包,這裏MSG_CHAT 是我們自行定義的標識符,當然,這個標識符在服務器和客戶端要統一。玩家的ID 則根據遊戲需要來進行設置,這裏1000 只作爲假設,現在繼續:

char* p = (char*)&Msg;
long lLength = sizeof( tagMessage );
send( sSocket, p, lLength );
// 獲取數據結構的長度

    我們通過強行轉換把結構體轉變爲char 類型的數據指針,這樣就可以通過這個指針來進行流式數據處理,這裏通過

    sizeof() 獲得結構體長度,然後用WinSock 的Send() 函數將數據發送出去。

    接下來看看如何接收數據:

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

    在通過WinSock 的recv() 函數獲取網絡數據之後,我們同樣通過強行轉換把獲取出來的緩衝數據轉換爲相應結構體,這樣就可以方便地對數據進行訪問。(注:強行轉換僅僅作爲數據轉換的 一種手段,實際應用中有更多可選方式,這裏只爲簡潔地說明邏輯)談到此處,不得不提到服務器/ 客戶端如何去篩選處理各種消息以及如何對通訊數據包進行管理。無論是服務器還是客戶端,在收到網絡消息的時候,通過上面的數據解析之後,還必須對消息類型 進行一次篩選和派分,簡單來說就是類似Windows 的消息循環,不同消息進行不同處理。這可以通過一個switch 語句(熟悉Windows 消息循環的朋友相信已經明白此意),基於消

    息封包裏的lType 信息,對消息進行區分處理,考慮如下代碼片段:

switch( p->lType ) // 這裏的p->lType爲我們解析出來的消息類型標識
{
 case MSG_CHAT: // 聊天消息
  break;
 case MSG_MOVE: // 玩家移動消息
  break;
 case MSG_EXIT: // 玩家離開消息
  break;
 default:
  break;
}

    面片段中的MSG_MOVE 和MSG_EXIT 都是我們虛擬的消息標識(一個真實遊戲中的標識可能會有上百個,這就需要考慮優化和優先消息處理問題)。此外,一個網絡遊戲服務器面對的是成百上千的連接 用戶,我們還需要一些合理的數據組織管理方式來進行相關處理。普通的單體遊戲服務器,可能會因爲當機或者用戶過多而導致整個遊戲網絡癱瘓,而這也就引入分 組服務器機制,我們把服務器分開進行數據的分佈式處理。

    我們把每個模塊提取出來,做成專用的服務器系統,然後建立一個連接所有服務器的數據中心來進行數據交互,這裏每個模塊均與數據中心創建了連接,保證了每個 模塊的相關性,同時玩家轉變爲與當前提供服務的服務器進行連接通訊,這樣就可以緩解單獨一臺服務器所承受的負擔,把壓力分散到多臺服務器上,同時保證了數 據的統一,而且就算某臺服務因爲異常而當機也不會影響其他模塊的遊戲玩家,從而提高了整體穩定性。分組式服務器緩解了服務器的壓力,但也帶來了服務器調度 問題,分組式服務器需要對服務器跳轉進行處理,就以一個玩家進行遊戲場景跳轉作爲討論基礎:假設有一玩家處於遊戲場景A,他想從場景A 跳轉到場景B,在遊戲中,我們稱之場景切換,這時玩家就會觸發跳轉需求,比如走到了場景中的切換點,這樣服務器就把玩家數據從"遊戲場景A 服務器"刪除,同時在"遊戲場景B 服務器"中把玩家建立起來。

    這裏描述了場景切換的簡單模型,當中處理還有很多步驟,不過通過這樣的思考相信大家可以派生出很多應用技巧。

    不過需要注意的是,在場景切換或者說模塊間切換的時候,需要切實考慮好數據的傳輸安全以及邏輯合理性,否則切換很可能會成爲將來玩家複製物品的橋樑。

    總結

    本篇講述的都是通過一些簡單的過程來進行網絡遊戲通訊,提供了一個製作的思路,雖然具體實現起來還有許多要做 ,但只要順着這個思路去擴展、去完善,相信大家很快就能夠編寫出自己的網絡通訊模塊。由於時間倉促,本文在很多細節方面都有省略,文中若有錯誤之處也望大 家見諒



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

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

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

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


然後要用新立得裝如下庫: 
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一下,及時修改PATH 

posted @ 2009-09-18 19:12 暗夜教父 閱讀(167) | 評論 (0) | 編輯 收藏
前提: 
需要下載as3corelib來爲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();   
            }   
        }   
    }   

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

這裏打印出了Erlang Socket Server接收到的AS3 Client發過來的JSON decode過的一個person對象 

運行AS3客戶端: 
client.html上首先顯示“test”,然後異步處理完Socket消息發送和接受後,decode Erlang Server端發過來的person對象,將頁面上的TextField替換爲“Name: Hideto2, age: 24”
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章