教父的告白
一切都是纸老虎
posts - 82,  comments - 7,  trackbacks - 0
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

基本都是些用于逻辑运算、位运算以及特殊表达式的符号

2.Erlang的类型,除了在前面入门一提到的类型外,还包括:
1)Binary,用于表示某段未知类型的内存区域
比如:
1> <<10,20>>.
<<10,20>>
2> <<"ABC">>.
 <<65,66,67>>

2)Reference,通过调用mk_ref/0产生的运行时的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}
 在编译后其实已经被转化为tuple。可以通过Name#person.name来访问Name Record的name属性。

3.模块的预定义属性
-module(Module).    声明模块名称,必须与文件名相同
-export(Functions).   指定向外界导出的函数列表
-import(Module,Functions).   引入函数,引入的函数可以被当作本地定义的函数使用
-compile(Options).     设置编译选项,比如export_all
-vsn(Vsn).         模块版本,设置了此项,可以通过beam_lib:version/1 获取此项信息
可以通过-include和-include_lib来包含文件,两者的区别是include-lib不能通过绝对路径查找文件,而是在你当前Erlang的lib目录进行查找。

4.try表达式,try表达式可以与catch结合使用,比如:
try Expr
catch
throw:Term -> Term;
exit:Reason -> {'EXIT',Reason}
error:Reason -> {'EXIT',{Reason,erlang:get_stacktrace()}}
end

不仅如此,try还可以与after结合使用,类似java中的try..finally,用于进行清除作用,比如:
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),函数式语言特性之一,Erlang中的语法类似:
[Expr || Qualifier1,...,QualifierN]
Expr可以是任意的表达式,而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.宏,定义常量或者函数等等,语法如下:
-define(Const, Replacement).
-define(Func(Var1,...,VarN), Replacement).

使用的时候在宏名前加个问号?,比如?Const,Replacement将插入宏出现的位置。系统预定义了一些宏:
?MODULE 表示当前模块名

?MODULE_STRING 同上,但是以字符串形式
?FILE 当前模块的文件名
?LINE 调用的当前代码行数
?MACHINE 机器名

Erlang的宏与C语言的宏很相似,同样有宏指示符,包括:
-undef(Macro).
取消宏定义
-ifdef(Macro).
当宏Macro有定义的时候,执行以下代码
-ifndef(Macro).
同上,反之
-else.
接在ifdef或者ifndef之后,表示不满足前者条件时执行以下代码

-endif.
if终止符
假设宏-define(Square(X),X*X).用于计算平方,那么??X将返回X表达式的字符串形式,类似C语言中#arg

一个简单的宏例子:
ruby 代码
 
  1. -module(macros_demo).  
  2. -ifdef(debug).  
  3. -define(LOG(X), io:format("{~p,~p}: ~p~n", [?MODULE,?LINE,X])).  
  4. -else.  
  5. -define(LOG(X), true).  
  6. -endif.  
  7. -define(Square(X),X*X).  
  8. -compile(export_all).  
  9. test()->  
  10.     A=3,  
  11.     ?LOG(A),  
  12.     B=?Square(A),  
  13.     io:format("square(~w) is ~w~n",[A,B]).  

当编译时不开启debug选项的时候:
17> c(macros_demo).
{ok,macros_demo}
18> macros_demo:test().
square(3) is 9

当编译时开启debug之后:

19> c(macros_demo,{d,debug}).
{ok,macros_demo}
20> macros_demo:test().
{macros_demo,11}: 3
square(3) is 9
ok

可以看到LOG的输出了,行数、模块名以及参数

7、Process Dictionary,每个进程都有自己的process dictionary,用于存储这个进程内的全局变量,可以通过下列
BIFs操作:
put(Key, Value)
get(Key)
get()
get_keys(Value)
erase(Key)
erase()

8、关于分布式编程,需要补充的几点
1)节点之间的连接默认是transitive,也就是当节点A连接了节点B,节点B连接了节点C,那么节点A也与节点C互相连接
可以通过启动节点时指定参数-connect_all false来取消默认行为

2)隐藏节点,某些情况下,你希望连接一个节点而不去连接其他节点,你可以通过在节点启动时指定-hidden选项
来启动一个hidden node。在此情况下,通过nodes()查看所有连接的节点将不会出现隐藏的节点,想看到隐藏的节点
可以通过nodes(hidden)或者nodes(connected)来查看。

完整的erl选项如下:

-connect_all false 上面已经解释。
-hidden 启动一个hidden node
-name Name 启动一个系统成为节点,使用long name.
-setcookie Cookie Erlang:set_cookie(node(), Cookie).相同,设置magic cookie
-sname Name 启动一个Erlang系统作为节点,使用short name


注意,short name启动的节点是无法与long name节点通信的

.一个小细节,在Erlang中小于等于是用=<表示,而不是一般语言中的<=语法,我犯过错误的地方,同样,不等于都是用/号,而不是
!,比如/=、=/=。

10.and or 和andalso orelse的区别

and和or会计算两边的表达式,而andalso和orelse的求值采用短路机制,比如exp1 andalso exp2,当exp1返回false之后,就不会去求值
exp2,而是直接返回false,而exp1 and exp2会对exp1和exp2都进行求值,or与orelse也类似。
posted @ 2009-09-11 10:16 暗夜教父 阅读(657) | 评论 (0)编辑 收藏
  任何一门语言都有自己的错误处理机制,Erlang也不例外,语法错误编译器可以帮你指出,而逻辑错误和运行时错误就只有靠程序员利用Erlang提供的机制来妥善处理,放置程序的崩溃。
    Erlang的机制有:
1)监控某个表达式的执行
2)监控其他进程的行为
3)捕捉未定义函数执行错误等

一、catch和throw语句
    调用某个会产生错误的表达式会导致调用进程的非正常退出,比如错误的模式匹配(2=3),这种情况下可以用catch语句:
                                      catch expression
    试看一个例子,一个函数foo:

java 代码
 
  1. foo(1) ->  
  2. hello;  
  3. foo(2) ->  
  4. throw({myerror, abc});  
  5. foo(3) ->  
  6. tuple_to_list(a);  
  7. foo(4) ->  
  8. exit({myExit, 222}).  

当没有使用catch的时候,假设有一个标识符为Pid的进程调用函数foo(在一个模块中),那么:
foo(1) - 返回hello
foo(2) - 语句throw({myerror, abc})执行,因为我们没有在一个catch中调用foo(2),因此进程Pid将因为错误而终止。

foo(3) - tuple_to_list将一个元组转化为列表,因为a不是元组,因此进程Pid同样因为错误而终止

foo(4) - 因为没有使用catch,因此foo(4)调用了exit函数将使进程Pid终止,{myExit, 222} 参数用于说明退出的原因。

foo(5) - 进程Pid将因为foo(5)的调用而终止,因为没有和foo(5)匹配的函数foo/1。

    让我们看看用catch之后是什么样:
java 代码
 
  1. demo(X) ->  
  2. case catch foo(X) of  
  3.   {myerror, Args} ->  
  4.        {user_error, Args};  
  5.   {'EXIT', What} ->  
  6.        {caught_error, What};  
  7.   Other ->  
  8.        Other  
  9. end.  

再看看结果,
demo(1) - 没有错误发生,因此catch语句将返回表达式结果hello
demo(2) - foo(2)抛出错误{myerror, abc},被catch返回,因此将返回{user_error,abc}

