使用下列JSON庫:
http://www.lshift.net/blog/2007/02/17/json-and-json-rpc-for-erlang 該JSON庫采用
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.)
測試如下:
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,[]}

posted @
2009-09-18 15:38 暗夜教父 閱讀(3035) |
評論 (0) |
編輯 收藏
轉自http://www.javaeye.com/topic/401041
學erlang有一段時間了,現在在維護一套webim系統
并打算擴展成 webgame 的服務程序
在沒有使用包協議的時候,遇到好多粘包問題,實在惱火
查閱了相關資料:
Flash Socket 的 writeUTF() 會自動增加包頭長度的協議,剛好對應了
Erlang的Socket選項 {packet,2}
這使得兩者的通信非常完美,再也不用擔心粘包什么的問題了
下面是我寫的一個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);
}
}
}
}

posted @
2009-09-18 12:40 暗夜教父 閱讀(896) |
評論 (0) |
編輯 收藏
MMORPG不同于其它的局域網的網絡游戲,它是一個面向整個Internet的連接人數過萬的網絡游戲,因此他的服務器端設計則極為重要
服務器的基本設置 在大型網絡游戲里,通常設計為C/S結構,客戶端不再對數據進行邏輯處理,而只是一個收發裝置,從玩家那里接受到操作信息,然后反饋給服務器,再由服務器進行處理后發回客戶端,經客戶端通過圖形化處理,給玩家呈現出一個繽紛的游戲世界。

登陸服務器
在這里也可以稱之為連接服務器,網絡游戲的客戶端一般是連接到這里,然后再由該連接服務器根據不同的需要,把游戲消息轉發給其它相應的服務器(邏輯和地圖服務器)也因為它是客戶端直接連接的對象,它同時也負擔了驗證客戶身份的工作。
地圖服務器
在這里也可以稱之為連續事件服務器。在這個服務器里要處理的對象(玩家)所做的動作都是一個連續事件。例如玩家從A點移動到B點,這樣一個動作,需要一定的時間進行移動,因此說移動是一個連續事件。
邏輯服務器
在這里可以稱之為瞬時事件服務器,在這個服務器里,處理對象(玩家)所做的動作均可以在非常斷時間內完成完成。例如玩家從商店購買一瓶藥書,當玩家確認
購買后,服務器先扣除玩家的游戲幣,然后再把相應的藥水瓶加入玩家的背包里。這2個操作對于服務器來說,只是2個數字的加減,計算完這兩個數字的加減,這
個事件就可以結束了。因此,我們可以說這個事件是一個瞬時事件
服務器組的改進 不過在實際應用的過程中,游戲服務器的結構要比上面所說的3種服務結構要復雜些,不過也都是在這3種最基本的服務器架構下進行擴充,擴充的主要是其它輔助功能。在實際應用里可能增加的2種服務器,數據庫服務器,計費服務器,由邏輯服務器獨立出來的聊天服務器。

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