本文作者: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上留言提出。
本文作者: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ā)。歡迎有相同感受的朋友或從事相同工作的朋友一起討論。
本文作者: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)入游戲中。
本文作者: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討論。
本文作者: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è)究竟。
一直以來(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 MsgToFlash
ublic 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ù),
原文: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)諒
原文: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ì)更清晰。
先要這樣
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
前提:
需要下載
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”