demo(3) - foo(3)执行失败,因为参数错误,因此catch返回{'EXIT',badarg'},最后返回{caught_error,badarg}

demo(4) - 返回{caught_error,{myexit,222}}
demo(5) - 返回{caught_error,function_clause}

    使用catch和throw可以将可能产生错误的代码包装起来,throw可以用于尾递归的退出等等。Erlang是和scheme一样进行尾递归优化的,它们都没有显式的迭代结构(比如for循环)

二、进程的终止
    在进程中调用exit的BIFs就可以显式地终止进程,exit(normal)表示正常终止,exit(Reason)通过Reason给出非正常终止的原因。进程的终止也完全有可能是因为运行时错误引起的。

三、连接的进程
    进程之间的连接是双向的,也就是说进程A打开一个连接到B,也意味着有一个从B到A的连接。当进程终止的时候,有一个EXIT信号将发给所有与它连接的进程。信号的格式如下:
               {'EXIT', Exiting_Process_Id, Reason}
Exiting_Process_Id 是指终止的进程标记符
Reason 是进程终止的原因。如果Reason是normal,接受这个信号的进程的默认行为是忽略这个信号。默认对Exit信号的处理可以被重写,以允许进程对Exit信号的接受做出不同的反应。
1.连接进程:
通过link(Pid),就可以在调用进程与进程Pid之间建立连接
2.取消连接
反之通过unlink(Pid)取消连接。
3.创立进程并连接:
通过spawn_link(Module, Function, ArgumentList)创建进程并连接,该方法返回新创建的进程Pid

    通过进程的相互连接,许多的进程可以组织成一个网状结构,EXIT信号(非normal)从某个进程发出(该进程终止),所有与它相连的进程以及与这些进 程相连的其他进程,都将收到这个信号并终止,除非它们实现了自定义的EXIT信号处理方法。一个进程链状结构的例子:
java 代码
 
  1. -module(normal).  
  2. -export([start/1, p1/1, test/1]).  
  3. start(N) ->  
  4. register(start, spawn_link(normal, p1, [N - 1])).  
  5.  p1(0) ->  
  6.    top1();  
  7.  p1(N) ->  
  8.    top(spawn_link(normal, p1, [N - 1]),N).  
  9. top(Next, N) ->  
  10. receive  
  11. X ->  
  12. Next ! X,  
  13. io:format("Process ~w received ~w~n", [N,X]),  
  14. top(Next,N)  
  15. end.  
  16. top1() ->  
  17. receive  
  18. stop ->  
  19. io:format("Last process now exiting ~n", []),  
  20. exit(finished);  
  21. X ->  
  22. io:format("Last process received ~w~n", [X]),  
  23. top1()  
  24. end.  
  25. test(Mess) ->  
  26. start ! Mess.  

执行:
java 代码
 
  1. > normal:start(3).  
  2. true  
  3. > normal:test(123).  
  4. Process 2 received 123  
  5. Process 1 received 123  
  6. Last process received 123  
  7.   
  8. > normal:test(stop).  
  9. Process 2 received stop  
  10. Process 1 received stop  
  11. Last process now exiting  
  12. stop  

四、运行时失败
    一个运行时错误将导致进程的非正常终止,伴随着非正常终止EXIT信号将发出给所有连接的进程,EXIT信号中有Reason并且Reason中包含一个atom类型用于说明错误的原因,常见的原因如下:

badmatch - 匹配失败,比如一个进程进行1=3的匹配,这个进程将终止,并发出{'EXIT', From, badmatch}信号给连接的进程

badarg  - 顾名思义,参数错误,比如atom_to_list(123),数字不是atom,因此将发出{'EXIT', From, badarg}信号给连接进程

case_clause - 缺少分支匹配,比如
   
java 代码
 
  1. M = 3,  
  2. case M of  
  3.   1 ->  
  4.     yes;  
  5.   2 ->  
  6.     no  
  7. end.  

没有分支3,因此将发出{'EXIT', From, case_clause}给连接进程

if_clause - 同理,if语句缺少匹配分支

function_clause - 缺少匹配的函数,比如:
java 代码
 
  1. foo(1) ->  
  2.   yes;  
  3. foo(2) ->  
  4.   no.  

如果我们调用foo(3),因为没有匹配的函数,将发出{'EXIT', From, function_clause} 给连接的进程。

undef - 进程执行一个不存在的函数

badarith - 非法的算术运算,比如1+foo。

timeout_value - 非法的超时时间设置,必须是整数或者infinity

nocatch - 使用了throw,没有相应的catch去通讯。

五、修改默认的信号接收action
   当进程接收到EXIT信号,你可以通过process_flag/2方法来修改默认的接收行为。执行process_flag(trap_exit, true)设置捕获EXIT信号为真来改变默认行为,也就是将EXIT信号作为一般的进程间通信的信号进行接受并处理;process_flag (trap_exit,false)将重新开启默认行为。
   例子:
java 代码
 
  1. -module(link_demo).  
  2. -export([start/0, demo/0, demonstrate_normal/0, demonstrate_exit/1,  
  3. demonstrate_error/0, demonstrate_message/1]).  
  4. start() ->  
  5.   register(demo, spawn(link_demo, demo, [])).  
  6. demo() ->  
  7.   process_flag(trap_exit, true),  
  8. demo1().  
  9.   demo1() ->  
  10.   receive  
  11.     {'EXIT', From, normal} ->  
  12.       io:format("Demo process received normal exit from ~w~n",[From]),  
  13.      demo1();  
  14.     {'EXIT', From, Reason} ->  
  15.       io:format("Demo process received exit signal ~w from ~w~n",[Reason, From]),  
  16.      demo1();  
  17.     finished_demo ->  
  18.       io:format("Demo finished ~n", []);  
  19.     Other ->  
  20.       io:format("Demo process message ~w~n", [Other]),  
  21.      demo1()  
  22.   end.  
  23. demonstrate_normal() ->  
  24.   link(whereis(demo)).  
  25. demonstrate_exit(What) ->  
  26.   link(whereis(demo)),  
  27.   exit(What).  
  28. demonstrate_message(What) ->  
  29.   demo ! What.  
  30. demonstrate_error() ->  
  31.   link(whereis(demo)),  
  32.   1 = 2.  
  33.    

    创建的进程执行demo方法,demo方法中设置了trap_exit为true,因此,在receive中可以像对待一般的信息一样处理EXIT信号,这个程序是很简单了,测试看看:
java 代码
 
  1. > link_demo:start().  
  2. true  
  3. > link_demo:demonstrate_normal().  
  4. true  
  5. Demo process received normal exit from <0.13.1>  
  6. > link_demo:demonstrate_exit(hello).  
  7. Demo process received exit signal hello from <0.14.1>  
  8. ** exited: hello **  
  9.   
  10. > link_demo:demonstrate_exit(normal).  
  11. Demo process received normal exit from <0.13.1>  
  12. ** exited: normal **  
  13.   
  14. > link_demo:demonstrate_error().  
  15. !!! Error in process <0.17.1> in function  
  16. !!! link_demo:demonstrate_error()  
  17. !!! reason badmatch  
  18. ** exited: badmatch **  
  19. Demo process received exit signal badmatch from <0.17.1>  

六、未定义函数和未注册名字
1.当调用一个未定义的函数时,Mod:Func(Arg0,...,ArgN),这个调用将被转为:
error_handler:undefined_function(Mod, Func, [Arg0,...,ArgN])
其中的error_handler模块是系统自带的错误处理模块

2.当给一个未注册的进程名发送消息时,调用将被转为:
error_handler:unregistered_name(Name,Pid,Message)

3.如果不使用系统自带的error_handler,可以通过process_flag(error_handler, MyMod) 设置自己的错误处理模块。

七、Catch Vs. Trapping Exits
这两者的区别在于应用场景不同,Trapping Exits应用于当接收到其他进程发送的EXIT信号时,而catch仅用于表达式的执行。

第8章介绍了如何利用错误处理机制去构造一个健壮的系统,用了几个例子,我将8.2节的例子完整写了下,并添加客户端进程用于测试:
java 代码
 
  1. -module(allocator).  
  2. -export([start/1,server/2,allocate/0,free/1,start_client/0,loop/0]).  
  3. start(Resources) ->  
  4.    Pid = spawn(allocator, server, [Resources,[]]),  
  5. register(resource_alloc, Pid).  
  6. %函数接口  
  7. allocate() ->  
  8.    request(alloc).  
  9. free(Resource) ->  
  10.   request({free,Resource}).  
  11. request(Request) ->  
  12.   resource_alloc ! {self(),Request},  
  13.   receive  
  14.     {resource_alloc, error} ->  
  15.       exit(bad_allocation); % exit added here  
  16.     {resource_alloc, Reply} ->  
  17.       Reply  
  18.  end.  
  19. % The server.  
  20. server(Free, Allocated) ->  
  21.  process_flag(trap_exit, true),  
  22.  receive  
  23.    {From,alloc} ->  
  24.          allocate(Free, Allocated, From);  
  25.    {From,{free,R}} ->  
  26.         free(Free, Allocated, From, R);  
  27.    {'EXIT', From, _ } ->  
  28.        check(Free, Allocated, From)  
  29.  end.  
  30. allocate([R|Free], Allocated, From) ->  
  31.    link(From),  
  32.    io:format("连接客户端进程~w~n",[From]),  
  33.    From ! {resource_alloc,{yes,R}},  
  34.    server(Free, [{R,From}|Allocated]);  
  35. allocate([], Allocated, From) ->  
  36.    From ! {resource_alloc,no},  
  37.    server([], Allocated).  
  38. free(Free, Allocated, From, R) ->  
  39.   case lists:member({R,From}, Allocated) of  
  40.    true ->  
  41.               From ! {resource_alloc,ok},  
  42.               Allocated1 = lists:delete({R, From}, Allocated),  
  43.               case lists:keysearch(From,2,Allocated1) of  
  44.                      false->  
  45.                             unlink(From),  
  46.                         io:format("从进程~w断开~n",[From]);  
  47.                      _->  
  48.                             true  
  49.               end,  
  50.              server([R|Free],Allocated1);  
  51.    false ->  
  52.            From ! {resource_alloc,error},  
  53.          server(Free, Allocated)  
  54.  end.  
  55.   
  56. check(Free, Allocated, From) ->  
  57.    case lists:keysearch(From, 2, Allocated) of  
  58.          false ->  
  59.            server(Free, Allocated);  
  60.         {value, {R, From}} ->  
  61.            check([R|Free],  
  62.            lists:delete({R, From}, Allocated), From)  
  63. end.  
  64. start_client()->  
  65.     Pid2=spawn(allocator,loop,[]),  
  66.     register(client, Pid2).  
  67. loop()->  
  68.     receive  
  69.         allocate->  
  70.             allocate(),  
  71.             loop();  
  72.         {free,Resource}->  
  73.             free(Resource),  
  74.             loop();  
  75.         stop->  
  76.             true;  
  77.         _->  
  78.             loop()  
  79.     end.  
  80.       

回家了,有空再详细说明下这个例子吧。执行:
java 代码
 
  1. 1> c(allocator).  
  2. {ok,allocator}  
  3. 2> allocator:start([1,2,3,4,5,6]).  
  4. true  
  5. 3> allocator:start_client().  
  6. true  
  7. 4> client!allocate  
  8. .  
  9. allocate连接客户端进程<0.37.0>  
  10.   
  11. 5> client!allocate.  
  12. allocate连接客户端进程<0.37.0>  
  13.   
  14. 6> client!allocate.  
  15. allocate连接客户端进程<0.37.0>  
  16.   
  17. 7> allocator:allocate().  
  18. 连接客户端进程<0.28.0>  
  19. {yes,4}  
  20. 8> client!{free,1}.  
  21. {free,1}  
  22. 9> client!{free,2}.  
  23. {free,2}  
  24. 10> client!allocate.  
  25. allocate连接客户端进程<0.37.0>  
  26.   
  27. 11> client!allocate.  
  28. allocate连接客户端进程<0.37.0>  
  29.   
  30. 12> client!stop.  
  31. stop  
  32. 13> allocator:allocate().  
  33. 连接客户端进程<0.28.0>  
  34. {yes,3}  
  35. 14> allocator:allocate().  
  36. 连接客户端进程<0.28.0>  
  37. {yes,2}  
  38. 15> allocator:allocate().  
  39. 连接客户端进程<0.28.0>  
  40. {yes,1}  
  41. 16>  

posted @ 2009-09-11 10:13 暗夜教父 阅读(291) | 评论 (0)编辑 收藏
    所谓分布式的Erlang应用是运行在一系列Erlang节点组成的网络之上。这样的系统的性质与单一节点上的Erlang系统并没有什么不同。分布式这是个“大词”,Erlang从语言原生角度支持分布式编程,相比于java简单不少。
一、分布式机制
下列的BIFs是用于分布式编程:
spawn(Node, Mod, Func, Args)
启动远程节点的一个进程

spawn_link(Node, Mod, Func, Args)
启动远程节点的一个进程并创建连接到该进程

monitor_node(Node, Flag)
如果Flag是true,这个函数将使调用(该函数)的进程可以监控节点Node。如果节点已经舍弃或者并不存在,调用的进程将收到一个{nodedown,Node}的消息。如果Flag是false,监控将被关闭

node()
返回我们自己的进程name

nodes()
返回其他已知的节点name列表

node(Item)
返回原来Item的节点名称,Item可以是Pid,引用(reference)或者端口(port)

disconnect_node(Nodename)
从节点Nodename断开。

    节点是分布式Erlang的核心概念。在一个分布式Erlang应用中,术语(term)节点(node)意味着一个可以加入分布式 transactions的运行系统。通过一个称为net kernal的特殊进程,一个独立的Erlang系统可以成为一个分布式Erlang系统的一部分。当net kernal进程启动的时候,我们称系统是alive的。

    与远程节点上的进程进行通信,与同一节点内的进程通信只有一点不同:
java 代码
  1. {Name, Node} ! Mess.  
  
显然,需要接收方增加一个参数Node用于指定接受进程所在的节点。节点的name一般是用@隔开的atom类型,比如pong@dennis,表示计算机名为dennis上的pong节点。通过执行:
java 代码
  1. erl -sname pong  

将在执行的计算机中创建一个节点pong。为了运行下面的例子,你可能需要两台计算机,如果只有一台,只要同时开两个Erlang系统并以不同的节点名称运行也可以。

二、一些例子。
    这个例子完全来自上面提到的翻译的连接,关于分布式编程的章节。我增加了截图和说明。
首先是代码:
java 代码
 
  1. -module(tut17).  
  2.   
  3. -export([start_ping/1, start_pong/0,  ping/2, pong/0]).  
  4.   
  5. ping(0, Pong_Node) ->  
  6.     {pong, Pong_Node} ! finished,  
  7.     io:format("ping finished~n", []);  
  8.   
  9. ping(N, Pong_Node) ->  
  10.     {pong, Pong_Node} ! {ping, self()},  
  11.     receive  
  12.         pong ->  
  13.             io:format("Ping received pong~n", [])  
  14.     end,  
  15.     ping(N - 1, Pong_Node).  
  16.   
  17. pong() ->  
  18.     receive  
  19.         finished ->  
  20.             io:format("Pong finished~n", []);  
  21.         {ping, Ping_PID} ->  
  22.             io:format("Pong received ping~n", []),  
  23.             Ping_PID ! pong,  
  24.             pong()  
  25.     end.  
  26.   
  27. start_pong() ->  
  28.     register(pong, spawn(tut17, pong, [])).  
  29.   
  30. start_ping(Pong_Node) ->  
  31.     spawn(tut17, ping, [3, Pong_Node]).  

    代码是创建两个相互通信的进程,相互发送消息并通过io显示在屏幕上,本来是一个单一系统的例子,现在我们让两个进程运行在不同的两个节点上。注意 start_ping方法,创建的进程调用ping方法,ping方法有两个参数,一个是发送消息的次数,一个就是远程节点的name了,也就是我们将要 创建的进程pong的所在节点。start_pong创建一个调用函数pong的进程,并注册为名字pong(因此在ping方法中可以直接发送消息给 pong)。
    我是在windows机器上测试,首先打开两个cmd窗口,并cd到Erlang的安装目录下的bin目录,比如C:\Program Files\erl5.5.3\bin,将上面的程序存为tut17.erl,并拷贝到同一个目录下。我们将创建两个节点,一个叫 ping@dennis,一个叫pong@dennis,其中dennis是我的机器名。见下图:

采用同样的命令

erl 
-sname ping

创建ping节点。然后在pong节点下执行start_pong():


OK,这样就在节点pong上启动了pong进程,然后在ping节点调用start_ping,传入参数就是pong@dennis
java 代码
 
  1. tut17:start_ping(pong@dennis).  

执行结果如下图:

同样在pong节点上也可以看到:


    结果如我们预期的那样,不同节点上的两个进程相互通信如此简单。我们给模块tut17增加一个方法,用于启动远程进程,也就是调用spawn(Node,Module,Func,Args)方法:
java 代码
 
  1. start(Ping_Node) ->  
  2.     register(pong, spawn(tut17, pong, [])),  
  3.     spawn(Ping_Node, tut17, ping, [3, node()]).  

pong进程启动Ping_Node节点上的进程ping。具体结果不再给出。
posted @ 2009-09-11 10:13 暗夜教父 阅读(379) | 评论 (0)编辑 收藏
    Erlang中的process——进程是轻量级的,并且进程间无共享。查了很多资料,似乎没人说清楚轻量级进程算是什么概念,继续查找中。。。闲话不 提,进入并发编程的世界。本文算是学习笔记,也可以说是《Concurrent Programming in ERLANG》第五张的简略翻译。
1.进程的创建
    进程是一种自包含的、分隔的计算单元,并与其他进程并发运行在系统中,在进程间并没有一个继承体系,当然,应用开发者可以设计这样一个继承体系。
    进程的创建使用如下语法:
java 代码
  1. Pid = spawn(Module, FunctionName, ArgumentList)  

spawn接受三个参数:模块名,函数名以及参数列表,并返回一个代表创建的进程的标识符(Pid)。
如果在一个已知进程Pid1中执行:
java 代码
  1. Pid2 = spawn(Mod, Func, Args)  

那么,Pid2仅仅能被Pid1可见,Erlang系统的安全性就构建在限制进程扩展的基础上。

2.进程间通信
    Erlang进程间的通信只能通过发送消息来实现,消息的发送使用!符号:
java 代码
  1. Pid ! Message  

    其中Pid是接受消息的进程标记符,Message就是消息。接受方和消息可以是任何的有效的Erlang结构,只要他们的结果返回的是进程标记符和消息。
    消息的接受是使用receive关键字,语法如下:
java 代码
  1. receive  
  2.       Message1 [when Guard1] ->  
  3.           Actions1 ;  
  4.       Message2 [when Guard2] ->  
  5.           Actions2 ;  
  6.   
  7. end  

    每一个Erlang进程都有一个“邮箱”,所有发送到进程的消息都按照到达的顺序存储在“邮箱”里,上面所示的消息Message1,Message2, 当它们与“邮箱”里的消息匹配,并且约束(Guard)通过,那么相应的ActionN将执行,并且receive返回的是ActionN的最后一条执行 语句的结果。Erlang对“邮箱”里的消息匹配是有选择性的,只有匹配的消息将被触发相应的Action,而没有匹配的消息将仍然保留在“邮箱”里。这 一机制保证了没有消息会阻塞其他消息的到达。
    消息到达的顺序并不决定消息的优先级,进程将轮流检查“邮箱”里的消息进行尝试匹配。消息的优先级别下文再讲。

    如何接受特定进程的消息呢?答案很简单,将发送方(sender)也附送在消息当中,接收方通过模式匹配决定是否接受,比如:
java 代码
  1. Pid ! {self(),abc}  

给进程Pid发送消息{self(),abc},利用self过程得到发送方作为消息发送。然后接收方:
java 代码
  1. receive  
  2.   {Pid1,Msg} ->  
  3.   
  4. end  

通过模式匹配决定只有Pid1进程发送的消息才接受。

3.一些例子
    仅说明下书中计数的进程例子,我添加了简单注释:
java 代码
 
  1. -module(counter).  
  2. -compile(export_all).  
  3. % start(),返回一个新进程,进程执行函数loop  
  4. start()->spawn(counter, loop,[0]).  
  5. % 调用此操作递增计数  
  6. increment(Counter)->  
  7.     Counter!increament.  
  8. % 返回当前计数值  
  9. value(Counter)->  
  10.     Counter!{self(),value},  
  11.     receive  
  12.         {Counter,Value}->  
  13.             %返回给调用方  
  14.             Value  
  15.         end.  
  16.   %停止计数        
  17.  stop(Counter)->  
  18.      Counter!{self(),stop}.  
  19.  loop(Val)->  
  20.      receive  
  21.          %接受不同的消息,决定返回结果  
  22.          increament->  
  23.              loop(Val+1);  
  24.          {From,value}->  
  25.              From!{self(),Val},  
  26.              loop(Val);  
  27.          stop->  
  28.              true;  
  29.          %不是以上3种消息,就继续等待  
  30.          Other->  
  31.              loop(Val)  
  32.       end.     
  33.                
  34.                           
  35.           

调用方式:

java 代码
 
  1. 1> Counter1=counter:start().  
  2. <0.30.0>  
  3. 2> counter:value(Counter1).  
  4. 0  
  5. 3> counter:increment(Counter1).  
  6. increament  
  7. 4> counter:value(Counter1).  
  8. 1  

基于进程的消息传递机制可以很容易地实现有限状态机(FSM),状态使用函数表示,而事件就是消息。具体不再展开

4.超时设置
    Erlang中的receive语法可以添加一个额外选项:timeout,类似:
java 代码
  1. receive  
  2.    Message1 [when Guard1] ->  
  3.      Actions1 ;  
  4.    Message2 [when Guard2] ->  
  5.      Actions2 ;  
  6.      
  7.    after  
  8.       TimeOutExpr ->  
  9.          ActionsT  
  10. end  

after之后的TimeOutExpr表达式返回一个整数time(毫秒级别),时间的精确程度依赖于Erlang在操作系统或者硬件的实现。如果在time毫秒内,没有一个消息被选中,超时设置将生效,也就是ActionT将执行。time有两个特殊值:
1)infinity(无穷大),infinity是一个atom,指定了超时设置将永远不会被执行。
2) 0,超时如果设定为0意味着超时设置将立刻执行,但是系统将首先尝试当前“邮箱”里的消息。

    超时的常见几个应用,比如挂起当前进程多少毫秒:
java 代码
 
  1. sleep(Time) ->  
  2.   receive  
  3.     after Time ->  
  4.     true  
  5. end.  

    比如清空进程的“邮箱”,丢弃“邮箱”里的所有消息:
java 代码
 
  1. flush_buffer() ->  
  2.   receive  
  3.     AnyMessage ->  
  4.       flush_buffer()  
  5.   after 0 ->  
  6.     true  
  7. end.  
   
    将当前进程永远挂起:
java 代码
 
  1. suspend() ->  
  2.     receive  
  3.     after  
  4.         infinity ->  
  5.             true  
  6.     end.  

       超时也可以应用于实现定时器,比如下面这个例子,创建一个进程,这个进程将在设定时间后向自己发送消息:
java 代码
 
  1. -module(timer).  
  2. -export([timeout/2,cancel/1,timer/3]).  
  3. timeout(Time, Alarm) ->  
  4.    spawn(timer, timer, [self(),Time,Alarm]).  
  5. cancel(Timer) ->  
  6.    Timer ! {self(),cancel}.  
  7. timer(Pid, Time, Alarm) ->  
  8.    receive  
  9.     {Pid,cancel} ->  
  10.        true  
  11.    after Time ->  
  12.        Pid ! Alarm  
  13. end.  

   
5、注册进程
    为了给进程发送消息,我们需要知道进程的Pid,但是在某些情况下:在一个很大系统里面有很多的全局servers,或者为了安全考虑需要隐藏进程 Pid。为了达到可以发送消息给一个不知道Pid的进程的目的,我们提供了注册进程的办法,给进程们注册名字,这些名字必须是atom。
    基本的调用形式:
java 代码
  1. register(Name, Pid)  
  2. 将Name与进程Pid联系起来  
  3.   
  4. unregister(Name)  
  5. 取消Name与相应进程的对应关系。  
  6.   
  7. whereis(Name)  
  8. 返回Name所关联的进程的Pid,如果没有进程与之关联,就返回atom:undefined  
  9.   
  10. registered()  
  11. 返回当前注册的进程的名字列表  

6.进程的优先级
设定进程的优先级可以使用BIFs:
process_flag(priority, Pri)

Pri可以是normal、low,默认都是normal
优先级高的进程将相对低的执行多一点。

7.进程组(process group)
    所有的ERLANG进程都有一个Pid与一个他们共有的称为Group Leader相关联,当一个新的进程被创建的时候将被加入同一个进程组。最初的系统进程的Group Leader就是它自身,因此它也是所有被创建进程及子进程的Group Leader。这就意味着Erlang的进程被组织为一棵Tree,其中的根节点就是第一个被创建的进程。下面的BIFs被用于操纵进程组:
group_leader()
返回执行进程的Group Leader的Pid
group_leader(Leader, Pid)
设置进程Pid的Group Leader为进程的Leader

8.Erlang的进程模型很容易去构建Client-Server的模型,书中有一节专门讨论了这一点,着重强调了接口的设计以及抽象层次的隔离问题,不翻译了。
posted @ 2009-09-11 10:12 暗夜教父 阅读(273) | 评论 (0)编辑 收藏

   读erlang.org上面的Erlang Course四天教程
1.数字类型,需要注意两点
1)B#Val表示以B进制存储的数字Val,比如

ruby 代码
 
  1. 7> 2#101.  
  2. 5  

进制存储的101就是10进制的5了
2)$Char表示字符Char的ascii编码,比如$A表示65

2.比较难以翻译的概念——atom,可以理解成常量,它可以包含任何字符,以小写字母开头,如果不是以小写字母开头或者是字母之外的符号,需要用单引号包括起来,比如abc,'AB'

3.另一个概念——Tuple,有人翻译成元组,可以理解成定长数组,是Erlang的基础数据结构之一:

ruby 代码
  1. 8> {1,2,3,4,5}.  
  2. {1,2,3,4,5}  
  3. 9> {a,b,c,1,2}.  
  4. {a,b,c,1,2}  
  5. 10> size({1,2,3,a,b,c}).  
  6. 6  


内置函数size求长度,元组可以嵌套元组或者其他结构。下面所讲的列表也一样。

4.另外一个基础数据结构就是各个语言都有的list(列表),在[]内以,隔开,可以动态改变大小,

python 代码
 
  1. [123, xyz]  
  2. [123, def, abc]  
  3. [{person, 'Joe', 'Armstrong'},  
  4.     {person, 'Robert', 'Virding'},  
  5.     {person, 'Mike', 'Williams'}  
  6. ]  


可以使用内置函数length求列表大小。以""包含的ascii字母代表一个列表,里面的元素就是这些字母的ascii值,比如"abc"表示列表[97,98,99]。

5.通过这两个数据结构可以组合成各种复杂结构,与Lisp的cons、list演化出各种结构一样的奇妙。

6.Erlang中变量有两个特点:
1)变量必须以大写字母开头
2)变量只能绑定一次,或者以一般的说法就是只能赋值一次,其实Erlang并没有赋值这样的概念,=号也是用于验证匹配。

7.模式匹配——Pattern Matching,Erlang的模式匹配非常强大,看了buaawhl的《Erlang语法提要》的介绍,模式匹配的功能不仅仅在课程中介绍的数据结构的拆解,在程序的分派也扮演重要角色,或者说Erlang的控制的流转是通过模式匹配来实现的。具体功能参见链接,给出书中拆解列表的例子:

