• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            教父的告白
            一切都是紙老虎
            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 暗夜教父 閱讀(686) | 評論 (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 暗夜教父 閱讀(323) | 評論 (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 暗夜教父 閱讀(400) | 評論 (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 暗夜教父 閱讀(298) | 評論 (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 暗夜教父 閱讀(540) | 評論 (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 暗夜教父 閱讀(585) | 評論 (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 暗夜教父 閱讀(362) | 評論 (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 暗夜教父 閱讀(477) | 評論 (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的單組服務器架構通??梢苑譃閮煞N:第一種是帶網關的服務器架構;第二種是不帶網關的服務器架構。兩種方案各有利弊。

              就帶網關的服務器架構而言,由于它對外只向玩家提供唯一的一個通信端口,所以在玩家一側會有比較流暢的游戲體驗,這通常也是那些超大規模無縫地圖網游所采用的方案,但這種方案的缺點是服務器組內的通信架構設計相對復雜、調試不方便、網關的通信壓力過大、對網關的通信模型設計要求較高等。第二種方案會同時向玩家開放多個游戲服務器端口,除了游戲場景服務器的通信端口外,同時還可能提供諸如聊天服務器等的通信端口。這種方案的主要缺點是在進行場景服務器的切換時,玩家客戶端的表現中通常會有一個諸如場景調入的界面出現,影響了游戲的流暢感。基于這種方案的游戲在客戶端的界面處理方面,比較典型的表現是:當要進行場景切換時,只能通過相應的“傳送功能”傳送到另外的場景去,或者需要進入新的場景時,客戶端會有比較長時間的等待進入新場景的等待界面(Loading界面)。

              從技術角度而言,筆者更傾向于將獨立的服務器組設計成帶網關的模型,雖然這加大了服務器的設計難度,但卻增強了游戲的流暢感和安全性,這種花費還是值得的。
              筆者在下面附上了帶網關的MMORPG通信架構圖,希望能給業內的朋友們一點有益的啟迪。
            posted @ 2009-09-09 10:43 暗夜教父 閱讀(302) | 評論 (0)編輯 收藏

            我們一開始的游戲邏輯層是基于網絡包驅動的,也就是將 client 消息定義好結構打包發送出去,然后再 server 解析這些數據包,做相應的處理。

            寫了一段時間后,覺得這種方案雜亂不利于復雜的項目。跟同事商量以后,改成了非阻塞的 RPC 模式。

            首先由處理邏輯的 server 調用 client 的遠程方法在 client 創建出只用于顯示表現的影子對象;然后 server 對邏輯對象的需要client 做出相應表現的操作,變成調用 client 端影子對象的遠程方法來實現。

            這使得游戲邏輯編寫變的清晰了很多,基本可以無視網絡層的存在,和單機游戲的編寫一樣簡單。

            本質上,這樣一個系統跟網絡包驅動的方式沒有區別;但是從編碼表現形式上要自然很多。正如 C 語言也可以實現面向對象,但卻沒有 C++ 實現的自然一樣。在這個系統中,引擎封裝了對象管理的部分,使得邏輯編寫的時候不再需要處理討厭的對象數字 id ;還隱藏了消息發送或廣播的問題。

            我把玩家控制的角色,和服務器上你的角色分做兩個東西。即,你控制的你,和服務器認為的你就分開了。服務器認為的你,你看見的服務器上的其他人是一類東西。操作自己的角色行動時,你通過 client 上的控制器的遠程方法向服務器發送指令;而服務器通過遠程調用每個角色的遠程方法讓 client 可以收到感興趣的所有角色的行為。

            這樣,client 永遠都是通過一個控制器調用其遠程方法來告訴服務器"我要干什么",而服務器的邏輯層則通過調用其上所有邏輯對象的遠程方法來改變每個對象的狀態。而引擎就根據每個鏈接的需要,廣播這些消息,使得每個 client 上對應的影子對象可以收到狀態改變的消息。

            這些,就是半個月來我跟同事一起做的工作。當然,由于我們用腳本編寫邏輯層,這樣,腳本接口可以比 C 接口實現的漂亮的多。

            首先是自定義格式的接口描述文件,用自編寫的工具自動編譯成對應腳本代碼。我們只需要在腳本中編寫對應的類,就可以自動響應遠端調用的方法了。而調用遠程方法,也跟本地方法保持同樣的形式,寫起來跟本地函數調用沒有區別。這在以前用 C/C++ 編寫邏輯的時候是很難做到的。

            其次,引擎內部做好對象的管理工作,負責把通訊協議上的 id 轉換成邏輯層中的對象傳遞給邏輯層使用。

            再次,enum 這樣的類型再也不需要用一些數字的常數了,也不需要在腳本額外的定義出來。可以在接口文件中定義好,經過引擎的處理后,邏輯層可以直接用更為友好的字符串代替,而不失去效率。

            編寫邏輯的程序員不再需要關心網絡的問題后,就可以把心思放在細節上。

            最后,對于實現行為預測來補償網絡延遲的特性上。在先前的版本中,我們為了實現這個,花了不少的氣力。主要是將時間戳信息放在基礎通訊協議中來輔助實現。具體的消息包收到后,再計算延遲時間來推算當前的狀態?,F在,可以把時間信息封裝到 RPC 中,讓每個遠程方法自動帶有延遲時間,方便計算。按模擬程序的實際效果上看,單單位置同步的預測策略,可以讓延遲在 8 秒之內的玩家可以忍受;而延遲小于 1 秒的時候,幾乎不會受到滯后的影響了。

            關于每個鏈接感興趣的信息的問題,決定了每個邏輯對象的狀態改變要通知哪些人。目前的想法是獨立到單獨進程去處理,我們在處理連接的服務器和處理邏輯的服務器之間設置單獨的服務器來管理每個鏈接感興趣的對象,這個任務相對單一且責任重大,獨立出來可以大大減輕邏輯服務器的復雜度。

            posted @ 2009-09-09 10:42 暗夜教父 閱讀(868) | 評論 (1)編輯 收藏
            僅列出標題
            共9頁: 1 2 3 4 5 6 7 8 9 

            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            常用鏈接

            留言簿(2)

            隨筆分類

            隨筆檔案

            文章分類

            文章檔案

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            久久伊人精品一区二区三区| 久久国产亚洲精品无码| 久久国产精品免费| 亚洲欧美日韩精品久久亚洲区 | 99久久99这里只有免费的精品| 91精品国产高清91久久久久久| 国产农村妇女毛片精品久久| 久久久久久综合网天天| 久久精品国产福利国产秒| 思思久久99热只有频精品66| 国产亚洲欧美精品久久久| 精品国产婷婷久久久| 亚洲va久久久噜噜噜久久| 久久精品国产乱子伦| 久久香蕉国产线看观看乱码| 国产精品亚洲综合久久| 99久久国产综合精品网成人影院| 四虎国产精品免费久久| 94久久国产乱子伦精品免费| 久久精品天天中文字幕人妻| 久久久久久青草大香综合精品| 国产精品无码久久久久久| 久久久久久久久久久精品尤物| 精品久久久久久国产免费了| 996久久国产精品线观看| 精品久久久无码人妻中文字幕 | 亚洲AV无码久久| 香蕉久久夜色精品国产尤物| 品成人欧美大片久久国产欧美| 久久人人爽人人爽人人AV东京热| 亚洲一级Av无码毛片久久精品| 狠狠色综合网站久久久久久久| 久久亚洲精品视频| 久久精品国产亚洲AV麻豆网站| 久久亚洲日韩精品一区二区三区| 伊人热热久久原色播放www| 久久婷婷人人澡人人| 99久久精品免费看国产一区二区三区 | 久久免费美女视频| 精品久久久久久国产潘金莲| 欧美熟妇另类久久久久久不卡 |