使用下列JSON庫(kù):
http://www.lshift.net/blog/2007/02/17/json-and-json-rpc-for-erlang 該JSON庫(kù)采用
Joe Armstrong prefered Data type mapping 即:

JSON Obj = type obj() =
{obj, [
{key(), val()}]}
JSON Array = type array() = [val()]
JSON Number = type num() = int() | float()
JSON String = type str() = bin()
JSON true false null = true, false null (atoms)
With Type val() = obj() | array() | num() | str() | true | false | null
and key() being a str(). (Or a binary or atom, during JSON encoding.)
測(cè)試如下:
Eshell V5.6.3 (abort with ^G)

1> O = rfc4627:encode(
{obj, [
{name, hideto},
{age, 23}]}).
"{\"name\":\"hideto\",\"age\":23}"
2> rfc4627:decode(O).


{ok,
{obj,[
{"name",<<"hideto">>},
{"age",23}]},[]}
3> A = rfc4627:encode([1,2,3,4,5]).
"[1,2,3,4,5]"
4> rfc4627:decode(A).


{ok,[1,2,3,4,5],[]}
5> N = rfc4627:encode(12345).
"12345"
6> rfc4627:decode(N).


{ok,12345,[]}
7> S = rfc4627:encode("12345").
"[49,50,51,52,53]"
8> rfc4627:decode(S).


{ok,"12345",[]}
9> T = rfc4627:encode(true).
"true"
10> rfc4627:decode(T).


{ok,true,[]}
11> F = rfc4627:encode(false).
"false"
12> rfc4627:decode(F).


{ok,false,[]}
13> Null = rfc4627:encode(null).
"null"
14> rfc4627:decode(Null).


{ok,null,[]}

轉(zhuǎn)自http://www.javaeye.com/topic/401041
學(xué)erlang有一段時(shí)間了,現(xiàn)在在維護(hù)一套webim系統(tǒng)
并打算擴(kuò)展成 webgame 的服務(wù)程序
在沒(méi)有使用包協(xié)議的時(shí)候,遇到好多粘包問(wèn)題,實(shí)在惱火
查閱了相關(guān)資料:
Flash Socket 的 writeUTF() 會(huì)自動(dòng)增加包頭長(zhǎng)度的協(xié)議,剛好對(duì)應(yīng)了
Erlang的Socket選項(xiàng) {packet,2}
這使得兩者的通信非常完美,再也不用擔(dān)心粘包什么的問(wèn)題了
下面是我寫(xiě)的一個(gè)Flash Socket 接口:SocketBridge.as

package
{
import flash.display.Sprite;
import flash.events.*;
import flash.net.Socket;
import flash.utils.*;
import flash.external.ExternalInterface;
import flash.system.*;

public class SocketBridge extends Sprite
{
Socket.prototype.timeout =3000;
private var socket:Socket;
public function SocketBridge()

{
socket = new Socket();
socket.addEventListener( Event.CONNECT, onConnect );
socket.addEventListener( ProgressEvent.SOCKET_DATA, onDataRecevice);
socket.addEventListener( Event.CLOSE, onClose);
socket.addEventListener( IOErrorEvent.IO_ERROR, onError);
if(ExternalInterface.available)

{
ExternalInterface.addCallback("socket_connect",socket_connect);
ExternalInterface.addCallback("socket_send",socket_send);
ExternalInterface.addCallback("load_policy",load_policy);
}
}
public function onError(e):void

{
ExternalInterface.call("sb_onerror",e.text);
socket.close();
}
public function load_policy(host:String,port):void

{
Security.loadPolicyFile("xmlsocket://"+host+":"+port);
}
public function socket_connect(host:String,port):void

{

try
{
socket.connect(host,port);

}catch(e)
{
ExternalInterface.call("sb_onerror",e.text);
}
}
public function socket_send(msg:String)

{
socket.writeUTF(msg);
socket.flush();
}
private function onConnect(event:Event):void

{
ExternalInterface.call("sb_onconnect",true);
}
private function onClose(event:Event):void

{
socket.close();
ExternalInterface.call("sb_onclose",true);
}
private function onDataRecevice( eventrogressEvent ):void

{
var sdata:String;

while(socket.bytesAvailable)
{
sdata = socket.readUTF();
ExternalInterface.call("sb_ondata",sdata);
}
}
}
}

目前,我們的游戲服務(wù)器組是按多進(jìn)程的方式設(shè)計(jì)的。強(qiáng)調(diào)多進(jìn)程,是想提另外一點(diǎn),我們每個(gè)進(jìn)程上是單線程的。所以,我們?cè)谠O(shè)計(jì)中,系統(tǒng)的復(fù)雜點(diǎn)在于進(jìn)程間如何交換數(shù)據(jù);而不需要考慮線程間的數(shù)據(jù)鎖問(wèn)題。
如果肆意的做進(jìn)程間通訊,在進(jìn)程數(shù)量不斷增加后,會(huì)使系統(tǒng)混亂不可控。經(jīng)過(guò)分析后,我決定做如下的限制:
-
如果一個(gè)進(jìn)程需要和多個(gè)服務(wù)器做雙向通訊,那么這個(gè)進(jìn)程不能處理復(fù)雜的邏輯,而只是過(guò)濾和轉(zhuǎn)發(fā)數(shù)據(jù)用。即,這樣的一個(gè)進(jìn)程 S
,只會(huì)把進(jìn)程 A 發(fā)過(guò)來(lái)的數(shù)據(jù)轉(zhuǎn)發(fā)到 B ;或把進(jìn)程 B 發(fā)過(guò)來(lái)的數(shù)據(jù)轉(zhuǎn)發(fā)到 A
。或者從一端發(fā)過(guò)來(lái)的數(shù)據(jù),經(jīng)過(guò)簡(jiǎn)單的協(xié)議分析后,可以分發(fā)到不同的地方。例如,把客戶端發(fā)過(guò)來(lái)的數(shù)據(jù)包中的聊天信息分離處理,交到聊天進(jìn)程處理。
-
有邏輯處理的進(jìn)程上的數(shù)據(jù)流一定是單向的,它可以從多個(gè)數(shù)據(jù)源讀取數(shù)據(jù),但是處理后一定反饋到另外的地方,而不需要和數(shù)據(jù)源做邏輯上的交互。
-
每個(gè)進(jìn)程盡可能的保持單個(gè)輸入點(diǎn),或是單個(gè)輸出點(diǎn)。
-
所有費(fèi)時(shí)的操作均發(fā)到獨(dú)立的進(jìn)程,以隊(duì)列方式處理。
-
按功能和場(chǎng)景劃分進(jìn)程,單一服務(wù)和單一場(chǎng)景中不再分離出多個(gè)進(jìn)程做負(fù)載均衡。
性能問(wèn)題上,我是這樣考慮的:
我們應(yīng)該充分利用多核的優(yōu)勢(shì),這會(huì)是日后的發(fā)展方向。讓每個(gè)進(jìn)程要么處理大流量小計(jì)算量的工作;要么處理小流量大計(jì)算量的工作。這樣多個(gè)進(jìn)程放在一臺(tái)物理機(jī)器上可以更加充分的利用機(jī)器的資源。
單線程多進(jìn)程的設(shè)計(jì),個(gè)人認(rèn)為更能發(fā)揮多核的優(yōu)勢(shì)。這是因?yàn)闆](méi)有了鎖,每個(gè)線程都可以以最大吞吐量工作。增加的負(fù)擔(dān)只是進(jìn)程間的數(shù)據(jù)復(fù)制,在網(wǎng)游這種復(fù)雜邏輯的系統(tǒng)中,一般不會(huì)比邏輯計(jì)算更早成為瓶頸。如果擔(dān)心,單線程沒(méi)有利用多核計(jì)算的優(yōu)勢(shì),不妨考慮以下的例子:
計(jì)算 a/b+c/d+e/f ,如果我們?cè)谝粋€(gè)進(jìn)程中開(kāi)三條線程利用三個(gè)核同時(shí)計(jì)算 a/b c/d e/f
固然不錯(cuò),但它增加了程序設(shè)計(jì)的復(fù)雜度。而換個(gè)思路,做成三個(gè)進(jìn)程,第一個(gè)只算 a/b 把結(jié)果交給第二個(gè)進(jìn)程去算 c/d
于之的和,再交個(gè)第三個(gè)進(jìn)程算 e/f
。對(duì)于單次運(yùn)算來(lái)算,雖然成本增加了。它需要做額外的進(jìn)程間通訊復(fù)制中間結(jié)果。但,如果我們有大量連續(xù)的這樣的計(jì)算要做,整體的吞吐量卻增加了。因?yàn)樵谒?
某次的 a/b 的時(shí)候,前一次的 c/d 可能在另一個(gè)核中并行計(jì)算著。
具體的設(shè)計(jì)中,我們只需要把處理數(shù)據(jù)包的任務(wù)切細(xì),適當(dāng)增加處理流水線的長(zhǎng)度,就可以提高整個(gè)系統(tǒng)的吞吐量了。由于邏輯操作是單線程的,所以另需要注意的一點(diǎn)是,所有費(fèi)時(shí)的操作都應(yīng)該轉(zhuǎn)發(fā)到獨(dú)立的進(jìn)程中異步完成。比如下面會(huì)提到的數(shù)據(jù)存取服務(wù)。
對(duì)于具體的場(chǎng)景管理是這樣做的:
玩
家連接進(jìn)來(lái)后,所有數(shù)據(jù)包會(huì)經(jīng)過(guò)一個(gè)叫做位置服務(wù)的進(jìn)程中。這個(gè)進(jìn)程可以區(qū)分玩家所在的位置,然后把玩家數(shù)據(jù)分發(fā)到對(duì)應(yīng)的場(chǎng)景服務(wù)進(jìn)程中。這個(gè)位置服務(wù)同
時(shí)還管理玩家間消息的廣播。即,單個(gè)的場(chǎng)景(邏輯)服務(wù)并不關(guān)心每個(gè)數(shù)據(jù)包為哪幾個(gè)玩家所見(jiàn),而由這個(gè)服務(wù)將其復(fù)制分發(fā)。
當(dāng)玩家切換場(chǎng)景,場(chǎng)景服務(wù)器將玩家的數(shù)據(jù)發(fā)送給數(shù)據(jù)服務(wù),數(shù)據(jù)服務(wù)進(jìn)程 cache 玩家數(shù)據(jù),并將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)。然后把玩家的新的場(chǎng)景編號(hào)發(fā)回位置服務(wù)進(jìn)程,這樣位置服務(wù)器可以將后續(xù)的玩家數(shù)據(jù)包正確的轉(zhuǎn)發(fā)到新的場(chǎng)景服務(wù)進(jìn)程中。
掉落物品和資源生產(chǎn)同樣可以統(tǒng)一管理,所以的場(chǎng)景(邏輯)進(jìn)程都將生產(chǎn)新物件的請(qǐng)求發(fā)給物品分配服務(wù),由物品分配服務(wù)生產(chǎn)出新物件后通知位置服務(wù)器產(chǎn)生新物品。
這樣一系列的做法,最終保證了,每個(gè)場(chǎng)景服務(wù)器都有一個(gè)唯一的數(shù)據(jù)源——位置服務(wù)進(jìn)程。它跟持久化在數(shù)據(jù)庫(kù)中的數(shù)據(jù)無(wú)關(guān),跟時(shí)鐘也無(wú)關(guān)。由此帶來(lái)的調(diào)試便利是很顯著的。
最近,面臨諸多進(jìn)程的設(shè)計(jì)時(shí),最先面臨的一個(gè)復(fù)雜點(diǎn)在于啟動(dòng)階段。顯然,每個(gè)進(jìn)程都配有一套配置文件指出其它進(jìn)程的地址并不是一個(gè)好主意。而為每個(gè)
服務(wù)都分配一個(gè)子域名在開(kāi)發(fā)期也不太合適。結(jié)果我們采取了一個(gè)簡(jiǎn)單的方案:?jiǎn)为?dú)開(kāi)發(fā)了一個(gè)名字服務(wù)器。它的功能類似 DNS
,但是可以讓每個(gè)進(jìn)程自由的注冊(cè)自己的位置,還可以定期匯報(bào)自己的當(dāng)前狀態(tài)。這樣,我們可以方便的用程序查詢到需要的服務(wù)。名字服務(wù)器的協(xié)議用的類似
POP3 的文本協(xié)議,這讓我們可以人手工 telnet 上去查閱。我相信以后我們的維護(hù)人員會(huì)喜歡這樣的設(shè)計(jì)的。:D
以上,國(guó)慶假期結(jié)束以來(lái)的工作。感謝項(xiàng)目組其他同事的辛勤編碼。
MMORPG不同于其它的局域網(wǎng)的網(wǎng)絡(luò)游戲,它是一個(gè)面向整個(gè)Internet的連接人數(shù)過(guò)萬(wàn)的網(wǎng)絡(luò)游戲,因此他的服務(wù)器端設(shè)計(jì)則極為重要
服務(wù)器的基本設(shè)置 在大型網(wǎng)絡(luò)游戲里,通常設(shè)計(jì)為C/S結(jié)構(gòu),客戶端不再對(duì)數(shù)據(jù)進(jìn)行邏輯處理,而只是一個(gè)收發(fā)裝置,從玩家那里接受到操作信息,然后反饋給服務(wù)器,再由服務(wù)器進(jìn)行處理后發(fā)回客戶端,經(jīng)客戶端通過(guò)圖形化處理,給玩家呈現(xiàn)出一個(gè)繽紛的游戲世界。

