1。Erlang的保留字有:
after and andalso band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse query receive rem try when xor
基本都是些用于邏輯運算、位運算以及特殊表達(dá)式的符號
2.Erlang的類型,除了在前面入門一提到的類型外,還包括:
1)Binary,用于表示某段未知類型的內(nèi)存區(qū)域
比如:
1>
<<10,20>>. <<10,20>>
2>
<<"ABC">>. <<65,66,67>>
2)Reference,通過調(diào)用mk_ref/0產(chǎn)生的運行時的unique term
3)String,字符串,Erlang中的字符串用雙引號包括起來,其實也是list。編譯時期,兩個鄰近的字符串將被連接起來,比如"string" "42" 等價于 "string42"
4)Record,記錄類型,與c語言中的struct類似,模塊可以通過-record屬性聲明,比如:
-module(person).
-export([new/2]).
-record(person, {name, age}).
new(Name, Age) ->
#person{name=Name, age=Age}.
1>
person:new(dennis, 44). {person,dennis,44}
在編譯后其實已經(jīng)被轉(zhuǎn)化為tuple。可以通過Name#person.name來訪問Name Record的name屬性。
3.模塊的預(yù)定義屬性:
-module(Module).
聲明模塊名稱,必須與文件名相同
-export(Functions).
指定向外界導(dǎo)出的函數(shù)列表
-import(Module,Functions).
引入函數(shù),引入的函數(shù)可以被當(dāng)作本地定義的函數(shù)使用
-compile(Options).
設(shè)置編譯選項,比如export_all
-vsn(Vsn). 模塊版本,設(shè)置了此項,可以通過
beam_lib:version/1
獲取此項信息
可以通過-include和-include_lib來包含文件,兩者的區(qū)別是include-lib不能通過絕對路徑查找文件,而是在你當(dāng)前Erlang的lib目錄進(jìn)行查找。
4.try表達(dá)式,try表達(dá)式可以與catch結(jié)合使用,比如:
try Expr
catch
throw:Term -> Term;
exit:Reason -> {'EXIT',Reason}
error:Reason -> {'EXIT',{Reason,erlang:get_stacktrace()}}
end
不僅如此,try還可以與after結(jié)合使用,類似java中的try..finally,用于進(jìn)行清除作用,比如:
termize_file(Name) ->
{ok,F} = file:open(Name, [read,binary]),
try
{ok,Bin} = file:read(F, 1024*1024),
binary_to_term(Bin)
after
file:close(F)
end.
5.列表推斷(List Comprehensions),函數(shù)式語言特性之一,Erlang中的語法類似:
[Expr || Qualifier1,...,QualifierN]
Expr可以是任意的表達(dá)式,而Qualifier是generator或者filter。還是各舉例子說明下。
1> [X*2 || X <- [1,2,3]].
[2,4,6]
2> L=[1,2,3,4,5,6,7].
[1,2,3,4,5,6,7]
3> [X|X<-L,X>=3].
[3,4,5,6,7]
再看幾個比較酷的例子,來自Programming Erlang,
比如快速排序:
-module(qsort).
-export([qsort/1]).
qsort([])->[];
qsort([Pivot|T])->
qsort([X||X<-T,X
6.宏,定義常量或者函數(shù)等等,語法如下:
-define(Const, Replacement).
-define(Func(Var1,...,VarN), Replacement).
使用的時候在宏名前加個問號?,比如?Const,Replacement將插入宏出現(xiàn)的位置。系統(tǒng)預(yù)定義了一些宏:
?MODULE 表示當(dāng)前模塊名
?MODULE_STRING 同上,但是以字符串形式
?FILE 當(dāng)前模塊的文件名
?LINE 調(diào)用的當(dāng)前代碼行數(shù)
?MACHINE 機器名
Erlang的宏與C語言的宏很相似,同樣有宏指示符,包括:
-undef(Macro).
- 取消宏定義
-ifdef(Macro).
- 當(dāng)宏Macro有定義的時候,執(zhí)行以下代碼
-ifndef(Macro).
- 同上,反之
-else.
- 接在ifdef或者ifndef之后,表示不滿足前者條件時執(zhí)行以下代碼
-endif.
- if終止符
假設(shè)宏-define(Square(X),X*X).用于計算平方,那么??X將返回X表達(dá)式的字符串形式,類似C語言中#arg
一個簡單的宏例子:
ruby 代碼
- -module(macros_demo).
- -ifdef(debug).
- -define(LOG(X), io:format("{~p,~p}: ~p~n", [?MODULE,?LINE,X])).
- -else.
- -define(LOG(X), true).
- -endif.
- -define(Square(X),X*X).
- -compile(export_all).
- test()->
- A=3,
- ?LOG(A),
- B=?Square(A),
- io:format("square(~w) is ~w~n",[A,B]).
當(dāng)編譯時不開啟debug選項的時候:
17> c(macros_demo).
{ok,macros_demo}
18> macros_demo:test().
square(3) is 9
當(dāng)編譯時開啟debug之后:
19> c(macros_demo,{d,debug}).
{ok,macros_demo}
20> macros_demo:test().
{macros_demo,11}: 3
square(3) is 9
ok
可以看到LOG的輸出了,行數(shù)、模塊名以及參數(shù)
7、
Process Dictionary,每個進(jìn)程都有自己的process dictionary,用于存儲這個進(jìn)程內(nèi)的全局變量,可以通過下列
BIFs操作:
put(Key, Value)
get(Key)
get()
get_keys(Value)
erase(Key)
erase()
8、關(guān)于分布式編程,需要補充的幾點
1)節(jié)點之間的連接默認(rèn)是transitive,也就是當(dāng)節(jié)點A連接了節(jié)點B,節(jié)點B連接了節(jié)點C,那么節(jié)點A也與節(jié)點C互相連接
可以通過啟動節(jié)點時指定參數(shù)-connect_all false來取消默認(rèn)行為
2)隱藏節(jié)點,某些情況下,你希望連接一個節(jié)點而不去連接其他節(jié)點,你可以通過在節(jié)點啟動時指定-hidden選項
來啟動一個hidden node。在此情況下,通過nodes()查看所有連接的節(jié)點將不會出現(xiàn)隱藏的節(jié)點,想看到隱藏的節(jié)點
可以通過nodes(hidden)或者nodes(connected)來查看。
完整的erl選項如下:
-connect_all false |
上面已經(jīng)解釋。 |
-hidden |
啟動一個hidden node
|
-name Name |
啟動一個系統(tǒng)成為節(jié)點,使用long name. |
-setcookie Cookie |
與Erlang:set_cookie(node(), Cookie) .相同,設(shè)置magic cookie
|
-sname Name |
啟動一個Erlang系統(tǒng)作為節(jié)點,使用short name
|
注意,
short name啟動的節(jié)點是無法與long name節(jié)點通信的。
.一個小細(xì)節(jié),在Erlang中小于等于是用=<表示,而不是一般語言中的<=語法,我犯過錯誤的地方,同樣,不等于都是用/號,而不是
!,比如/=、=/=。
10.and or 和andalso orelse的區(qū)別
and和or會計算兩邊的表達(dá)式,而andalso和orelse的求值采用短路機制,比如exp1 andalso exp2,當(dāng)exp1返回false之后,就不會去求值
exp2,而是直接返回false,而exp1 and exp2會對exp1和exp2都進(jìn)行求值,or與orelse也類似。
posted @
2009-09-11 10:16 暗夜教父 閱讀(691) |
評論 (0) |
編輯 收藏
所謂分布式的Erlang應(yīng)用是運行在一系列Erlang節(jié)點組成的網(wǎng)絡(luò)之上。這樣的系統(tǒng)的性質(zhì)與單一節(jié)點上的Erlang系統(tǒng)并沒有什么不同。分布式這是個“大詞”,Erlang從語言原生角度支持分布式編程,相比于java簡單不少。
一、分布式機制
下列的BIFs是用于分布式編程:
spawn(Node, Mod, Func, Args)啟動遠(yuǎn)程節(jié)點的一個進(jìn)程
spawn_link(Node, Mod, Func, Args)啟動遠(yuǎn)程節(jié)點的一個進(jìn)程并創(chuàng)建連接到該進(jìn)程
monitor_node(Node, Flag)如果Flag是true,這個函數(shù)將使調(diào)用(該函數(shù))的進(jìn)程可以監(jiān)控節(jié)點Node。如果節(jié)點已經(jīng)舍棄或者并不存在,調(diào)用的進(jìn)程將收到一個{nodedown,Node}的消息。如果Flag是false,監(jiān)控將被關(guān)閉
node()返回我們自己的進(jìn)程name
nodes()返回其他已知的節(jié)點name列表
node(Item)返回原來Item的節(jié)點名稱,Item可以是Pid,引用(reference)或者端口(port)
disconnect_node(Nodename)從節(jié)點Nodename斷開。
節(jié)點是分布式Erlang的核心概念。在一個分布式Erlang應(yīng)用中,術(shù)語(term)節(jié)點(node)意味著一個可以加入分布式 transactions的運行系統(tǒng)。通過一個稱為net kernal的特殊進(jìn)程,一個獨立的Erlang系統(tǒng)可以成為一個分布式Erlang系統(tǒng)的一部分。當(dāng)net kernal進(jìn)程啟動的時候,我們稱系統(tǒng)是alive的。
與遠(yuǎn)程節(jié)點上的進(jìn)程進(jìn)行通信,與同一節(jié)點內(nèi)的進(jìn)程通信只有一點不同:
java 代碼
顯然,需要接收方增加一個參數(shù)Node用于指定接受進(jìn)程所在的節(jié)點。節(jié)點的name一般是用@隔開的atom類型,比如pong@dennis,表示計算機名為dennis上的pong節(jié)點。通過執(zhí)行:
java 代碼
將在執(zhí)行的計算機中創(chuàng)建一個節(jié)點pong。為了運行下面的例子,你可能需要兩臺計算機,如果只有一臺,只要同時開兩個Erlang系統(tǒng)并以不同的節(jié)點名稱運行也可以。
二、一些例子。
這個例子完全來自上面提到的翻譯的連接,關(guān)于分布式編程的章節(jié)。我增加了截圖和說明。
首先是代碼:
java 代碼
- -module(tut17).
-
- -export([start_ping/1, start_pong/0, ping/2, pong/0]).
-
- ping(0, Pong_Node) ->
- {pong, Pong_Node} ! finished,
- io:format("ping finished~n", []);
-
- ping(N, Pong_Node) ->
- {pong, Pong_Node} ! {ping, self()},
- receive
- pong ->
- io:format("Ping received pong~n", [])
- end,
- ping(N - 1, Pong_Node).
-
- pong() ->
- receive
- finished ->
- io:format("Pong finished~n", []);
- {ping, Ping_PID} ->
- io:format("Pong received ping~n", []),
- Ping_PID ! pong,
- pong()
- end.
-
- start_pong() ->
- register(pong, spawn(tut17, pong, [])).
-
- start_ping(Pong_Node) ->
- spawn(tut17, ping, [3, Pong_Node]).
代碼是創(chuàng)建兩個相互通信的進(jìn)程,相互發(fā)送消息并通過io顯示在屏幕上,本來是一個單一系統(tǒng)的例子,現(xiàn)在我們讓兩個進(jìn)程運行在不同的兩個節(jié)點上。注意 start_ping方法,創(chuàng)建的進(jìn)程調(diào)用ping方法,ping方法有兩個參數(shù),一個是發(fā)送消息的次數(shù),一個就是遠(yuǎn)程節(jié)點的name了,也就是我們將要 創(chuàng)建的進(jìn)程pong的所在節(jié)點。start_pong創(chuàng)建一個調(diào)用函數(shù)pong的進(jìn)程,并注冊為名字pong(因此在ping方法中可以直接發(fā)送消息給 pong)。
我是在windows機器上測試,首先打開兩個cmd窗口,并cd到Erlang的安裝目錄下的bin目錄,比如C:\Program Files\erl5.5.3\bin,將上面的程序存為tut17.erl,并拷貝到同一個目錄下。我們將創(chuàng)建兩個節(jié)點,一個叫 ping@dennis,一個叫pong@dennis,其中dennis是我的機器名。見下圖:

采用同樣的命令
erl -sname ping創(chuàng)建ping節(jié)點。然后在pong節(jié)點下執(zhí)行start_pong():

OK,這樣就在節(jié)點pong上啟動了pong進(jìn)程,然后在ping節(jié)點調(diào)用start_ping,傳入?yún)?shù)就是pong@dennis
java 代碼
- tut17:start_ping(pong@dennis).
執(zhí)行結(jié)果如下圖:

同樣在pong節(jié)點上也可以看到:

結(jié)果如我們預(yù)期的那樣,不同節(jié)點上的兩個進(jìn)程相互通信如此簡單。我們給模塊tut17增加一個方法,用于啟動遠(yuǎn)程進(jìn)程,也就是調(diào)用spawn(Node,Module,Func,Args)方法:
java 代碼
- start(Ping_Node) ->
- register(pong, spawn(tut17, pong, [])),
- spawn(Ping_Node, tut17, ping, [3, node()]).
pong進(jìn)程啟動Ping_Node節(jié)點上的進(jìn)程ping。具體結(jié)果不再給出。
posted @
2009-09-11 10:13 暗夜教父 閱讀(407) |
評論 (0) |
編輯 收藏
任何一門語言都有自己的錯誤處理機制,Erlang也不例外,語法錯誤編譯器可以幫你指出,而邏輯錯誤和運行時錯誤就只有靠程序員利用Erlang提供的機制來妥善處理,放置程序的崩潰。
Erlang的機制有:
1)監(jiān)控某個表達(dá)式的執(zhí)行
2)監(jiān)控其他進(jìn)程的行為
3)捕捉未定義函數(shù)執(zhí)行錯誤等
一、catch和throw語句 調(diào)用某個會產(chǎn)生錯誤的表達(dá)式會導(dǎo)致調(diào)用進(jìn)程的非正常退出,比如錯誤的模式匹配(2=3),這種情況下可以用catch語句:
catch expression 試看一個例子,一個函數(shù)foo:
java 代碼
- foo(1) ->
- hello;
- foo(2) ->
- throw({myerror, abc});
- foo(3) ->
- tuple_to_list(a);
- foo(4) ->
- exit({myExit, 222}).
當(dāng)沒有使用catch的時候,假設(shè)有一個標(biāo)識符為Pid的進(jìn)程調(diào)用函數(shù)foo(在一個模塊中),那么:
foo(1) - 返回hello
foo(2) - 語句throw({myerror, abc})執(zhí)行,因為我們沒有在一個catch中調(diào)用foo(2),因此進(jìn)程Pid將因為錯誤而終止。
foo(3) - tuple_to_list將一個元組轉(zhuǎn)化為列表,因為a不是元組,因此進(jìn)程Pid同樣因為錯誤而終止
foo(4) - 因為沒有使用catch,因此foo(4)調(diào)用了exit函數(shù)將使進(jìn)程Pid終止,{myExit, 222} 參數(shù)用于說明退出的原因。
foo(5) - 進(jìn)程Pid將因為foo(5)的調(diào)用而終止,因為沒有和foo(5)匹配的函數(shù)foo/1。
讓我們看看用catch之后是什么樣:
java 代碼
- demo(X) ->
- case catch foo(X) of
- {myerror, Args} ->
- {user_error, Args};
- {'EXIT', What} ->
- {caught_error, What};
- Other ->
- Other
- end.
再看看結(jié)果,
demo(1) - 沒有錯誤發(fā)生,因此catch語句將返回表達(dá)式結(jié)果hello
demo(2) - foo(2)拋出錯誤{myerror, abc},被catch返回,因此將返回{user_error,abc}
demo(3) - foo(3)執(zhí)行失敗,因為參數(shù)錯誤,因此catch返回{'EXIT',badarg'},最后返回{caught_error,badarg}
demo(4) - 返回{caught_error,{myexit,222}}
demo(5) - 返回{caught_error,function_clause}
使用catch和throw可以將可能產(chǎn)生錯誤的代碼包裝起來,throw可以用于尾遞歸的退出等等。Erlang是和scheme一樣進(jìn)行尾遞歸優(yōu)化的,它們都沒有顯式的迭代結(jié)構(gòu)(比如for循環(huán))
二、進(jìn)程的終止 在進(jìn)程中調(diào)用exit的BIFs就可以顯式地終止進(jìn)程,exit(normal)表示正常終止,exit(Reason)通過Reason給出非正常終止的原因。進(jìn)程的終止也完全有可能是因為運行時錯誤引起的。
三、連接的進(jìn)程 進(jìn)程之間的連接是雙向的,也就是說進(jìn)程A打開一個連接到B,也意味著有一個從B到A的連接。當(dāng)進(jìn)程終止的時候,有一個EXIT信號將發(fā)給所有與它連接的進(jìn)程。信號的格式如下:
{'EXIT', Exiting_Process_Id, Reason}
Exiting_Process_Id 是指終止的進(jìn)程標(biāo)記符
Reason 是進(jìn)程終止的原因。如果Reason是normal,接受這個信號的進(jìn)程的默認(rèn)行為是忽略這個信號。默認(rèn)對Exit信號的處理可以被重寫,以允許進(jìn)程對Exit信號的接受做出不同的反應(yīng)。
1.連接進(jìn)程:
通過link(Pid),就可以在調(diào)用進(jìn)程與進(jìn)程Pid之間建立連接
2.取消連接
反之通過unlink(Pid)取消連接。
3.創(chuàng)立進(jìn)程并連接:
通過spawn_link(Module, Function, ArgumentList)創(chuàng)建進(jìn)程并連接,該方法返回新創(chuàng)建的進(jìn)程Pid
通過進(jìn)程的相互連接,許多的進(jìn)程可以組織成一個網(wǎng)狀結(jié)構(gòu),EXIT信號(非normal)從某個進(jìn)程發(fā)出(該進(jìn)程終止),所有與它相連的進(jìn)程以及與這些進(jìn) 程相連的其他進(jìn)程,都將收到這個信號并終止,除非它們實現(xiàn)了自定義的EXIT信號處理方法。一個進(jìn)程鏈狀結(jié)構(gòu)的例子:
java 代碼
- -module(normal).
- -export([start/1, p1/1, test/1]).
- start(N) ->
- register(start, spawn_link(normal, p1, [N - 1])).
- p1(0) ->
- top1();
- p1(N) ->
- top(spawn_link(normal, p1, [N - 1]),N).
- top(Next, N) ->
- receive
- X ->
- Next ! X,
- io:format("Process ~w received ~w~n", [N,X]),
- top(Next,N)
- end.
- top1() ->
- receive
- stop ->
- io:format("Last process now exiting ~n", []),
- exit(finished);
- X ->
- io:format("Last process received ~w~n", [X]),
- top1()
- end.
- test(Mess) ->
- start ! Mess.
執(zhí)行:
java 代碼
- > normal:start(3).
- true
- > normal:test(123).
- Process 2 received 123
- Process 1 received 123
- Last process received 123
-
- > normal:test(stop).
- Process 2 received stop
- Process 1 received stop
- Last process now exiting
- stop
四、運行時失敗 一個運行時錯誤將導(dǎo)致進(jìn)程的非正常終止,伴隨著非正常終止EXIT信號將發(fā)出給所有連接的進(jìn)程,EXIT信號中有Reason并且Reason中包含一個atom類型用于說明錯誤的原因,常見的原因如下:
badmatch - 匹配失敗,比如一個進(jìn)程進(jìn)行1=3的匹配,這個進(jìn)程將終止,并發(fā)出{'EXIT', From, badmatch}信號給連接的進(jìn)程
badarg - 顧名思義,參數(shù)錯誤,比如atom_to_list(123),數(shù)字不是atom,因此將發(fā)出{'EXIT', From, badarg}信號給連接進(jìn)程
case_clause - 缺少分支匹配,比如
java 代碼
- M = 3,
- case M of
- 1 ->
- yes;
- 2 ->
- no
- end.
沒有分支3,因此將發(fā)出{'EXIT', From, case_clause}給連接進(jìn)程
if_clause - 同理,if語句缺少匹配分支
function_clause - 缺少匹配的函數(shù),比如:
java 代碼
- foo(1) ->
- yes;
- foo(2) ->
- no.
如果我們調(diào)用foo(3),因為沒有匹配的函數(shù),將發(fā)出{'EXIT', From, function_clause} 給連接的進(jìn)程。
undef - 進(jìn)程執(zhí)行一個不存在的函數(shù)
badarith - 非法的算術(shù)運算,比如1+foo。
timeout_value - 非法的超時時間設(shè)置,必須是整數(shù)或者infinity
nocatch - 使用了throw,沒有相應(yīng)的catch去通訊。
五、修改默認(rèn)的信號接收action 當(dāng)進(jìn)程接收到EXIT信號,你可以通過process_flag/2方法來修改默認(rèn)的接收行為。執(zhí)行process_flag(trap_exit, true)設(shè)置捕獲EXIT信號為真來改變默認(rèn)行為,也就是將EXIT信號作為一般的進(jìn)程間通信的信號進(jìn)行接受并處理;process_flag (trap_exit,false)將重新開啟默認(rèn)行為。
例子:
java 代碼
- -module(link_demo).
- -export([start/0, demo/0, demonstrate_normal/0, demonstrate_exit/1,
- demonstrate_error/0, demonstrate_message/1]).
- start() ->
- register(demo, spawn(link_demo, demo, [])).
- demo() ->
- process_flag(trap_exit, true),
- demo1().
- demo1() ->
- receive
- {'EXIT', From, normal} ->
- io:format("Demo process received normal exit from ~w~n",[From]),
- demo1();
- {'EXIT', From, Reason} ->
- io:format("Demo process received exit signal ~w from ~w~n",[Reason, From]),
- demo1();
- finished_demo ->
- io:format("Demo finished ~n", []);
- Other ->
- io:format("Demo process message ~w~n", [Other]),
- demo1()
- end.
- demonstrate_normal() ->
- link(whereis(demo)).
- demonstrate_exit(What) ->
- link(whereis(demo)),
- exit(What).
- demonstrate_message(What) ->
- demo ! What.
- demonstrate_error() ->
- link(whereis(demo)),
- 1 = 2.
-
創(chuàng)建的進(jìn)程執(zhí)行demo方法,demo方法中設(shè)置了trap_exit為true,因此,在receive中可以像對待一般的信息一樣處理EXIT信號,這個程序是很簡單了,測試看看:
java 代碼
- > link_demo:start().
- true
- > link_demo:demonstrate_normal().
- true
- Demo process received normal exit from <0.13.1>
- > link_demo:demonstrate_exit(hello).
- Demo process received exit signal hello from <0.14.1>
- ** exited: hello **
-
- > link_demo:demonstrate_exit(normal).
- Demo process received normal exit from <0.13.1>
- ** exited: normal **
-
- > link_demo:demonstrate_error().
- !!! Error in process <0.17.1> in function
- !!! link_demo:demonstrate_error()
- !!! reason badmatch
- ** exited: badmatch **
- Demo process received exit signal badmatch from <0.17.1>
六、未定義函數(shù)和未注冊名字1.當(dāng)調(diào)用一個未定義的函數(shù)時,Mod:Func(Arg0,...,ArgN),這個調(diào)用將被轉(zhuǎn)為:
error_handler:undefined_function(Mod, Func, [Arg0,...,ArgN])
其中的error_handler模塊是系統(tǒng)自帶的錯誤處理模塊
2.當(dāng)給一個未注冊的進(jìn)程名發(fā)送消息時,調(diào)用將被轉(zhuǎn)為:
error_handler:unregistered_name(Name,Pid,Message)
3.如果不使用系統(tǒng)自帶的error_handler,可以通過process_flag(error_handler, MyMod) 設(shè)置自己的錯誤處理模塊。
七、Catch Vs. Trapping Exits這兩者的區(qū)別在于應(yīng)用場景不同,Trapping Exits應(yīng)用于當(dāng)接收到其他進(jìn)程發(fā)送的EXIT信號時,而catch僅用于表達(dá)式的執(zhí)行。
第8章介紹了如何利用錯誤處理機制去構(gòu)造一個健壯的系統(tǒng),用了幾個例子,我將8.2節(jié)的例子完整寫了下,并添加客戶端進(jìn)程用于測試:
java 代碼
- -module(allocator).
- -export([start/1,server/2,allocate/0,free/1,start_client/0,loop/0]).
- start(Resources) ->
- Pid = spawn(allocator, server, [Resources,[]]),
- register(resource_alloc, Pid).
- %函數(shù)接口
- allocate() ->
- request(alloc).
- free(Resource) ->
- request({free,Resource}).
- request(Request) ->
- resource_alloc ! {self(),Request},
- receive
- {resource_alloc, error} ->
- exit(bad_allocation); % exit added here
- {resource_alloc, Reply} ->
- Reply
- end.
- % The server.
- server(Free, Allocated) ->
- process_flag(trap_exit, true),
- receive
- {From,alloc} ->
- allocate(Free, Allocated, From);
- {From,{free,R}} ->
- free(Free, Allocated, From, R);
- {'EXIT', From, _ } ->
- check(Free, Allocated, From)
- end.
- allocate([R|Free], Allocated, From) ->
- link(From),
- io:format("連接客戶端進(jìn)程~w~n",[From]),
- From ! {resource_alloc,{yes,R}},
- server(Free, [{R,From}|Allocated]);
- allocate([], Allocated, From) ->
- From ! {resource_alloc,no},
- server([], Allocated).
- free(Free, Allocated, From, R) ->
- case lists:member({R,From}, Allocated) of
- true ->
- From ! {resource_alloc,ok},
- Allocated1 = lists:delete({R, From}, Allocated),
- case lists:keysearch(From,2,Allocated1) of
- false->
- unlink(From),
- io:format("從進(jìn)程~w斷開~n",[From]);
- _->
- true
- end,
- server([R|Free],Allocated1);
- false ->
- From ! {resource_alloc,error},
- server(Free, Allocated)
- end.
-
- check(Free, Allocated, From) ->
- case lists:keysearch(From, 2, Allocated) of
- false ->
- server(Free, Allocated);
- {value, {R, From}} ->
- check([R|Free],
- lists:delete({R, From}, Allocated), From)
- end.
- start_client()->
- Pid2=spawn(allocator,loop,[]),
- register(client, Pid2).
- loop()->
- receive
- allocate->
- allocate(),
- loop();
- {free,Resource}->
- free(Resource),
- loop();
- stop->
- true;
- _->
- loop()
- end.
-
回家了,有空再詳細(xì)說明下這個例子吧。執(zhí)行:
java 代碼
- 1> c(allocator).
- {ok,allocator}
- 2> allocator:start([1,2,3,4,5,6]).
- true
- 3> allocator:start_client().
- true
- 4> client!allocate
- .
- allocate連接客戶端進(jìn)程<0.37.0>
-
- 5> client!allocate.
- allocate連接客戶端進(jìn)程<0.37.0>
-
- 6> client!allocate.
- allocate連接客戶端進(jìn)程<0.37.0>
-
- 7> allocator:allocate().
- 連接客戶端進(jìn)程<0.28.0>
- {yes,4}
- 8> client!{free,1}.
- {free,1}
- 9> client!{free,2}.
- {free,2}
- 10> client!allocate.
- allocate連接客戶端進(jìn)程<0.37.0>
-
- 11> client!allocate.
- allocate連接客戶端進(jìn)程<0.37.0>
-
- 12> client!stop.
- stop
- 13> allocator:allocate().
- 連接客戶端進(jìn)程<0.28.0>
- {yes,3}
- 14> allocator:allocate().
- 連接客戶端進(jìn)程<0.28.0>
- {yes,2}
- 15> allocator:allocate().
- 連接客戶端進(jìn)程<0.28.0>
- {yes,1}
- 16>
posted @
2009-09-11 10:13 暗夜教父 閱讀(327) |
評論 (0) |
編輯 收藏
Erlang中的process——進(jìn)程是輕量級的,并且進(jìn)程間無共享。查了很多資料,似乎沒人說清楚輕量級進(jìn)程算是什么概念,繼續(xù)查找中。。。閑話不 提,進(jìn)入并發(fā)編程的世界。本文算是學(xué)習(xí)筆記,也可以說是《Concurrent Programming in ERLANG》第五張的簡略翻譯。
1.進(jìn)程的創(chuàng)建 進(jìn)程是一種自包含的、分隔的計算單元,并與其他進(jìn)程并發(fā)運行在系統(tǒng)中,在進(jìn)程間并沒有一個繼承體系,當(dāng)然,應(yīng)用開發(fā)者可以設(shè)計這樣一個繼承體系。
進(jìn)程的創(chuàng)建使用如下語法:
java 代碼
- Pid = spawn(Module, FunctionName, ArgumentList)
spawn接受三個參數(shù):模塊名,函數(shù)名以及參數(shù)列表,并返回一個代表創(chuàng)建的進(jìn)程的標(biāo)識符(Pid)。
如果在一個已知進(jìn)程Pid1中執(zhí)行:
java 代碼
- Pid2 = spawn(Mod, Func, Args)
那么,Pid2僅僅能被Pid1可見,Erlang系統(tǒng)的安全性就構(gòu)建在限制進(jìn)程擴展的基礎(chǔ)上。
2.進(jìn)程間通信 Erlang進(jìn)程間的通信只能通過發(fā)送消息來實現(xiàn),消息的發(fā)送使用!符號:
java 代碼
其中Pid是接受消息的進(jìn)程標(biāo)記符,Message就是消息。接受方和消息可以是任何的有效的Erlang結(jié)構(gòu),只要他們的結(jié)果返回的是進(jìn)程標(biāo)記符和消息。
消息的接受是使用receive關(guān)鍵字,語法如下:
java 代碼
- receive
- Message1 [when Guard1] ->
- Actions1 ;
- Message2 [when Guard2] ->
- Actions2 ;
-
- end
每一個Erlang進(jìn)程都有一個“郵箱”,所有發(fā)送到進(jìn)程的消息都按照到達(dá)的順序存儲在“郵箱”里,上面所示的消息Message1,Message2, 當(dāng)它們與“郵箱”里的消息匹配,并且約束(Guard)通過,那么相應(yīng)的ActionN將執(zhí)行,并且receive返回的是ActionN的最后一條執(zhí)行 語句的結(jié)果。Erlang對“郵箱”里的消息匹配是有選擇性的,只有匹配的消息將被觸發(fā)相應(yīng)的Action,而沒有匹配的消息將仍然保留在“郵箱”里。這 一機制保證了沒有消息會阻塞其他消息的到達(dá)。
消息到達(dá)的順序并不決定消息的優(yōu)先級,進(jìn)程將輪流檢查“郵箱”里的消息進(jìn)行嘗試匹配。消息的優(yōu)先級別下文再講。
如何接受特定進(jìn)程的消息呢?答案很簡單,將發(fā)送方(sender)也附送在消息當(dāng)中,接收方通過模式匹配決定是否接受,比如:
java 代碼
給進(jìn)程Pid發(fā)送消息{self(),abc},利用self過程得到發(fā)送方作為消息發(fā)送。然后接收方:
java 代碼
- receive
- {Pid1,Msg} ->
-
- end
通過模式匹配決定只有Pid1進(jìn)程發(fā)送的消息才接受。
3.一些例子 僅說明下書中計數(shù)的進(jìn)程例子,我添加了簡單注釋:
java 代碼
- -module(counter).
- -compile(export_all).
- % start(),返回一個新進(jìn)程,進(jìn)程執(zhí)行函數(shù)loop
- start()->spawn(counter, loop,[0]).
- % 調(diào)用此操作遞增計數(shù)
- increment(Counter)->
- Counter!increament.
- % 返回當(dāng)前計數(shù)值
- value(Counter)->
- Counter!{self(),value},
- receive
- {Counter,Value}->
- %返回給調(diào)用方
- Value
- end.
- %停止計數(shù)
- stop(Counter)->
- Counter!{self(),stop}.
- loop(Val)->
- receive
- %接受不同的消息,決定返回結(jié)果
- increament->
- loop(Val+1);
- {From,value}->
- From!{self(),Val},
- loop(Val);
- stop->
- true;
- %不是以上3種消息,就繼續(xù)等待
- Other->
- loop(Val)
- end.
-
-
-
調(diào)用方式:
java 代碼
- 1> Counter1=counter:start().
- <0.30.0>
- 2> counter:value(Counter1).
- 0
- 3> counter:increment(Counter1).
- increament
- 4> counter:value(Counter1).
- 1
基于進(jìn)程的消息傳遞機制可以很容易地實現(xiàn)有限狀態(tài)機(FSM),狀態(tài)使用函數(shù)表示,而事件就是消息。具體不再展開
4.超時設(shè)置 Erlang中的receive語法可以添加一個額外選項:timeout,類似:
java 代碼
- receive
- Message1 [when Guard1] ->
- Actions1 ;
- Message2 [when Guard2] ->
- Actions2 ;
-
- after
- TimeOutExpr ->
- ActionsT
- end
after之后的TimeOutExpr表達(dá)式返回一個整數(shù)time(毫秒級別),時間的精確程度依賴于Erlang在操作系統(tǒng)或者硬件的實現(xiàn)。如果在time毫秒內(nèi),沒有一個消息被選中,超時設(shè)置將生效,也就是ActionT將執(zhí)行。time有兩個特殊值:
1)
infinity(無窮大),infinity是一個atom,指定了超時設(shè)置將永遠(yuǎn)不會被執(zhí)行。
2)
0,超時如果設(shè)定為0意味著超時設(shè)置將立刻執(zhí)行,但是系統(tǒng)將首先嘗試當(dāng)前“郵箱”里的消息。
超時的常見幾個應(yīng)用,比如掛起當(dāng)前進(jìn)程多少毫秒:
java 代碼
- sleep(Time) ->
- receive
- after Time ->
- true
- end.
比如清空進(jìn)程的“郵箱”,丟棄“郵箱”里的所有消息:
java 代碼
- flush_buffer() ->
- receive
- AnyMessage ->
- flush_buffer()
- after 0 ->
- true
- end.
將當(dāng)前進(jìn)程永遠(yuǎn)掛起:
java 代碼
- suspend() ->
- receive
- after
- infinity ->
- true
- end.
超時也可以應(yīng)用于實現(xiàn)定時器,比如下面這個例子,創(chuàng)建一個進(jìn)程,這個進(jìn)程將在設(shè)定時間后向自己發(fā)送消息:
java 代碼
- -module(timer).
- -export([timeout/2,cancel/1,timer/3]).
- timeout(Time, Alarm) ->
- spawn(timer, timer, [self(),Time,Alarm]).
- cancel(Timer) ->
- Timer ! {self(),cancel}.
- timer(Pid, Time, Alarm) ->
- receive
- {Pid,cancel} ->
- true
- after Time ->
- Pid ! Alarm
- end.
5、注冊進(jìn)程
為了給進(jìn)程發(fā)送消息,我們需要知道進(jìn)程的Pid,但是在某些情況下:在一個很大系統(tǒng)里面有很多的全局servers,或者為了安全考慮需要隱藏進(jìn)程 Pid。為了達(dá)到可以發(fā)送消息給一個不知道Pid的進(jìn)程的目的,我們提供了注冊進(jìn)程的辦法,給進(jìn)程們注冊名字,這些名字必須是atom。
基本的調(diào)用形式:
java 代碼
- register(Name, Pid)
- 將Name與進(jìn)程Pid聯(lián)系起來
-
- unregister(Name)
- 取消Name與相應(yīng)進(jìn)程的對應(yīng)關(guān)系。
-
- whereis(Name)
- 返回Name所關(guān)聯(lián)的進(jìn)程的Pid,如果沒有進(jìn)程與之關(guān)聯(lián),就返回atom:undefined
-
- registered()
- 返回當(dāng)前注冊的進(jìn)程的名字列表
6.進(jìn)程的優(yōu)先級
設(shè)定進(jìn)程的優(yōu)先級可以使用BIFs:
process_flag(priority, Pri)
Pri可以是normal、low,默認(rèn)都是normal
優(yōu)先級高的進(jìn)程將相對低的執(zhí)行多一點。
7.進(jìn)程組(process group)
所有的ERLANG進(jìn)程都有一個Pid與一個他們共有的稱為Group Leader相關(guān)聯(lián),當(dāng)一個新的進(jìn)程被創(chuàng)建的時候?qū)⒈患尤胪粋€進(jìn)程組。最初的系統(tǒng)進(jìn)程的Group Leader就是它自身,因此它也是所有被創(chuàng)建進(jìn)程及子進(jìn)程的Group Leader。這就意味著Erlang的進(jìn)程被組織為一棵Tree,其中的根節(jié)點就是第一個被創(chuàng)建的進(jìn)程。下面的BIFs被用于操縱進(jìn)程組:
group_leader()
返回執(zhí)行進(jìn)程的Group Leader的Pid
group_leader(Leader, Pid)
設(shè)置進(jìn)程Pid的Group Leader為進(jìn)程的Leader
8.Erlang的進(jìn)程模型很容易去構(gòu)建Client-Server的模型,書中有一節(jié)專門討論了這一點,著重強調(diào)了接口的設(shè)計以及抽象層次的隔離問題,不翻譯了。
posted @
2009-09-11 10:12 暗夜教父 閱讀(306) |
評論 (0) |
編輯 收藏
讀erlang.org上面的Erlang Course四天教程
1.數(shù)字類型,需要注意兩點
1)B#Val表示以B進(jìn)制存儲的數(shù)字Val,比如
ruby 代碼
二進(jìn)制存儲的101就是10進(jìn)制的5了
2)$Char表示字符Char的ascii編碼,比如$A表示65
2.比較難以翻譯的概念——atom,可以理解成常量,它可以包含任何字符,以小寫字母開頭,如果不是以小寫字母開頭或者是字母之外的符號,需要用單引號包括起來,比如abc,'AB'
3.另一個概念——Tuple,有人翻譯成元組,可以理解成定長數(shù)組,是Erlang的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)之一:
ruby 代碼
- 8> {1,2,3,4,5}.
- {1,2,3,4,5}
- 9> {a,b,c,1,2}.
- {a,b,c,1,2}
- 10> size({1,2,3,a,b,c}).
- 6
內(nèi)置函數(shù)size求長度,元組可以嵌套元組或者其他結(jié)構(gòu)。下面所講的列表也一樣。
4.另外一個基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)就是各個語言都有的list(列表),在[]內(nèi)以,隔開,可以動態(tài)改變大小,
python 代碼
- [123, xyz]
- [123, def, abc]
- [{person, 'Joe', 'Armstrong'},
- {person, 'Robert', 'Virding'},
- {person, 'Mike', 'Williams'}
- ]
可以使用內(nèi)置函數(shù)length求列表大小。以""包含的ascii字母代表一個列表,里面的元素就是這些字母的ascii值,比如"abc"表示列表[97,98,99]。
5.通過這兩個數(shù)據(jù)結(jié)構(gòu)可以組合成各種復(fù)雜結(jié)構(gòu),與Lisp的cons、list演化出各種結(jié)構(gòu)一樣的奇妙。
6.Erlang中變量有兩個特點:
1)變量必須以大寫字母開頭
2)變量只能綁定一次,或者以一般的說法就是只能賦值一次,其實Erlang并沒有賦值這樣的概念,=號也是用于驗證匹配。
7.模式匹配——Pattern Matching,Erlang的模式匹配非常強大,看了buaawhl的《Erlang語法提要》的介紹,模式匹配的功能不僅僅在課程中介紹的數(shù)據(jù)結(jié)構(gòu)的拆解,在程序的分派也扮演重要角色,或者說Erlang的控制的流轉(zhuǎn)是通過模式匹配來實現(xiàn)的。具體功能參見鏈接,給出書中拆解列表的例子:
python 代碼
- [A,B|C] = [1,2,3,4,5,6,7]
- Succeeds - binds A = 1, B = 2,
- C = [3,4,5,6,7]
-
- [H|T] = [1,2,3,4]
- Succeeds - binds H = 1, T = [2,3,4]
-
- [H|T] = [abc]
- Succeeds - binds H = abc, T = []
-
- [H|T] = []
- Fails
下面會給出更多模式匹配的例子,給出一個模塊用來計算列表等
8.Erlang中函數(shù)的定義必須在一個模塊內(nèi)(Module),并且模塊和函數(shù)的名稱都必須是atom,函數(shù)的參數(shù)可以是任何的Erlang類型或者數(shù)據(jù)結(jié)構(gòu),函數(shù)要被調(diào)用需要從模塊中導(dǎo)出,函數(shù)調(diào)用的形式類似:
moduleName:funcName(Arg1,Arg2,...).
寫我們的第一個Erlang程序,人見人愛的Hello World:
java 代碼
- -module(helloWorld).
- -export([run/1]).
- run(Name)->
- io:format("Hello World ~w~n",[Name]).
存為helloWorld.erl,在Erlang Shell中執(zhí)行:
java 代碼
- 2> c(helloWorld).
- {ok,helloWorld}
- 3> helloWorld:run(dennis).
- Hello World dennis
- ok
打印出來了,現(xiàn)在解釋下程序構(gòu)造,
java 代碼
這一行聲明了模塊helloWorld,函數(shù)必須定義在模塊內(nèi),并且模塊名稱必須與源文件名相同。
java 代碼
而這一行聲明導(dǎo)出的函數(shù),run/1指的是有一個參數(shù)的run函數(shù),因為Erlang允許定義同名的有不同參數(shù)的多個函數(shù),通過指定/1來說明要導(dǎo)出的是哪個函數(shù)。
接下來就是函數(shù)定義了:
java 代碼
- run(Name)->
- io:format("Hello World ~w~n",[Name]).
大寫開頭的是變量Name,調(diào)用io模塊的format方法輸出,~w可以理解成占位符,將被實際Name取代,~n就是換行了。注意,函數(shù)定義完了要以句號.結(jié)束。然后執(zhí)行c(helloWorld).編譯源代碼,執(zhí)行:
java 代碼
9.內(nèi)置的常用函數(shù):
java 代碼
- date()
- time()
- length([1,2,3,4,5])
- size({a,b,c})
- atom_to_list(an_atom)
- list_to_tuple([1,2,3,4])
- integer_to_list(2234)
- tuple_to_list({})
- hd([1,2,3,4]) %輸出1,也就是列表的head
- tl([1,2,3,4]) %輸出[2,3,4],也就是列表的tail
10.常見Shell命令:
1)h(). 用來打印最近的20條歷史命令
2)b(). 查看所有綁定的變量
3) f(). 取消(遺忘)所有綁定的變量。
4) f(Val). 取消指定的綁定變量
5) e(n). 執(zhí)行第n條歷史命令
6) e(-1). 執(zhí)行上一條shell命令
11.又一個不知道怎么翻譯的概念——Guard。翻譯成約束?呵呵。用于限制變量的類型和范圍,比如:
java 代碼
- number(X) - X 是數(shù)字
- integer(X) - X 是整數(shù)
- float(X) - X 是浮點數(shù)
- atom(X) - X 是一個atom
- tuple(X) - X 是一個元組
- list(X) - X 是一個列表
-
- length(X) == 3 - X 是一個長度為3的列表
- size(X) == 2 - X 是一個長度為2的元組
-
- X > Y + Z - X >Y+Z
- X == Y - X 與Y相等
- X =:= Y - X 全等于Y
- (比如: 1 == 1.0 成功
- 1 =:= 1.0 失敗)
為了方便比較,Erlang規(guī)定如下的比較順序:
java 代碼
- number < atom < reference < port < pid < tuple < list
12.忘了介紹apply函數(shù),這個函數(shù)對于熟悉javascript的人來說很親切,javascript實現(xiàn)mixin就得靠它,它的調(diào)用方式如下:
apply(Mod, Func, Args),三個參數(shù)分別是模塊、函數(shù)以及參數(shù)列表,比如調(diào)用我們的第一個Erlang程序:
java 代碼
- apply(helloWorld,run,[dennis]).
13.if和case語句,if語句的結(jié)構(gòu)如下:
java 代碼
- if
- Guard1 ->
- Sequence1 ;
- Guard2 ->
- Sequence2 ;
- ...
- end
而case語句的結(jié)構(gòu)如下:
java 代碼
- case Expr of
- Pattern1 [when Guard1] -> Seq1;
- Pattern2 [when Guard2] -> Seq2;
-
- PatternN [when GuardN] -> SeqN
- end
if和case語句都有一個問題,就是當(dāng)沒有模式匹配或者Grard都是false的時候會導(dǎo)致error,這個問題case可以增加一個類似java中default的:
java 代碼
通過_指代任意的Expr,返回true,而if可以這樣:
java 代碼
一樣的道理。case語句另一個需要注意的問題就是變量范圍,每個case分支中定義的變量都將默認(rèn)導(dǎo)出case語句,也就是在case語句結(jié)束后可以被引用,因此一個規(guī)則就是每個case分支定義的變量應(yīng)該一致,不然算是非法的,編譯器會給出警告,比如:
java 代碼
- f(X) ->
- case g(X) of
- true -> A = h(X), B = A + 7;
- false -> B = 6
- end,
- h(A).
如果執(zhí)行true分支,變量A和變量B都被定義,而如果執(zhí)行的false分支,只有變量B被引用,可在case語句執(zhí)行后,h(A)調(diào)用了變量A,這是不安全的,因為變量A完全可能沒有被定義,編譯器將給出警告
variable 'A' unsafe in 'case' (line 10)
14.給出一些稍微復(fù)雜的模型匹配例子,比如用于計算數(shù)字列表的和、平均值、長度、查找某元素是否在列表中,我們把這個模塊定義為list:
java 代碼
- -module(list).
- -export([average/1,sum/1,len/1,double/1,member/2]).
- average(X)->sum(X)/len(X).
- sum([H|T]) when number(H)->H+sum(T);
- sum([])->0.
- len([_|T])->1+len(T);
- len([])->0.
- double([H|T]) -> [2*H|double(T)];
- double([]) -> [].
- member(H, [H|_]) -> true;
- member(H, [_|T]) -> member(H, T);
- member(_, []) -> false.
-
細(xì)細(xì)體會,利用遞歸來實現(xiàn),比較有趣。_用于指代任意的變量,當(dāng)我們只關(guān)注此處有變量,但并不關(guān)心變量的值的時候使用。用分號;來說明是同一個函數(shù)定義,只是不同的定義分支,通過模式匹配來決定調(diào)用哪個函數(shù)定義分支。
另一個例子,計算各種圖形的面積,也是課程中給出的例子:
java 代碼
- -module(mathStuff).
- -export([factorial/1,area/1]).
- factorial(0)->1;
- factorial(N) when N>0->N*factorial(N-1).
- %計算正方形面積,參數(shù)元組的第一個匹配square
- area({square, Side}) ->
- Side * Side;
- %計算圓的面積,匹配circle
- area({circle, Radius}) ->
- % almost :-)
- 3 * Radius * Radius;
- %計算三角形的面積,利用海倫公式,匹配triangle
- area({triangle, A, B, C}) ->
- S = (A + B + C)/2,
- math:sqrt(S*(S-A)*(S-B)*(S-C));
- %其他
- area(Other) ->
- {invalid_object, Other}.
執(zhí)行一下看看:
java 代碼
- 1> c(mathStuff).
- {ok,mathStuff}
- 2> mathStuff:area({square,2}).
- 4
- 3> mathStuff:area({circle,2}).
- 12
- 4> mathStuff:area({triangle,2,3,4}).
- 2.90474
- 5> mathStuff:area({other,2,3,4}).
- {invalid_object,{other,2,3,4}}
Erlang使用%開始單行注釋。
posted @
2009-09-11 10:11 暗夜教父 閱讀(549) |
評論 (0) |
編輯 收藏
大多數(shù)實時網(wǎng)絡(luò)游戲,將 server 的時間和 client 的時間校對一致是可以帶來許多其他系統(tǒng)設(shè)計上的便利的。這里說的對時,并非去調(diào)整 client 的 os 中的時鐘,而是把 game client 內(nèi)部的邏輯時間調(diào)整跟 server 一致即可。
一個粗略的對時方案可以是這樣的,client 發(fā)一個數(shù)據(jù)包給 server,里面記錄下發(fā)送時刻。server 收到后,立刻給這個數(shù)據(jù)包添加一個server 當(dāng)前時刻信息,并發(fā)還給 client 。因為大部分情況下,game server 不會立刻處理這個包,所以,可以在處理時再加一個時刻。兩者相減,client 可以算得包在 server 內(nèi)部耽擱時間。
client 收到 server 發(fā)還的對時包時,因為他可以取出當(dāng)初發(fā)送時自己附加的時刻信息,并知道當(dāng)前時刻,也就可以算出這個數(shù)據(jù)包來回的行程時間。這里,我們假定數(shù)據(jù)包來回時間想同,那么把 server 通知的時間,加上行程時間的一半,則可以將 client 時間和 server 時間校對一致。
這個過程用 udp 協(xié)議做比用 tcp 協(xié)議來的好。因為 tcp 協(xié)議可能因為丟包重發(fā)引起教大誤差,而 udp 則是自己控制,這個誤差要小的多。只是,現(xiàn)在網(wǎng)絡(luò)游戲用 tcp 協(xié)議實現(xiàn)要比 udp 有優(yōu)勢的多,我們也不必為對時另起一套協(xié)議走 udp 。
一般的解決方法用多次校對就可以了。因為,如果雙方時鐘快慢一致的情況下,對時包在網(wǎng)絡(luò)上行程時間越短,就一定表明誤差越小。這個誤差是不會超過包來回時間的一半的。我們一旦在對時過程中得到一個很小的行程時間,并在我們游戲邏輯的時間誤差允許范圍內(nèi),就不需要再校對了。
或者校對多次,發(fā)現(xiàn)網(wǎng)絡(luò)比較穩(wěn)定(雖然網(wǎng)速很慢),也可以認(rèn)為校對準(zhǔn)確。這種情況下,潛在的時間誤差可能比較大。好在,一般,我們在時間敏感的包上都會攜帶時間戳。當(dāng)雙方時間校對誤差很小的時候,client 發(fā)過來的時間戳是不應(yīng)該早于 server 真實時刻的。(當(dāng)時間校對準(zhǔn)確后,server 收到的包上的時間戳加上數(shù)據(jù)包單行時間,應(yīng)該等于 server 當(dāng)前時刻)
一旦 server 發(fā)現(xiàn) client 的包“提前”收到了,只有一種解釋:當(dāng)初校對時間時糟糕的網(wǎng)絡(luò)狀態(tài)帶來了很多的時間誤差,而現(xiàn)在的網(wǎng)絡(luò)狀態(tài)要明顯優(yōu)于那個時候。這時,server 應(yīng)該勒令 client 重新對時。同理,client 發(fā)現(xiàn) server 的數(shù)據(jù)包“提前”到達(dá),也可以主動向 server 重新對時。
一個良好的對時協(xié)議的設(shè)定,在協(xié)議上避免 client 時間作弊(比如加速器,或者減速器)是可行的。這里不討論也不分析更高級的利用游戲邏輯去時間作弊的方式,我們給數(shù)據(jù)包打上時間戳的主要目的也非防止時間作弊。
校對時間的一般通途是用來實現(xiàn)更流暢的戰(zhàn)斗系統(tǒng)和位置同步。因為不依賴網(wǎng)絡(luò)傳輸?shù)慕y(tǒng)一時間參照標(biāo)準(zhǔn)可以使游戲看起來更為實時。
首先談?wù)勎恢猛健?/p>
好的位置同步一定要考慮網(wǎng)絡(luò)延遲的影響,所以,簡單把 entity 的坐標(biāo)廣播到 clients 不是一個好的方案。我們應(yīng)該同步的是一個運動矢量以及時間信息。既,無論是 client 還是 server ,發(fā)出和收到的信息都應(yīng)該是每個 entity 在某個時刻的位置和運動方向。這樣,接收方可以根據(jù)收到的時刻,估算出 entity 的真實位置。對于 server 一方的處理,只要要求 client 按一個頻率(一般來說戰(zhàn)斗時 10Hz 即可,而非戰(zhàn)斗狀態(tài)或 player 不改變運動狀態(tài)時可以更低) 給它發(fā)送位置信息。server 可以在網(wǎng)絡(luò)狀態(tài)不好的情況下依據(jù)最近收到的包估算出現(xiàn)在 player 位置。而 client 發(fā)出的每次 player 位置信息,都應(yīng)該被 server 信任,用來去修正上次的估算值。而 server 要做的只是抽查,或交給另一個模塊去校驗數(shù)據(jù)包的合法性(防止作弊)。
在 server 端,每個 entity 的位置按 10Hz 的頻率做離散運動即可。
client 因為涉及顯示問題,玩家希望看到的是 entity 的連續(xù)運動,所以處理起來麻煩一點。server 發(fā)過來的位置同步信息也可能因為網(wǎng)絡(luò)延遲晚收到。client 同樣根據(jù)最近收到的包做估算,但是再收到的包和之前已經(jīng)收到的信息估算結(jié)果不同的時候,應(yīng)該做的是運動方向和速度的修正,盡可能的讓下次的估算更準(zhǔn)確。
關(guān)于戰(zhàn)斗指令同步,我希望是給所有戰(zhàn)斗指令都加上冷卻時間和引導(dǎo)時間,這正是 wow 的設(shè)計。這樣,信任 client 的時間戳,就可以得到 client 準(zhǔn)確的指令下達(dá)時間。引導(dǎo)時間(或者是公共冷卻時間)可以充當(dāng)網(wǎng)絡(luò)延遲時間的緩沖。當(dāng)然我們現(xiàn)在的設(shè)計會更復(fù)雜一些,這里不再列出。對于距離敏感的技能,例如遠(yuǎn)程攻擊和范圍魔法,我們的設(shè)計是有一個模糊的 miss 判定公式,解決距離邊界的判定問題。
這里, server 對攻擊目標(biāo)的位置做估算的時候,可以不按上次發(fā)出包的運動方向去做位置估計,而選擇用最有利于被攻擊者的運動方向來做。這樣,可以減少網(wǎng)絡(luò)狀況差的玩家的劣勢。
對于 PVE 的戰(zhàn)斗,甚至可以做更多的取舍,達(dá)到游戲流暢的效果。比如一個網(wǎng)絡(luò)狀態(tài)差的玩家去打 npc,他攻擊 npc 的時刻,npc 是處于攻擊范圍之內(nèi)的。但是由于網(wǎng)絡(luò)延遲,數(shù)據(jù)包被 server 收到的時候,npc 已經(jīng)離開。這個時候 server 可以以 client 的邏輯來將 npc 拉會原來的坐標(biāo)。
雖然,這樣做,可能會引起其他玩家(旁觀者) client 上表現(xiàn)的不同。但是,網(wǎng)絡(luò)游戲很多情況下是不需要嚴(yán)格同步的。在不影響主要游戲邏輯的情況下,player 的手感更為重要。
posted @
2009-09-10 19:27 暗夜教父 閱讀(591) |
評論 (0) |
編輯 收藏
看到這篇文章的時候,我覺得很驚訝,雖然我對這方面的了解并不多,但在自己的想像中,還是對網(wǎng)游這些東西稍有一點想法,因為曾經(jīng)有朋友做過簡單的外掛,比如,抓包發(fā)包然后嘗試模擬包,來使網(wǎng)游達(dá)到你想實現(xiàn)的效果。
外掛這東西,在2003年左右應(yīng)該是一個巔峰吧,那時候,奇跡外掛、傳奇外掛,確實讓一部分人先富起來,可是后來的零點行動,這些人都永遠(yuǎn)的消失在外掛長河中。
那時候我就在想,外掛是什么原理,為什么我這邊的動作,可以讓服務(wù)端產(chǎn)生那樣的效果?其實,這就是一個同步的問題,我個人理解是服務(wù)器上有個觸發(fā)器,這邊發(fā)包后,然后那邊判斷包是否正常,然后就會有一個相應(yīng)的動作。當(dāng)然,動作程序還是在本機上,地圖也在本機上,發(fā)出去的包,只是告訴服務(wù)器我是這樣在動作的。于是就出現(xiàn)了瞬移,卡點這種情況,因為發(fā)出去的包,和坐標(biāo)位置在服務(wù)器上都是正常的。(以上是我的猜測)
下面是文章:
不知道大家是否碰到過這種情況,當(dāng)某個玩家發(fā)出一個火球,這個火球有自己的運動軌跡,那么如何來判斷火球是否打中了人呢?大部分情況,當(dāng)策劃提出這個要求的時候,一般會被程序否認(rèn),原因是:太麻煩了,呵呵。復(fù)雜點的還有包括兩個火球相撞之類的事情發(fā)生。
那么網(wǎng)絡(luò)游戲中,是否真的無法模擬實現(xiàn)這種模擬呢?
首先我們來看看模擬此種操作會帶來什么樣的麻煩:
1,服務(wù)器必須trace火球的運行軌跡,乍一想,挺慢的。
2,網(wǎng)絡(luò)延遲,傳過來有延遲,傳過去有延遲,延遲還不穩(wěn)定,麻煩。
3,都有兩點解決不了了,接下來不愿意再想了。
呵呵,實際上呢,對火球的模擬比對人物運動的模擬要輕松很多,原因很簡單,火球的方向不會變。下面來看看具體用什么樣的結(jié)構(gòu)來實現(xiàn):
不知道大家是否還記得我去年這個時候提到過的Dead Reckoning算法,我們要模擬火球運動的關(guān)鍵就在于一個叫Moving Objects Tracing Server的服務(wù)器程序,這個服務(wù)器是干什么的呢。這個服務(wù)器接收主游戲服務(wù)器發(fā)過來的注冊事件的信息,比如有個玩家,開始移動了,那么主游戲服務(wù)器就 把該玩家的運動PDU,包括方向,速度,加速度,起點發(fā)給MOTS (Moving Objects Tracing Server),然后MOTS自己開始對其運行進(jìn)行模擬,當(dāng)游戲服務(wù)器發(fā)來第二個PDU包的時候,則對各個物件的位置進(jìn)行修正,并重新開始模擬。那么,我 們模擬的目的是什么呢?當(dāng)然是發(fā)生某些事件,比如說碰撞,或者掉入地圖的某個陷阱的時候,會將該事件回發(fā)給主邏輯服務(wù)器。然后邏輯服務(wù)器來處理該事件。
那么,對于火球的處理,也和處理其他玩家的同步一樣,當(dāng)接收到玩家的發(fā)火球的指令以后,產(chǎn)生一個火球,并指定其PDU信息,在MOTS上注冊該個運 動物 體。當(dāng)MOTS自行模擬到這個物體和其他玩家或者NPC物體產(chǎn)生碰撞,則通知主邏輯服務(wù)器,然后主邏輯服務(wù)器產(chǎn)生相應(yīng)的動作。
那么關(guān)于延遲呢?有些人也許會說,比如說前面有個火球,我本地操縱的小人其實躲過去了,但是因為網(wǎng)絡(luò)延遲,在服務(wù)器上我并沒有躲過去,那么怎么算? 呵呵, 不知道大家玩過星際沒有,有沒有發(fā)現(xiàn)在星際中玩多人連線模式的時候,有一點最特別的地方,就是控制一個小兵的時候,點了地圖上的某個位置,但是小兵并不會 馬上開始移動,而是有一定的延遲,但是這一小點延遲并不能掩蓋星際的經(jīng)典,同樣的理論用到這里也成立。對于客戶端的控制,當(dāng)玩家操縱的主角改變PDU信息 的時候,確保信息發(fā)送到服務(wù)器之后,再開始處理本地的操作指令,這樣就能保證本地的預(yù)測和服務(wù)器的預(yù)測幾乎是沒有什么誤差的,即使有很小的誤差產(chǎn)生,以服 務(wù)器為主,這樣玩家也不會有太大的抱怨。
————————————————————————————————————————-
網(wǎng)絡(luò)游戲同步詳解之一
同步在網(wǎng)絡(luò)游戲中是非常重要的,它保證了每個玩家在屏幕上看到的東西大體是一樣的。其實呢,解決同步問題的最簡單的方法就是把每個玩家的動作都向其 他玩家廣播一遍,這里其實就存在兩個問題:1,向哪些玩家廣播,廣播哪些消息。2,如果網(wǎng)絡(luò)延遲怎么辦。事實上呢,第一個問題是個非常簡單的問題,不過之 所以我提出這個問題來,是提醒大家在設(shè)計自己的消息結(jié)構(gòu)的時候,需要把這個因素考慮進(jìn)去。而對于第二個問題,則是一個挺麻煩的問題,大家可以來看這么個例 子:
比如有一個玩家A向服務(wù)器發(fā)了條指令,說我現(xiàn)在在P1點,要去P2點。指令發(fā)出的時間是T0,服務(wù)器收到指令的時間是T1,然后向周圍的玩家廣播這條 消息,消息的內(nèi)容是“玩家A從P1到P2”有一個在A附近的玩家B,收到服務(wù)器的這則廣播的消息的時間是T2,然后開始在客戶端上畫圖,A從P1到P2 點。這個時候就存在一個不同步的問題,玩家A和玩家B的屏幕上顯示的畫面相差了T2-T1的時間。這個時候怎么辦呢?
有個解決方案,我給它取名叫預(yù)測拉扯,雖然有些怪異了點,不過基本上大家也能從字面上來理解它的意思。要解決這個問題,首先要定義一個值叫:預(yù) 測誤差。然后需要在服務(wù)器端每個玩家連接的類里面加一項屬性,叫TimeModified,然后在玩家登陸的時候,對客戶端的時間和服務(wù)器的時間進(jìn)行比 較,得出來的差值保存在TimeModified里面。還是上面的那個例子,服務(wù)器廣播消息的時候,就根據(jù)要廣播對象的TimeModified,計算出 一個客戶端的CurrentTime,然后在消息頭里面包含這個CurrentTime,然后再進(jìn)行廣播。并且同時在玩家A的客戶端本地建立一個隊列,保 存該條消息,只到獲得服務(wù)器驗證就從未被驗證的消息隊列里面將該消息刪除,如果驗證失敗,則會被拉扯回P1點。然后當(dāng)玩家B收到了服務(wù)器發(fā)過來的消息“玩 家A從P1到P2”這個時候就檢查消息里面服務(wù)器發(fā)出的時間和本地時間做比較,如果大于定義的預(yù)測誤差,就算出在T2這個時間,玩家A的屏幕上走到的地點 P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再繼續(xù)走下去,這樣就能保證同步。更進(jìn)一步,為了保證客戶端運行起來更加smooth,我并不推薦直接 把玩家拉扯過去,而是算出P3偏后的一點P4,然后用(P4-P1)/T(P4-P3)來算出一個很快的速度S,然后讓玩家A用速度S快速移動到P4,這 樣的處理方法是比較合理的,這種解決方案的原形在國際上被稱為(Full plesiochronous),當(dāng)然,該原形被我篡改了很多來適應(yīng)網(wǎng)絡(luò)游戲的同步,所以而變成所謂的:預(yù)測拉扯。
另外一個解決方案,我給它取名叫驗證同步,聽名字也知道,大體的意思就是每條指令在經(jīng)過 服務(wù)器驗證通過了以后再執(zhí)行動作。具體的思路如下:首先 也需要在每個玩家連接類型里面定義一個 TimeModified,然后在客戶端響應(yīng)玩家鼠標(biāo)行走的同時,客戶端并不會先行走動,而是發(fā)一條走路的指令給服務(wù)器,然后等待服務(wù)器的驗證。服務(wù)器接 受到這條消息以后,進(jìn)行邏輯層的驗證,然后計算出需要廣播的范圍,包括玩家A在內(nèi),根據(jù)各個客戶端不同的TimeModified生成不同的消息頭,開始 廣播,這個時候這個玩家的走路信息就是完全同步的了。這個方法的優(yōu)點是能保證各個客戶端之間絕對的同步,缺點是當(dāng)網(wǎng)絡(luò)延遲比較大的時候,玩家的客戶端的行 為會變得比較不流暢,給玩家?guī)砗懿凰母杏X。該種解決方案的原形在國際上被稱為(Hierarchical master-slave synchronization),80年代以后被廣泛應(yīng)用于網(wǎng)絡(luò)的各個領(lǐng)域。
最后一種解決方案是一種理想化的解決方案,在國際上被稱為Mutual synchronization,是一種對未來網(wǎng)絡(luò)的前景的良好預(yù)測出來的解決方案。這里之所以要提這個方案,并不是說我們已經(jīng)完全的實現(xiàn)了這種方案,而 只是在網(wǎng)絡(luò)游戲領(lǐng)域的某些方面應(yīng)用到這種方案的某些思想。我對該種方案取名為:半服務(wù)器同步。大體的設(shè)計思路如下:
首先客戶端需要在登陸世界的時候建立很多張廣播列表,這些列表在客戶端后臺和服務(wù)器要進(jìn)行不及時同步,之所以要建立多張列表,是因為要廣播的類 型是不止一種的,比如說有l(wèi)ocal message,有remote message,還有g(shù)lobal message 等等,這些列表都需要在客戶端登陸的時候根據(jù)服務(wù)器發(fā)過來的消息建立好。在建立列表的同時,還需要獲得每個列表中廣播對象的TimeModified,并 且要維護(hù)一張完整的用戶狀態(tài)列表在后臺,也是不及時的和服務(wù)器進(jìn)行同步,根據(jù)本地的用戶狀態(tài)表,可以做到一部分決策由客戶端自己來決定,當(dāng)客戶端發(fā)送這部 分決策的時候,則直接將最終決策發(fā)送到各個廣播列表里面的客戶端,并對其時間進(jìn)行校對,保證每個客戶端在收到的消息的時間是和根據(jù)本地時間進(jìn)行校對過的。 那么再采用預(yù)測拉扯中提到過的計算提前量,提高速度行走過去的方法,將會使同步變得非常的smooth。該方案的優(yōu)點是不通過服務(wù)器,客戶端自己之間進(jìn)行 同步,大大的降低了由于網(wǎng)絡(luò)延遲而帶來的誤差,并且由于大部分決策都可以由客戶端來做,也大大的降低了服務(wù)器的資源。由此帶來的弊端就是由于消息和決策權(quán) 都放在客戶端本地,所以給外掛提供了很大的可乘之機。
綜合以上三種關(guān)于網(wǎng)絡(luò)同步派系的優(yōu)缺點,綜合出一套關(guān)于網(wǎng)絡(luò)游戲傳輸同步的較完整的解決方案,我稱它為綜合同步法(colligate synchronization)。大體設(shè)計思路如下:
首先將服務(wù)器需要同步的所有消息從劃分一個優(yōu)先等級,然后按照3/4的比例劃分出重要消息和非重要消息,對于非重要消息,把決策權(quán)放在客戶端,在客戶端邏輯上建立相關(guān)的決策機構(gòu)和各種消息緩存區(qū),以及相關(guān)的消息緩存區(qū)管理機構(gòu),如下圖所示:
上圖簡單說明了對于非重要消息,客戶端的大體處理流程,其中有一個客戶端被動行為值得大家注意,其中包括對服務(wù)器發(fā)過來的某些驗證代碼做返回, 來確保消息緩存中的消息和服務(wù)器端是一致的,從而有效的防止外掛來篡改本地消息緩存。其中的消息來源是包括本地的客戶端響應(yīng)玩家的消息以及遠(yuǎn)程服務(wù)器傳遞 過來的消息。
對于重要消息,比如說戰(zhàn)斗或者是某些牽扯到玩家一些比較敏感數(shù)據(jù)的操作,則采用另外一套方案,該方案首先需要在服務(wù)器和客戶端之間建立一套 Ping System,然后服務(wù)器保存和用戶的及時的ping值,當(dāng)ping比較小的時候,響應(yīng)玩家消息的同時先不進(jìn)行動作,而是先把該消息反饋給服務(wù)器,并且阻 塞,服務(wù)器收到該消息,進(jìn)行邏輯驗證之后向所有該詳細(xì)廣播的有效對象進(jìn)行廣播(包括消息發(fā)起者),然后客戶端收到該消息的驗證,才開始執(zhí)行動作。而當(dāng) ping比較大的時候,客戶端響應(yīng)玩家消息的同時立刻進(jìn)行動作,并且同時把該消息反饋給服務(wù)器,值得注意的是這個時候還需要在本地建立一個無驗證消息的隊 列,把該消息入隊,執(zhí)行動作的同時等待服務(wù)器的驗證,還需要保存當(dāng)前狀態(tài)。服務(wù)器收到客戶端的請求后,進(jìn)行邏輯驗證,并把消息反饋到各個客戶端,帶上各個 客戶端校對過的本地時間。如果驗證通過不過,則通知消息發(fā)起者,該消息驗證失敗,然后客戶端自動把已經(jīng)在進(jìn)行中的動作取消,恢復(fù)原來狀態(tài)。如果驗證通過, 則廣播到的各個客戶端根據(jù)從服務(wù)器獲得校對時間進(jìn)行對其進(jìn)行拉扯,保證在該行為完成之前完成同步。
至此,一個比較成熟的網(wǎng)絡(luò)游戲的同步機制已經(jīng)初步建立起來了,接下來的邏輯代碼就根據(jù)各自不同的游戲風(fēng)格以及側(cè)重點來寫了。
同步是網(wǎng)絡(luò)游戲最重要的問題,如何同步也牽扯到各個方面的問題,比如說游戲的規(guī)模,游戲的類型以及各種各樣的方面,對于規(guī)模比較大的游戲,在同 步方面可以下很多的工夫,把消息分得十分的細(xì)膩,對于不同的消息采用不同的同步機制,而對于規(guī)模比較小的游戲,則可以采用大體上一樣的同步機制,究竟怎么 樣同步,沒有個定式,是需要根據(jù)自己的不同情況來做出不同的同步?jīng)Q策的網(wǎng)游同步算法之導(dǎo)航推測(Dead Reckoning)算法:
——————————————————————————————————————————
網(wǎng)絡(luò)游戲同步詳解之二
在了解該算法前,我們先來談?wù)勗撍惴ǖ囊恍┍尘百Y料。大家都知道,在網(wǎng)絡(luò)傳輸?shù)臅r候,延遲現(xiàn)象是很普遍的,而在基于Server/Client結(jié)構(gòu) 下的網(wǎng)絡(luò)游戲的同步也就成了很頭疼的問題,在保證客戶端響應(yīng)用戶本地指令流暢的情況下,沒法有效的保證的同步的及時性。同樣,在軍方也有類似的事情發(fā)生, 即使是同一LAN里面的機器,也會因為傳輸?shù)难舆t,導(dǎo)致一些運算的失誤,介于此,美國國防部投入了大量的資金用于研究一種比較的好的方案來解決分布式系統(tǒng) 中的延遲問題,特別是一個叫分布式模擬運動(Distributed Interactive Simulation)的系統(tǒng),這套系統(tǒng)呢,其中就提出了一套號稱是Latency Hiding & Bandwidth Reduction的方案,命名為Dead Reckoning。呵呵,來頭很大吧,恩,那么我們下面就來看看這套系統(tǒng)的一些觀點,以及我們?nèi)绾伟阉\用到我們的網(wǎng)絡(luò)游戲的同步中。
首先,這套同步方案是基于我那篇《網(wǎng)絡(luò)游戲的同步》一文中的Mutual Synchronization同步方案的,也就是說,它并不是Server/Client結(jié)構(gòu)的,而是基于客戶端之間的同步的。下面我們先來說一些本文中將用到的名詞概念:
網(wǎng)狀網(wǎng)絡(luò):客戶端之間構(gòu)成的網(wǎng)絡(luò)
節(jié)點:網(wǎng)狀網(wǎng)絡(luò)中的每個客戶端
極限誤差:進(jìn)行同步的時候可能產(chǎn)生的誤差的極值
恩,在探討其原理的之前,我們先來看看我們需要一個什么樣的環(huán)境。首先,需要一個網(wǎng)狀網(wǎng)絡(luò),網(wǎng)狀網(wǎng)絡(luò)如何構(gòu)成呢?當(dāng)有新節(jié)點進(jìn)入的時候,通知該 網(wǎng)絡(luò)里面的所有節(jié)點,各節(jié)點為該客戶端在本地創(chuàng)建一個副本,登出的時候,則通知所有節(jié)點銷毀本地關(guān)于該節(jié)點的副本。然后每個節(jié)點該保存一些什么數(shù)據(jù)呢?首 先有一個很重要的包需要保存,叫做協(xié)議數(shù)據(jù)包(PDU Protocol Data Unit),PDU包含節(jié)點的一些相關(guān)的運動信息,比如當(dāng)前位置,速度,運動方向,或者還有加速度等一些信息。除PDU之外,還有其他信息需要保存,比如 說節(jié)點客戶端人物的HP,MP之類的。然后,保證每個節(jié)點在最少8秒之內(nèi)要向其它節(jié)點廣播一次PDU信息。最后,設(shè)置一個極限誤差值。到此,其環(huán)境就算搭 建完成了。下面,我們就來看看相關(guān)的具體算法:
假設(shè)在節(jié)點A有一個小人(路人甲),開始跑路了,這個時候,就像所有的節(jié)點廣播一次他的PDU信息,包括:速度(S),方向(O),加速度 (A)。那么所有的節(jié)點就開始模擬路人甲的運動軌跡和路線,包括節(jié)點A本身(這點很重要),同時,路人甲在某某玩家的控制下,會不時的改變一下方向,讓其 跑路的路線變得不是那么正規(guī)。在跑路的過程中,節(jié)點A有一個值在不停的記錄著其真實坐標(biāo)和在后臺模擬運動的坐標(biāo)的差值,當(dāng)差值大于極限誤差的時候,則計算 出當(dāng)前的速度S,方向O和速度A(算法將在后面介紹),并廣播給網(wǎng)絡(luò)中其他所有節(jié)點。其他節(jié)點在收到這條消息之后呢,就可以用一些很平滑的移動把路人甲拉 扯過去,然后重新調(diào)整模擬跑路的數(shù)據(jù),讓其繼續(xù)在后臺模擬跑路。
很顯然,如果極限誤差定義得大了,其他節(jié)點看到的偏差就會過大,如果極限偏差定義得小了,網(wǎng)絡(luò)帶寬就會增大。如果定義這個極限誤差,就該根據(jù)各 種數(shù)據(jù)的重要性來設(shè)計了。如果是回合制的網(wǎng)絡(luò)游戲,那么在走路上把極限誤差定義得大些無所謂,可以減少帶寬。但是如果是及時打斗的網(wǎng)絡(luò)游戲,那么就得把極 限誤差定義得小一些,否則會出現(xiàn)某人看到某人老遠(yuǎn)把自己給砍死的情況。
Dead Reckoning的主要算法有9種,但是只有兩種是解決主要問題的,其他的基本上只是針對不同的坐標(biāo)系的一些不同的算法,這里就不一一介紹了。好,那么我們下面來看傳說中的最主要的兩種算法:
第一:目標(biāo)點 = 原點 + 速度 * 時間差
第二:目標(biāo)點 = 原點 + 速度 * 時間差 + 1/2 * 加速度 * 時間差
呵呵,傳說中的算法都是很經(jīng)典的,雖然我們早在初中物理的時候就學(xué)過。
該算法的好處呢,正如它開始所說的,Latency Hiding & Bandwidth Reduction,從原則上解決了網(wǎng)絡(luò)延遲導(dǎo)致的不同步的問題,并且有效的減少了帶寬,不好的地方就是該算法基本上只能使用于移動中的同步,當(dāng)然,移動 的同步是網(wǎng)絡(luò)游戲中同步的最大的問題。
該方法結(jié)合我在《網(wǎng)絡(luò)游戲的同步》一文中提出的綜合同步法的構(gòu)架可以基本上解決掉網(wǎng)絡(luò)游戲中走路同步的問題。相關(guān)問題歡迎大家一起討論。
有關(guān)導(dǎo)航推測算法(Dead Reckoning)中的平滑處理:
根據(jù)我上篇文章所介紹的,在節(jié)點A收到節(jié)點B新的PDU包時,如果和A本地的關(guān)于B的模擬運動的坐標(biāo)不一致時,怎么樣在A的屏幕上把B拽到新的 PDU包所描敘的點上面去呢,上文中只提了用“很平滑的移動”把B“拉扯”過去,那么實際中應(yīng)該怎么操作呢?這里介紹四種方法。
第一種方法,我取名叫直接拉扯法,大家聽名字也知道,就是直接把B硬生生的拽到新的PDU包所描敘的坐標(biāo)上去,該方法的好處是:簡單。壞處是:看了以下三種方法之后你就不會用這種方法了。
第二種方法,叫直線行走(Linear),即讓B從它的當(dāng)前坐標(biāo)走直線到新的PDU包所描敘的坐標(biāo),行走速度用上文中所介紹的經(jīng)典算法:
目標(biāo)點 = 原點 + 速度 * 時間差 + 1/2 * 加速度 * 時間差算出:
首先算出從當(dāng)前坐標(biāo)到PDU包中描敘的坐標(biāo)所需要的時間:
T = Dest( TargetB – OriginB ) / Speed
然后根據(jù)新PDU包中所描敘的坐標(biāo)信息模擬計算出在時間T之后,按照新的PDU包中的運動信息所應(yīng)該達(dá)到的位置:
_TargetB = NewPDU.Speed * T
然后根據(jù)當(dāng)前模擬行動中的B和_TargetB的距離配合時間T算出一個修正過的速度_S:
_S = Dest( _TargetB – OriginB ) / T
然后在畫面上讓B以速度_S走直線到Target_B,并且在走到之后調(diào)整其速度,方向,加速度等信息為新的PDU包中所描敘的。
這種方法呢,非常的土,會讓物體在畫面上移動起來變得非常的不現(xiàn)實,經(jīng)常會出現(xiàn)很生硬的拐角,而且對于經(jīng)常要修改的速度_S,在玩家A的畫面上,玩家B的行動會變得非常的詭異。其好處是:比第一種方法要好。
第三種方法,叫二次方程行走(Quadratic),該方法的原理呢,就是在直線行走的過程中,加入二次方程來計算一條曲線路徑,讓Dest( _TargetB – OriginB )的過程是一條曲線,而不是一條直線,恩,具體的實現(xiàn)方法,就是在Linear方法的計算中,設(shè)定一個二次方程,在Dest函數(shù)計算距離的時候根據(jù)設(shè)定的 二次方程來計算,這樣一來,可以使B在玩家A屏幕上的移動變得比較的有人性化一些。但是該方法的考慮也是不周全的,僅僅只考慮了TargetB到 _TargetB的方向,而沒有考慮新的PDU包中的方向描敘,那么從_TargetB開始模擬行走的時候,仍然是會出現(xiàn)比較生硬的拐角,那么下面提出的 最終解決方案,將徹底解決這個問題。
——————————————————————————————————————————
網(wǎng)絡(luò)游戲同步詳解之三
最后一種方法叫:立方體抖動(Cubic Splines),這個東東比較復(fù)雜,它需要四個坐標(biāo)信息作為它的參數(shù)來進(jìn)行運算,第一個參數(shù)Pos1是OriginB,第二個參數(shù)Pos2是 OriginB在模擬運行一秒以后的位置,第三個參數(shù)Pos3是到達(dá)_TargetB前一秒的位置,第四個參數(shù)pos4是_TargetB的位置。
Struct pos {
Coordinate X;
Coordinate Y;
}
Pos1 = OriginB
Pos2 = OriginB + V
Pos3 = _TargetB – V
Pos4 = _TargetB
運動軌跡中(x, y)的坐標(biāo)。
x = At^3 + Bt^2 + Ct + D
y = Et^3 + Ft^2 + Gt + H
(其中時間t的取值范圍為0-1,在Pos1的時候為0,在Pos4的時候為1)
x(0-3)代表Pos1-Pos4中x的值,y(0-3)代表Pos1-Pos4中y的值
A = x3 – 3 * x2 +3 * x1 – x0
B = 3 * x2 – 6 * x1 + 3 * x0
C = 3 * x1 – 3 * x0
D = x0
E = y3 – 3 * y2 +3 * y1 – y0
F = 3 * y2 – 6 * y1 + 3 * y0
G = 3 * y1 – 3 * y0
H = y0
上面是公式,那么下面我們來看看如何獲得Pos1-Pos4:首先,Pos1和 Pos2的取值會比較容易獲得,根據(jù)OriginB配合當(dāng)前的速度和方向可以獲得,然而Pos3和Pos4呢,怎么獲得呢?如果在從Pos1到Pos4的 過程中有新的PDU到達(dá),那么我們定義它為NewPackage。
Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2
Pos4 = Pos3 – (NewPackage.V + NewPackage.a * t)
如果沒有NewPackage的情況下,則Pos3和Pos4按照開始所規(guī)定的方法獲得。
至此,關(guān)于導(dǎo)航推測的算法大致介紹完畢。
原文來自:http://xinsync.xju.edu.cn/index.php/archives/4079
posted @
2009-09-10 19:21 暗夜教父 閱讀(366) |
評論 (0) |
編輯 收藏
http://canremember.com/?p=8
http://canremember.com/?p=10
過去一年中,花了很多時間在考慮服務(wù)器架構(gòu)設(shè)計方面的問題。看了大量文章、也研究了不少開源項目,眼界倒是開闊了不少,不過回過頭來看,對網(wǎng)游架構(gòu)設(shè)計方面的幫助卻是不多。老外還是玩兒console game的多,MMO Games方面涉及的還是不如國內(nèi)廣泛。看看 Massively Multiplayer Games Development 1 & 2 這兩本書吧,質(zhì)量說實話很一般,幫助自然也很有限。當(dāng)然這也是好事,對國內(nèi)的研發(fā)公司/團(tuán)隊來說,在網(wǎng)游服務(wù)器技術(shù)方面當(dāng)然就存在超越老外的可能性,而且在這方面技術(shù)超越的機會更大,當(dāng)然前提是要有積累、要舍得投入,研發(fā)人員更要耐得住寂寞、經(jīng)得起誘惑,在平均每天收到超過3個獵頭電話的時候——依然不動心。
上面有點兒扯遠(yuǎn)了,下面聊聊無縫世界架構(gòu)(Seamless world server architecture)設(shè)計方面的一點兒看法。
先說架構(gòu)設(shè)計的目標(biāo)——我的看法,服務(wù)器組架構(gòu)設(shè)計的目標(biāo)就是確定各服務(wù)器拓補關(guān)系和主要的業(yè)務(wù)邏輯處理方法。主要要解決的問題就是在滿足游戲內(nèi)容設(shè)計需要的前提下,如何提高帶負(fù)載能力的問題。
最簡單的架構(gòu)就是基本的C/S架構(gòu),一臺Server直接構(gòu)成一個Cluster,所有Client直接連接這個Server,這個Server完成所有邏輯和數(shù)據(jù)處理。這架構(gòu)其實很好,最大的好處就是它架構(gòu)上的 Simplicity ,Cluster內(nèi)部的跨進(jìn)程交互完全被排除,復(fù)雜度立刻就降下來了,而且——完全可以實現(xiàn)一個無縫(Seamless world)的游戲世界。但是即使我不說,大家也知道這種單Server架構(gòu)會有什么問題。不過我們不妨以另外一個角度來看這個Server——一個黑盒子。從系統(tǒng)外部的角度來看,什么樣的系統(tǒng)都可以看成一個整體、一個黑盒,而不管系統(tǒng)內(nèi)部的拓補關(guān)系和實現(xiàn)復(fù)雜度方面的問題。在不考慮這個系統(tǒng)的實現(xiàn)的前提下,理論上Cluster的處理能力就是由硬件的數(shù)量和能力決定的,也就是說一個Server Cluster內(nèi)包含越多的服務(wù)器、服務(wù)器越‘快’,那么這個Cluster的處理能力越好、帶負(fù)載能力越好。那么我們要面對的帶負(fù)載能力的問題,就是如何高效的利用這些Server的問題,基本上也可以理解為如何提高玩家請求的并發(fā)處理能力的問題。
CPU廠商在很久以前就在考慮這方面的問題了,CPU其實也可以看成個黑盒。看看他們用過的技術(shù)——流水線(pipeline)技術(shù)、多CPU/多核(multicore)技術(shù),以及這些技術(shù)的衍生技術(shù)。我想了很久讓 Server Cluster 內(nèi)部處理并行的方法、并且有了比較清晰的思路之后,才發(fā)現(xiàn)其實早就可以參照CPU廠商的方法。流水線的方法就是把一個指令處理拆分成很多個步驟,這樣指令的處理被分解之后就可以部分重疊(相當(dāng)于變成并發(fā)的了)執(zhí)行。我們的Server Cluster一樣可以用這種方法來拆分,我想了個名字——
Services-based Architecture——基于服務(wù)的架構(gòu)。在這種架構(gòu)內(nèi)部,我們根據(jù)處理數(shù)據(jù)、邏輯的相關(guān)性來劃分組內(nèi)各個服務(wù)器的工作任務(wù)。例如:位置服務(wù)提供物體可見性信息、物品服務(wù)處理所有物品相關(guān)的邏輯、社會關(guān)系服務(wù)提供行會家族等等方面的邏輯、戰(zhàn)斗服務(wù)器只處理戰(zhàn)斗相關(guān)的邏輯,等等。這樣劃分的話、邏輯處理的并發(fā)就有了可能性。舉例來說:A砍B一刀這件事情與C從奸商手里買到一件武器這個事情是完全不相干的,而且這2個請求本來就在不同的服務(wù)器上被處理,他們是被不同的Service Server并發(fā)處理的。這就是 Services-based Architecture 的并發(fā)方法。
基本上,把游戲邏輯的處理拆分成一個個的service,就和設(shè)計cpu的時候把機器指令的具體處理拆分,然后設(shè)計出一個個流水線單元是一個道理。
Cells-based Architecture——基于cell的架構(gòu)。每個cell都在不同的物理 server上面運行著完全一樣的應(yīng)用程序服務(wù)器,但是他們負(fù)責(zé)承載不同的游戲場景區(qū)域的游戲邏輯。和 services-based arch. 明顯不同的就是,每個cell都是個‘在邏輯上完整的’服務(wù)器。它得處理物品操作、人物移動、戰(zhàn)斗計算等等幾乎所有的游戲邏輯。盡管這么做會帶來一些(可能是很復(fù)雜)的問題,但是它完全是可行的。舉例來說:在吳國A砍B一刀顯然地和千里之外在越國的C砍D一刀不搭界,他們完全可以被不同的Cell并發(fā)地處理。
基本上,這就相當(dāng)于一個主板上面插多個CPU或者一個CPU但是有多個內(nèi)核,每個CPU能做的事情都是一樣的,而且能一起做。
從一組服務(wù)器的角度來看,一般來說,我們的服務(wù)器組(Cluster)內(nèi)都會有登陸驗證服務(wù)器(Login Server)、持久性數(shù)據(jù)服務(wù)器(DB及DB Proxy)、連接代理服務(wù)器(Gate Server、FEP Server、Client Proxy等)以及Auto Patch Server、還有用于集中管理及控制組的服務(wù)器等等,由于這些服務(wù)器基本上什么樣的架構(gòu)設(shè)計都會用到,所以——現(xiàn)在不考慮以上這些服務(wù)器,只考慮具體處理游戲邏輯、游戲規(guī)則的各個服務(wù)器。以此為前提來分析一下 Services-based Architecture 和 Cells-based Architecture 的優(yōu)缺點。
對Services-based Architecture 的分析
基于服務(wù)的架構(gòu),顧名思義這種架構(gòu)的實現(xiàn)(程序)會是和服務(wù)的具體內(nèi)容(策劃)相關(guān)的,這是因為——各種【服務(wù)】內(nèi)容的確定是建立于項目的【需求分析】基礎(chǔ)上的,【需求分析】的前提是基本確定了【策劃設(shè)計】,至少是項目的概要設(shè)計。
我想多數(shù)做過游戲項目的人都應(yīng)該對需求變更有很深的感觸,每個人都說“開始想做的那個和最后實際做出來的那個不一樣”。特別是在項目的早期階段,團(tuán)隊的不同成員對項目做完之后的樣子有相當(dāng)不同的看法(很可能大家互相都不知道對方怎么看的),這很容易理解,誰也不可能從幾頁紙幾張圖就確切地知道這個游戲做完了什么樣子,即使不考慮需求變更。涉及到項目開發(fā)方法方面的東西這里就不多說了,總之我的看法就是——盡管我們不大可能設(shè)計出一個架構(gòu)能夠適應(yīng)任何的游戲設(shè)計,但是不同開發(fā)任務(wù)間的耦合度顯然還是越低越好,基于服務(wù)的架構(gòu)適應(yīng)需求變更的能力較差。
關(guān)于服務(wù)耦合
不管如何劃分service,不同 service之間都一定存在不同程度的耦合(coupling)關(guān)系,不同的 service 之間會有相互依賴關(guān)系。而你們的策劃設(shè)計可能會讓這種關(guān)系復(fù)雜到程序在運行時的狀態(tài)很難以琢磨的程度。
假設(shè):
服務(wù)器組內(nèi)的戰(zhàn)斗處理和物品處理分別由兩個不同的服務(wù)(器)提供
游戲規(guī)則:
人物被攻擊后自己攜帶的物品可能掉落到地上
某些物品掉落后會爆炸
物品在地上爆炸可能傷及周圍(半徑10米內(nèi))人物
人物之間的‘仇恨度’影響戰(zhàn)斗數(shù)值計算
被攻擊時掉落的物品爆炸后傷及的人物,會增加對‘被攻擊人’的‘仇恨度’
我想我還能想出很多很多“看上去不算過分”的規(guī)則來讓這個事情變得復(fù)雜無比,很可能你們的策劃也在無意中,已經(jīng)擁有我這種能力 :) 而且他們在寫文檔時候的表達(dá)還多半不如我上面寫的清楚,另外,他們還會把這些規(guī)則分到很多不同的文檔里面去寫。好吧,你肯定會想“把這兩個服務(wù)合二為一好了 ”,實際上不管你想把哪兩個(或多個)服務(wù)合并為一個服務(wù)的時候,都應(yīng)該先考慮一下當(dāng)時是為什么把他們獨立為不同服務(wù)的?
實際上很多這樣“看上去不算過分”的規(guī)則都會導(dǎo)致service間的頻繁交互,所以每個service最好都是stateless service,這樣的話情況會好很多,但是對于游戲來說這很難做到。
請求處理的時序問題
服務(wù)耦合的問題在不考慮開發(fā)復(fù)雜度比較高的情況下,還是可以被搞定的,只要腦袋夠清醒,愿意花夠多的時間,那么還有更難以搞定的么?我看確實還有,如果你對將要面對的問題,了解得足夠多的話:)