python 代码
  1. [A,B|C] = [1,2,3,4,5,6,7]  
  2.      Succeeds - binds A = 1, B = 2,  
  3.      C = [3,4,5,6,7]  
  4.    
  5.  [H|T] = [1,2,3,4]  
  6.      Succeeds - binds H = 1, T = [2,3,4]  
  7.    
  8.  [H|T] = [abc]  
  9.      Succeeds - binds H = abc, T = []  
  10.    
  11.  [H|T] = []  
  12.      Fails  

 
下面会给出更多模式匹配的例子,给出一个模块用来计算列表等

8.Erlang中函数的定义必须在一个模块内(Module),并且模块和函数的名称都必须是atom,函数的参数可以是任何的Erlang类型或者数据结构,函数要被调用需要从模块中导出,函数调用的形式类似:
moduleName:funcName(Arg1,Arg2,...).
写我们的第一个Erlang程序,人见人爱的Hello World:

java 代码
 
  1. -module(helloWorld).  
  2. -export([run/1]).  
  3. run(Name)->  
  4.     io:format("Hello World ~w~n",[Name]).  


存为helloWorld.erl,在Erlang Shell中执行:

java 代码
 
  1. 2> c(helloWorld).  
  2. {ok,helloWorld}  
  3. 3> helloWorld:run(dennis).  
  4. Hello World dennis  
  5. ok  


打印出来了,现在解释下程序构造,

java 代码
  1. -module(helloWorld).  


这一行声明了模块helloWorld,函数必须定义在模块内,并且模块名称必须与源文件名相同。

java 代码
 
  1. -export([run/1]).  


而这一行声明导出的函数,run/1指的是有一个参数的run函数,因为Erlang允许定义同名的有不同参数的多个函数,通过指定/1来说明要导出的是哪个函数。
接下来就是函数定义了:

java 代码
 
  1. run(Name)->  
  2.     io:format("Hello World ~w~n",[Name]).  


大写开头的是变量Name,调用io模块的format方法输出,~w可以理解成占位符,将被实际Name取代,~n就是换行了。注意,函数定义完了要以句号.结束。然后执行c(helloWorld).编译源代码,执行:

java 代码
  1. helloWorld:run(dennis);  


9.内置的常用函数:

java 代码
 
  1. date()  
  2. time()  
  3. length([1,2,3,4,5])  
  4. size({a,b,c})  
  5. atom_to_list(an_atom)  
  6. list_to_tuple([1,2,3,4])  
  7. integer_to_list(2234)  
  8. tuple_to_list({})  
  9. hd([1,2,3,4])  %输出1,也就是列表的head  
  10. tl([1,2,3,4])  %输出[2,3,4],也就是列表的tail  


10.常见Shell命令:
1)h(). 用来打印最近的20条历史命令
2)b(). 查看所有绑定的变量
3) f(). 取消(遗忘)所有绑定的变量。
4) f(Val).  取消指定的绑定变量
5) e(n).   执行第n条历史命令
6) e(-1).  执行上一条shell命令

11.又一个不知道怎么翻译的概念——Guard。翻译成约束?呵呵。用于限制变量的类型和范围,比如:

java 代码
 
  1. number(X)    - X 是数字  
  2. integer(X)    - X 是整数  
  3. float(X)    - X 是浮点数  
  4. atom(X)        - X 是一个atom  
  5. tuple(X)    - X 是一个元组  
  6. list(X)        - X 是一个列表  
  7.   
  8. length(X) == 3    - X 是一个长度为3的列表  
  9. size(X) == 2    - X 是一个长度为2的元组  
  10.   
  11. X > Y + Z    - X >Y+Z  
  12. X == Y        - X 与Y相等  
  13. X =:= Y        - X 全等于Y  
  14. (比如: 1 == 1.0 成功  
  15.            1 =:= 1.0 失败)  


为了方便比较,Erlang规定如下的比较顺序:

java 代码
  1. number < atom < reference < port < pid < tuple < list  



12.忘了介绍apply函数,这个函数对于熟悉javascript的人来说很亲切,javascript实现mixin就得靠它,它的调用方式如下:

apply(Mod, Func, Args),三个参数分别是模块、函数以及参数列表,比如调用我们的第一个Erlang程序:
java 代码
  1. apply(helloWorld,run,[dennis]).  

13.if和case语句,if语句的结构如下:
java 代码
 
  1. if  
  2.    Guard1 ->  
  3.         Sequence1 ;  
  4.    Guard2 ->  
  5.         Sequence2 ;  
  6. ...  
  7. end  

而case语句的结构如下:
java 代码
 
  1. case Expr of  
  2.    Pattern1 [when Guard1] -> Seq1;  
  3.    Pattern2 [when Guard2] -> Seq2;  
  4.   
  5.    PatternN [when GuardN] -> SeqN  
  6. end  

if和case语句都有一个问题,就是当没有模式匹配或者Grard都是false的时候会导致error,这个问题case可以增加一个类似java中default的:

java 代码
 
  1. case Fn of  
  2.   
  3.    _ ->  
  4.    true  
  5. end  


通过_指代任意的Expr,返回true,而if可以这样:

java 代码
 
  1. if  
  2.     
  3.   true ->  
  4.    true  
  5. end  


一样的道理。case语句另一个需要注意的问题就是变量范围,每个case分支中定义的变量都将默认导出case语句,也就是在case语句结束后可以被引用,因此一个规则就是每个case分支定义的变量应该一致,不然算是非法的,编译器会给出警告,比如:

java 代码
 
  1. f(X) ->  
  2. case g(X) of  
  3. true -> A = h(X), B = A + 7;  
  4. false -> B = 6  
  5. end,  
  6. h(A).  


如果执行true分支,变量A和变量B都被定义,而如果执行的false分支,只有变量B被引用,可在case语句执行后,h(A)调用了变量A,这是不安全的,因为变量A完全可能没有被定义,编译器将给出警告
variable 'A' unsafe in 'case' (line 10)



14.给出一些稍微复杂的模型匹配例子,比如用于计算数字列表的和、平均值、长度、查找某元素是否在列表中,我们把这个模块定义为list:

java 代码
 
  1. -module(list).  
  2. -export([average/1,sum/1,len/1,double/1,member/2]).  
  3. average(X)->sum(X)/len(X).  
  4. sum([H|T]) when number(H)->H+sum(T);  
  5. sum([])->0.  
  6. len([_|T])->1+len(T);  
  7. len([])->0.  
  8. double([H|T]) -> [2*H|double(T)];  
  9. double([]) -> [].  
  10. member(H, [H|_]) -> true;  
  11. member(H, [_|T]) -> member(H, T);  
  12. member(_, []) -> false.  
  13.                   


细细体会,利用递归来实现,比较有趣。_用于指代任意的变量,当我们只关注此处有变量,但并不关心变量的值的时候使用。用分号;来说明是同一个函数定义,只是不同的定义分支,通过模式匹配来决定调用哪个函数定义分支。
另一个例子,计算各种图形的面积,也是课程中给出的例子:

java 代码
 
  1. -module(mathStuff).  
  2. -export([factorial/1,area/1]).  
  3. factorial(0)->1;  
  4. factorial(N) when N>0->N*factorial(N-1).  
  5. %计算正方形面积,参数元组的第一个匹配square      
  6. area({square, Side}) ->  
  7.     Side * Side;  
  8. %计算圆的面积,匹配circle    
  9. area({circle, Radius}) ->  
  10.    % almost :-)  
  11.    3 * Radius * Radius;  
  12. %计算三角形的面积,利用海伦公式,匹配triangle   
  13. area({triangle, A, B, C}) ->  
  14.    S = (A + B + C)/2,  
  15. math:sqrt(S*(S-A)*(S-B)*(S-C));  
  16. %其他  
  17. area(Other) ->  
  18.    {invalid_object, Other}.  


执行一下看看:

java 代码
 
  1. 1> c(mathStuff).  
  2. {ok,mathStuff}  
  3. 2> mathStuff:area({square,2}).  
  4. 4  
  5. 3> mathStuff:area({circle,2}).  
  6. 12  
  7. 4> mathStuff:area({triangle,2,3,4}).  
  8. 2.90474  
  9. 5> mathStuff:area({other,2,3,4}).  
  10. {invalid_object,{other,2,3,4}}  


Erlang使用%开始单行注释。

posted @ 2009-09-11 10:11 暗夜教父 阅读(507) | 评论 (0)编辑 收藏

大多数实时网络游戏,将 server 的时间和 client 的时间校对一致是可以带来许多其他系统设计上的便利的。这里说的对时,并非去调整 client 的 os 中的时钟,而是把 game client 内部的逻辑时间调整跟 server 一致即可。

一个粗略的对时方案可以是这样的,client 发一个数据包给 server,里面记录下发送时刻。server 收到后,立刻给这个数据包添加一个server 当前时刻信息,并发还给 client 。因为大部分情况下,game server 不会立刻处理这个包,所以,可以在处理时再加一个时刻。两者相减,client 可以算得包在 server 内部耽搁时间。

client 收到 server 发还的对时包时,因为他可以取出当初发送时自己附加的时刻信息,并知道当前时刻,也就可以算出这个数据包来回的行程时间。这里,我们假定数据包来回时间想同,那么把 server 通知的时间,加上行程时间的一半,则可以将 client 时间和 server 时间校对一致。

这个过程用 udp 协议做比用 tcp 协议来的好。因为 tcp 协议可能因为丢包重发引起教大误差,而 udp 则是自己控制,这个误差要小的多。只是,现在网络游戏用 tcp 协议实现要比 udp 有优势的多,我们也不必为对时另起一套协议走 udp 。

一般的解决方法用多次校对就可以了。因为,如果双方时钟快慢一致的情况下,对时包在网络上行程时间越短,就一定表明误差越小。这个误差是不会超过包来回时间的一半的。我们一旦在对时过程中得到一个很小的行程时间,并在我们游戏逻辑的时间误差允许范围内,就不需要再校对了。

或者校对多次,发现网络比较稳定(虽然网速很慢),也可以认为校对准确。这种情况下,潜在的时间误差可能比较大。好在,一般,我们在时间敏感的包上都会携带时间戳。当双方时间校对误差很小的时候,client 发过来的时间戳是不应该早于 server 真实时刻的。(当时间校对准确后,server 收到的包上的时间戳加上数据包单行时间,应该等于 server 当前时刻)

一旦 server 发现 client 的包“提前”收到了,只有一种解释:当初校对时间时糟糕的网络状态带来了很多的时间误差,而现在的网络状态要明显优于那个时候。这时,server 应该勒令 client 重新对时。同理,client 发现 server 的数据包“提前”到达,也可以主动向 server 重新对时。

一个良好的对时协议的设定,在协议上避免 client 时间作弊(比如加速器,或者减速器)是可行的。这里不讨论也不分析更高级的利用游戏逻辑去时间作弊的方式,我们给数据包打上时间戳的主要目的也非防止时间作弊。

校对时间的一般通途是用来实现更流畅的战斗系统和位置同步。因为不依赖网络传输的统一时间参照标准可以使游戏看起来更为实时。

首先谈谈位置同步。

好的位置同步一定要考虑网络延迟的影响,所以,简单把 entity 的坐标广播到 clients 不是一个好的方案。我们应该同步的是一个运动矢量以及时间信息。既,无论是 client 还是 server ,发出和收到的信息都应该是每个 entity 在某个时刻的位置和运动方向。这样,接收方可以根据收到的时刻,估算出 entity 的真实位置。对于 server 一方的处理,只要要求 client 按一个频率(一般来说战斗时 10Hz 即可,而非战斗状态或 player 不改变运动状态时可以更低) 给它发送位置信息。server 可以在网络状态不好的情况下依据最近收到的包估算出现在 player 位置。而 client 发出的每次 player 位置信息,都应该被 server 信任,用来去修正上次的估算值。而 server 要做的只是抽查,或交给另一个模块去校验数据包的合法性(防止作弊)。

在 server 端,每个 entity 的位置按 10Hz 的频率做离散运动即可。