登陸服務(wù)器
在這里也可以稱之為連接服務(wù)器,網(wǎng)絡(luò)游戲的客戶端一般是連接到這里,然后再由該連接服務(wù)器根據(jù)不同的需要,把游戲消息轉(zhuǎn)發(fā)給其它相應(yīng)的服務(wù)器(邏輯和地圖服務(wù)器)也因?yàn)樗强蛻舳酥苯舆B接的對(duì)象,它同時(shí)也負(fù)擔(dān)了驗(yàn)證客戶身份的工作。
地圖服務(wù)器
在這里也可以稱之為連續(xù)事件服務(wù)器。在這個(gè)服務(wù)器里要處理的對(duì)象(玩家)所做的動(dòng)作都是一個(gè)連續(xù)事件。例如玩家從A點(diǎn)移動(dòng)到B點(diǎn),這樣一個(gè)動(dòng)作,需要一定的時(shí)間進(jìn)行移動(dòng),因此說(shuō)移動(dòng)是一個(gè)連續(xù)事件。
邏輯服務(wù)器
在這里可以稱之為瞬時(shí)事件服務(wù)器,在這個(gè)服務(wù)器里,處理對(duì)象(玩家)所做的動(dòng)作均可以在非常斷時(shí)間內(nèi)完成完成。例如玩家從商店購(gòu)買一瓶藥書(shū),當(dāng)玩家確認(rèn)
購(gòu)買后,服務(wù)器先扣除玩家的游戲幣,然后再把相應(yīng)的藥水瓶加入玩家的背包里。這2個(gè)操作對(duì)于服務(wù)器來(lái)說(shuō),只是2個(gè)數(shù)字的加減,計(jì)算完這兩個(gè)數(shù)字的加減,這
個(gè)事件就可以結(jié)束了。因此,我們可以說(shuō)這個(gè)事件是一個(gè)瞬時(shí)事件
服務(wù)器組的改進(jìn) 不過(guò)在實(shí)際應(yīng)用的過(guò)程中,游戲服務(wù)器的結(jié)構(gòu)要比上面所說(shuō)的3種服務(wù)結(jié)構(gòu)要復(fù)雜些,不過(guò)也都是在這3種最基本的服務(wù)器架構(gòu)下進(jìn)行擴(kuò)充,擴(kuò)充的主要是其它輔助功能。在實(shí)際應(yīng)用里可能增加的2種服務(wù)器,數(shù)據(jù)庫(kù)服務(wù)器,計(jì)費(fèi)服務(wù)器,由邏輯服務(wù)器獨(dú)立出來(lái)的聊天服務(wù)器。