上面兩個序列圖描述的是某個玩家做了連續(xù)做了兩次同樣的操作但是很可能得到了不同的結(jié)果,當(dāng)然這些請求都是異步地被處理。問題的關(guān)鍵在于——盡管兩次玩家執(zhí)行的命令一樣、順序一樣,甚至?xí)r間間隔都一樣,但是結(jié)果卻很不同——因為圖(1)里面C2CS::Request_to_attack請求被處理的時候,C2IS::Request_equip_item 這個請求還沒有被處理完,但是圖(2)顯示的情況就不一樣了。因為C2IS::Request_equip_item這個操作很可能會改變游戲人物的屬性,這個屬性又很可能影響attack的結(jié)果。這兩幅圖實際上省略了 Combat Server 與 Item Server 之間的交互過程。但是已經(jīng)足以說明問題了,每個Service處理每個Request時具體會消耗的時間,是無法在設(shè)計時確定的!
誰喜歡這類結(jié)果上的不確定性?舉個例子:玩家很可能已經(jīng)裝備上了“只能使用1次的魔獸必殺刀”然后攻擊了一下魔獸,但是它卻沒死!這會導(dǎo)致什么樣的結(jié)果?請自行想象。另外,這種不確定性還會表現(xiàn)為“在項目開發(fā)期和運營期的行為差異”,或者“出現(xiàn)某些偶然的奇怪現(xiàn)象”。
那還有解決方案么?有的,其實只要序列化玩家請求的處理,使處理有序進(jìn)行就可以了。但是又一次的,這會帶來新的復(fù)雜度——在某個范圍(整個服務(wù)器組?一個行會?一個隊伍?)內(nèi),以每個玩家為單位,序列化他(們)的(可能是所有)操作,但是也顯而易見,這在某種程度上降低了請求處理的并發(fā)性,盡管它對并發(fā)性的影響可能只局限于不大(最少是一個玩家)的范圍。
對Cells-based Architecture 的分析
基于Cell的架構(gòu)有個明顯的優(yōu)勢就是Cell如何劃分和你的策劃沒有關(guān)系J這是真的。而且Cell間如何交互可以被放到系統(tǒng)的底層,具體有多底層、多隱蔽(實際上可以隱蔽到對開發(fā)上層游戲邏輯的程序員都不可見的程度)要看你的實現(xiàn)如何了。如果做到了某個系統(tǒng)的程序設(shè)計與游戲設(shè)計完全無關(guān)的話,顯然,這個系統(tǒng)受到游戲設(shè)計變更(需求變更)的影響就會很小很小,甚至?xí)酵耆皇苡绊懙某潭龋?dāng)然這是理想情況。
關(guān)于跨邊界對象交互
在基于Cell的服務(wù)器架構(gòu)里面,實現(xiàn)無縫世界(Seamless World)的主要難點在于實現(xiàn)跨邊界對象的交互時會出現(xiàn)的一些問題,因為這些對象在不同的Cell進(jìn)程里面,這些Cell一般來說是在不同的物理服務(wù)器上運行。
無縫世界的特點自然就是無縫,并且因為無縫給玩家?guī)砀玫挠螒蝮w驗,所以顯然我們希望“跨邊界對象交互”問題不把事情搞砸,那么這種交互的表現(xiàn)就必須滿足穩(wěn)定、高效的前提。一般來說,高于300ms的延遲對玩家操作來說就屬于“明顯可見”的程度了,不能讓玩家騎著500塊RMB買來的虛擬馬在一片大草原上面暢快的奔跑的時候,在某個地方突然就被“看不見的墻”給“擋”了一下,因為這“墻”根本看不見,所以會很影響“上帝”的游戲心情。
關(guān)于組成整個虛擬世界的Cell之間的關(guān)系,下面來分析兩種情況:
<!--[if !supportLists]-->一, <!--[endif]-->Cell 承載的場景不重疊