client 因为涉及显示问题,玩家希望看到的是 entity 的连续运动,所以处理起来麻烦一点。server 发过来的位置同步信息也可能因为网络延迟晚收到。client 同样根据最近收到的包做估算,但是再收到的包和之前已经收到的信息估算结果不同的时候,应该做的是运动方向和速度的修正,尽可能的让下次的估算更准确。

关于战斗指令同步,我希望是给所有战斗指令都加上冷却时间和引导时间,这正是 wow 的设计。这样,信任 client 的时间戳,就可以得到 client 准确的指令下达时间。引导时间(或者是公共冷却时间)可以充当网络延迟时间的缓冲。当然我们现在的设计会更复杂一些,这里不再列出。对于距离敏感的技能,例如远程攻击和范围魔法,我们的设计是有一个模糊的 miss 判定公式,解决距离边界的判定问题。

这里, server 对攻击目标的位置做估算的时候,可以不按上次发出包的运动方向去做位置估计,而选择用最有利于被攻击者的运动方向来做。这样,可以减少网络状况差的玩家的劣势。

对于 PVE 的战斗,甚至可以做更多的取舍,达到游戏流畅的效果。比如一个网络状态差的玩家去打 npc,他攻击 npc 的时刻,npc 是处于攻击范围之内的。但是由于网络延迟,数据包被 server 收到的时候,npc 已经离开。这个时候 server 可以以 client 的逻辑来将 npc 拉会原来的坐标。

虽然,这样做,可能会引起其他玩家(旁观者) client 上表现的不同。但是,网络游戏很多情况下是不需要严格同步的。在不影响主要游戏逻辑的情况下,player 的手感更为重要。

posted @ 2009-09-10 19:27 暗夜教父 阅读(558) | 评论 (0)编辑 收藏

看到这篇文章的时候,我觉得很惊讶,虽然我对这方面的了解并不多,但在自己的想像中,还是对网游这些东西稍有一点想法,因为曾经有朋友做过简单的外挂,比如,抓包发包然后尝试模拟包,来使网游达到你想实现的效果。
外挂这东西,在2003年左右应该是一个巅峰吧,那时候,奇迹外挂、传奇外挂,确实让一部分人先富起来,可是后来的零点行动,这些人都永远的消失在外挂长河中。
那时候我就在想,外挂是什么原理,为什么我这边的动作,可以让服务端产生那样的效果?其实,这就是一个同步的问题,我个人理解是服务器上有个触发器,这边发包后,然后那边判断包是否正常,然后就会有一个相应的动作。当然,动作程序还是在本机上,地图也在本机上,发出去的包,只是告诉服务器我是这样在动作的。于是就出现了瞬移,卡点这种情况,因为发出去的包,和坐标位置在服务器上都是正常的。(以上是我的猜测)

下面是文章:
不知道大家是否碰到过这种情况,当某个玩家发出一个火球,这个火球有自己的运动轨迹,那么如何来判断火球是否打中了人呢?大部分情况,当策划提出这个要求的时候,一般会被程序否认,原因是:太麻烦了,呵呵。复杂点的还有包括两个火球相撞之类的事情发生。

那么网络游戏中,是否真的无法模拟实现这种模拟呢?

首先我们来看看模拟此种操作会带来什么样的麻烦:

1,服务器必须trace火球的运行轨迹,乍一想,挺慢的。

2,网络延迟,传过来有延迟,传过去有延迟,延迟还不稳定,麻烦。

3,都有两点解决不了了,接下来不愿意再想了。

呵呵,实际上呢,对火球的模拟比对人物运动的模拟要轻松很多,原因很简单,火球的方向不会变。下面来看看具体用什么样的结构来实现:

不知道大家是否还记得我去年这个时候提到过的Dead Reckoning算法,我们要模拟火球运动的关键就在于一个叫Moving Objects Tracing Server的服务器程序,这个服务器是干什么的呢。这个服务器接收主游戏服务器发过来的注册事件的信息,比如有个玩家,开始移动了,那么主游戏服务器就 把该玩家的运动PDU,包括方向,速度,加速度,起点发给MOTS (Moving Objects Tracing Server),然后MOTS自己开始对其运行进行模拟,当游戏服务器发来第二个PDU包的时候,则对各个物件的位置进行修正,并重新开始模拟。那么,我 们模拟的目的是什么呢?当然是发生某些事件,比如说碰撞,或者掉入地图的某个陷阱的时候,会将该事件回发给主逻辑服务器。然后逻辑服务器来处理该事件。

那么,对于火球的处理,也和处理其他玩家的同步一样,当接收到玩家的发火球的指令以后,产生一个火球,并指定其PDU信息,在MOTS上注册该个运 动物 体。当MOTS自行模拟到这个物体和其他玩家或者NPC物体产生碰撞,则通知主逻辑服务器,然后主逻辑服务器产生相应的动作。

那么关于延迟呢?有些人也许会说,比如说前面有个火球,我本地操纵的小人其实躲过去了,但是因为网络延迟,在服务器上我并没有躲过去,那么怎么算? 呵呵, 不知道大家玩过星际没有,有没有发现在星际中玩多人连线模式的时候,有一点最特别的地方,就是控制一个小兵的时候,点了地图上的某个位置,但是小兵并不会 马上开始移动,而是有一定的延迟,但是这一小点延迟并不能掩盖星际的经典,同样的理论用到这里也成立。对于客户端的控制,当玩家操纵的主角改变PDU信息 的时候,确保信息发送到服务器之后,再开始处理本地的操作指令,这样就能保证本地的预测和服务器的预测几乎是没有什么误差的,即使有很小的误差产生,以服 务器为主,这样玩家也不会有太大的抱怨。

————————————————————————————————————————-

网络游戏同步详解之一

同步在网络游戏中是非常重要的,它保证了每个玩家在屏幕上看到的东西大体是一样的。其实呢,解决同步问题的最简单的方法就是把每个玩家的动作都向其 他玩家广播一遍,这里其实就存在两个问题:1,向哪些玩家广播,广播哪些消息。2,如果网络延迟怎么办。事实上呢,第一个问题是个非常简单的问题,不过之 所以我提出这个问题来,是提醒大家在设计自己的消息结构的时候,需要把这个因素考虑进去。而对于第二个问题,则是一个挺麻烦的问题,大家可以来看这么个例 子:
比如有一个玩家A向服务器发了条指令,说我现在在P1点,要去P2点。指令发出的时间是T0,服务器收到指令的时间是T1,然后向周围的玩家广播这条 消息,消息的内容是“玩家A从P1到P2”有一个在A附近的玩家B,收到服务器的这则广播的消息的时间是T2,然后开始在客户端上画图,A从P1到P2 点。这个时候就存在一个不同步的问题,玩家A和玩家B的屏幕上显示的画面相差了T2-T1的时间。这个时候怎么办呢?

有个解决方案,我给它取名叫预测拉扯,虽然有些怪异了点,不过基本上大家也能从字面上来理解它的意思。要解决这个问题,首先要定义一个值叫:预 测误差。然后需要在服务器端每个玩家连接的类里面加一项属性,叫TimeModified,然后在玩家登陆的时候,对客户端的时间和服务器的时间进行比 较,得出来的差值保存在TimeModified里面。还是上面的那个例子,服务器广播消息的时候,就根据要广播对象的TimeModified,计算出 一个客户端的CurrentTime,然后在消息头里面包含这个CurrentTime,然后再进行广播。并且同时在玩家A的客户端本地建立一个队列,保 存该条消息,只到获得服务器验证就从未被验证的消息队列里面将该消息删除,如果验证失败,则会被拉扯回P1点。然后当玩家B收到了服务器发过来的消息“玩 家A从P1到P2”这个时候就检查消息里面服务器发出的时间和本地时间做比较,如果大于定义的预测误差,就算出在T2这个时间,玩家A的屏幕上走到的地点 P3,然后把玩家B屏幕上的玩家A直接拉扯到P3,再继续走下去,这样就能保证同步。更进一步,为了保证客户端运行起来更加smooth,我并不推荐直接 把玩家拉扯过去,而是算出P3偏后的一点P4,然后用(P4-P1)/T(P4-P3)来算出一个很快的速度S,然后让玩家A用速度S快速移动到P4,这 样的处理方法是比较合理的,这种解决方案的原形在国际上被称为(Full plesiochronous),当然,该原形被我篡改了很多来适应网络游戏的同步,所以而变成所谓的:预测拉扯。

另外一个解决方案,我给它取名叫验证同步,听名字也知道,大体的意思就是每条指令在经过 服务器验证通过了以后再执行动作。具体的思路如下:首先 也需要在每个玩家连接类型里面定义一个 TimeModified,然后在客户端响应玩家鼠标行走的同时,客户端并不会先行走动,而是发一条走路的指令给服务器,然后等待服务器的验证。服务器接 受到这条消息以后,进行逻辑层的验证,然后计算出需要广播的范围,包括玩家A在内,根据各个客户端不同的TimeModified生成不同的消息头,开始 广播,这个时候这个玩家的走路信息就是完全同步的了。这个方法的优点是能保证各个客户端之间绝对的同步,缺点是当网络延迟比较大的时候,玩家的客户端的行 为会变得比较不流畅,给玩家带来很不爽的感觉。该种解决方案的原形在国际上被称为(Hierarchical master-slave synchronization),80年代以后被广泛应用于网络的各个领域。

最后一种解决方案是一种理想化的解决方案,在国际上被称为Mutual synchronization,是一种对未来网络的前景的良好预测出来的解决方案。这里之所以要提这个方案,并不是说我们已经完全的实现了这种方案,而 只是在网络游戏领域的某些方面应用到这种方案的某些思想。我对该种方案取名为:半服务器同步。大体的设计思路如下:

首先客户端需要在登陆世界的时候建立很多张广播列表,这些列表在客户端后台和服务器要进行不及时同步,之所以要建立多张列表,是因为要广播的类 型是不止一种的,比如说有local message,有remote message,还有global message 等等,这些列表都需要在客户端登陆的时候根据服务器发过来的消息建立好。在建立列表的同时,还需要获得每个列表中广播对象的TimeModified,并 且要维护一张完整的用户状态列表在后台,也是不及时的和服务器进行同步,根据本地的用户状态表,可以做到一部分决策由客户端自己来决定,当客户端发送这部 分决策的时候,则直接将最终决策发送到各个广播列表里面的客户端,并对其时间进行校对,保证每个客户端在收到的消息的时间是和根据本地时间进行校对过的。 那么再采用预测拉扯中提到过的计算提前量,提高速度行走过去的方法,将会使同步变得非常的smooth。该方案的优点是不通过服务器,客户端自己之间进行 同步,大大的降低了由于网络延迟而带来的误差,并且由于大部分决策都可以由客户端来做,也大大的降低了服务器的资源。由此带来的弊端就是由于消息和决策权 都放在客户端本地,所以给外挂提供了很大的可乘之机。

综合以上三种关于网络同步派系的优缺点,综合出一套关于网络游戏传输同步的较完整的解决方案,我称它为综合同步法(colligate synchronization)。大体设计思路如下:

首先将服务器需要同步的所有消息从划分一个优先等级,然后按照3/4的比例划分出重要消息和非重要消息,对于非重要消息,把决策权放在客户端,在客户端逻辑上建立相关的决策机构和各种消息缓存区,以及相关的消息缓存区管理机构,如下图所示:

上图简单说明了对于非重要消息,客户端的大体处理流程,其中有一个客户端被动行为值得大家注意,其中包括对服务器发过来的某些验证代码做返回, 来确保消息缓存中的消息和服务器端是一致的,从而有效的防止外挂来篡改本地消息缓存。其中的消息来源是包括本地的客户端响应玩家的消息以及远程服务器传递 过来的消息。