數(shù)據(jù)庫(kù)服務(wù)器
數(shù)據(jù)庫(kù)服務(wù)器其實(shí)就是專門(mén)利用一臺(tái)服務(wù)器進(jìn)行數(shù)據(jù)庫(kù)的讀寫(xiě)操作。這點(diǎn)特別是在大型的網(wǎng)絡(luò)游戲里尤為重要。因?yàn)樵诖笮途W(wǎng)絡(luò)游戲里,要處理玩家的數(shù)據(jù)量非常大,如果不利用專門(mén)的服務(wù)器進(jìn)行處理,很有可能會(huì)拖累這個(gè)服務(wù)器組。
計(jì)費(fèi)服務(wù)器
通常在商業(yè)的網(wǎng)絡(luò)游戲里出現(xiàn),用于記錄玩家在線的時(shí)間,給收費(fèi)提供依據(jù),同時(shí)也是整個(gè)服務(wù)器組里最重要的部分,一旦出現(xiàn)問(wèn)題,運(yùn)營(yíng)商就不用賺錢了。
聊天服務(wù)器
在游戲里的聊天功能是屬于一種瞬時(shí)動(dòng)作,理論上是放在邏輯服務(wù)器里進(jìn)行處理。不過(guò)在大型網(wǎng)絡(luò)游戲里,因?yàn)檫@個(gè)部分功能與游戲里的其它部分聯(lián)系并不緊密,因此可以獨(dú)立出來(lái)做一個(gè)功能服務(wù)器。
服務(wù)器的集群設(shè)置 在大型游戲的應(yīng)用過(guò)程中,實(shí)際需要處理的玩家數(shù)量可能過(guò)萬(wàn),一臺(tái)普通的服務(wù)器是無(wú)法完成所要完成的工作,因此,在實(shí)際應(yīng)用的時(shí)候,通常是由一組多臺(tái)服務(wù)器共同完成一個(gè)功能。
例如地圖服務(wù)器,可以根據(jù)需要,把游戲里所有的地域進(jìn)行劃分,劃分為N個(gè)區(qū)域,然后讓這一個(gè)區(qū)域里發(fā)生的事件都用一個(gè)特定的服務(wù)器進(jìn)行處理。這樣做的目的是減少一個(gè)服務(wù)器所承擔(dān)的計(jì)算量,把整個(gè)系統(tǒng)組成一個(gè)分布式的網(wǎng)絡(luò)。
不過(guò)這樣做的同時(shí)會(huì)造成一個(gè)麻煩:當(dāng)一位玩家從區(qū)域1,移動(dòng)到區(qū)域2。這個(gè)時(shí)候,就必須先在服務(wù)器1里把玩家刪除,然后再在區(qū)域2里加入玩家。同時(shí)需要
由服務(wù)器1向服務(wù)器2轉(zhuǎn)移玩家的數(shù)據(jù)信息(因?yàn)榉?wù)器組在工作的時(shí)候,玩家的信息只能保存在當(dāng)前所在區(qū)域的服務(wù)器里),也就是說(shuō)一旦玩家發(fā)生服務(wù)器間區(qū)域
移動(dòng),服務(wù)器端就不可避免的造成數(shù)據(jù)通訊。因?yàn)檫@種移動(dòng)并不是有規(guī)律的,玩家所在的服務(wù)器都有可能到達(dá)其它服務(wù)器。這樣,如果服務(wù)器組里有N臺(tái)地圖服務(wù)
器,那么,每個(gè)服務(wù)器都可能向其它N-1臺(tái)服務(wù)器產(chǎn)生連接,總共就可能產(chǎn)生N×N個(gè)連接。如此數(shù)量連接如果只是使用普通的socket設(shè)計(jì),就很有可能會(huì)
給服務(wù)器通訊間的各種問(wèn)題所困擾,為此,在商業(yè)網(wǎng)絡(luò)游戲的服務(wù)器之間,通常都使用成熟的第三方的通訊中間件,如ACE,ICE等作為網(wǎng)絡(luò)連接的傳輸層。
作者博客:
http://blog.csdn.net/yahle
大綱:
項(xiàng)目的歷史背景
服務(wù)器的設(shè)計(jì)思路
服務(wù)器的技術(shù)
服務(wù)器的設(shè)計(jì)
服務(wù)器的改進(jìn)
圖形引擎myhoho及UI庫(kù)的設(shè)計(jì)
客戶端與服務(wù)器的集成
網(wǎng)
絡(luò)游戲一般采用C\S模式,網(wǎng)絡(luò)游戲的設(shè)計(jì)重點(diǎn),我認(rèn)為在于Server端,也就是我們說(shuō)的服務(wù)器。在服務(wù)器端的設(shè)計(jì),我把服務(wù)器按照功能分為2個(gè)部分,
一個(gè)負(fù)責(zé)游戲世界的處理,一個(gè)服務(wù)器服務(wù)器與客戶端的通訊。在負(fù)責(zé)游戲世界的處理的服務(wù)器,我又按照功能分為地圖服務(wù)器和邏輯服務(wù)器。這樣劃分的依據(jù)是他
們處理的內(nèi)容不同進(jìn)行。當(dāng)初的設(shè)計(jì)還考慮到系統(tǒng)的集群功能,可以把游戲的地圖移動(dòng)處理和游戲的邏輯處理都分別分?jǐn)偟狡渌?wù)器里面去。但是做到最后,發(fā)現(xiàn)
這樣的設(shè)計(jì)也不是太好,主要是因?yàn)樵谔幚硪恍┯螒蚴录臅r(shí)候需要兩個(gè)服務(wù)器之間進(jìn)行協(xié)同,這樣勢(shì)必要?jiǎng)?chuàng)建一定的網(wǎng)絡(luò)游戲消息,在開(kāi)始制作游戲的時(shí)候,因?yàn)?
需要系統(tǒng)的東西不是很多,所以沒(méi)有太注意,到項(xiàng)目的后期,想增加一個(gè)功能的時(shí)候,就發(fā)現(xiàn)在處理船只沉沒(méi)的時(shí)候,服務(wù)器需要傳遞很多同步數(shù)據(jù),而且服務(wù)器各
自在設(shè)置玩家數(shù)據(jù)的時(shí)候,也有很多重復(fù)的地方。如果今后還要再加點(diǎn)什么其它功能,那要同步的地方就實(shí)在是太多了,所以按照功能把服務(wù)器分為2個(gè)部分的設(shè)計(jì)
還是存在缺陷的,如果讓我重新再來(lái),我會(huì)選擇單服務(wù)器的設(shè)計(jì),當(dāng)然這個(gè)服務(wù)器還是要和連接服務(wù)器進(jìn)行分離,因?yàn)橛螒虻倪壿嬏幚砗团c玩家的通訊還是很好分開(kāi)
的,而且分開(kāi)的話,也有利于邏輯服務(wù)器的設(shè)計(jì)。
登陸(連接)服務(wù)器的設(shè)計(jì):
在網(wǎng)絡(luò)游戲里,其中一個(gè)很大的難點(diǎn)就是玩家與服務(wù)器的通訊,在Windos的服務(wù)器架構(gòu)下,網(wǎng)絡(luò)游戲服務(wù)器端采用的I/O模型,通常是完成端口。在項(xiàng)目
開(kāi)始時(shí)研究完成端口,感覺(jué)很難,根本看不懂,因?yàn)樗诤芏嗟胤脚c以前寫(xiě)網(wǎng)絡(luò)通訊軟件時(shí)用的方法不同。但是當(dāng)我分析過(guò)3個(gè)完成端口的程序后,基本了解的它的
使用方法。而且在懂以后,回過(guò)頭來(lái)看,其它完成端口的概念也不是很復(fù)雜,只要能清楚的了解幾個(gè)函數(shù)的使用方法以及基本的處理框架流程,你就會(huì)發(fā)現(xiàn)它其實(shí)非
常的簡(jiǎn)單。
完成端口的一些需要理解的地方:
1。消息隊(duì)列
2。工作線程
3。網(wǎng)絡(luò)消息返回結(jié)構(gòu)體
一般我們?cè)谠O(shè)計(jì)服務(wù)器端的時(shí)候,最關(guān)鍵的地方是如何分辯剛剛收到的網(wǎng)絡(luò)數(shù)據(jù)是由那個(gè)玩家發(fā)送過(guò)來(lái)的,如果是采用消息事件驅(qū)動(dòng)的話,是可以得到一個(gè)
socket的值,然后再用這個(gè)值與系統(tǒng)里存在的socket進(jìn)行比對(duì),這樣就可以得到是那位玩家發(fā)送過(guò)來(lái)的游戲消息。我在還沒(méi)有使用完成端口的時(shí)候,就
是使用這個(gè)方法。這樣的設(shè)計(jì)有一個(gè)缺點(diǎn)就是每次收到數(shù)據(jù)的時(shí)候回浪費(fèi)很多時(shí)間在于確定消息發(fā)送者身份上。但是在完成端口的設(shè)計(jì)里,我們可以采用一個(gè)取巧的
方法進(jìn)行設(shè)計(jì)。所以,這個(gè)問(wèn)題很輕易的就結(jié)局了,而且系統(tǒng)開(kāi)銷也不是很大,關(guān)于完成端口,可以參考一下的文章:
《關(guān)于Winsock異步I/O模型中的事件模型》
http://search.csdn.net/Expert/topic/166/166227.xml?temp=.4639093
《手把手教你玩轉(zhuǎn)SOCKET模型之重疊I/O篇》
http://blog.csdn.net/piggyxp/archive/2004/09/23/114883.aspx
《學(xué)習(xí)日記](méi)
IOCP的學(xué)習(xí)--初步理解》
http://www.gameres.com/bbs/showthread.asp?threadid=25898
《用完成端口開(kāi)發(fā)大響應(yīng)規(guī)模的Winsock應(yīng)用程序》
http://www.xiaozhou.net/ReadNews.asp?NewsID=901
《理解I/O Completion Port》
http://dev.gameres.com/Program/Control/IOCP.htm
幾個(gè)關(guān)鍵函數(shù)的說(shuō)明:
http://msdn.microsoft.com/library/en-us/fileio/fs/postqueuedcompletionstatus.asp?frame=true
http://msdn.microsoft.com/library/en-us/fileio/fs/createiocompletionport.asp?frame=true
http://msdn.microsoft.com/library/en-us/fileio/fs/getqueuedcompletionstatus.asp?frame=true
http://msdn.microsoft.com/library/en-us/winsock/winsock/wsarecv_2.asp?frame=true
如果你能認(rèn)真的搞清楚上面的東西,我估計(jì)你離理解完成端口就只有一步了。剩下的這一步就是自己編碼實(shí)現(xiàn)一個(gè)下了。有些時(shí)候,看得懂了不一定會(huì)實(shí)際應(yīng)用,不實(shí)實(shí)在在的寫(xiě)一點(diǎn)程序,驗(yàn)證一下你的想法,是不會(huì)真正搞清楚原理的。
不
過(guò)除非你想深入的研究網(wǎng)絡(luò)技術(shù),否則只要知道怎么用就可以了,剩下的就是尋找一個(gè)合適的別人封裝好的類來(lái)使用。這樣可以節(jié)省你很多的事件,當(dāng)然拿來(lái)的東西
最好有源代碼,這樣如果發(fā)生什么問(wèn)題,你也好確定是在那個(gè)地方出錯(cuò),要改或者擴(kuò)充功能都會(huì)方便很多。當(dāng)然,還要注意人家的版權(quán),最好在引用別人代碼的地方
加一些小小的注解,這樣用不了多少時(shí)間,而且對(duì)你,對(duì)原作者都有好處^_^。
不過(guò)在
完成端口上我還是沒(méi)有成為拿來(lái)主義者,還是自己封裝了完成端口的操作,原因找到的源代碼代碼封裝的接口函數(shù)我怎么看怎么覺(jué)得別扭,所以最后還是自己封裝了
一個(gè)完成端口,有興趣的可以去看我的源代碼,里面有很詳細(xì)的注解。而且就我看來(lái),要拿我封裝的完成端口類使用起來(lái)還是很簡(jiǎn)單的。使用的時(shí)候,只要繼承我的
CIOCP,然后,根據(jù)需要覆蓋3個(gè)虛函數(shù)(OnAccept,OnRead,OnClose)就可以了,最多是在連接函數(shù)里,需要用一個(gè)函數(shù)去設(shè)置一下
完成端口信息。當(dāng)然,我封裝的類稍微簡(jiǎn)單了一些,如果要拿來(lái)響應(yīng)大規(guī)模連接,還是存在很多的問(wèn)題,但是如果只是針對(duì)少量連接,還是可以應(yīng)付的。
對(duì)
于客戶端的I/O模型,我就沒(méi)有那么用心的去尋找什么好的解決方案,采用了一個(gè)最簡(jiǎn)單的,最原始的阻塞線程的方法做。原理很簡(jiǎn)單:創(chuàng)建一個(gè)sockt,把
socket設(shè)置為阻塞,連接服務(wù)器成功后,啟動(dòng)一個(gè)線程,在線程里面用recv()等待服務(wù)器發(fā)過(guò)來(lái)的消息。在我的代碼里,也是把阻塞線程的方法封裝成
一個(gè)類,在使用的時(shí)候,先繼承TClientSocket,然后覆蓋(重載)里面的OnRead()函數(shù),并在里面寫(xiě)入一些處理收到數(shù)據(jù)后的操作代碼。在
用的時(shí)候,只要connect成功,系統(tǒng)就會(huì)自動(dòng)啟動(dòng)一個(gè)接收線程,一旦有數(shù)據(jù)就觸發(fā)剛才覆蓋的OnRead函數(shù)。這個(gè)類我也不是完全直接寫(xiě)的,在里面使
用了別人的一些代碼,主要是讓每個(gè)類都能把線程封裝起來(lái),這樣在創(chuàng)建不同的類的實(shí)體的時(shí)候,每個(gè)類的實(shí)體自己都會(huì)有一個(gè)單獨(dú)的數(shù)據(jù)接收線程。
當(dāng)
然除了阻塞線程的方法,比較常用的還有就是用消息事件的方法收取數(shù)據(jù)了。我剛開(kāi)始的時(shí)候,也是采用這個(gè)方法(以前用過(guò)^_^),但是后來(lái)發(fā)現(xiàn)不太好封裝,
最后采用阻塞線程的方法,這樣做還有一個(gè)好處可以讓我的代碼看起來(lái)更加舒服一些。不過(guò)就我分析《航海世紀(jì)》客戶端采用的是消息事件的I/O模型。其它的網(wǎng)
絡(luò)游戲就不太清楚了,我想也應(yīng)該是采用消息事件方式的吧。。
我記得在gameres上看到過(guò)某人寫(xiě)的一篇關(guān)于完成端口的筆記,他在篇末結(jié)束的時(shí)候,提出一個(gè)思考題:我們?cè)趯W(xué)習(xí)完成端口的時(shí)候,都知道它是用于server端的操作,而且很多文章也是這樣寫(xiě)的,但是不知道有沒(méi)有考慮過(guò),用完成端口做客戶端來(lái)使用?
其實(shí)這個(gè)問(wèn)題很好回答,答案是OK。拿IOCP做客戶端也是可行的,就以封裝的IOCP為例,只要在繼承原來(lái)的CIOCP類的基礎(chǔ)上,再寫(xiě)一個(gè)Connect(char * ip, int port)的函數(shù),就可以實(shí)現(xiàn)客戶端的要求了。
- bool CIOCPClient::Connect(char *ip, int port)
- {
-
-
- if (!bInit)
-
- if (!Init())
-
- return false;
-
-
- SOCKET m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
-
- if (m_socket == SOCKET_ERROR)
- return false;
-
-
-
- sockaddr_in ClientAddr;
-
- ClientAddr.sin_family = AF_INET;
-
- ClientAddr.sin_port = htons(port);
-
- ClientAddr.sin_addr.s_addr = inet_addr(ip);
-
- bind(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr));
-
- if (connect(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)
-
- return false;
- this->m_workThread = true;
-
-
-
- g_hwThread = CreateThread(NULL, 0, WorkThread, (LPVOID)this, 0, &m_wthreadID);
- this->SetIoCompletionPort(m_socket, NULL);
- return true;
-
- }
前面一段是用來(lái)連接服務(wù)器,所有的客戶端程序都是要這樣做的,當(dāng)連接成功后,m_socket就是我們想要的用于與服務(wù)器
端通訊的socket,然后,我們啟動(dòng)工作線程,并使用SetIoCompletionPort來(lái)設(shè)置完成端口監(jiān)聽(tīng)的socket。只要在原來(lái)的基礎(chǔ)上增
加一個(gè)函數(shù),就可以把用于服務(wù)器的ICOP變成用于客戶端的IOCP。
在收到網(wǎng)絡(luò)數(shù)據(jù)以后,下一步就是根據(jù)需要,把收到的網(wǎng)絡(luò)數(shù)據(jù)包轉(zhuǎn)變?yōu)橛螒蛳?shù)據(jù)包。在轉(zhuǎn)換之前,首先是要從收到的網(wǎng)絡(luò)數(shù)據(jù)里面提取出有效的消息。這里
為什么說(shuō)是要提取有效部分?其主要原因是,我們創(chuàng)建的游戲消息數(shù)據(jù),在進(jìn)行網(wǎng)絡(luò)傳輸?shù)臅r(shí)候,不是以消息的長(zhǎng)度來(lái)傳的,而是根據(jù)系統(tǒng)在接收到發(fā)送數(shù)據(jù)請(qǐng)求的
時(shí)候,根據(jù)實(shí)際情況來(lái)發(fā)送的。例如我這里有一條很長(zhǎng)的游戲消息,有3k,但是系統(tǒng)一次只能發(fā)送1k的數(shù)據(jù),所以,我們的游戲消息,只能把我們的游戲消息分
為3個(gè)包,分3次發(fā)送,這樣在我們接收消息的時(shí)候,就會(huì)觸發(fā)3次OnRead,而這3次OnRead收到的數(shù)據(jù)都不是一次完整的游戲消息。所以,我們?cè)谑?
到網(wǎng)絡(luò)數(shù)據(jù)后,要先和上一次收到的網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行合并,然后再在里面提取出有效的游戲消息,并在提取后,把已經(jīng)提取的部分刪除。我在這里把這一步操作封裝到
一個(gè)類里CBuftoMsg。這里順便說(shuō)明一下:一條游戲消息的網(wǎng)絡(luò)數(shù)據(jù)包是以0x00EEEE(16進(jìn)制)為結(jié)束標(biāo)記(《航海世紀(jì)》的做法)。
我在這里把 CBuftoMsg
的代碼貼出來(lái),主要是因?yàn)椋以趯?xiě)本文的時(shí)候,發(fā)現(xiàn)一個(gè)驚天動(dòng)地的bug,有興趣的讀者可以自己去找一下。不過(guò)一開(kāi)始寫(xiě)代碼的時(shí)候,還不是這樣的,當(dāng)初的
代碼bug比這個(gè)還要多,問(wèn)題還要嚴(yán)重,嚴(yán)重到經(jīng)常讓服務(wù)器程序莫名其妙的崩潰,而且這個(gè)問(wèn)題,一直到5月份,系統(tǒng)在進(jìn)行集成測(cè)試的時(shí)候才發(fā)現(xiàn)并解決(還
沒(méi)有徹底解決,至少目前我還發(fā)現(xiàn)了bug,),以前一直都沒(méi)有怎么注意到這個(gè)問(wèn)題,而且我們還把因?yàn)檫@個(gè)bug造成的問(wèn)題,歸結(jié)到線程的互斥上去^_^!
我的登陸服務(wù)器,除了基本的處理網(wǎng)絡(luò)數(shù)據(jù)包以外,還負(fù)責(zé)玩家系統(tǒng)的登陸驗(yàn)證,這部分東西不是很復(fù)雜,在我的程序里,只是簡(jiǎn)單的從ini文件里讀取玩家的信息而已,有興趣的自己去看我的代碼(不過(guò)這部分遠(yuǎn)還沒(méi)有真正的完善,存在很多問(wèn)題)。
除
了登陸驗(yàn)證以外,在登陸程序還負(fù)責(zé)進(jìn)行消息轉(zhuǎn)發(fā),就是把客戶端的消息分別發(fā)送到不同的服務(wù)器。如果當(dāng)初設(shè)計(jì)的是一個(gè)邏輯服務(wù)器,這個(gè)功能就可以簡(jiǎn)單很多,
只要發(fā)送到一個(gè)服務(wù)器里就可以了。現(xiàn)在的要發(fā)到2個(gè)服務(wù)器,所以還需要對(duì)收到的游戲消息進(jìn)行分類。為了方便,我對(duì)原來(lái)定義消息的ID進(jìn)行了分類,所以,在
GameMessageID.h文件里定義的游戲消息對(duì)應(yīng)的ID編號(hào)不是順序編排的。不過(guò)也因?yàn)檫@樣,在現(xiàn)在看來(lái),這樣的設(shè)計(jì),有一些不太好。在整個(gè)系統(tǒng)
里,存在有4個(gè)主體,他們之間互相發(fā)送,就用了12組的數(shù)據(jù),為了方便計(jì)算,我把一個(gè)變量的范圍分為16個(gè)不同的區(qū)域,這樣每個(gè)區(qū)域只有16個(gè)值可以用
(我這里是用char類型256/16=16)。在加上用另外一個(gè)變量表示邏輯上上的分類(目前按照功能分了12組,有登陸、貿(mào)易、銀行、船廠等)這樣對(duì)
于貿(mào)易這個(gè)類型的游戲消息,從客戶端發(fā)送到邏輯服務(wù)器上,只能有16中可能性,如果要發(fā)送更多消息,可能要增加另外一個(gè)邏輯分類:貿(mào)易2^_^!當(dāng)初這樣
的設(shè)計(jì)只是想簡(jiǎn)化一下系統(tǒng)的處理過(guò)程,不過(guò)卻造成了系統(tǒng)的擴(kuò)充困難,要解決也不是沒(méi)有辦法,把類型分類的變量由char類型,改為int類型,這樣對(duì)一個(gè)
變量分區(qū),在范圍上會(huì)款很多,而且不會(huì)造成邏輯分類上的困擾,但是,這樣存在一個(gè)弊端就是就是每條網(wǎng)絡(luò)消息數(shù)據(jù)包的長(zhǎng)度增加了一點(diǎn)點(diǎn)。不要小看這一個(gè)字節(jié)
的變量,現(xiàn)在設(shè)計(jì)的一條游戲消息頭的長(zhǎng)度是10個(gè)字節(jié),如果把char改為int,無(wú)形中就增加了3個(gè)字節(jié),在和原來(lái)的比較,這樣每條消息在消息頭部分,
就多出23%,也就是我們100M的網(wǎng)絡(luò)現(xiàn)在只能利用77%而已。
^_^呵呵看出什么問(wèn)題沒(méi)有?
沒(méi)有,那我告訴你,有一個(gè)概念被偷換了,消息頭的數(shù)據(jù)不等于整條游戲的消息數(shù)據(jù),所以,消息頭部分雖然多出了23%,但是整條游戲消息并不會(huì)增加這么
多,最多增加17%,最少應(yīng)該不會(huì)操作5%。平均起來(lái),應(yīng)該在10%左右(游戲消息里,很多消息的實(shí)際部分可能就一個(gè)int變量而已)。不過(guò),就算是
10%,也占用了帶寬。
^_^呵呵還看出什么問(wèn)題沒(méi)有?
^_^先去讀一下我的代碼,再回頭看看,上面的論述還有什么問(wèn)題。
實(shí)際上,每條游戲消息由:消息頭、消息實(shí)體、結(jié)束標(biāo)記組成,其中固定的是消息頭和結(jié)束標(biāo)記,所以,實(shí)際上一條實(shí)際上游戲消息的數(shù)據(jù)包,最多比原來(lái)的多15%,平均起來(lái),應(yīng)該是8%~10%的增量而異。
好了,不在這個(gè)計(jì)算細(xì)節(jié)上扣太多精力了。要解決這個(gè)問(wèn)題,要么是增加網(wǎng)絡(luò)數(shù)據(jù)的發(fā)送量,要么,就是調(diào)整游戲結(jié)構(gòu),例如,把兩個(gè)功能服務(wù)器合并為一個(gè)服務(wù)
器,這樣服務(wù)器的對(duì)象實(shí)體就由原來(lái)的4個(gè)分為3個(gè),兩兩間的通訊,就由原來(lái)的12路縮減為6路,只要分8個(gè)區(qū)域就ok了。這樣每個(gè)邏輯分類就有32條游戲
消息可以使用。當(dāng)然,如果進(jìn)一步合并服務(wù)器,把服務(wù)器端都合并到一個(gè)程序,那就不用分類了^_^!
在登陸服務(wù)器目錄下,還有一組mynet.h/mynet.cpp的文件,是我當(dāng)初為服務(wù)器端設(shè)計(jì)的函數(shù),封裝的是消息事件網(wǎng)絡(luò)響應(yīng)模型。只不過(guò)封裝得
不是怎么好,被拋棄不用了,有興趣的可以去看看,反正我是不推薦看的。只不過(guò)是在這里說(shuō)明一下整個(gè)工程目錄的結(jié)構(gòu)而已。
轉(zhuǎn)自http://coderplay.javaeye.com/blog/94209
前些天給echo_server寫(xiě)了個(gè)非常簡(jiǎn)單的連接壓力測(cè)試程序,
1. -module(stress_test).
2.
3. -export([start/0, tests/1]).
4.
5. start() ->
6. tests(12345).
7.
8. tests(Port) ->
9. io:format("starting~n"),
10. spawn(fun() -> test(Port) end),
11. spawn(fun() -> test(Port) end),
12. spawn(fun() -> test(Port) end),
13. spawn(fun() -> test(Port) end).
14.
15. test(Port) ->
16. case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of
17. {ok, _} ->
18. test(Port);
19. _ ->
20. test(Port)
21. end.
一開(kāi)始我的這個(gè)stress_test客戶端運(yùn)行在windows上面, echo_server服務(wù)器端運(yùn)行在linux上面。
結(jié)果接受了1016個(gè)連接就停止了. 于是我用ulimit -n 改了服務(wù)器端的文件描述符數(shù)量為10240.
接著還是如此,折騰了幾天,最終還是沒(méi)有搞明白。
于是就求助于公司的linux編程牛人,結(jié)果讓我一倒... 客戶端沒(méi)有修改文件描述符個(gè)數(shù). windows上得在注冊(cè)表里面改.
牛人開(kāi)始對(duì)這東西的性能感興趣了,剛好我摸了一陣子erlang的文檔,于是我倆就走向了erlang網(wǎng)絡(luò)連接的性能調(diào)優(yōu)之旅啦~~過(guò)程真是讓人興奮。 我們很快通過(guò)了1024這一關(guān)~~到了4999個(gè)連接,很興奮.
但為什么4999個(gè)連接呢, 檢查一下代碼終于發(fā)現(xiàn)echo_server.erl定義了一個(gè)宏, 最大連接數(shù)為5000. 我又倒~~
修改編譯之后, 連接數(shù)跑到101xx多了, 太哈皮了!
再測(cè)102400個(gè)連接時(shí),到32767個(gè)連接數(shù)erl掛了~說(shuō)是進(jìn)程開(kāi)得太多了. 好在記得這個(gè)erl的參數(shù)+P,可以定義erlang能生成的進(jìn)程數(shù). 默認(rèn)是32768. 改了!
后面不知怎么著,在81231個(gè)連接停止了. 新的性能瓶頸又卡了我們. 好在牛人對(duì)linux熟, 用strace(這東西會(huì)莫名地退出),
stap查出一些苗頭.
我也想到在otp文檔好像提過(guò)另一個(gè)limit,那就是端口數(shù)...在此同時(shí)我們發(fā)現(xiàn)erlang在linux上是用的傳統(tǒng)poll模型.
但查erlang的源代碼發(fā)現(xiàn)是支持epoll的. 在網(wǎng)上搜了半天,終于搜到了個(gè)maillist的帖子.
代碼
- $./configure --enable-kernel-poll
由于我們的測(cè)試服務(wù)器是雙核的,我們?cè)谂渲玫臅r(shí)候也打開(kāi)了smp支持. 歡快的make & make install之后....
把 /proc/sys/net/ipv4/ip_local_port_range 的內(nèi)容改成了1024到65535. 最多也也能改成65535 :)
代碼
- $echo 1024 65535 > ip_local_port_range
另外再添加一個(gè)erl的環(huán)境變量
代碼
- $export ERL_MAX_PORTS=102400
于是開(kāi)始跑了,不過(guò)這次跑不一樣了
echo_server
- $erl -noshell +P 102400 +K true
+S 2 -smp
-s echo_server start
stress_test
- $erl -noshell +P 102400 +K true
+S 2 -smp
-s stress_test start
這里的+K true,
表示使用內(nèi)核poll,+S 2 表示兩個(gè)核.
這樣可歡快啦~~~ 10w大關(guān)過(guò)咯! 而且比剛才沒(méi)用epoll的速度快暴多~~
于是我們又開(kāi)始了204800個(gè)連接發(fā)測(cè)試了~~~
用top一看cpu占用率極低,服務(wù)器只在5%左右。 內(nèi)存也不是很大~~
轉(zhuǎn)自http://coderplay.javaeye.com/blog/93403
1. -module(echo_server).
2. -export([start/0,stop/0]).
3.
4. -define(LISTEN_PORT,12345). % 開(kāi)放端口
5. -define(MAX_CONN, 5000). % 最大連接數(shù)
6.
7. start() ->
8. process_flag(trap_exit, true), % 設(shè)置退出陷阱
9. tcp_server:start_raw_server(?LISTEN_PORT,
10. fun(Socket) -> socket_handler(Socket,self()) end,
11. ?MAX_CONN,
12. 0).
13.
14. %% 處理數(shù)據(jù)
15. socket_handler(Socket,Controller) ->
16. receive
17. {tcp, Socket, Bin} ->
18. gen_tcp:send(Socket, Bin); % echo
19. {tcp_closed, Socket} ->
20. ok;
21. _ ->
22. socket_handler(Socket,Controller)
23. end.
24.
25. stop() ->
26. tcp_server:stop(?LISTEN_PORT).
基于Joe Armstrong 的tcp_server模塊來(lái)做的, 試試先 :)
編譯
erl -noshell -s make all -s init stop
運(yùn)行
erl -noshell -sname coderplay -s echo_server start
原文網(wǎng)址:
erlang網(wǎng)絡(luò)編程的幾個(gè)性能調(diào)優(yōu)和注意點(diǎn)
原文作者:
coderplay
前些天給echo_server寫(xiě)了個(gè)非常簡(jiǎn)單的連接壓力測(cè)試程序,
下載:
stress_test.erl
- -module(stress_test).
- -export([start/0, tests/1]).
- start() ->
tests(12345).
- tests(Port) ->
io:format("starting~n"),
spawn(fun() -> test(Port)
end),
spawn(fun() -> test(Port)
end),
spawn(fun() -> test(Port)
end),
spawn(fun() -> test(Port)
end).
- test(Port) ->
case
gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}])
of
- {ok, _} ->
test(Port);
_ ->
test(Port)
end.
一開(kāi)始我的這個(gè)stress_test客戶端運(yùn)行在
windows上面,echo_server服務(wù)器端運(yùn)行在linux上面。結(jié)果接受了1016個(gè)連接就停止了. 于是我用ulimit -n改了服務(wù)器端的
文件描述符數(shù)量為10240. 接著還是如此,折騰了幾天,最終還是沒(méi)有搞明白。
于是就求助于公司的linux編程牛人,結(jié)果讓我一倒… 客戶端沒(méi)有修改文件描述符個(gè)數(shù). windows上得在注冊(cè)表里面改.
牛人開(kāi)始對(duì)這東西的性能感興趣了,剛好我摸了一陣子erlang的
文檔,于是我倆就走向了erlang網(wǎng)絡(luò)連接的性能調(diào)優(yōu)之旅啦~~過(guò)程真是讓人興奮。 我們很快通過(guò)了1024這一關(guān)~~到了4999個(gè)連接,很興奮.
但為什么4999個(gè)連接呢, 檢查一下代碼終于發(fā)現(xiàn)echo_server.erl定義了一個(gè)宏, 最大連接數(shù)為5000. 我又倒~~
修改
編譯之后, 連接數(shù)跑到101xx多了, 太哈皮了!
再測(cè)102400個(gè)連接時(shí),到32767個(gè)連接數(shù)erl掛了~說(shuō)是
進(jìn)程開(kāi)得太多了. 好在記得這個(gè)erl的參數(shù)+P,可以定義erlang能生成的進(jìn)程數(shù). 默認(rèn)是32768. 改了!
后面不知怎么著,在81231個(gè)連接停止了. 新的性能瓶頸又卡了我們. 好在牛人對(duì)linux熟,
用strace(這東西會(huì)莫名地退出),stap查出一些苗頭.我也想到在otp文檔好像提過(guò)另一個(gè)limit,那就是端口數(shù)…在此同時(shí)我們發(fā)現(xiàn)
erlang在linux上是用的傳統(tǒng)poll模型.但查erlang的源代碼發(fā)現(xiàn)是支持epoll的.
在網(wǎng)上搜了半天,終于搜到了個(gè)maillist的帖子.
- $./configure --enable-kernel-poll
由于我們的測(cè)試服務(wù)器是雙核的,我們?cè)谂渲玫臅r(shí)候也打開(kāi)了smp支持. 歡快的make & make install之后….
把 /proc/sys/net/ipv4/ip_local_port_range 的內(nèi)容改成了1024到65535. 最多也也能改成65535
- $echo 1024 65535 > ip_local_port_range
另外再添加一個(gè)erl的環(huán)境
變量
- $export ERL_MAX_PORTS=102400
于是開(kāi)始跑了,不過(guò)這次跑不一樣了
echo_server
- $erl -noshell +P 102400 +K true +S 2 -smp -s echo_server start
stress_test
- $erl -noshell +P 102400 +K true +S 2 -smp -s stress_test start
這里的+K true,表示
使用內(nèi)核poll,+S 2 表示兩個(gè)核. 這樣可歡快啦~~~ 10w大關(guān)過(guò)咯! 而且比剛才沒(méi)用epoll的速度快暴多~~
于是我們又開(kāi)始了204800個(gè)連接發(fā)測(cè)試了~~~
用top一看cpu占用率極低,服務(wù)器只在5%左右。內(nèi)存也不是很大~~
網(wǎng)路的硬件也有限,而人的創(chuàng)造也無(wú)限,在公網(wǎng)平均130ms的Latency下,是不存在“完全的”的同步情況。如何通過(guò)消除/隱藏延時(shí),將用戶帶入快速的交互式實(shí)時(shí)游戲中,體驗(yàn)完美的互動(dòng)娛樂(lè)呢?
以下六點(diǎn),將助你分清楚哪些我們可以努力,哪些我們不值得努力,弄明白實(shí)時(shí)游戲中同步問(wèn)題關(guān)鍵之所在,巧妙的化解與規(guī)避游戲,最終在適合普遍用戶網(wǎng)絡(luò)環(huán)境中(200ms),實(shí)現(xiàn)實(shí)時(shí)快速互動(dòng)游戲:
1. 基本情況:
(A) 網(wǎng)絡(luò)性能指標(biāo)一:帶寬,限制了實(shí)時(shí)游戲的人數(shù)容量
(B) 網(wǎng)絡(luò)性能指標(biāo)二:延時(shí),決定了實(shí)時(shí)游戲的最低反應(yīng)時(shí)間
2. 兩個(gè)基本原則:
(A) 讓所有的用戶屏幕上面表現(xiàn)出完全不同的表象是完全沒(méi)有問(wèn)題的。
(B) 把這些完全不同表象完全柔和在一個(gè)統(tǒng)一的邏輯中也是完全沒(méi)有問(wèn)題的。
3. 同步的十二條應(yīng)對(duì)策略:
(A) 最大可能減少游戲中的數(shù)據(jù)傳輸
(B) 將阻塞通信放到線程池中實(shí)現(xiàn)
(C) 永遠(yuǎn)不要為了等待某個(gè)數(shù)據(jù)而不讓游戲進(jìn)行下去
(D) 利用預(yù)測(cè)和插值改進(jìn)游戲的效果
(E) 當(dāng)使用預(yù)測(cè)插值的時(shí)候傳送的數(shù)據(jù)不僅包括坐標(biāo),還需要速度和加速度
(F) 將輸入數(shù)據(jù)枷鎖或者隊(duì)列化(例如鍵盤(pán)消息隊(duì)列),直到下次發(fā)送數(shù)據(jù)的時(shí)刻,傳統(tǒng)的方法是在固定的時(shí)間(發(fā)送數(shù)據(jù)前)檢測(cè)鍵盤(pán),在游戲的原理上隱藏延時(shí)
(G) 使用事件調(diào)度表,將需要在所有用戶客戶端同時(shí)發(fā)生的事件,提前廣播到所有用戶
(H) 使用多次攻擊來(lái)殺死一個(gè)精靈,盡量減少一次性的、確定性的、延時(shí)敏感的事件
(I) 延長(zhǎng)子彈或者火箭在空中飛行的時(shí)間(在其飛行的同時(shí),在所有客戶端進(jìn)行預(yù)測(cè)插值)
(J) 所有物體從一個(gè)地方移動(dòng)到另外一個(gè)地方都需要時(shí)間,避免諸如“瞬間移動(dòng)”的設(shè)計(jì)
(K) 盡量使游戲中所有精靈,飛船或者其他物體,都按照可預(yù)測(cè)的軌跡運(yùn)行,比如在移動(dòng)中增加慣性
(L) 充分發(fā)揮創(chuàng)造力,盡最大可能的合并游戲中前后相關(guān)的事件,合并游戲中存在的延時(shí)此問(wèn)題,需要在技術(shù)上改進(jìn)的同時(shí)也需要策劃有所重視,規(guī)避一些影響較大的設(shè)計(jì),巧妙的隱藏"延時(shí)"
4. 同步問(wèn)題現(xiàn)狀:
(A) 重視程度不夠:很多人尚未意識(shí)到此問(wèn)題的存在,曾有公司花半年時(shí)間打算做一款“松鼠大戰(zhàn)”的網(wǎng)絡(luò)版。
(B) 技術(shù)上無(wú)徹底解決方案:對(duì)于多數(shù)程序員,單機(jī)游戲技術(shù)善未成熟就匆匆步入網(wǎng)絡(luò)時(shí)代。
(C) 研究這個(gè)技術(shù)需要條件:需要有實(shí)力的公司才能提供,無(wú)此條件,即便有能力的程序員也無(wú)法成功。
5. 目前網(wǎng)游的三大技術(shù)難題:
(A) 服務(wù)器的響應(yīng)問(wèn)題:如何使服務(wù)器在支持越來(lái)越多的人數(shù)的情況下提供最高的響應(yīng)。
(B) 同步問(wèn)題:如何在有限的網(wǎng)絡(luò)響應(yīng)情況下,實(shí)現(xiàn)快速實(shí)時(shí)類游戲,提供最完美的交互。
(C) 服務(wù)器分布式問(wèn)題:如何在統(tǒng)一用戶數(shù)據(jù)的情況下,利用分部式將各個(gè)分散的“世界”統(tǒng)一到一個(gè)“世界”中。
誰(shuí)能真正解決好以上三個(gè)問(wèn)題,配合策劃在設(shè)計(jì)上的突破,將使其他人在至少兩年內(nèi)無(wú)法超越。
6. 相關(guān)補(bǔ)充:
(A) 網(wǎng)格技術(shù)現(xiàn)在還是抄作,真正用到游戲中,還有很多技術(shù)難點(diǎn)需要突破(比如:目前網(wǎng)格的單位計(jì)算時(shí)間是以秒計(jì)算).
(B) 其實(shí)與很多人想法相反的是現(xiàn)在3D技術(shù)早已不是主要的矛盾。而現(xiàn)在國(guó)內(nèi)外對(duì)于以上三個(gè)問(wèn)題可以說(shuō)處于同一個(gè)起跑線上,完全有機(jī)會(huì)取得先機(jī)。
(C) 現(xiàn)在解決同步問(wèn)題已經(jīng)很緊迫,而同時(shí)所需要的環(huán)境也已經(jīng)成熟,只要有所關(guān)注,半年之內(nèi)可以得出較成熟的結(jié)論
那么具體怎么解決呢?再下一步怎么辦?
這就得自己去實(shí)踐了,我只說(shuō)這么多了,哈哈,不然又教懶了那些成天再網(wǎng)上搜方案的人。
MMO的同步策略目前已經(jīng)沒(méi)有什么秘密可言了,簡(jiǎn)單敘述下各要素:
MMO人行走:MMO中如果是鼠標(biāo)點(diǎn)擊行走傳目的地+中間尋路接點(diǎn)就行了,服務(wù)器簡(jiǎn)單驗(yàn)證一下。如果是鍵盤(pán)控制行走的話,需要做簡(jiǎn)單的預(yù)測(cè)插值。
MMO打怪:要看怎么個(gè)打法,客戶端根本不必管服務(wù)端什么時(shí)候傳來(lái)消息,直接演示動(dòng)畫(huà)就行了,最好把被砍的過(guò)程動(dòng)畫(huà)做長(zhǎng)一點(diǎn),可以在播放動(dòng)畫(huà)的過(guò)程中等待服務(wù)器傳過(guò)來(lái)的延遲敏感事件,比如--該人已死。這樣處理起來(lái)比較容易,最重要的是客戶端看起來(lái)要流暢。
MMO插值:所謂“把不同表象柔和在一起”算法很多,簡(jiǎn)單的可以參考DR,位置不同的時(shí)候可以做一次線性插值,直接把人拉扯過(guò)去,或者二次線形插值,做一條平滑的曲線修正,看具體項(xiàng)目選擇了。
時(shí)鐘:就是時(shí)鐘同步,所有時(shí)鐘都是以“貞”為單位的,服務(wù)器主邏輯循環(huán)可以是5-10fps,多則無(wú)益,客戶端同樣維持一個(gè)和服務(wù)端頻率相同的消息同步貞。
時(shí)間貞:把ping值加到時(shí)間計(jì)算里面是多余的,按貞計(jì)算的話,服務(wù)端告訴客戶端在n貞發(fā)生xx事件的時(shí)候,客戶端收到的時(shí)候如果客戶端貞數(shù)大于n,說(shuō)明是發(fā)生過(guò)了的,那么做插值,如果小于n,說(shuō)明是未來(lái)發(fā)生的,那么加入時(shí)間表。
1 游戲中的行走,則是一個(gè)需要同步的重頭戲,當(dāng)今游戲分成兩種同步方式,一種是以服務(wù)器端為準(zhǔn),如果發(fā)現(xiàn)客戶端的坐標(biāo)和服務(wù)器的坐標(biāo)不符合的話,則拉回。這點(diǎn)的好處是可以彌補(bǔ)瞬移外掛的出現(xiàn),但在網(wǎng)絡(luò)環(huán)境差的情況下,則出現(xiàn)游戲不流暢。
所
以,我比較推薦使用以客戶端計(jì)算為準(zhǔn)的方法(當(dāng)然是我因?yàn)槲疫@個(gè)游戲?qū)τ螒蛄鲿承砸蠛芨撸蛻舳讼蚍?wù)器發(fā)送計(jì)算好的數(shù)據(jù),服務(wù)器經(jīng)過(guò)一定的預(yù)測(cè)判
斷,廣播給其他玩家,這個(gè)雖然是能引起和wow一樣的問(wèn)題,瞬移外掛,但完全可以從游戲設(shè)計(jì)方面進(jìn)行根本避免。(這里就不談策劃方面的問(wèn)題了)
2
游戲的戰(zhàn)斗,戰(zhàn)斗的同步性要求也相當(dāng)高,這里我也引用樓主說(shuō)的,在固定時(shí)間檢測(cè)鍵盤(pán),以一定頻率發(fā)送攻擊消息,這里有一個(gè)關(guān)鍵,就是服務(wù)器和客戶端都進(jìn)行
攻擊判定,即使服務(wù)器中沒(méi)有攻擊判定成功,但在客戶端判定成功,也要播放攻擊效果。不過(guò)一切計(jì)算效果以服務(wù)器為準(zhǔn)。這是一個(gè)欺騙玩家的手段。
ErLang語(yǔ)法中充滿了一些約定。大寫(xiě)字母開(kāi)頭的名字(比如Address),表示一個(gè)變量,包括參數(shù)、局部變量等;小寫(xiě)字母開(kāi)頭的單詞(比如ok),表示一個(gè)常量,叫做atom(原子的意思),包括常量名、函數(shù)名、模塊名等。
ErLang的注釋用%開(kāi)頭。ErLang用下劃線“_”表示任意變量,類似于Java的switch語(yǔ)法里面的default選項(xiàng)。
ErLang脫胎于Prolog,不過(guò),我覺(jué)得,ErLang語(yǔ)法和Haskell語(yǔ)法比較象,都是采用 -> 定義函數(shù)。
ErLang語(yǔ)句中的標(biāo)點(diǎn)符號(hào)用法很象文章的標(biāo)點(diǎn)符號(hào)。
整個(gè)函數(shù)定義結(jié)束用一個(gè)句號(hào)“.”;同一個(gè)函數(shù)中,并列的邏輯分支之間,用分號(hào)“;”分界;順序語(yǔ)句之間,用逗號(hào)“,”分隔。
ErLang中,{ }不是表示程序塊的開(kāi)頭和結(jié)尾,而是表示一種特殊的數(shù)據(jù)結(jié)構(gòu)類型——Tuple(元組),比如,{12, 3, ok}。我們可以把Tuple理解為定長(zhǎng)數(shù)組。
[ ] 則表示最基本的函數(shù)式編程的數(shù)據(jù)結(jié)構(gòu)類型——List。List數(shù)據(jù)結(jié)構(gòu)很基本,寫(xiě)法和用法也有一定的復(fù)雜度,不是表面上看起來(lái)那么簡(jiǎn)單,后面講解Closure的章節(jié)會(huì)詳細(xì)介紹List的最基本的構(gòu)造原理。
下面我們來(lái)看一個(gè)簡(jiǎn)單的例子。
我們首先定義一個(gè)最簡(jiǎn)單的函數(shù),把一個(gè)參數(shù)乘以10,然后加1。
times10( Number ) –>
Temp = 10 * Number,
Temp + 1.
為了說(shuō)明問(wèn)題,上面的代碼把乘法操作和加法操作分成兩個(gè)步驟。Temp = 10 * Number語(yǔ)句后面是逗號(hào),因?yàn)檫@是兩條順序執(zhí)行的語(yǔ)句。Temp + 1語(yǔ)句后面是句號(hào),表示整個(gè)函數(shù)定義結(jié)束。而且,可以看出,ErLang沒(méi)有return語(yǔ)句,最后執(zhí)行的那條語(yǔ)句的執(zhí)行結(jié)果就是返回值。
下面,我們把這個(gè)函數(shù)優(yōu)化一下。當(dāng)參數(shù)等于0的時(shí)候,直接返1;否則,就乘以10,然后加1,然后返回。這時(shí)候,我們就要用到case of邏輯分支語(yǔ)句,相當(dāng)于java的switch語(yǔ)句。
times10( Number ) –>
case Number of
0 -> 1;
_ ->
Temp = 10 * Number,
Temp + 1
end.
我們來(lái)仔細(xì)觀察這段ErLang程序。
當(dāng)Number等于0的時(shí)候,直接返回1。由于這是一條分支語(yǔ)句,和后面的分支是并列的關(guān)系,所以,1的后面的標(biāo)點(diǎn)符號(hào)是分號(hào)。后面這個(gè)分支,下劃線“_”表示任何其它值,這里就表示除了1之外的任何其它數(shù)值。
需要注意的一點(diǎn)是,case of語(yǔ)句需要用end結(jié)尾,end之前不需要有標(biāo)點(diǎn)符號(hào)。
上述代碼中的case of 語(yǔ)句,其實(shí)就是Pattern Match的一種。ErLang的Pattern Match很強(qiáng)大,能夠大幅度簡(jiǎn)化程序邏輯,后面進(jìn)行專門(mén)介紹。
Pattern Match
Pattern Match主要有兩個(gè)功能——比較分派和變量賦值。
其中,比較分派是最主要的功能。比較分派的意思是,根據(jù)參數(shù)值進(jìn)行條件分支的分派。可以把比較分派功能看作是一種類似于if, else等條件分支語(yǔ)句的簡(jiǎn)潔強(qiáng)大寫(xiě)法。
上面的例子中,case Number of 就是根據(jù)Number的值進(jìn)行比較分派。更常見(jiàn)的寫(xiě)法是,可以把Pattern Match部分提到函數(shù)定義分支的高度。于是,上述代碼可以寫(xiě)成下面的形式:
times10( 0 ) –> 1;
times10( Number ) –>
Temp = 10 * Number,
Temp + 1.
這段代碼由兩個(gè)函數(shù)定義分支構(gòu)成,由于兩個(gè)函數(shù)分支的函數(shù)名相同,而且參數(shù)個(gè)數(shù)相同,而且兩個(gè)函數(shù)定義分支之間采用分號(hào)“;”分隔,說(shuō)明這是同一個(gè)函數(shù)的定義。函數(shù)式編程語(yǔ)言中,這種定義方式很常見(jiàn),看起來(lái)形式很整齊,宛如數(shù)學(xué)公式。
這段代碼的含義是,當(dāng)參數(shù)值等于0的時(shí)候,那么,程序走第一個(gè)函數(shù)定義分支(即分號(hào)“;”結(jié)尾的“times10( 0 ) –> 1;”),否則,走下面的函數(shù)定義分支(即“times10( Number ) –>…”)。
第二個(gè)分支中的參數(shù)不是一個(gè)常數(shù),而是一個(gè)變量Number,表示這個(gè)分支可以接受任何除了0之外的參數(shù)值,比如,1、2、12等等,這些值將賦給變量Number。
因此,這個(gè)地方也體現(xiàn)了Pattern Match的第二個(gè)功能——變量賦值。
Pattern Match的形式可以很復(fù)雜,下面舉幾個(gè)典型的例子。
(1)數(shù)據(jù)結(jié)構(gòu)拆解賦值
前面將到了ErLang語(yǔ)言有一種相當(dāng)于定長(zhǎng)數(shù)組的Tuple類型,我們可以很方便地根據(jù)元素的位置進(jìn)行并行賦值。比如,
{First, Second} = {1, 2}
我們還可以對(duì)復(fù)合Tuple數(shù)據(jù)結(jié)構(gòu)進(jìn)行賦值,比如
{A, {B, C}, D} = { 1, {2, 3}, 4 }
List數(shù)據(jù)結(jié)構(gòu)的賦值也是類似。由于List的寫(xiě)法和用法不是那么簡(jiǎn)單,三言兩語(yǔ)也說(shuō)不清楚,還徒增困擾,這里不再贅述。
(2)assertEquals語(yǔ)句
在Java等語(yǔ)言中,我們寫(xiě)單元測(cè)試的時(shí)候,會(huì)寫(xiě)一些assert語(yǔ)句,驗(yàn)證程序運(yùn)行結(jié)果。這些assert語(yǔ)句通常是以API的方式提供,比如,assertTrue()、assertEquals()等。
在ErLang中,可以用簡(jiǎn)單的語(yǔ)句達(dá)到類似于assertTrue()、assertEquals()等API的效果。
比如,ErLang中,true = testA() 這樣的語(yǔ)句表示testA的返回結(jié)果必須是true,否則就會(huì)拋出異常。這個(gè)用法很巧妙。這里解釋一下。
前面講過(guò),ErLang語(yǔ)法約定,小寫(xiě)字母開(kāi)頭的名字,都是常量名。這里的true自然也是一個(gè)常量,既然是常量,我們不可能對(duì)它賦值,那么true = testA()的意思就不是賦值,而是進(jìn)行匹配比較。
(3)匹配和賦值同時(shí)進(jìn)行
我們來(lái)看這樣一段代碼。
case Result of
{ok, Message} -> save(Message);
{error, ErrorMessage} -> log(ErrorMessage)
end.
這段代碼中,Result是一個(gè)Tuple類型,包含兩個(gè)元素,第一個(gè)元素表示成功(ok)或者失敗(error),第二個(gè)元素表示具體的信息。
可以看到,這兩個(gè)條件分支中,同時(shí)出現(xiàn)了常量和變量。第一個(gè)條件分支中的ok是常量,Message是變量;第二個(gè)條件分支中的error是常量,ErrorMessage是變量。
這兩個(gè)條件分支都既有比較判斷,也有變量賦值。首先,判斷ResultTuple中的第一個(gè)元素和哪一個(gè)分支的第一個(gè)元素匹配,如果相配,那么把ResultTuple中的第二個(gè)元素賦給這個(gè)分支的第二個(gè)變量元素。即,如果Result的第一個(gè)元素是ok,那么走第一個(gè)條件分支,并且把Result的第二個(gè)元素賦給Message變量;如果Result的第二個(gè)元素是error,那么走第二個(gè)條件分支,并且把Result的第二個(gè)元素賦給ErrorMessage變量。
在Java等語(yǔ)言中,實(shí)現(xiàn)上述的條件分支邏輯,則需要多寫(xiě)幾條語(yǔ)句ErLang語(yǔ)法可以從形式上美化和簡(jiǎn)化邏輯分支分派復(fù)雜的程序。
除了支持?jǐn)?shù)相等比較,Pattern Match還可以進(jìn)行范圍比較、大小比較等,需要用到關(guān)鍵字when,不過(guò)用到when的情況,就比if else簡(jiǎn)潔不了多少,這里不再贅述。
匿名函數(shù)
ErLang允許在一個(gè)函數(shù)體內(nèi)部定義另一個(gè)匿名函數(shù),這是函數(shù)式編程的最基本的功能。這樣,函數(shù)式語(yǔ)言才可以支持Closure。我們來(lái)看一個(gè)ErLang的匿名函數(shù)的例子。
outer( C ) –>
Inner = fun(A, B) -> A + B + C end,
Inner(2, 3).
這段代碼首先定義了一個(gè)命名函數(shù)outer,然后在outer函數(shù)內(nèi)部定義了一個(gè)匿名函數(shù)。可以看到,這個(gè)匿名函數(shù)采用關(guān)鍵字fun來(lái)定義。前面講過(guò),函數(shù)式編程的函數(shù)就相當(dāng)于面向?qū)ο缶幊痰念悓?shí)例對(duì)象,匿名函數(shù)自然也是這樣,也相當(dāng)于類實(shí)例,我們可以把這個(gè)匿名函數(shù)賦給一個(gè)變量Inner,然后我們還可以把這個(gè)變量當(dāng)作函數(shù)來(lái)調(diào)用,比如,Inner(2, 3)。
fun是ErLang用來(lái)定義匿名函數(shù)的關(guān)鍵字。這個(gè)關(guān)鍵字很重要。fun定義匿名函數(shù)的用法不是很復(fù)雜,和命名函數(shù)定義類似。
函數(shù)分支的定義也是類似,只是需要用end結(jié)尾,而不是用句號(hào)“.”結(jié)尾,而且fun只需要寫(xiě)一次,不需要向命名函數(shù)那樣,每個(gè)分支都要寫(xiě)。比如,
MyFunction = fun(0) -> 0;
(Number) -> Number * 10 + 1 end,
MyFunction(3),
函數(shù)作為變量
匿名函數(shù)可以當(dāng)作對(duì)象賦給變量,命名函數(shù)同樣也可以賦給變量。具體用法還是需要借助重要的fun關(guān)鍵字。比如,
MyFunction = fun outer / 1
就可以把上述定義的outer函數(shù)賦給MyFunction變量。后面的 / 0表示這個(gè)outer函數(shù)只有一個(gè)參數(shù)。因?yàn)镋rLang允許有多個(gè)同名函數(shù)的定義,只要參數(shù)個(gè)數(shù)不同,就是不同的函數(shù)。
我們可以看到,任何函數(shù)都可以作為變量,也可以作為參數(shù)和返回值傳來(lái)傳去,這些變量也可以隨時(shí)作為函數(shù)進(jìn)行調(diào)用,于是就具有了一定的動(dòng)態(tài)性。
函數(shù)的動(dòng)態(tài)調(diào)用
ErLang有一個(gè)apply函數(shù),可以動(dòng)態(tài)調(diào)用某一個(gè)函數(shù)變量。
基本用法是 apply( 函數(shù)變量,函數(shù)參數(shù)列表 )。比如,上面的MyFunciton函數(shù)變量,就可以這么調(diào)用,apply( MyFunction, [ 5 ])。
那么我們能否根據(jù)一個(gè)字符串作為函數(shù)名獲取一個(gè)函數(shù)變量呢?這樣我們就可以根據(jù)一個(gè)字符串來(lái)動(dòng)態(tài)調(diào)用某個(gè)函數(shù)了。
ErLang中,做到這一點(diǎn)很簡(jiǎn)單。前面講過(guò),函數(shù)名一旦定義了,自然就固定了,這也類似于常量名,屬于不可變的atom(原子)。所有的atom都可以轉(zhuǎn)換成字符串,也可以從字符串轉(zhuǎn)換過(guò)來(lái)。ErLang中的字符串實(shí)質(zhì)上都是List。字符串和atom之間的轉(zhuǎn)換通過(guò)list_to_atom和atom_to_list來(lái)轉(zhuǎn)換。
于是我們可以這樣獲取MyFunciton:MyFunction = list_to_atom(“outer”)
如果outer函數(shù)已經(jīng)定義,那么MyFucntion就等于outer函數(shù),如果outer函數(shù)沒(méi)有定義,那么list_to_atom(“outer”)會(huì)產(chǎn)生一個(gè)新的叫做outer的atom,MyFucntion就等于這個(gè)新產(chǎn)生的atom。
如果需要強(qiáng)制產(chǎn)生一個(gè)已經(jīng)存在的atom,那么我們需要調(diào)用list_to_existing_atom轉(zhuǎn)換函數(shù),這個(gè)函數(shù)不會(huì)產(chǎn)生新的atom,而是返回一個(gè)已經(jīng)存在了的atom。
Tuple作為數(shù)據(jù)成員集合
前面講解函數(shù)式編程特性的時(shí)候,提到了函數(shù)式編程沒(méi)有面向?qū)ο缶幊痰某蓡T變量,這是一個(gè)限制。
ErLang的Tuple類型可以一定程度克服這個(gè)限制。Tuple可以一定程度上擔(dān)當(dāng)容納成員變量的職責(zé)。
面向?qū)ο蟮念惗x,其實(shí)就是一群數(shù)據(jù)和函數(shù)的集合,只是集合的成員之間都有一個(gè)this指針相關(guān)聯(lián),可以相互找到。
ErLang的Tuple類型就是數(shù)據(jù)的集合,可以很自然地發(fā)揮成員變量的作用,比如,{Member1, Member2}。
讀者可能會(huì)說(shuō),ErLang的函數(shù)也可以作為變量,也可以放到Tuple里面,比如, { Memer1, Member2, Funtion1, Function2}。這不就和面向?qū)ο缶幊桃粯恿藛幔?
遺憾的是,這樣做是得不償失的。因?yàn)楹瘮?shù)式編程沒(méi)有面向?qū)ο蟮哪欠N內(nèi)在的this指針支持,自然也沒(méi)有內(nèi)在的多態(tài)和繼承支持,硬把數(shù)據(jù)和函數(shù)糅合在一個(gè)Tuple里面,一點(diǎn)好處都沒(méi)有,而且還喪失了函數(shù)作為實(shí)例對(duì)象的靈活性。
所以,函數(shù)式編程的最佳實(shí)踐(Best Practice)應(yīng)該是:Tuple用來(lái)容納成員數(shù)據(jù),函數(shù)操作Tuple。Tuple定義和函數(shù)定義加在一起,就構(gòu)成了松散的數(shù)據(jù)結(jié)構(gòu),功能上類似于面向?qū)ο蟮念惗x。Tuple + 函數(shù)的數(shù)據(jù)結(jié)構(gòu),具有多態(tài)的特性,因?yàn)楹瘮?shù)本身能夠作為變量替換;但是不具有繼承的特性,因?yàn)闆](méi)有this指針的內(nèi)在支持。
正是因?yàn)門(mén)uple在數(shù)據(jù)類型構(gòu)造方面的重大作用,所以,ErLang專門(mén)引入了一種叫做Record的宏定義,可以對(duì)Tuple的數(shù)組下標(biāo)位置命名。比如,把第一個(gè)元素叫做Address,第二個(gè)元素叫做Zipcode,這樣程序員就可以這些名字訪問(wèn)Tuple里面的元素,而不需要按照數(shù)組下標(biāo)位置來(lái)訪問(wèn)。
Tuple和Record的具體用法還是有一定復(fù)雜度,限于篇幅,本章沒(méi)有展開(kāi)說(shuō)明,只提了一些原理方面的要點(diǎn)。
其它
ErLang還有其它語(yǔ)法特性和細(xì)節(jié),不再一一贅述。有興趣的讀者,可以自行去ErLang網(wǎng)站(www.erlang.org)進(jìn)行研究。