如圖(1),一個連續(xù)的虛擬世界場景被分成左右兩塊,分別在不同的Cell Server上面運行。A、B、C分別是3個不同的游戲角色。在這種情況下B與C的交互并不存在任何障礙,因為B和C只不過是同一個物理服務(wù)器上同一個進(jìn)程內(nèi)的兩塊不同的內(nèi)存數(shù)據(jù)而已。但是A與B/C的交互就不那么直接了,盡管他們所在的場景看上去是“連續(xù)的、一體的”但是事情不會像表面上那么簡單。A與B發(fā)生交互時候會發(fā)生什么事情?例如A攻擊了B、A與B交易物品等等,因為在這種結(jié)構(gòu)下做數(shù)據(jù)同步會帶來很多問題,例如對象狀態(tài)不確定性、開發(fā)復(fù)雜度等等、相對來說兩個Cell Server之間做網(wǎng)絡(luò)通訊而帶來的延遲可能反而是最小的問題,這些問題不需要很復(fù)雜的分析就可以得出結(jié)論,在此不再多說了。
<!--[if !supportLists]-->二,Cell 承載的場景(部分地)重疊

如圖(2),一個連續(xù)的虛擬世界場景被分成左右兩塊,分別在不用的Cell Server上面運行。A、B、C、D分別是4個不同的游戲角色。這個情況下,中間的區(qū)域為2個Cell所共同維護(hù),中間區(qū)域的對象同屬于2個Cell所‘擁有’。這有什么好處?現(xiàn)在,任意兩個對象之間,除了A與C之間的交互,都變得更‘直接’了。變得直接肯定是一件好事兒,那么A與C之間呢?他們之間其實也沒有任何問題J 因為雙方都已經(jīng)超出了對方的Area of Interest(AoI)區(qū)域,游戲規(guī)則可以限制他們不能直接交互。
上面提到的第二種方案算不上什么魔法,但是肯定是比第一種方案更有效。接下來怎么辦?假設(shè)B是個玩家,他站在中間這塊區(qū)域上面時,并不會產(chǎn)生“我到底是在哪里”這樣的疑問J 問題的關(guān)鍵在于對于Cell Server來說,怎么樣同步那些處于重疊區(qū)域?qū)ο蟮臓顟B(tài)。游戲世界內(nèi)的對象可能同時處于1個、2個、3個或者4個不同的Cell Server。如果你的Cell分隔方法不限于水平線和垂直線、或者有人故意搗亂的話,還可能會更多。需要被同步的對象也不只是玩家本身,還包括怪物、NPC、一顆會走的樹、某玩家在地上吐的痰等等。
由于我們的基于無縫世界的游戲規(guī)則不大會直接去限制游戲世界某處玩家的行為,也就是說玩家如果能相互交易物品的話,他們肯定希望在任何地方都能交易,“為什么其他地方都行,但是在某個墻角做交易就會導(dǎo)致物品丟失?”所以比較可靠的方法是建立一套的用于同步的底層機制,來同步這些跨邊界對象。
怎么實現(xiàn)?這個話題很大,恐怕再寫幾篇Blog我也講不完,但是有一些東西可以作為參考,例如:DCOM和CORBA規(guī)范,Java的RMI,基于Python的 PYRO,TAO(The ACE ORB)等等。好在分布式處理的問題不止是網(wǎng)絡(luò)游戲會涉及到,可以借鑒的東西還是很多的。
總結(jié)
很顯然,這篇文章在兩種架構(gòu)的評價上面存在某些傾向性,但是傾向性本身只是副產(chǎn)品。另外一個副產(chǎn)品就是關(guān)于一些技術(shù)分析方法。
在考慮采用何種技術(shù)的時候,我們往往很容易地就會忽略對程序之外那些事情的影響。上面我提到的關(guān)于Services-based架構(gòu)實現(xiàn)的時候,提到劃分service及數(shù)據(jù)設(shè)計對程序設(shè)計能力的挑戰(zhàn)、對策劃設(shè)計的制約,對適應(yīng)需求變更能力的影響,都不會只是空談。這些問題也不是只在實現(xiàn)這種架構(gòu)的時候才出現(xiàn)。
不要高估自己的智商,Keep It Simple and Stupid :) 應(yīng)該可以讓我們離成功更近一點兒。
本文來自CSDN博客,轉(zhuǎn)載請標(biāo)明出處:http://blog.csdn.net/romandion/archive/2009/04/02/4044368.aspx
posted @
2009-09-09 10:53 暗夜教父 閱讀(479) |
評論 (0) |
編輯 收藏
由于網(wǎng)游服務(wù)器的設(shè)計牽涉到太多內(nèi)容,比如:網(wǎng)絡(luò)通信方面、人工智能、數(shù)據(jù)庫設(shè)計等等,所以本文將重點從網(wǎng)絡(luò)通信方面的內(nèi)容展開論述。談到網(wǎng)絡(luò)通信,就不能不涉及如下五個問題:
1、 常見的網(wǎng)游服務(wù)通信器架構(gòu)概述
2、 網(wǎng)游服務(wù)器設(shè)計的基本原則
3、 網(wǎng)游服務(wù)器通信架構(gòu)設(shè)計所需的基本技術(shù)
4、 網(wǎng)游服務(wù)器通信架構(gòu)的測試
5、 網(wǎng)游服務(wù)器通信架構(gòu)設(shè)計的常見問題
下面我們就從第一個問題說起:
常見的網(wǎng)游服務(wù)器通信架構(gòu)概述
目前,國內(nèi)的網(wǎng)游市場中大體存在兩種類型的網(wǎng)游游戲:MMORPG(如:魔獸世界)和休閑網(wǎng)游(如:QQ休閑游戲和聯(lián)眾游戲,而如泡泡堂一類的游戲與QQ休閑游戲有很多相同點,因此也歸為此類)。由于二者在游戲風(fēng)格上的截然不同,導(dǎo)致了他們在通信架構(gòu)設(shè)計思路上的較大差別。下面筆者將分別描述這兩種網(wǎng)游的通信架構(gòu)。
1.MMORPG類網(wǎng)游的通信架構(gòu)
網(wǎng)游的通信架構(gòu),通常是根據(jù)幾個方面來確定的:游戲的功能組成、游戲的預(yù)計上線人數(shù)以及游戲的可擴展性。
目前比較通用的MMORPG游戲流程是這樣的:
a. 玩家到游戲官方網(wǎng)站注冊用戶名和密碼。
b. 注冊完成后,玩家選擇在某一個區(qū)激活游戲賬號。
c. 玩家在游戲客戶端中登錄進(jìn)入已經(jīng)被激活的游戲分區(qū),建立游戲角色進(jìn)行游戲。
通常,在這樣的模式下,玩家的角色數(shù)據(jù)是不能跨區(qū)使用的,即:在A區(qū)建立的游戲角色在B區(qū)是無法使用的,各區(qū)之間的數(shù)據(jù)保持各自獨立性。我們將這樣獨立的A區(qū)或B區(qū)稱為一個獨立的服務(wù)器組,一個獨立的服務(wù)器組就是一個相對完整的游戲世界。而網(wǎng)游服務(wù)器的通信架構(gòu)設(shè)計,則包括了基于服務(wù)器組之上的整個游戲世界的通信架構(gòu),以及在一個服務(wù)器組之內(nèi)的服務(wù)器通信架構(gòu)。
我們先來看看單獨的服務(wù)器組內(nèi)部的通信是如何設(shè)計的。
一個服務(wù)器組內(nèi)的各服務(wù)器組成,要依據(jù)游戲功能進(jìn)行劃分。不同的游戲內(nèi)容策劃會對服務(wù)器的組成造成不同的影響。一般地,我們可以將一個組內(nèi)的服務(wù)器簡單地分成兩類:場景相關(guān)的(如:行走、戰(zhàn)斗等)以及場景不相關(guān)的(如:公會聊天、不受區(qū)域限制的貿(mào)易等)。為了保證游戲的流暢性,可以將這兩類不同的功能分別交由不同的服務(wù)器去各自完成。另外,對于那些在服務(wù)器運行中進(jìn)行的比較耗時的計算,一般也會將其單獨提煉出來,交由單獨的線程或單獨的進(jìn)程去完成。
各個網(wǎng)游項目會根據(jù)游戲特點的不同,而靈活選擇自己的服務(wù)器組成方案。經(jīng)常可以見到的一種方案是:場景服務(wù)器、非場景服務(wù)器、服務(wù)器管理器、AI服務(wù)器以及數(shù)據(jù)庫代理服務(wù)器。
以上各服務(wù)器的主要功能是:
場景服務(wù)器:它負(fù)責(zé)完成主要的游戲邏輯,這些邏輯包括:角色在游戲場景中的進(jìn)入與退出、角色的行走與跑動、角色戰(zhàn)斗(包括打怪)、任務(wù)的認(rèn)領(lǐng)等。場景服務(wù)器設(shè)計的好壞是整個游戲世界服務(wù)器性能差異的主要體現(xiàn),它的設(shè)計難度不僅僅在于通信模型方面,更主要的是整個服務(wù)器的體系架構(gòu)和同步機制的設(shè)計。
非場景服務(wù)器:它主要負(fù)責(zé)完成與游戲場景不相關(guān)的游戲邏輯,這些邏輯不依靠游戲的地圖系統(tǒng)也能正常進(jìn)行,比如公會聊天或世界聊天,之所以把它從場景服務(wù)器中獨立出來,是為了節(jié)省場景服務(wù)器的CPU和帶寬資源,讓場景服務(wù)器能夠盡可能快地處理那些對游戲流暢性影響較大的游戲邏輯。
服務(wù)器管理器:為了實現(xiàn)眾多的場景服務(wù)器之間以及場景服務(wù)器與非場景服務(wù)器之間的數(shù)據(jù)同步,我們必須建立一個統(tǒng)一的管理者,這個管理者就是服務(wù)器組中的服務(wù)器管理器。它的任務(wù)主要是在各服務(wù)器之間作數(shù)據(jù)同步,比如玩家上下線信息的同步。其最主要的功能還是完成場景切換時的數(shù)據(jù)同步。當(dāng)玩家需要從一個場景A切換到另一個場景B時,服務(wù)器管理器負(fù)責(zé)將玩家的數(shù)據(jù)從場景A轉(zhuǎn)移到場景B,并通過協(xié)議通知這兩個場景數(shù)據(jù)同步的開始與結(jié)束。所以,為了實現(xiàn)這些內(nèi)容繁雜的數(shù)據(jù)同步任務(wù),服務(wù)器管理器通常會與所有的場景服務(wù)器和非場景服務(wù)器保持socket連接。
AI(人工智能)服務(wù)器:由于怪物的人工智能計算非常消耗系統(tǒng)資源,所以我們把它獨立成單獨的服務(wù)器。AI服務(wù)器的主要作用是負(fù)責(zé)計算怪物的AI,并將計算結(jié)果返回給場景服務(wù)器,也就是說,AI服務(wù)器是單獨為場景服務(wù)器服務(wù)的,它完成從場景服務(wù)器交過來的計算任務(wù),并將計算結(jié)果返回給場景服務(wù)器。所以,從網(wǎng)絡(luò)通信方面來說,AI服務(wù)器只與眾多場景服務(wù)器保持socket連接。
數(shù)據(jù)庫代理服務(wù)器:在網(wǎng)游的數(shù)據(jù)庫讀寫方面,通常有兩種作法,一種是在應(yīng)用服務(wù)器中直接加進(jìn)數(shù)據(jù)庫訪問的代碼進(jìn)行數(shù)據(jù)庫訪問,還有一種方式是將數(shù)據(jù)庫讀寫?yīng)毩⒊鰜恚瑔为氉鞒蓴?shù)據(jù)庫代理,由它統(tǒng)一進(jìn)行數(shù)據(jù)庫訪問并返回訪問結(jié)果。
其中,非場景服務(wù)器在不同的游戲項目中可能會被設(shè)計成不同的功能,比如以組隊、公會或全頻道聊天為特色的游戲,很可能為了滿足玩家的聊天需求而設(shè)立單獨的聊天服務(wù)器;而如果是以物品貿(mào)易(如拍賣等)為特色的游戲,很可能為了滿足拍賣的需求而單獨設(shè)立拍賣服務(wù)器。到底是不是有必要將某一項游戲功能獨立處理成一個服務(wù)器,要視該功能對游戲的主場景邏輯(指行走、戰(zhàn)斗等玩家日常游戲行為)的影響程度而定。如果該功能對主場景邏輯的影響比較大,可能對主場景邏輯的運行造成比較嚴(yán)重的性能和效率損失,那么應(yīng)考慮將其從主場景邏輯中剝離,但能否剝離還有另一個前提:此功能是否與游戲場景(即地圖坐標(biāo)系統(tǒng))相關(guān)。如果此功能與場景相關(guān)又確實影響到了主場景邏輯的執(zhí)行效率,則可能需要在場景服務(wù)器上設(shè)立專門的線程來處理而不是將它獨立成一個單獨的服務(wù)器。
以上是一個服務(wù)器組內(nèi)的各服務(wù)器組成情況介紹,那么,各服務(wù)器之間是如何通信的呢?它的基本通信構(gòu)架有哪些呢?
MMORPG的單組服務(wù)器架構(gòu)通常可以分為兩種:第一種是帶網(wǎng)關(guān)的服務(wù)器架構(gòu);第二種是不帶網(wǎng)關(guān)的服務(wù)器架構(gòu)。兩種方案各有利弊。
就帶網(wǎng)關(guān)的服務(wù)器架構(gòu)而言,由于它對外只向玩家提供唯一的一個通信端口,所以在玩家一側(cè)會有比較流暢的游戲體驗,這通常也是那些超大規(guī)模無縫地圖網(wǎng)游所采用的方案,但這種方案的缺點是服務(wù)器組內(nèi)的通信架構(gòu)設(shè)計相對復(fù)雜、調(diào)試不方便、網(wǎng)關(guān)的通信壓力過大、對網(wǎng)關(guān)的通信模型設(shè)計要求較高等。第二種方案會同時向玩家開放多個游戲服務(wù)器端口,除了游戲場景服務(wù)器的通信端口外,同時還可能提供諸如聊天服務(wù)器等的通信端口。這種方案的主要缺點是在進(jìn)行場景服務(wù)器的切換時,玩家客戶端的表現(xiàn)中通常會有一個諸如場景調(diào)入的界面出現(xiàn),影響了游戲的流暢感。基于這種方案的游戲在客戶端的界面處理方面,比較典型的表現(xiàn)是:當(dāng)要進(jìn)行場景切換時,只能通過相應(yīng)的“傳送功能”傳送到另外的場景去,或者需要進(jìn)入新的場景時,客戶端會有比較長時間的等待進(jìn)入新場景的等待界面(Loading界面)。
從技術(shù)角度而言,筆者更傾向于將獨立的服務(wù)器組設(shè)計成帶網(wǎng)關(guān)的模型,雖然這加大了服務(wù)器的設(shè)計難度,但卻增強了游戲的流暢感和安全性,這種花費還是值得的。
筆者在下面附上了帶網(wǎng)關(guān)的MMORPG通信架構(gòu)圖,希望能給業(yè)內(nèi)的朋友們一點有益的啟迪。
posted @
2009-09-09 10:43 暗夜教父 閱讀(304) |
評論 (0) |
編輯 收藏
我們一開始的游戲邏輯層是基于網(wǎng)絡(luò)包驅(qū)動的,也就是將 client 消息定義好結(jié)構(gòu)打包發(fā)送出去,然后再 server 解析這些數(shù)據(jù)包,做相應(yīng)的處理。
寫了一段時間后,覺得這種方案雜亂不利于復(fù)雜的項目。跟同事商量以后,改成了非阻塞的 RPC 模式。
首先由處理邏輯的 server 調(diào)用 client 的遠(yuǎn)程方法在 client 創(chuàng)建出只用于顯示表現(xiàn)的影子對象;然后 server 對邏輯對象的需要client 做出相應(yīng)表現(xiàn)的操作,變成調(diào)用 client 端影子對象的遠(yuǎn)程方法來實現(xiàn)。
這使得游戲邏輯編寫變的清晰了很多,基本可以無視網(wǎng)絡(luò)層的存在,和單機游戲的編寫一樣簡單。
本質(zhì)上,這樣一個系統(tǒng)跟網(wǎng)絡(luò)包驅(qū)動的方式?jīng)]有區(qū)別;但是從編碼表現(xiàn)形式上要自然很多。正如 C 語言也可以實現(xiàn)面向?qū)ο螅珔s沒有 C++ 實現(xiàn)的自然一樣。在這個系統(tǒng)中,引擎封裝了對象管理的部分,使得邏輯編寫的時候不再需要處理討厭的對象數(shù)字 id ;還隱藏了消息發(fā)送或廣播的問題。
我把玩家控制的角色,和服務(wù)器上你的角色分做兩個東西。即,你控制的你,和服務(wù)器認(rèn)為的你就分開了。服務(wù)器認(rèn)為的你,你看見的服務(wù)器上的其他人是一類東西。操作自己的角色行動時,你通過 client 上的控制器的遠(yuǎn)程方法向服務(wù)器發(fā)送指令;而服務(wù)器通過遠(yuǎn)程調(diào)用每個角色的遠(yuǎn)程方法讓 client 可以收到感興趣的所有角色的行為。
這樣,client 永遠(yuǎn)都是通過一個控制器調(diào)用其遠(yuǎn)程方法來告訴服務(wù)器"我要干什么",而服務(wù)器的邏輯層則通過調(diào)用其上所有邏輯對象的遠(yuǎn)程方法來改變每個對象的狀態(tài)。而引擎就根據(jù)每個鏈接的需要,廣播這些消息,使得每個 client 上對應(yīng)的影子對象可以收到狀態(tài)改變的消息。
這些,就是半個月來我跟同事一起做的工作。當(dāng)然,由于我們用腳本編寫邏輯層,這樣,腳本接口可以比 C 接口實現(xiàn)的漂亮的多。
首先是自定義格式的接口描述文件,用自編寫的工具自動編譯成對應(yīng)腳本代碼。我們只需要在腳本中編寫對應(yīng)的類,就可以自動響應(yīng)遠(yuǎn)端調(diào)用的方法了。而調(diào)用遠(yuǎn)程方法,也跟本地方法保持同樣的形式,寫起來跟本地函數(shù)調(diào)用沒有區(qū)別。這在以前用 C/C++ 編寫邏輯的時候是很難做到的。
其次,引擎內(nèi)部做好對象的管理工作,負(fù)責(zé)把通訊協(xié)議上的 id 轉(zhuǎn)換成邏輯層中的對象傳遞給邏輯層使用。
再次,enum 這樣的類型再也不需要用一些數(shù)字的常數(shù)了,也不需要在腳本額外的定義出來。可以在接口文件中定義好,經(jīng)過引擎的處理后,邏輯層可以直接用更為友好的字符串代替,而不失去效率。
編寫邏輯的程序員不再需要關(guān)心網(wǎng)絡(luò)的問題后,就可以把心思放在細(xì)節(jié)上。
最后,對于實現(xiàn)行為預(yù)測來補償網(wǎng)絡(luò)延遲的特性上。在先前的版本中,我們?yōu)榱藢崿F(xiàn)這個,花了不少的氣力。主要是將時間戳信息放在基礎(chǔ)通訊協(xié)議中來輔助實現(xiàn)。具體的消息包收到后,再計算延遲時間來推算當(dāng)前的狀態(tài)。現(xiàn)在,可以把時間信息封裝到 RPC 中,讓每個遠(yuǎn)程方法自動帶有延遲時間,方便計算。按模擬程序的實際效果上看,單單位置同步的預(yù)測策略,可以讓延遲在 8 秒之內(nèi)的玩家可以忍受;而延遲小于 1 秒的時候,幾乎不會受到滯后的影響了。
關(guān)于每個鏈接感興趣的信息的問題,決定了每個邏輯對象的狀態(tài)改變要通知哪些人。目前的想法是獨立到單獨進(jìn)程去處理,我們在處理連接的服務(wù)器和處理邏輯的服務(wù)器之間設(shè)置單獨的服務(wù)器來管理每個鏈接感興趣的對象,這個任務(wù)相對單一且責(zé)任重大,獨立出來可以大大減輕邏輯服務(wù)器的復(fù)雜度。
posted @
2009-09-09 10:42 暗夜教父 閱讀(872) |
評論 (1) |
編輯 收藏