对于重要消息,比如说战斗或者是某些牵扯到玩家一些比较敏感数据的操作,则采用另外一套方案,该方案首先需要在服务器和客户端之间建立一套 Ping System,然后服务器保存和用户的及时的ping值,当ping比较小的时候,响应玩家消息的同时先不进行动作,而是先把该消息反馈给服务器,并且阻 塞,服务器收到该消息,进行逻辑验证之后向所有该详细广播的有效对象进行广播(包括消息发起者),然后客户端收到该消息的验证,才开始执行动作。而当 ping比较大的时候,客户端响应玩家消息的同时立刻进行动作,并且同时把该消息反馈给服务器,值得注意的是这个时候还需要在本地建立一个无验证消息的队 列,把该消息入队,执行动作的同时等待服务器的验证,还需要保存当前状态。服务器收到客户端的请求后,进行逻辑验证,并把消息反馈到各个客户端,带上各个 客户端校对过的本地时间。如果验证通过不过,则通知消息发起者,该消息验证失败,然后客户端自动把已经在进行中的动作取消,恢复原来状态。如果验证通过, 则广播到的各个客户端根据从服务器获得校对时间进行对其进行拉扯,保证在该行为完成之前完成同步。

至此,一个比较成熟的网络游戏的同步机制已经初步建立起来了,接下来的逻辑代码就根据各自不同的游戏风格以及侧重点来写了。

同步是网络游戏最重要的问题,如何同步也牵扯到各个方面的问题,比如说游戏的规模,游戏的类型以及各种各样的方面,对于规模比较大的游戏,在同 步方面可以下很多的工夫,把消息分得十分的细腻,对于不同的消息采用不同的同步机制,而对于规模比较小的游戏,则可以采用大体上一样的同步机制,究竟怎么 样同步,没有个定式,是需要根据自己的不同情况来做出不同的同步决策的网游同步算法之导航推测(Dead Reckoning)算法:

——————————————————————————————————————————

网络游戏同步详解之二

在了解该算法前,我们先来谈谈该算法的一些背景资料。大家都知道,在网络传输的时候,延迟现象是很普遍的,而在基于Server/Client结构 下的网络游戏的同步也就成了很头疼的问题,在保证客户端响应用户本地指令流畅的情况下,没法有效的保证的同步的及时性。同样,在军方也有类似的事情发生, 即使是同一LAN里面的机器,也会因为传输的延迟,导致一些运算的失误,介于此,美国国防部投入了大量的资金用于研究一种比较的好的方案来解决分布式系统 中的延迟问题,特别是一个叫分布式模拟运动(Distributed Interactive Simulation)的系统,这套系统呢,其中就提出了一套号称是Latency Hiding & Bandwidth Reduction的方案,命名为Dead Reckoning。呵呵,来头很大吧,恩,那么我们下面就来看看这套系统的一些观点,以及我们如何把它运用到我们的网络游戏的同步中。

首先,这套同步方案是基于我那篇《网络游戏的同步》一文中的Mutual Synchronization同步方案的,也就是说,它并不是Server/Client结构的,而是基于客户端之间的同步的。下面我们先来说一些本文中将用到的名词概念:
网状网络:客户端之间构成的网络
节点:网状网络中的每个客户端
极限误差:进行同步的时候可能产生的误差的极值

恩,在探讨其原理的之前,我们先来看看我们需要一个什么样的环境。首先,需要一个网状网络,网状网络如何构成呢?当有新节点进入的时候,通知该 网络里面的所有节点,各节点为该客户端在本地创建一个副本,登出的时候,则通知所有节点销毁本地关于该节点的副本。然后每个节点该保存一些什么数据呢?首 先有一个很重要的包需要保存,叫做协议数据包(PDU Protocol Data Unit),PDU包含节点的一些相关的运动信息,比如当前位置,速度,运动方向,或者还有加速度等一些信息。除PDU之外,还有其他信息需要保存,比如 说节点客户端人物的HP,MP之类的。然后,保证每个节点在最少8秒之内要向其它节点广播一次PDU信息。最后,设置一个极限误差值。到此,其环境就算搭 建完成了。下面,我们就来看看相关的具体算法:

假设在节点A有一个小人(路人甲),开始跑路了,这个时候,就像所有的节点广播一次他的PDU信息,包括:速度(S),方向(O),加速度 (A)。那么所有的节点就开始模拟路人甲的运动轨迹和路线,包括节点A本身(这点很重要),同时,路人甲在某某玩家的控制下,会不时的改变一下方向,让其 跑路的路线变得不是那么正规。在跑路的过程中,节点A有一个值在不停的记录着其真实坐标和在后台模拟运动的坐标的差值,当差值大于极限误差的时候,则计算 出当前的速度S,方向O和速度A(算法将在后面介绍),并广播给网络中其他所有节点。其他节点在收到这条消息之后呢,就可以用一些很平滑的移动把路人甲拉 扯过去,然后重新调整模拟跑路的数据,让其继续在后台模拟跑路。

很显然,如果极限误差定义得大了,其他节点看到的偏差就会过大,如果极限偏差定义得小了,网络带宽就会增大。如果定义这个极限误差,就该根据各 种数据的重要性来设计了。如果是回合制的网络游戏,那么在走路上把极限误差定义得大些无所谓,可以减少带宽。但是如果是及时打斗的网络游戏,那么就得把极 限误差定义得小一些,否则会出现某人看到某人老远把自己给砍死的情况。

Dead Reckoning的主要算法有9种,但是只有两种是解决主要问题的,其他的基本上只是针对不同的坐标系的一些不同的算法,这里就不一一介绍了。好,那么我们下面来看传说中的最主要的两种算法:
第一:目标点 = 原点 + 速度 * 时间差
第二:目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差
呵呵,传说中的算法都是很经典的,虽然我们早在初中物理的时候就学过。

该算法的好处呢,正如它开始所说的,Latency Hiding & Bandwidth Reduction,从原则上解决了网络延迟导致的不同步的问题,并且有效的减少了带宽,不好的地方就是该算法基本上只能使用于移动中的同步,当然,移动 的同步是网络游戏中同步的最大的问题。

该方法结合我在《网络游戏的同步》一文中提出的综合同步法的构架可以基本上解决掉网络游戏中走路同步的问题。相关问题欢迎大家一起讨论。

有关导航推测算法(Dead Reckoning)中的平滑处理:

根据我上篇文章所介绍的,在节点A收到节点B新的PDU包时,如果和A本地的关于B的模拟运动的坐标不一致时,怎么样在A的屏幕上把B拽到新的 PDU包所描叙的点上面去呢,上文中只提了用“很平滑的移动”把B“拉扯”过去,那么实际中应该怎么操作呢?这里介绍四种方法。

第一种方法,我取名叫直接拉扯法,大家听名字也知道,就是直接把B硬生生的拽到新的PDU包所描叙的坐标上去,该方法的好处是:简单。坏处是:看了以下三种方法之后你就不会用这种方法了。

第二种方法,叫直线行走(Linear),即让B从它的当前坐标走直线到新的PDU包所描叙的坐标,行走速度用上文中所介绍的经典算法:
目标点 = 原点 + 速度 * 时间差 + 1/2 * 加速度 * 时间差算出:
首先算出从当前坐标到PDU包中描叙的坐标所需要的时间:
T = Dest( TargetB – OriginB ) / Speed
然后根据新PDU包中所描叙的坐标信息模拟计算出在时间T之后,按照新的PDU包中的运动信息所应该达到的位置:
_TargetB = NewPDU.Speed * T
然后根据当前模拟行动中的B和_TargetB的距离配合时间T算出一个修正过的速度_S:
_S = Dest( _TargetB – OriginB ) / T
然后在画面上让B以速度_S走直线到Target_B,并且在走到之后调整其速度,方向,加速度等信息为新的PDU包中所描叙的。

这种方法呢,非常的土,会让物体在画面上移动起来变得非常的不现实,经常会出现很生硬的拐角,而且对于经常要修改的速度_S,在玩家A的画面上,玩家B的行动会变得非常的诡异。其好处是:比第一种方法要好。

第三种方法,叫二次方程行走(Quadratic),该方法的原理呢,就是在直线行走的过程中,加入二次方程来计算一条曲线路径,让Dest( _TargetB – OriginB )的过程是一条曲线,而不是一条直线,恩,具体的实现方法,就是在Linear方法的计算中,设定一个二次方程,在Dest函数计算距离的时候根据设定的 二次方程来计算,这样一来,可以使B在玩家A屏幕上的移动变得比较的有人性化一些。但是该方法的考虑也是不周全的,仅仅只考虑了TargetB到 _TargetB的方向,而没有考虑新的PDU包中的方向描叙,那么从_TargetB开始模拟行走的时候,仍然是会出现比较生硬的拐角,那么下面提出的 最终解决方案,将彻底解决这个问题。

——————————————————————————————————————————

网络游戏同步详解之三

最后一种方法叫:立方体抖动(Cubic Splines),这个东东比较复杂,它需要四个坐标信息作为它的参数来进行运算,第一个参数Pos1是OriginB,第二个参数Pos2是 OriginB在模拟运行一秒以后的位置,第三个参数Pos3是到达_TargetB前一秒的位置,第四个参数pos4是_TargetB的位置。

Struct pos {
Coordinate X;
Coordinate Y;
}
Pos1 = OriginB
Pos2 = OriginB + V
Pos3 = _TargetB – V
Pos4 = _TargetB
运动轨迹中(x, y)的坐标。
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的取值会比较容易获得,根据OriginB配合当前的速度和方向可以获得,然而Pos3和Pos4呢,怎么获得呢?如果在从Pos1到Pos4的 过程中有新的PDU到达,那么我们定义它为NewPackage。

Pos3 = NewPackage.X + NewPackage.Y * t + 1/2 * NewPackage.a * t^2
Pos4 = Pos3 – (NewPackage.V + NewPackage.a * t)

如果没有NewPackage的情况下,则Pos3和Pos4按照开始所规定的方法获得。

至此,关于导航推测的算法大致介绍完毕。

原文来自:http://xinsync.xju.edu.cn/index.php/archives/4079

posted @ 2009-09-10 19:21 暗夜教父 阅读(346) | 评论 (0)编辑 收藏

http://canremember.com/?p=8

http://canremember.com/?p=10

过去一年中,花了很多时间在考虑服务器架构设计方面的问题。看了大量文章、也研究了不少开源项目,眼界倒是开阔了不少,不过回过头来看,对网游架构设计方面的帮助却是不多。老外还是玩儿console game的多,MMO Games方面涉及的还是不如国内广泛。看看 Massively Multiplayer Games Development 1 & 2 这两本书吧,质量说实话很一般,帮助自然也很有限。当然这也是好事,对国内的研发公司/团队来说,在网游服务器技术方面当然就存在超越老外的可能性,而且在这方面技术超越的机会更大,当然前提是要有积累、要舍得投入,研发人员更要耐得住寂寞、经得起诱惑,在平均每天收到超过3个猎头电话的时候——依然不动心。

上面有点儿扯远了,下面聊聊无缝世界架构(Seamless world server architecture)设计方面的一点儿看法。

先说架构设计的目标——我的看法,服务器组架构设计的目标就是确定各服务器拓补关系和主要的业务逻辑处理方法。主要要解决的问题就是在满足游戏内容设计需要的前提下,如何提高带负载能力的问题。

最简单的架构就是基本的C/S架构,一台Server直接构成一个Cluster,所有Client直接连接这个Server,这个Server完成所有逻辑和数据处理。这架构其实很好,最大的好处就是它架构上的 Simplicity ,Cluster内部的跨进程交互完全被排除,复杂度立刻就降下来了,而且——完全可以实现一个无缝(Seamless world)的游戏世界。但是即使我不说,大家也知道这种单Server架构会有什么问题。不过我们不妨以另外一个角度来看这个Server——一个黑盒子。从系统外部的角度来看,什么样的系统都可以看成一个整体、一个黑盒,而不管系统内部的拓补关系和实现复杂度方面的问题。在不考虑这个系统的实现的前提下,理论上Cluster的处理能力就是由硬件的数量和能力决定的,也就是说一个Server Cluster内包含越多的服务器、服务器越‘快’,那么这个Cluster的处理能力越好、带负载能力越好。那么我们要面对的带负载能力的问题,就是如何高效的利用这些Server的问题,基本上也可以理解为如何提高玩家请求的并发处理能力的问题。

CPU厂商在很久以前就在考虑这方面的问题了,CPU其实也可以看成个黑盒。看看他们用过的技术——流水线(pipeline)技术、多CPU/多核(multicore)技术,以及这些技术的衍生技术。我想了很久让 Server Cluster 内部处理并行的方法、并且有了比较清晰的思路之后,才发现其实早就可以参照CPU厂商的方法。流水线的方法就是把一个指令处理拆分成很多个步骤,这样指令的处理被分解之后就可以部分重叠(相当于变成并发的了)执行。我们的Server Cluster一样可以用这种方法来拆分,我想了个名字——

Services-based Architecture——基于服务的架构。在这种架构内部,我们根据处理数据、逻辑的相关性来划分组内各个服务器的工作任务。例如:位置服务提供物体可见性信息、物品服务处理所有物品相关的逻辑、社会关系服务提供行会家族等等方面的逻辑、战斗服务器只处理战斗相关的逻辑,等等。这样划分的话、逻辑处理的并发就有了可能性。举例来说:A砍B一刀这件事情与C从奸商手里买到一件武器这个事情是完全不相干的,而且这2个请求本来就在不同的服务器上被处理,他们是被不同的Service Server并发处理的。这就是 Services-based Architecture 的并发方法。

基本上,把游戏逻辑的处理拆分成一个个的service,就和设计cpu的时候把机器指令的具体处理拆分,然后设计出一个个流水线单元是一个道理。

Cells-based Architecture——基于cell的架构。每个cell都在不同的物理 server上面运行着完全一样的应用程序服务器,但是他们负责承载不同的游戏场景区域的游戏逻辑。和 services-based arch. 明显不同的就是,每个cell都是个‘在逻辑上完整的’服务器。它得处理物品操作、人物移动、战斗计算等等几乎所有的游戏逻辑。尽管这么做会带来一些(可能是很复杂)的问题,但是它完全是可行的。举例来说:在吴国A砍B一刀显然地和千里之外在越国的C砍D一刀不搭界,他们完全可以被不同的Cell并发地处理。

基本上,这就相当于一个主板上面插多个CPU或者一个CPU但是有多个内核,每个CPU能做的事情都是一样的,而且能一起做。

从一组服务器的角度来看,一般来说,我们的服务器组(Cluster)内都会有登陆验证服务器(Login Server)、持久性数据服务器(DB及DB Proxy)、连接代理服务器(Gate Server、FEP Server、Client Proxy等)以及Auto Patch Server、还有用于集中管理及控制组的服务器等等,由于这些服务器基本上什么样的架构设计都会用到,所以——现在不考虑以上这些服务器,只考虑具体处理游戏逻辑、游戏规则的各个服务器。以此为前提来分析一下 Services-based Architecture 和 Cells-based Architecture 的优缺点。

对Services-based Architecture 的分析
 

基于服务的架构,顾名思义这种架构的实现(程序)会是和服务的具体内容(策划)相关的,这是因为——各种【服务】内容的确定是建立于项目的【需求分析】基础上的,【需求分析】的前提是基本确定了【策划设计】,至少是项目的概要设计。

我想多数做过游戏项目的人都应该对需求变更有很深的感触,每个人都说“开始想做的那个和最后实际做出来的那个不一样”。特别是在项目的早期阶段,团队的不同成员对项目做完之后的样子有相当不同的看法(很可能大家互相都不知道对方怎么看的),这很容易理解,谁也不可能从几页纸几张图就确切地知道这个游戏做完了什么样子,即使不考虑需求变更。涉及到项目开发方法方面的东西这里就不多说了,总之我的看法就是——尽管我们不大可能设计出一个架构能够适应任何的游戏设计,但是不同开发任务间的耦合度显然还是越低越好,基于服务的架构适应需求变更的能力较差。

关于服务耦合
不管如何划分service,不同 service之间都一定存在不同程度的耦合(coupling)关系,不同的 service 之间会有相互依赖关系。而你们的策划设计可能会让这种关系复杂到程序在运行时的状态很难以琢磨的程度。

假设:
服务器组内的战斗处理和物品处理分别由两个不同的服务(器)提供
游戏规则:
人物被攻击后自己携带的物品可能掉落到地上
某些物品掉落后会爆炸
物品在地上爆炸可能伤及周围(半径10米内)人物
人物之间的‘仇恨度’影响战斗数值计算
被攻击时掉落的物品爆炸后伤及的人物,会增加对‘被攻击人’的‘仇恨度’

我想我还能想出很多很多“看上去不算过分”的规则来让这个事情变得复杂无比,很可能你们的策划也在无意中,已经拥有我这种能力 :) 而且他们在写文档时候的表达还多半不如我上面写的清楚,另外,他们还会把这些规则分到很多不同的文档里面去写。好吧,你肯定会想“把这两个服务合二为一好了 ”,实际上不管你想把哪两个(或多个)服务合并为一个服务的时候,都应该先考虑一下当时是为什么把他们独立为不同服务的?

实际上很多这样“看上去不算过分”的规则都会导致service间的频繁交互,所以每个service最好都是stateless service,这样的话情况会好很多,但是对于游戏来说这很难做到。

请求处理的时序问题
服务耦合的问题在不考虑开发复杂度比较高的情况下,还是可以被搞定的,只要脑袋够清醒,愿意花够多的时间,那么还有更难以搞定的么?我看确实还有,如果你对将要面对的问题,了解得足够多的话:)

 

 

 

上面两个序列图描述的是某个玩家做了连续做了两次同样的操作但是很可能得到了不同的结果,当然这些请求都是异步地被处理。问题的关键在于——尽管两次玩家执行的命令一样、顺序一样,甚至时间间隔都一样,但是结果却很不同——因为图(1)里面C2CS::Request_to_attack请求被处理的时候,C2IS::Request_equip_item 这个请求还没有被处理完,但是图(2)显示的情况就不一样了。因为C2IS::Request_equip_item这个操作很可能会改变游戏人物的属性,这个属性又很可能影响attack的结果。这两幅图实际上省略了 Combat Server 与 Item Server 之间的交互过程。但是已经足以说明问题了,每个Service处理每个Request时具体会消耗的时间,是无法在设计时确定的!

谁喜欢这类结果上的不确定性?举个例子:玩家很可能已经装备上了“只能使用1次的魔兽必杀刀”然后攻击了一下魔兽,但是它却没死!这会导致什么样的结果?请自行想象。另外,这种不确定性还会表现为“在项目开发期和运营期的行为差异”,或者“出现某些偶然的奇怪现象”。

那还有解决方案么?有的,其实只要序列化玩家请求的处理,使处理有序进行就可以了。但是又一次的,这会带来新的复杂度——在某个范围(整个服务器组?一个行会?一个队伍?)内,以每个玩家为单位,序列化他(们)的(可能是所有)操作,但是也显而易见,这在某种程度上降低了请求处理的并发性,尽管它对并发性的影响可能只局限于不大(最少是一个玩家)的范围。


 


对Cells-based Architecture 的分析
 

基于Cell的架构有个明显的优势就是Cell如何划分和你的策划没有关系J这是真的。而且Cell间如何交互可以被放到系统的底层,具体有多底层、多隐蔽(实际上可以隐蔽到对开发上层游戏逻辑的程序员都不可见的程度)要看你的实现如何了。如果做到了某个系统的程序设计与游戏设计完全无关的话,显然,这个系统受到游戏设计变更(需求变更)的影响就会很小很小,甚至会到完全不受影响的程度,当然这是理想情况。

关于跨边界对象交互
在基于Cell的服务器架构里面,实现无缝世界(Seamless World)的主要难点在于实现跨边界对象的交互时会出现的一些问题,因为这些对象在不同的Cell进程里面,这些Cell一般来说是在不同的物理服务器上运行。

无缝世界的特点自然就是无缝,并且因为无缝给玩家带来更好的游戏体验,所以显然我们希望“跨边界对象交互”问题不把事情搞砸,那么这种交互的表现就必须满足稳定、高效的前提。一般来说,高于300ms的延迟对玩家操作来说就属于“明显可见”的程度了,不能让玩家骑着500块RMB买来的虚拟马在一片大草原上面畅快的奔跑的时候,在某个地方突然就被“看不见的墙”给“挡”了一下,因为这“墙”根本看不见,所以会很影响“上帝”的游戏心情。

关于组成整个虚拟世界的Cell之间的关系,下面来分析两种情况:


<!--[if !supportLists]-->一, <!--[endif]-->Cell 承载的场景不重叠

 

如图(1),一个连续的虚拟世界场景被分成左右两块,分别在不同的Cell Server上面运行。A、B、C分别是3个不同的游戏角色。在这种情况下B与C的交互并不存在任何障碍,因为B和C只不过是同一个物理服务器上同一个进程内的两块不同的内存数据而已。但是A与B/C的交互就不那么直接了,尽管他们所在的场景看上去是“连续的、一体的”但是事情不会像表面上那么简单。A与B发生交互时候会发生什么事情?例如A攻击了B、A与B交易物品等等,因为在这种结构下做数据同步会带来很多问题,例如对象状态不确定性、开发复杂度等等、相对来说两个Cell Server之间做网络通讯而带来的延迟可能反而是最小的问题,这些问题不需要很复杂的分析就可以得出结论,在此不再多说了。

<!--[if !supportLists]-->二,Cell 承载的场景(部分地)重叠

 

如图(2),一个连续的虚拟世界场景被分成左右两块,分别在不用的Cell Server上面运行。A、B、C、D分别是4个不同的游戏角色。这个情况下,中间的区域为2个Cell所共同维护,中间区域的对象同属于2个Cell所‘拥有’。这有什么好处?现在,任意两个对象之间,除了A与C之间的交互,都变得更‘直接’了。变得直接肯定是一件好事儿,那么A与C之间呢?他们之间其实也没有任何问题J 因为双方都已经超出了对方的Area of Interest(AoI)区域,游戏规则可以限制他们不能直接交互。

上面提到的第二种方案算不上什么魔法,但是肯定是比第一种方案更有效。接下来怎么办?假设B是个玩家,他站在中间这块区域上面时,并不会产生“我到底是在哪里”这样的疑问J 问题的关键在于对于Cell Server来说,怎么样同步那些处于重叠区域对象的状态。游戏世界内的对象可能同时处于1个、2个、3个或者4个不同的Cell Server。如果你的Cell分隔方法不限于水平线和垂直线、或者有人故意捣乱的话,还可能会更多。需要被同步的对象也不只是玩家本身,还包括怪物、NPC、一颗会走的树、某玩家在地上吐的痰等等。

由于我们的基于无缝世界的游戏规则不大会直接去限制游戏世界某处玩家的行为,也就是说玩家如果能相互交易物品的话,他们肯定希望在任何地方都能交易,“为什么其他地方都行,但是在某个墙角做交易就会导致物品丢失?”所以比较可靠的方法是建立一套的用于同步的底层机制,来同步这些跨边界对象。

怎么实现?这个话题很大,恐怕再写几篇Blog我也讲不完,但是有一些东西可以作为参考,例如:DCOM和CORBA规范,Java的RMI,基于Python的 PYRO,TAO(The ACE ORB)等等。好在分布式处理的问题不止是网络游戏会涉及到,可以借鉴的东西还是很多的。

总结
很显然,这篇文章在两种架构的评价上面存在某些倾向性,但是倾向性本身只是副产品。另外一个副产品就是关于一些技术分析方法。

在考虑采用何种技术的时候,我们往往很容易地就会忽略对程序之外那些事情的影响。上面我提到的关于Services-based架构实现的时候,提到划分service及数据设计对程序设计能力的挑战、对策划设计的制约,对适应需求变更能力的影响,都不会只是空谈。这些问题也不是只在实现这种架构的时候才出现。

不要高估自己的智商,Keep It Simple and Stupid :) 应该可以让我们离成功更近一点儿。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/romandion/archive/2009/04/02/4044368.aspx

posted @ 2009-09-09 10:53 暗夜教父 阅读(451) | 评论 (0)编辑 收藏
由于网游服务器的设计牵涉到太多内容,比如:网络通信方面、人工智能、数据库设计等等,所以本文将重点从网络通信方面的内容展开论述。谈到网络通信,就不能不涉及如下五个问题:
1、 常见的网游服务通信器架构概述
2、 网游服务器设计的基本原则
3、 网游服务器通信架构设计所需的基本技术
4、 网游服务器通信架构的测试
5、 网游服务器通信架构设计的常见问题

下面我们就从第一个问题说起:

常见的网游服务器通信架构概述
  目前,国内的网游市场中大体存在两种类型的网游游戏:MMORPG(如:魔兽世界)和休闲网游(如:QQ休闲游戏和联众游戏,而如泡泡堂一类的游戏与QQ休闲游戏有很多相同点,因此也归为此类)。由于二者在游戏风格上的截然不同,导致了他们在通信架构设计思路上的较大差别。下面笔者将分别描述这两种网游的通信架构。

1.MMORPG类网游的通信架构
  网游的通信架构,通常是根据几个方面来确定的:游戏的功能组成、游戏的预计上线人数以及游戏的可扩展性。
  目前比较通用的MMORPG游戏流程是这样的:

a. 玩家到游戏官方网站注册用户名和密码。
b. 注册完成后,玩家选择在某一个区激活游戏账号。
c. 玩家在游戏客户端中登录进入已经被激活的游戏分区,建立游戏角色进行游戏。

  通常,在这样的模式下,玩家的角色数据是不能跨区使用的,即:在A区建立的游戏角色在B区是无法使用的,各区之间的数据保持各自独立性。我们将这样独立的A区或B区称为一个独立的服务器组,一个独立的服务器组就是一个相对完整的游戏世界。而网游服务器的通信架构设计,则包括了基于服务器组之上的整个游戏世界的通信架构,以及在一个服务器组之内的服务器通信架构。

  我们先来看看单独的服务器组内部的通信是如何设计的。
  一个服务器组内的各服务器组成,要依据游戏功能进行划分。不同的游戏内容策划会对服务器的组成造成不同的影响。一般地,我们可以将一个组内的服务器简单地分成两类:场景相关的(如:行走、战斗等)以及场景不相关的(如:公会聊天、不受区域限制的贸易等)。为了保证游戏的流畅性,可以将这两类不同的功能分别交由不同的服务器去各自完成。另外,对于那些在服务器运行中进行的比较耗时的计算,一般也会将其单独提炼出来,交由单独的线程或单独的进程去完成。

  各个网游项目会根据游戏特点的不同,而灵活选择自己的服务器组成方案。经常可以见到的一种方案是:场景服务器、非场景服务器、服务器管理器、AI服务器以及数据库代理服务器。
  以上各服务器的主要功能是:

  场景服务器:它负责完成主要的游戏逻辑,这些逻辑包括:角色在游戏场景中的进入与退出、角色的行走与跑动、角色战斗(包括打怪)、任务的认领等。场景服务器设计的好坏是整个游戏世界服务器性能差异的主要体现,它的设计难度不仅仅在于通信模型方面,更主要的是整个服务器的体系架构和同步机制的设计。

  非场景服务器:它主要负责完成与游戏场景不相关的游戏逻辑,这些逻辑不依靠游戏的地图系统也能正常进行,比如公会聊天或世界聊天,之所以把它从场景服务器中独立出来,是为了节省场景服务器的CPU和带宽资源,让场景服务器能够尽可能快地处理那些对游戏流畅性影响较大的游戏逻辑。

  服务器管理器:为了实现众多的场景服务器之间以及场景服务器与非场景服务器之间的数据同步,我们必须建立一个统一的管理者,这个管理者就是服务器组中的服务器管理器。它的任务主要是在各服务器之间作数据同步,比如玩家上下线信息的同步。其最主要的功能还是完成场景切换时的数据同步。当玩家需要从一个场景A切换到另一个场景B时,服务器管理器负责将玩家的数据从场景A转移到场景B,并通过协议通知这两个场景数据同步的开始与结束。所以,为了实现这些内容繁杂的数据同步任务,服务器管理器通常会与所有的场景服务器和非场景服务器保持socket连接。

  AI(人工智能)服务器:由于怪物的人工智能计算非常消耗系统资源,所以我们把它独立成单独的服务器。AI服务器的主要作用是负责计算怪物的AI,并将计算结果返回给场景服务器,也就是说,AI服务器是单独为场景服务器服务的,它完成从场景服务器交过来的计算任务,并将计算结果返回给场景服务器。所以,从网络通信方面来说,AI服务器只与众多场景服务器保持socket连接。

  数据库代理服务器:在网游的数据库读写方面,通常有两种作法,一种是在应用服务器中直接加进数据库访问的代码进行数据库访问,还有一种方式是将数据库读写独立出来,单独作成数据库代理,由它统一进行数据库访问并返回访问结果。

  其中,非场景服务器在不同的游戏项目中可能会被设计成不同的功能,比如以组队、公会或全频道聊天为特色的游戏,很可能为了满足玩家的聊天需求而设立单独的聊天服务器;而如果是以物品贸易(如拍卖等)为特色的游戏,很可能为了满足拍卖的需求而单独设立拍卖服务器。到底是不是有必要将某一项游戏功能独立处理成一个服务器,要视该功能对游戏的主场景逻辑(指行走、战斗等玩家日常游戏行为)的影响程度而定。如果该功能对主场景逻辑的影响比较大,可能对主场景逻辑的运行造成比较严重的性能和效率损失,那么应考虑将其从主场景逻辑中剥离,但能否剥离还有另一个前提:此功能是否与游戏场景(即地图坐标系统)相关。如果此功能与场景相关又确实影响到了主场景逻辑的执行效率,则可能需要在场景服务器上设立专门的线程来处理而不是将它独立成一个单独的服务器。

  以上是一个服务器组内的各服务器组成情况介绍,那么,各服务器之间是如何通信的呢?它的基本通信构架有哪些呢?
  MMORPG的单组服务器架构通常可以分为两种:第一种是带网关的服务器架构;第二种是不带网关的服务器架构。两种方案各有利弊。

  就带网关的服务器架构而言,由于它对外只向玩家提供唯一的一个通信端口,所以在玩家一侧会有比较流畅的游戏体验,这通常也是那些超大规模无缝地图网游所采用的方案,但这种方案的缺点是服务器组内的通信架构设计相对复杂、调试不方便、网关的通信压力过大、对网关的通信模型设计要求较高等。第二种方案会同时向玩家开放多个游戏服务器端口,除了游戏场景服务器的通信端口外,同时还可能提供诸如聊天服务器等的通信端口。这种方案的主要缺点是在进行场景服务器的切换时,玩家客户端的表现中通常会有一个诸如场景调入的界面出现,影响了游戏的流畅感。基于这种方案的游戏在客户端的界面处理方面,比较典型的表现是:当要进行场景切换时,只能通过相应的“传送功能”传送到另外的场景去,或者需要进入新的场景时,客户端会有比较长时间的等待进入新场景的等待界面(Loading界面)。

  从技术角度而言,笔者更倾向于将独立的服务器组设计成带网关的模型,虽然这加大了服务器的设计难度,但却增强了游戏的流畅感和安全性,这种花费还是值得的。
  笔者在下面附上了带网关的MMORPG通信架构图,希望能给业内的朋友们一点有益的启迪。
posted @ 2009-09-09 10:43 暗夜教父 阅读(289) | 评论 (0)编辑 收藏

我们一开始的游戏逻辑层是基于网络包驱动的,也就是将 client 消息定义好结构打包发送出去,然后再 server 解析这些数据包,做相应的处理。

写了一段时间后,觉得这种方案杂乱不利于复杂的项目。跟同事商量以后,改成了非阻塞的 RPC 模式。

首先由处理逻辑的 server 调用 client 的远程方法在 client 创建出只用于显示表现的影子对象;然后 server 对逻辑对象的需要client 做出相应表现的操作,变成调用 client 端影子对象的远程方法来实现。

这使得游戏逻辑编写变的清晰了很多,基本可以无视网络层的存在,和单机游戏的编写一样简单。

本质上,这样一个系统跟网络包驱动的方式没有区别;但是从编码表现形式上要自然很多。正如 C 语言也可以实现面向对象,但却没有 C++ 实现的自然一样。在这个系统中,引擎封装了对象管理的部分,使得逻辑编写的时候不再需要处理讨厌的对象数字 id ;还隐藏了消息发送或广播的问题。

我把玩家控制的角色,和服务器上你的角色分做两个东西。即,你控制的你,和服务器认为的你就分开了。服务器认为的你,你看见的服务器上的其他人是一类东西。操作自己的角色行动时,你通过 client 上的控制器的远程方法向服务器发送指令;而服务器通过远程调用每个角色的远程方法让 client 可以收到感兴趣的所有角色的行为。

这样,client 永远都是通过一个控制器调用其远程方法来告诉服务器"我要干什么",而服务器的逻辑层则通过调用其上所有逻辑对象的远程方法来改变每个对象的状态。而引擎就根据每个链接的需要,广播这些消息,使得每个 client 上对应的影子对象可以收到状态改变的消息。

这些,就是半个月来我跟同事一起做的工作。当然,由于我们用脚本编写逻辑层,这样,脚本接口可以比 C 接口实现的漂亮的多。

首先是自定义格式的接口描述文件,用自编写的工具自动编译成对应脚本代码。我们只需要在脚本中编写对应的类,就可以自动响应远端调用的方法了。而调用远程方法,也跟本地方法保持同样的形式,写起来跟本地函数调用没有区别。这在以前用 C/C++ 编写逻辑的时候是很难做到的。

其次,引擎内部做好对象的管理工作,负责把通讯协议上的 id 转换成逻辑层中的对象传递给逻辑层使用。

再次,enum 这样的类型再也不需要用一些数字的常数了,也不需要在脚本额外的定义出来。可以在接口文件中定义好,经过引擎的处理后,逻辑层可以直接用更为友好的字符串代替,而不失去效率。

编写逻辑的程序员不再需要关心网络的问题后,就可以把心思放在细节上。

最后,对于实现行为预测来补偿网络延迟的特性上。在先前的版本中,我们为了实现这个,花了不少的气力。主要是将时间戳信息放在基础通讯协议中来辅助实现。具体的消息包收到后,再计算延迟时间来推算当前的状态。现在,可以把时间信息封装到 RPC 中,让每个远程方法自动带有延迟时间,方便计算。按模拟程序的实际效果上看,单单位置同步的预测策略,可以让延迟在 8 秒之内的玩家可以忍受;而延迟小于 1 秒的时候,几乎不会受到滞后的影响了。

关于每个链接感兴趣的信息的问题,决定了每个逻辑对象的状态改变要通知哪些人。目前的想法是独立到单独进程去处理,我们在处理连接的服务器和处理逻辑的服务器之间设置单独的服务器来管理每个链接感兴趣的对象,这个任务相对单一且责任重大,独立出来可以大大减轻逻辑服务器的复杂度。

posted @ 2009-09-09 10:42 暗夜教父 阅读(849) | 评论 (1)编辑 收藏
仅列出标题
共9页: 1 2 3 4 5 6 7 8 9 

<2009年9月>
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

常用链接

留言簿(2)

随笔分类

随笔档案

文章分类

文章档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