• <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
            使用下列JSON庫(kù):
            http://www.lshift.net/blog/2007/02/17/json-and-json-rpc-for-erlang

            該JSON庫(kù)采用Joe Armstrong prefered Data type mapping
            即:
            JSON Obj    = type obj()   = {obj, [{key(), val()}]}   
            JSON Array  
            = type array() = [val()]   
            JSON Number 
            = type num()   = int() | float()    
            JSON String 
            = type str()   = bin()   
            JSON 
            true false null       = truefalse null (atoms)   
            With Type val() 
            = obj() | array() | num() | str() | true | false | null  
            and key() being a str(). (Or a binary or atom, during JSON encoding.)  

            測(cè)試如下:
            Eshell V5.6.3  (abort with ^G)   
            1> O = rfc4627:encode({obj, [{name, hideto}{age, 23}]}).   
            "{\"name\":\"hideto\",\"age\":23}"  
            2> rfc4627:decode(O).   
            {ok,{obj,[{"name",<<"hideto">>},{"age",23}]},[]}   
            3> A = rfc4627:encode([1,2,3,4,5]).   
            "[1,2,3,4,5]"  
            4> rfc4627:decode(A).   
            {ok,[1,2,3,4,5],[]}   
            5> N = rfc4627:encode(12345).   
            "12345"  
            6> rfc4627:decode(N).   
            {ok,12345,[]}   
            7> S = rfc4627:encode("12345").   
            "[49,50,51,52,53]"  
            8> rfc4627:decode(S).   
            {ok,"12345",[]}   
            9> T = rfc4627:encode(true).   
            "true"  
            10> rfc4627:decode(T).   
            {ok,true,[]}   
            11> F = rfc4627:encode(false).   
            "false"  
            12> rfc4627:decode(F).   
            {ok,false,[]}   
            13> Null = rfc4627:encode(null).   
            "null"  
            14> rfc4627:decode(Null).   
            {ok,null,[]}  

            posted @ 2009-09-18 15:38 暗夜教父 閱讀(3029) | 評(píng)論 (0)編輯 收藏

            轉(zhuǎn)自http://www.javaeye.com/topic/401041


            學(xué)erlang有一段時(shí)間了,現(xiàn)在在維護(hù)一套webim系統(tǒng)
            并打算擴(kuò)展成 webgame 的服務(wù)程序

            在沒(méi)有使用包協(xié)議的時(shí)候,遇到好多粘包問(wèn)題,實(shí)在惱火

            查閱了相關(guān)資料:

            Flash Socket 的 writeUTF() 會(huì)自動(dòng)增加包頭長(zhǎng)度的協(xié)議,剛好對(duì)應(yīng)了
            Erlang的Socket選項(xiàng) {packet,2}

            這使得兩者的通信非常完美,再也不用擔(dān)心粘包什么的問(wèn)題了

             

            下面是我寫(xiě)的一個(gè)Flash Socket 接口:SocketBridge.as

            package {   
                
            import flash.display.Sprite;   
                
            import flash.events.*;   
                
            import flash.net.Socket;   
                
            import flash.utils.*;   
                
            import flash.external.ExternalInterface;   
                
            import flash.system.*;   
                
            public class SocketBridge extends Sprite {   
                    Socket.prototype.timeout    
            =3000;   
                    
            private var socket:Socket;   
                    
            public function SocketBridge()   
                    
            {   
                        socket 
            = new Socket();   
                        socket.addEventListener( Event.CONNECT, onConnect );   
                        socket.addEventListener( ProgressEvent.SOCKET_DATA, onDataRecevice);   
                        socket.addEventListener( Event.CLOSE, onClose);   
                        socket.addEventListener( IOErrorEvent.IO_ERROR, onError);    
                           
                        
            if(ExternalInterface.available)   
                        
            {   
                               
                            ExternalInterface.addCallback(
            "socket_connect",socket_connect);   
                            ExternalInterface.addCallback(
            "socket_send",socket_send);   
                            ExternalInterface.addCallback(
            "load_policy",load_policy);   
                        }
               
                    }
               
                    
            public function onError(e):void  
                    
            {   
                        ExternalInterface.call(
            "sb_onerror",e.text);   
                        socket.close();   
                    }
               
                    
            public function load_policy(host:String,port):void  
                    
            {   
                        Security.loadPolicyFile(
            "xmlsocket://"+host+":"+port);     
                    }
               
                       
                    
            public function socket_connect(host:String,port):void  
                    
            {   
                        
            try{   
                            socket.connect(host,port);   
                        }
            catch(e){   
                            ExternalInterface.call(
            "sb_onerror",e.text);   
                        }
               
                    }
               
                       
                    
            public function socket_send(msg:String)   
                    
            {      
                        socket.writeUTF(msg);   
                        socket.flush();   
                    }
               
                       
                    
            private function onConnect(event:Event):void    
                    
            {   
                        ExternalInterface.call(
            "sb_onconnect",true);   
                    }
               
                       
                    
            private function onClose(event:Event):void    
                    
            {   
                        socket.close();   
                        ExternalInterface.call(
            "sb_onclose",true);   
                    }
               
              
                    
            private function onDataRecevice( eventrogressEvent ):void  
                    
            {   
                        var sdata:String;   
                        
            while(socket.bytesAvailable){   
                            sdata 
            = socket.readUTF();   
                            ExternalInterface.call(
            "sb_ondata",sdata);   
                        }
               
                    }
               
              
                }
               
            }
              
            posted @ 2009-09-18 12:40 暗夜教父 閱讀(890) | 評(píng)論 (0)編輯 收藏

            轉(zhuǎn)自云風(fēng)BLOG http://blog.codingnow.com/2006/10/multi_process_design.html


            目前,我們的游戲服務(wù)器組是按多進(jìn)程的方式設(shè)計(jì)的。強(qiáng)調(diào)多進(jìn)程,是想提另外一點(diǎn),我們每個(gè)進(jìn)程上是單線程的。所以,我們?cè)谠O(shè)計(jì)中,系統(tǒng)的復(fù)雜點(diǎn)在于進(jìn)程間如何交換數(shù)據(jù);而不需要考慮線程間的數(shù)據(jù)鎖問(wèn)題。

            如果肆意的做進(jìn)程間通訊,在進(jìn)程數(shù)量不斷增加后,會(huì)使系統(tǒng)混亂不可控。經(jīng)過(guò)分析后,我決定做如下的限制:

            1. 如果一個(gè)進(jìn)程需要和多個(gè)服務(wù)器做雙向通訊,那么這個(gè)進(jìn)程不能處理復(fù)雜的邏輯,而只是過(guò)濾和轉(zhuǎn)發(fā)數(shù)據(jù)用。即,這樣的一個(gè)進(jìn)程 S ,只會(huì)把進(jìn)程 A 發(fā)過(guò)來(lái)的數(shù)據(jù)轉(zhuǎn)發(fā)到 B ;或把進(jìn)程 B 發(fā)過(guò)來(lái)的數(shù)據(jù)轉(zhuǎn)發(fā)到 A 。或者從一端發(fā)過(guò)來(lái)的數(shù)據(jù),經(jīng)過(guò)簡(jiǎn)單的協(xié)議分析后,可以分發(fā)到不同的地方。例如,把客戶端發(fā)過(guò)來(lái)的數(shù)據(jù)包中的聊天信息分離處理,交到聊天進(jìn)程處理。

            2. 有邏輯處理的進(jìn)程上的數(shù)據(jù)流一定是單向的,它可以從多個(gè)數(shù)據(jù)源讀取數(shù)據(jù),但是處理后一定反饋到另外的地方,而不需要和數(shù)據(jù)源做邏輯上的交互。

            3. 每個(gè)進(jìn)程盡可能的保持單個(gè)輸入點(diǎn),或是單個(gè)輸出點(diǎn)。

            4. 所有費(fèi)時(shí)的操作均發(fā)到獨(dú)立的進(jìn)程,以隊(duì)列方式處理。

            5. 按功能和場(chǎng)景劃分進(jìn)程,單一服務(wù)和單一場(chǎng)景中不再分離出多個(gè)進(jìn)程做負(fù)載均衡。

            性能問(wèn)題上,我是這樣考慮的:

            我們應(yīng)該充分利用多核的優(yōu)勢(shì),這會(huì)是日后的發(fā)展方向。讓每個(gè)進(jìn)程要么處理大流量小計(jì)算量的工作;要么處理小流量大計(jì)算量的工作。這樣多個(gè)進(jìn)程放在一臺(tái)物理機(jī)器上可以更加充分的利用機(jī)器的資源。

            單線程多進(jìn)程的設(shè)計(jì),個(gè)人認(rèn)為更能發(fā)揮多核的優(yōu)勢(shì)。這是因?yàn)闆](méi)有了鎖,每個(gè)線程都可以以最大吞吐量工作。增加的負(fù)擔(dān)只是進(jìn)程間的數(shù)據(jù)復(fù)制,在網(wǎng)游這種復(fù)雜邏輯的系統(tǒng)中,一般不會(huì)比邏輯計(jì)算更早成為瓶頸。如果擔(dān)心,單線程沒(méi)有利用多核計(jì)算的優(yōu)勢(shì),不妨考慮以下的例子:

            計(jì)算 a/b+c/d+e/f ,如果我們?cè)谝粋€(gè)進(jìn)程中開(kāi)三條線程利用三個(gè)核同時(shí)計(jì)算 a/b c/d e/f 固然不錯(cuò),但它增加了程序設(shè)計(jì)的復(fù)雜度。而換個(gè)思路,做成三個(gè)進(jìn)程,第一個(gè)只算 a/b 把結(jié)果交給第二個(gè)進(jìn)程去算 c/d 于之的和,再交個(gè)第三個(gè)進(jìn)程算 e/f 。對(duì)于單次運(yùn)算來(lái)算,雖然成本增加了。它需要做額外的進(jìn)程間通訊復(fù)制中間結(jié)果。但,如果我們有大量連續(xù)的這樣的計(jì)算要做,整體的吞吐量卻增加了。因?yàn)樵谒? 某次的 a/b 的時(shí)候,前一次的 c/d 可能在另一個(gè)核中并行計(jì)算著。

            具體的設(shè)計(jì)中,我們只需要把處理數(shù)據(jù)包的任務(wù)切細(xì),適當(dāng)增加處理流水線的長(zhǎng)度,就可以提高整個(gè)系統(tǒng)的吞吐量了。由于邏輯操作是單線程的,所以另需要注意的一點(diǎn)是,所有費(fèi)時(shí)的操作都應(yīng)該轉(zhuǎn)發(fā)到獨(dú)立的進(jìn)程中異步完成。比如下面會(huì)提到的數(shù)據(jù)存取服務(wù)。

            對(duì)于具體的場(chǎng)景管理是這樣做的:

            玩 家連接進(jìn)來(lái)后,所有數(shù)據(jù)包會(huì)經(jīng)過(guò)一個(gè)叫做位置服務(wù)的進(jìn)程中。這個(gè)進(jìn)程可以區(qū)分玩家所在的位置,然后把玩家數(shù)據(jù)分發(fā)到對(duì)應(yīng)的場(chǎng)景服務(wù)進(jìn)程中。這個(gè)位置服務(wù)同 時(shí)還管理玩家間消息的廣播。即,單個(gè)的場(chǎng)景(邏輯)服務(wù)并不關(guān)心每個(gè)數(shù)據(jù)包為哪幾個(gè)玩家所見(jiàn),而由這個(gè)服務(wù)將其復(fù)制分發(fā)。

            當(dāng)玩家切換場(chǎng)景,場(chǎng)景服務(wù)器將玩家的數(shù)據(jù)發(fā)送給數(shù)據(jù)服務(wù),數(shù)據(jù)服務(wù)進(jìn)程 cache 玩家數(shù)據(jù),并將數(shù)據(jù)寫(xiě)入數(shù)據(jù)庫(kù)。然后把玩家的新的場(chǎng)景編號(hào)發(fā)回位置服務(wù)進(jìn)程,這樣位置服務(wù)器可以將后續(xù)的玩家數(shù)據(jù)包正確的轉(zhuǎn)發(fā)到新的場(chǎng)景服務(wù)進(jìn)程中。

            掉落物品和資源生產(chǎn)同樣可以統(tǒng)一管理,所以的場(chǎng)景(邏輯)進(jìn)程都將生產(chǎn)新物件的請(qǐng)求發(fā)給物品分配服務(wù),由物品分配服務(wù)生產(chǎn)出新物件后通知位置服務(wù)器產(chǎn)生新物品。

            這樣一系列的做法,最終保證了,每個(gè)場(chǎng)景服務(wù)器都有一個(gè)唯一的數(shù)據(jù)源——位置服務(wù)進(jìn)程。它跟持久化在數(shù)據(jù)庫(kù)中的數(shù)據(jù)無(wú)關(guān),跟時(shí)鐘也無(wú)關(guān)。由此帶來(lái)的調(diào)試便利是很顯著的。

            最近,面臨諸多進(jìn)程的設(shè)計(jì)時(shí),最先面臨的一個(gè)復(fù)雜點(diǎn)在于啟動(dòng)階段。顯然,每個(gè)進(jìn)程都配有一套配置文件指出其它進(jìn)程的地址并不是一個(gè)好主意。而為每個(gè) 服務(wù)都分配一個(gè)子域名在開(kāi)發(fā)期也不太合適。結(jié)果我們采取了一個(gè)簡(jiǎn)單的方案:?jiǎn)为?dú)開(kāi)發(fā)了一個(gè)名字服務(wù)器。它的功能類似 DNS ,但是可以讓每個(gè)進(jìn)程自由的注冊(cè)自己的位置,還可以定期匯報(bào)自己的當(dāng)前狀態(tài)。這樣,我們可以方便的用程序查詢到需要的服務(wù)。名字服務(wù)器的協(xié)議用的類似 POP3 的文本協(xié)議,這讓我們可以人手工 telnet 上去查閱。我相信以后我們的維護(hù)人員會(huì)喜歡這樣的設(shè)計(jì)的。:D

            以上,國(guó)慶假期結(jié)束以來(lái)的工作。感謝項(xiàng)目組其他同事的辛勤編碼。



            posted @ 2009-09-14 13:29 暗夜教父 閱讀(537) | 評(píng)論 (0)編輯 收藏
              MMORPG不同于其它的局域網(wǎng)的網(wǎng)絡(luò)游戲,它是一個(gè)面向整個(gè)Internet的連接人數(shù)過(guò)萬(wàn)的網(wǎng)絡(luò)游戲,因此他的服務(wù)器端設(shè)計(jì)則極為重要
              
              服務(wù)器的基本設(shè)置
              
              在大型網(wǎng)絡(luò)游戲里,通常設(shè)計(jì)為C/S結(jié)構(gòu),客戶端不再對(duì)數(shù)據(jù)進(jìn)行邏輯處理,而只是一個(gè)收發(fā)裝置,從玩家那里接受到操作信息,然后反饋給服務(wù)器,再由服務(wù)器進(jìn)行處理后發(fā)回客戶端,經(jīng)客戶端通過(guò)圖形化處理,給玩家呈現(xiàn)出一個(gè)繽紛的游戲世界。
               
              登陸服務(wù)器
              
              在這里也可以稱之為連接服務(wù)器,網(wǎng)絡(luò)游戲的客戶端一般是連接到這里,然后再由該連接服務(wù)器根據(jù)不同的需要,把游戲消息轉(zhuǎn)發(fā)給其它相應(yīng)的服務(wù)器(邏輯和地圖服務(wù)器)也因?yàn)樗强蛻舳酥苯舆B接的對(duì)象,它同時(shí)也負(fù)擔(dān)了驗(yàn)證客戶身份的工作。
              
              地圖服務(wù)器
              
              在這里也可以稱之為連續(xù)事件服務(wù)器。在這個(gè)服務(wù)器里要處理的對(duì)象(玩家)所做的動(dòng)作都是一個(gè)連續(xù)事件。例如玩家從A點(diǎn)移動(dòng)到B點(diǎn),這樣一個(gè)動(dòng)作,需要一定的時(shí)間進(jìn)行移動(dòng),因此說(shuō)移動(dòng)是一個(gè)連續(xù)事件。
              
              邏輯服務(wù)器
              
            在這里可以稱之為瞬時(shí)事件服務(wù)器,在這個(gè)服務(wù)器里,處理對(duì)象(玩家)所做的動(dòng)作均可以在非常斷時(shí)間內(nèi)完成完成。例如玩家從商店購(gòu)買一瓶藥書(shū),當(dāng)玩家確認(rèn) 購(gòu)買后,服務(wù)器先扣除玩家的游戲幣,然后再把相應(yīng)的藥水瓶加入玩家的背包里。這2個(gè)操作對(duì)于服務(wù)器來(lái)說(shuō),只是2個(gè)數(shù)字的加減,計(jì)算完這兩個(gè)數(shù)字的加減,這 個(gè)事件就可以結(jié)束了。因此,我們可以說(shuō)這個(gè)事件是一個(gè)瞬時(shí)事件
              
              服務(wù)器組的改進(jìn)
              
              不過(guò)在實(shí)際應(yīng)用的過(guò)程中,游戲服務(wù)器的結(jié)構(gòu)要比上面所說(shuō)的3種服務(wù)結(jié)構(gòu)要復(fù)雜些,不過(guò)也都是在這3種最基本的服務(wù)器架構(gòu)下進(jìn)行擴(kuò)充,擴(kuò)充的主要是其它輔助功能。在實(shí)際應(yīng)用里可能增加的2種服務(wù)器,數(shù)據(jù)庫(kù)服務(wù)器,計(jì)費(fèi)服務(wù)器,由邏輯服務(wù)器獨(dú)立出來(lái)的聊天服務(wù)器。
               
              數(shù)據(jù)庫(kù)服務(wù)器
              
              數(shù)據(jù)庫(kù)服務(wù)器其實(shí)就是專門(mén)利用一臺(tái)服務(wù)器進(jìn)行數(shù)據(jù)庫(kù)的讀寫(xiě)操作。這點(diǎn)特別是在大型的網(wǎng)絡(luò)游戲里尤為重要。因?yàn)樵诖笮途W(wǎng)絡(luò)游戲里,要處理玩家的數(shù)據(jù)量非常大,如果不利用專門(mén)的服務(wù)器進(jìn)行處理,很有可能會(huì)拖累這個(gè)服務(wù)器組。
              
              計(jì)費(fèi)服務(wù)器
              
              通常在商業(yè)的網(wǎng)絡(luò)游戲里出現(xiàn),用于記錄玩家在線的時(shí)間,給收費(fèi)提供依據(jù),同時(shí)也是整個(gè)服務(wù)器組里最重要的部分,一旦出現(xiàn)問(wèn)題,運(yùn)營(yíng)商就不用賺錢了。
              
              聊天服務(wù)器
              
              在游戲里的聊天功能是屬于一種瞬時(shí)動(dòng)作,理論上是放在邏輯服務(wù)器里進(jìn)行處理。不過(guò)在大型網(wǎng)絡(luò)游戲里,因?yàn)檫@個(gè)部分功能與游戲里的其它部分聯(lián)系并不緊密,因此可以獨(dú)立出來(lái)做一個(gè)功能服務(wù)器。
              
              服務(wù)器的集群設(shè)置
              
              在大型游戲的應(yīng)用過(guò)程中,實(shí)際需要處理的玩家數(shù)量可能過(guò)萬(wàn),一臺(tái)普通的服務(wù)器是無(wú)法完成所要完成的工作,因此,在實(shí)際應(yīng)用的時(shí)候,通常是由一組多臺(tái)服務(wù)器共同完成一個(gè)功能。
              例如地圖服務(wù)器,可以根據(jù)需要,把游戲里所有的地域進(jìn)行劃分,劃分為N個(gè)區(qū)域,然后讓這一個(gè)區(qū)域里發(fā)生的事件都用一個(gè)特定的服務(wù)器進(jìn)行處理。這樣做的目的是減少一個(gè)服務(wù)器所承擔(dān)的計(jì)算量,把整個(gè)系統(tǒng)組成一個(gè)分布式的網(wǎng)絡(luò)。
            不過(guò)這樣做的同時(shí)會(huì)造成一個(gè)麻煩:當(dāng)一位玩家從區(qū)域1,移動(dòng)到區(qū)域2。這個(gè)時(shí)候,就必須先在服務(wù)器1里把玩家刪除,然后再在區(qū)域2里加入玩家。同時(shí)需要 由服務(wù)器1向服務(wù)器2轉(zhuǎn)移玩家的數(shù)據(jù)信息(因?yàn)榉?wù)器組在工作的時(shí)候,玩家的信息只能保存在當(dāng)前所在區(qū)域的服務(wù)器里),也就是說(shuō)一旦玩家發(fā)生服務(wù)器間區(qū)域 移動(dòng),服務(wù)器端就不可避免的造成數(shù)據(jù)通訊。因?yàn)檫@種移動(dòng)并不是有規(guī)律的,玩家所在的服務(wù)器都有可能到達(dá)其它服務(wù)器。這樣,如果服務(wù)器組里有N臺(tái)地圖服務(wù) 器,那么,每個(gè)服務(wù)器都可能向其它N-1臺(tái)服務(wù)器產(chǎn)生連接,總共就可能產(chǎn)生N×N個(gè)連接。如此數(shù)量連接如果只是使用普通的socket設(shè)計(jì),就很有可能會(huì) 給服務(wù)器通訊間的各種問(wèn)題所困擾,為此,在商業(yè)網(wǎng)絡(luò)游戲的服務(wù)器之間,通常都使用成熟的第三方的通訊中間件,如ACE,ICE等作為網(wǎng)絡(luò)連接的傳輸層。
              
            posted @ 2009-09-14 13:29 暗夜教父 閱讀(1602) | 評(píng)論 (1)編輯 收藏
            作者博客:
            http://blog.csdn.net/yahle
            大綱:
            項(xiàng)目的歷史背景
            服務(wù)器的設(shè)計(jì)思路
            服務(wù)器的技術(shù)
            服務(wù)器的設(shè)計(jì)
            服務(wù)器的改進(jìn)
            圖形引擎myhoho及UI庫(kù)的設(shè)計(jì)

            客戶端與服務(wù)器的集成


            網(wǎng) 絡(luò)游戲一般采用C\S模式,網(wǎng)絡(luò)游戲的設(shè)計(jì)重點(diǎn),我認(rèn)為在于Server端,也就是我們說(shuō)的服務(wù)器。在服務(wù)器端的設(shè)計(jì),我把服務(wù)器按照功能分為2個(gè)部分, 一個(gè)負(fù)責(zé)游戲世界的處理,一個(gè)服務(wù)器服務(wù)器與客戶端的通訊。在負(fù)責(zé)游戲世界的處理的服務(wù)器,我又按照功能分為地圖服務(wù)器和邏輯服務(wù)器。這樣劃分的依據(jù)是他 們處理的內(nèi)容不同進(jìn)行。當(dāng)初的設(shè)計(jì)還考慮到系統(tǒng)的集群功能,可以把游戲的地圖移動(dòng)處理和游戲的邏輯處理都分別分?jǐn)偟狡渌?wù)器里面去。但是做到最后,發(fā)現(xiàn) 這樣的設(shè)計(jì)也不是太好,主要是因?yàn)樵谔幚硪恍┯螒蚴录臅r(shí)候需要兩個(gè)服務(wù)器之間進(jìn)行協(xié)同,這樣勢(shì)必要?jiǎng)?chuàng)建一定的網(wǎng)絡(luò)游戲消息,在開(kāi)始制作游戲的時(shí)候,因?yàn)? 需要系統(tǒng)的東西不是很多,所以沒(méi)有太注意,到項(xiàng)目的后期,想增加一個(gè)功能的時(shí)候,就發(fā)現(xiàn)在處理船只沉沒(méi)的時(shí)候,服務(wù)器需要傳遞很多同步數(shù)據(jù),而且服務(wù)器各 自在設(shè)置玩家數(shù)據(jù)的時(shí)候,也有很多重復(fù)的地方。如果今后還要再加點(diǎn)什么其它功能,那要同步的地方就實(shí)在是太多了,所以按照功能把服務(wù)器分為2個(gè)部分的設(shè)計(jì) 還是存在缺陷的,如果讓我重新再來(lái),我會(huì)選擇單服務(wù)器的設(shè)計(jì),當(dāng)然這個(gè)服務(wù)器還是要和連接服務(wù)器進(jìn)行分離,因?yàn)橛螒虻倪壿嬏幚砗团c玩家的通訊還是很好分開(kāi) 的,而且分開(kāi)的話,也有利于邏輯服務(wù)器的設(shè)計(jì)。







            登陸(連接)服務(wù)器的設(shè)計(jì):



               在網(wǎng)絡(luò)游戲里,其中一個(gè)很大的難點(diǎn)就是玩家與服務(wù)器的通訊,在Windos的服務(wù)器架構(gòu)下,網(wǎng)絡(luò)游戲服務(wù)器端采用的I/O模型,通常是完成端口。在項(xiàng)目 開(kāi)始時(shí)研究完成端口,感覺(jué)很難,根本看不懂,因?yàn)樗诤芏嗟胤脚c以前寫(xiě)網(wǎng)絡(luò)通訊軟件時(shí)用的方法不同。但是當(dāng)我分析過(guò)3個(gè)完成端口的程序后,基本了解的它的 使用方法。而且在懂以后,回過(guò)頭來(lái)看,其它完成端口的概念也不是很復(fù)雜,只要能清楚的了解幾個(gè)函數(shù)的使用方法以及基本的處理框架流程,你就會(huì)發(fā)現(xiàn)它其實(shí)非 常的簡(jiǎn)單。



               完成端口的一些需要理解的地方:



            1。消息隊(duì)列



            2。工作線程



            3。網(wǎng)絡(luò)消息返回結(jié)構(gòu)體







               一般我們?cè)谠O(shè)計(jì)服務(wù)器端的時(shí)候,最關(guān)鍵的地方是如何分辯剛剛收到的網(wǎng)絡(luò)數(shù)據(jù)是由那個(gè)玩家發(fā)送過(guò)來(lái)的,如果是采用消息事件驅(qū)動(dòng)的話,是可以得到一個(gè) socket的值,然后再用這個(gè)值與系統(tǒng)里存在的socket進(jìn)行比對(duì),這樣就可以得到是那位玩家發(fā)送過(guò)來(lái)的游戲消息。我在還沒(méi)有使用完成端口的時(shí)候,就 是使用這個(gè)方法。這樣的設(shè)計(jì)有一個(gè)缺點(diǎn)就是每次收到數(shù)據(jù)的時(shí)候回浪費(fèi)很多時(shí)間在于確定消息發(fā)送者身份上。但是在完成端口的設(shè)計(jì)里,我們可以采用一個(gè)取巧的 方法進(jìn)行設(shè)計(jì)。所以,這個(gè)問(wèn)題很輕易的就結(jié)局了,而且系統(tǒng)開(kāi)銷也不是很大,關(guān)于完成端口,可以參考一下的文章:



            《關(guān)于Winsock異步I/O模型中的事件模型》



            http://search.csdn.net/Expert/topic/166/166227.xml?temp=.4639093



            《手把手教你玩轉(zhuǎn)SOCKET模型之重疊I/O篇》



            http://blog.csdn.net/piggyxp/archive/2004/09/23/114883.aspx



            《學(xué)習(xí)日記](méi)IOCP的學(xué)習(xí)--初步理解》



            http://www.gameres.com/bbs/showthread.asp?threadid=25898



            《用完成端口開(kāi)發(fā)大響應(yīng)規(guī)模的Winsock應(yīng)用程序》



            http://www.xiaozhou.net/ReadNews.asp?NewsID=901



            《理解I/O Completion Port》



            http://dev.gameres.com/Program/Control/IOCP.htm



            幾個(gè)關(guān)鍵函數(shù)的說(shuō)明:



            http://msdn.microsoft.com/library/en-us/fileio/fs/postqueuedcompletionstatus.asp?frame=true



            http://msdn.microsoft.com/library/en-us/fileio/fs/createiocompletionport.asp?frame=true



            http://msdn.microsoft.com/library/en-us/fileio/fs/getqueuedcompletionstatus.asp?frame=true



            http://msdn.microsoft.com/library/en-us/winsock/winsock/wsarecv_2.asp?frame=true







            如果你能認(rèn)真的搞清楚上面的東西,我估計(jì)你離理解完成端口就只有一步了。剩下的這一步就是自己編碼實(shí)現(xiàn)一個(gè)下了。有些時(shí)候,看得懂了不一定會(huì)實(shí)際應(yīng)用,不實(shí)實(shí)在在的寫(xiě)一點(diǎn)程序,驗(yàn)證一下你的想法,是不會(huì)真正搞清楚原理的。







            不 過(guò)除非你想深入的研究網(wǎng)絡(luò)技術(shù),否則只要知道怎么用就可以了,剩下的就是尋找一個(gè)合適的別人封裝好的類來(lái)使用。這樣可以節(jié)省你很多的事件,當(dāng)然拿來(lái)的東西 最好有源代碼,這樣如果發(fā)生什么問(wèn)題,你也好確定是在那個(gè)地方出錯(cuò),要改或者擴(kuò)充功能都會(huì)方便很多。當(dāng)然,還要注意人家的版權(quán),最好在引用別人代碼的地方 加一些小小的注解,這樣用不了多少時(shí)間,而且對(duì)你,對(duì)原作者都有好處^_^。







            不過(guò)在 完成端口上我還是沒(méi)有成為拿來(lái)主義者,還是自己封裝了完成端口的操作,原因找到的源代碼代碼封裝的接口函數(shù)我怎么看怎么覺(jué)得別扭,所以最后還是自己封裝了 一個(gè)完成端口,有興趣的可以去看我的源代碼,里面有很詳細(xì)的注解。而且就我看來(lái),要拿我封裝的完成端口類使用起來(lái)還是很簡(jiǎn)單的。使用的時(shí)候,只要繼承我的 CIOCP,然后,根據(jù)需要覆蓋3個(gè)虛函數(shù)(OnAccept,OnRead,OnClose)就可以了,最多是在連接函數(shù)里,需要用一個(gè)函數(shù)去設(shè)置一下 完成端口信息。當(dāng)然,我封裝的類稍微簡(jiǎn)單了一些,如果要拿來(lái)響應(yīng)大規(guī)模連接,還是存在很多的問(wèn)題,但是如果只是針對(duì)少量連接,還是可以應(yīng)付的。







            對(duì) 于客戶端的I/O模型,我就沒(méi)有那么用心的去尋找什么好的解決方案,采用了一個(gè)最簡(jiǎn)單的,最原始的阻塞線程的方法做。原理很簡(jiǎn)單:創(chuàng)建一個(gè)sockt,把 socket設(shè)置為阻塞,連接服務(wù)器成功后,啟動(dòng)一個(gè)線程,在線程里面用recv()等待服務(wù)器發(fā)過(guò)來(lái)的消息。在我的代碼里,也是把阻塞線程的方法封裝成 一個(gè)類,在使用的時(shí)候,先繼承TClientSocket,然后覆蓋(重載)里面的OnRead()函數(shù),并在里面寫(xiě)入一些處理收到數(shù)據(jù)后的操作代碼。在 用的時(shí)候,只要connect成功,系統(tǒng)就會(huì)自動(dòng)啟動(dòng)一個(gè)接收線程,一旦有數(shù)據(jù)就觸發(fā)剛才覆蓋的OnRead函數(shù)。這個(gè)類我也不是完全直接寫(xiě)的,在里面使 用了別人的一些代碼,主要是讓每個(gè)類都能把線程封裝起來(lái),這樣在創(chuàng)建不同的類的實(shí)體的時(shí)候,每個(gè)類的實(shí)體自己都會(huì)有一個(gè)單獨(dú)的數(shù)據(jù)接收線程。



            當(dāng) 然除了阻塞線程的方法,比較常用的還有就是用消息事件的方法收取數(shù)據(jù)了。我剛開(kāi)始的時(shí)候,也是采用這個(gè)方法(以前用過(guò)^_^),但是后來(lái)發(fā)現(xiàn)不太好封裝, 最后采用阻塞線程的方法,這樣做還有一個(gè)好處可以讓我的代碼看起來(lái)更加舒服一些。不過(guò)就我分析《航海世紀(jì)》客戶端采用的是消息事件的I/O模型。其它的網(wǎng) 絡(luò)游戲就不太清楚了,我想也應(yīng)該是采用消息事件方式的吧。。



               我記得在gameres上看到過(guò)某人寫(xiě)的一篇關(guān)于完成端口的筆記,他在篇末結(jié)束的時(shí)候,提出一個(gè)思考題:我們?cè)趯W(xué)習(xí)完成端口的時(shí)候,都知道它是用于server端的操作,而且很多文章也是這樣寫(xiě)的,但是不知道有沒(méi)有考慮過(guò),用完成端口做客戶端來(lái)使用?



               其實(shí)這個(gè)問(wèn)題很好回答,答案是OK。拿IOCP做客戶端也是可行的,就以封裝的IOCP為例,只要在繼承原來(lái)的CIOCP類的基礎(chǔ)上,再寫(xiě)一個(gè)Connect(char * ip, int port)的函數(shù),就可以實(shí)現(xiàn)客戶端的要求了。
            1. bool CIOCPClient::Connect(char *ip, int port)  
            2. {  
            3.         //  連接服務(wù)器  
            4.   
            5.     if (!bInit)  
            6.   
            7.         if (!Init())  
            8.   
            9.             return false;  
            10.   
            11.     //  初始化連接socket  
            12.     SOCKET m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  
            13.   
            14.     if (m_socket == SOCKET_ERROR)  
            15.         return false;  
            16.   
            17.     // 填寫(xiě)服務(wù)器地址信息  
            18.   
            19.     sockaddr_in ClientAddr;  
            20.   
            21.     ClientAddr.sin_family = AF_INET;  
            22.   
            23.     ClientAddr.sin_port = htons(port);      
            24.   
            25.     ClientAddr.sin_addr.s_addr = inet_addr(ip);  
            26.     // 綁定監(jiān)聽(tīng)端口  
            27.     bind(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr));  
            28.   
            29.     if (connect(m_socket, (SOCKADDR *)&ClientAddr, sizeof(ClientAddr)) == SOCKET_ERROR)  
            30.   
            31.         return false;  
            32.     this->m_workThread = true;  
            33.   
            34.   
            35.   
            36.     g_hwThread = CreateThread(NULL, 0, WorkThread, (LPVOID)this, 0, &m_wthreadID);  //  創(chuàng)建工作線程,用來(lái)處理完成端口消息的  
            37.     this->SetIoCompletionPort(m_socket, NULL);  //  設(shè)置完成端口監(jiān)聽(tīng)的socket  
            38.     return true;  
            39.   
            40. }  

            前面一段是用來(lái)連接服務(wù)器,所有的客戶端程序都是要這樣做的,當(dāng)連接成功后,m_socket就是我們想要的用于與服務(wù)器 端通訊的socket,然后,我們啟動(dòng)工作線程,并使用SetIoCompletionPort來(lái)設(shè)置完成端口監(jiān)聽(tīng)的socket。只要在原來(lái)的基礎(chǔ)上增 加一個(gè)函數(shù),就可以把用于服務(wù)器的ICOP變成用于客戶端的IOCP。



               在收到網(wǎng)絡(luò)數(shù)據(jù)以后,下一步就是根據(jù)需要,把收到的網(wǎng)絡(luò)數(shù)據(jù)包轉(zhuǎn)變?yōu)橛螒蛳?shù)據(jù)包。在轉(zhuǎn)換之前,首先是要從收到的網(wǎng)絡(luò)數(shù)據(jù)里面提取出有效的消息。這里 為什么說(shuō)是要提取有效部分?其主要原因是,我們創(chuàng)建的游戲消息數(shù)據(jù),在進(jìn)行網(wǎng)絡(luò)傳輸?shù)臅r(shí)候,不是以消息的長(zhǎng)度來(lái)傳的,而是根據(jù)系統(tǒng)在接收到發(fā)送數(shù)據(jù)請(qǐng)求的 時(shí)候,根據(jù)實(shí)際情況來(lái)發(fā)送的。例如我這里有一條很長(zhǎng)的游戲消息,有3k,但是系統(tǒng)一次只能發(fā)送1k的數(shù)據(jù),所以,我們的游戲消息,只能把我們的游戲消息分 為3個(gè)包,分3次發(fā)送,這樣在我們接收消息的時(shí)候,就會(huì)觸發(fā)3次OnRead,而這3次OnRead收到的數(shù)據(jù)都不是一次完整的游戲消息。所以,我們?cè)谑? 到網(wǎng)絡(luò)數(shù)據(jù)后,要先和上一次收到的網(wǎng)絡(luò)數(shù)據(jù)進(jìn)行合并,然后再在里面提取出有效的游戲消息,并在提取后,把已經(jīng)提取的部分刪除。我在這里把這一步操作封裝到 一個(gè)類里CBuftoMsg。這里順便說(shuō)明一下:一條游戲消息的網(wǎng)絡(luò)數(shù)據(jù)包是以0x00EEEE(16進(jìn)制)為結(jié)束標(biāo)記(《航海世紀(jì)》的做法)。
            1. struct TMessage  
            2.   
            3. {  
            4.   
            5.     char * p;       //  消息頭所在的位置  
            6.   
            7.   
            8.   
            9.     long len;       //  整個(gè)消息的長(zhǎng)度  
            10.   
            11.   
            12.   
            13. };  
            14.   
            15.   
            16.   
            17.   
            18.    
            19.   
            20.   
            21. class CBuftoMsg  
            22.   
            23.   
            24.   
            25. {  
            26.   
            27.   
            28.   
            29. protected:  
            30.   
            31.   
            32.   
            33.     char msgbuf[BUF_LEN];     
            34.   
            35.   
            36.   
            37.     char * buf_end;  
            38.   
            39.   
            40.   
            41.     char * buf_begin;  
            42.   
            43.   
            44.   
            45.     int buf_len;  
            46.   
            47.   
            48.   
            49. public:  
            50.   
            51.   
            52.   
            53.     CBuftoMsg(void);  
            54.   
            55.   
            56.   
            57.     TMessage getMessage(void);  
            58.   
            59.   
            60.   
            61.     void cleanup_buf(void);  
            62.   
            63.   
            64.   
            65.     bool AddMsgBuf(const char *, int);  
            66.   
            67.   
            68.   
            69.     int tag;  
            70.   
            71.   
            72.   
            73. };  
            74.   
            75.   
            76.   
            77.   
            78.    
            79.   
            80.   
            81. CBuftoMsg::CBuftoMsg(void)  
            82.   
            83.   
            84.   
            85. {  
            86.   
            87.   
            88.   
            89.     buf_begin = msgbuf;  
            90.   
            91.   
            92.   
            93.     buf_end = msgbuf;  
            94.   
            95.   
            96.   
            97.     buf_len = 0;  
            98.   
            99.   
            100.   
            101. }  
            102.   
            103.   
            104.   
            105.   
            106.    
            107.   
            108.   
            109. TMessage CBuftoMsg::getMessage()  
            110.   
            111.   
            112.   
            113. {  
            114.   
            115.   
            116.   
            117.     char * p    = buf_begin;  
            118.   
            119.   
            120.   
            121.     TMessage result;  
            122.   
            123.   
            124.   
            125.     result.len  = 0;  
            126.   
            127.   
            128.   
            129.     result.p    = NULL;  
            130.   
            131.   
            132.   
            133.     while(p <= buf_begin + buf_len - 2)  
            134.   
            135.   
            136.   
            137.     {  
            138.   
            139.   
            140.   
            141.         if ( *p == 0x00)  
            142.   
            143.   
            144.   
            145.         {  
            146.   
            147.   
            148.   
            149.             const static char ce = 0xEE;  
            150.   
            151.   
            152.   
            153.             if (*(p + 1) == ce)  
            154.   
            155.   
            156.   
            157.                 if(*(p + 2) == ce)  
            158.   
            159.   
            160.   
            161.                 {  
            162.   
            163.   
            164.   
            165.                     //  每條消息都是以 00 EE EE 為結(jié)束標(biāo)志  
            166.   
            167.   
            168.   
            169.                     result.p    = buf_begin;  
            170.   
            171.   
            172.   
            173.                     result.len  = p - buf_begin + 3;  
            174.   
            175.   
            176.   
            177.                     buf_begin   =  p + 3;  
            178.   
            179.   
            180.   
            181.                     buf_end     = buf_begin + buf_len;  
            182.   
            183.   
            184.   
            185.                     buf_len -= result.len;  
            186.   
            187.   
            188.   
            189.                     break;  
            190.   
            191.   
            192.   
            193.                 }  
            194.   
            195.   
            196.   
            197.         }  
            198.   
            199.   
            200.   
            201.         p++;  
            202.   
            203.   
            204.   
            205.     }  
            206.   
            207.   
            208.   
            209.     return result;  
            210.   
            211.   
            212.   
            213. }  
            214.   
            215.   
            216.   
            217.   
            218.    
            219.   
            220.   
            221. void CBuftoMsg::cleanup_buf()  
            222.   
            223.   
            224.   
            225. {  
            226.   
            227.   
            228.   
            229.     if (buf_len < BUF_LEN)  
            230.   
            231.   
            232.   
            233.     {  
            234.   
            235.   
            236.   
            237.         if (buf_len == 0)  
            238.   
            239.   
            240.   
            241.         {  
            242.   
            243.   
            244.   
            245.             buf_begin   = msgbuf;  
            246.   
            247.   
            248.   
            249.             buf_end     = msgbuf;  
            250.   
            251.   
            252.   
            253.         }  
            254.   
            255.   
            256.   
            257.         else  
            258.   
            259.   
            260.   
            261.         {  
            262.   
            263.   
            264.   
            265.             memmove(msgbuf, buf_end - buf_len, buf_len);  
            266.   
            267.   
            268.   
            269.             buf_begin = msgbuf;  
            270.   
            271.   
            272.   
            273.             buf_end = buf_end - buf_len;  
            274.   
            275.         }  
            276.   
            277.   
            278.     }  
            279.   
            280.     else  
            281.   
            282.     {  
            283.   
            284.         //  加入緩沖區(qū)的數(shù)據(jù)過(guò)多,要拋棄原來(lái)的內(nèi)容  
            285.   
            286.         buf_begin   = msgbuf;  
            287.   
            288.         buf_end     = msgbuf;  
            289.   
            290.         buf_len     = 0;  
            291.   
            292.     }  
            293.   
            294. }  
            295.   
            296. bool CBuftoMsg::AddMsgBuf(const char * buf, int len)  
            297. {  
            298.   
            299.     if (len < 1)  
            300.   
            301.         return false;  
            302.   
            303.     bool result = true;  
            304.   
            305.     buf_len += len;  
            306.   
            307.     if (buf_len >= BUF_LEN)     //  如果緩沖區(qū)裝滿了則直接把原來(lái)的緩沖區(qū)清空再重新復(fù)制數(shù)據(jù)  
            308.     {  
            309.         this->cleanup_buf();      
            310.         result = false;  
            311.   
            312.     }  
            313.   
            314.     memcpy(buf_begin, buf, len);  
            315.   
            316.     return result;  
            317.   
            318. }  


            我在這里把 CBuftoMsg 的代碼貼出來(lái),主要是因?yàn)椋以趯?xiě)本文的時(shí)候,發(fā)現(xiàn)一個(gè)驚天動(dòng)地的bug,有興趣的讀者可以自己去找一下。不過(guò)一開(kāi)始寫(xiě)代碼的時(shí)候,還不是這樣的,當(dāng)初的 代碼bug比這個(gè)還要多,問(wèn)題還要嚴(yán)重,嚴(yán)重到經(jīng)常讓服務(wù)器程序莫名其妙的崩潰,而且這個(gè)問(wèn)題,一直到5月份,系統(tǒng)在進(jìn)行集成測(cè)試的時(shí)候才發(fā)現(xiàn)并解決(還 沒(méi)有徹底解決,至少目前我還發(fā)現(xiàn)了bug,),以前一直都沒(méi)有怎么注意到這個(gè)問(wèn)題,而且我們還把因?yàn)檫@個(gè)bug造成的問(wèn)題,歸結(jié)到線程的互斥上去^_^!







            我的登陸服務(wù)器,除了基本的處理網(wǎng)絡(luò)數(shù)據(jù)包以外,還負(fù)責(zé)玩家系統(tǒng)的登陸驗(yàn)證,這部分東西不是很復(fù)雜,在我的程序里,只是簡(jiǎn)單的從ini文件里讀取玩家的信息而已,有興趣的自己去看我的代碼(不過(guò)這部分遠(yuǎn)還沒(méi)有真正的完善,存在很多問(wèn)題)。







            除 了登陸驗(yàn)證以外,在登陸程序還負(fù)責(zé)進(jìn)行消息轉(zhuǎn)發(fā),就是把客戶端的消息分別發(fā)送到不同的服務(wù)器。如果當(dāng)初設(shè)計(jì)的是一個(gè)邏輯服務(wù)器,這個(gè)功能就可以簡(jiǎn)單很多, 只要發(fā)送到一個(gè)服務(wù)器里就可以了。現(xiàn)在的要發(fā)到2個(gè)服務(wù)器,所以還需要對(duì)收到的游戲消息進(jìn)行分類。為了方便,我對(duì)原來(lái)定義消息的ID進(jìn)行了分類,所以,在 GameMessageID.h文件里定義的游戲消息對(duì)應(yīng)的ID編號(hào)不是順序編排的。不過(guò)也因?yàn)檫@樣,在現(xiàn)在看來(lái),這樣的設(shè)計(jì),有一些不太好。在整個(gè)系統(tǒng) 里,存在有4個(gè)主體,他們之間互相發(fā)送,就用了12組的數(shù)據(jù),為了方便計(jì)算,我把一個(gè)變量的范圍分為16個(gè)不同的區(qū)域,這樣每個(gè)區(qū)域只有16個(gè)值可以用 (我這里是用char類型256/16=16)。在加上用另外一個(gè)變量表示邏輯上上的分類(目前按照功能分了12組,有登陸、貿(mào)易、銀行、船廠等)這樣對(duì) 于貿(mào)易這個(gè)類型的游戲消息,從客戶端發(fā)送到邏輯服務(wù)器上,只能有16中可能性,如果要發(fā)送更多消息,可能要增加另外一個(gè)邏輯分類:貿(mào)易2^_^!當(dāng)初這樣 的設(shè)計(jì)只是想簡(jiǎn)化一下系統(tǒng)的處理過(guò)程,不過(guò)卻造成了系統(tǒng)的擴(kuò)充困難,要解決也不是沒(méi)有辦法,把類型分類的變量由char類型,改為int類型,這樣對(duì)一個(gè) 變量分區(qū),在范圍上會(huì)款很多,而且不會(huì)造成邏輯分類上的困擾,但是,這樣存在一個(gè)弊端就是就是每條網(wǎng)絡(luò)消息數(shù)據(jù)包的長(zhǎng)度增加了一點(diǎn)點(diǎn)。不要小看這一個(gè)字節(jié) 的變量,現(xiàn)在設(shè)計(jì)的一條游戲消息頭的長(zhǎng)度是10個(gè)字節(jié),如果把char改為int,無(wú)形中就增加了3個(gè)字節(jié),在和原來(lái)的比較,這樣每條消息在消息頭部分, 就多出23%,也就是我們100M的網(wǎng)絡(luò)現(xiàn)在只能利用77%而已。



               ^_^呵呵看出什么問(wèn)題沒(méi)有?



               沒(méi)有,那我告訴你,有一個(gè)概念被偷換了,消息頭的數(shù)據(jù)不等于整條游戲的消息數(shù)據(jù),所以,消息頭部分雖然多出了23%,但是整條游戲消息并不會(huì)增加這么 多,最多增加17%,最少應(yīng)該不會(huì)操作5%。平均起來(lái),應(yīng)該在10%左右(游戲消息里,很多消息的實(shí)際部分可能就一個(gè)int變量而已)。不過(guò),就算是 10%,也占用了帶寬。



               ^_^呵呵還看出什么問(wèn)題沒(méi)有?



               ^_^先去讀一下我的代碼,再回頭看看,上面的論述還有什么問(wèn)題。



               實(shí)際上,每條游戲消息由:消息頭、消息實(shí)體、結(jié)束標(biāo)記組成,其中固定的是消息頭和結(jié)束標(biāo)記,所以,實(shí)際上一條實(shí)際上游戲消息的數(shù)據(jù)包,最多比原來(lái)的多15%,平均起來(lái),應(yīng)該是8%~10%的增量而異。



               好了,不在這個(gè)計(jì)算細(xì)節(jié)上扣太多精力了。要解決這個(gè)問(wèn)題,要么是增加網(wǎng)絡(luò)數(shù)據(jù)的發(fā)送量,要么,就是調(diào)整游戲結(jié)構(gòu),例如,把兩個(gè)功能服務(wù)器合并為一個(gè)服務(wù) 器,這樣服務(wù)器的對(duì)象實(shí)體就由原來(lái)的4個(gè)分為3個(gè),兩兩間的通訊,就由原來(lái)的12路縮減為6路,只要分8個(gè)區(qū)域就ok了。這樣每個(gè)邏輯分類就有32條游戲 消息可以使用。當(dāng)然,如果進(jìn)一步合并服務(wù)器,把服務(wù)器端都合并到一個(gè)程序,那就不用分類了^_^!

               在登陸服務(wù)器目錄下,還有一組mynet.h/mynet.cpp的文件,是我當(dāng)初為服務(wù)器端設(shè)計(jì)的函數(shù),封裝的是消息事件網(wǎng)絡(luò)響應(yīng)模型。只不過(guò)封裝得 不是怎么好,被拋棄不用了,有興趣的可以去看看,反正我是不推薦看的。只不過(guò)是在這里說(shuō)明一下整個(gè)工程目錄的結(jié)構(gòu)而已。
            posted @ 2009-09-14 13:26 暗夜教父 閱讀(371) | 評(píng)論 (0)編輯 收藏
            轉(zhuǎn)自http://coderplay.javaeye.com/blog/94209

            前些天給echo_server寫(xiě)了個(gè)非常簡(jiǎn)單的連接壓力測(cè)試程序,

               1.  -module(stress_test).  
               
            2.   
               
            3-export([start/0, tests/1]).  
               
            4.   
               
            5. start() ->  
               
            6.     tests(12345).  
               
            7.   
               
            8. tests(Port) ->  
               
            9.     io:format("starting~n"),  
              
            10.     spawn(fun() -> test(Port) end),  
              
            11.     spawn(fun() -> test(Port) end),  
              
            12.     spawn(fun() -> test(Port) end),  
              
            13.     spawn(fun() -> test(Port) end).  
              
            14.   
              
            15. test(Port) ->  
              
            16.      case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of  
              
            17.     {ok, _} ->  
              
            18.             test(Port);  
              
            19.     _ ->  
              
            20.         test(Port)  
              
            21.     end.  
            一開(kāi)始我的這個(gè)stress_test客戶端運(yùn)行在windows上面, echo_server服務(wù)器端運(yùn)行在linux上面。 結(jié)果接受了1016個(gè)連接就停止了. 于是我用ulimit -n 改了服務(wù)器端的文件描述符數(shù)量為10240. 接著還是如此,折騰了幾天,最終還是沒(méi)有搞明白。

            于是就求助于公司的linux編程牛人,結(jié)果讓我一倒...  客戶端沒(méi)有修改文件描述符個(gè)數(shù). windows上得在注冊(cè)表里面改.
            牛人開(kāi)始對(duì)這東西的性能感興趣了,剛好我摸了一陣子erlang的文檔,于是我倆就走向了erlang網(wǎng)絡(luò)連接的性能調(diào)優(yōu)之旅啦~~過(guò)程真是讓人興奮。 我們很快通過(guò)了1024這一關(guān)~~到了4999個(gè)連接,很興奮.

            但為什么4999個(gè)連接呢, 檢查一下代碼終于發(fā)現(xiàn)echo_server.erl定義了一個(gè)宏, 最大連接數(shù)為5000. 我又倒~~
            修改編譯之后, 連接數(shù)跑到101xx多了, 太哈皮了!
            再測(cè)102400個(gè)連接時(shí),到32767個(gè)連接數(shù)erl掛了~說(shuō)是進(jìn)程開(kāi)得太多了. 好在記得這個(gè)erl的參數(shù)+P,可以定義erlang能生成的進(jìn)程數(shù). 默認(rèn)是32768. 改了!

            后面不知怎么著,在81231個(gè)連接停止了. 新的性能瓶頸又卡了我們.  好在牛人對(duì)linux熟, 用strace(這東西會(huì)莫名地退出), stap查出一些苗頭.   我也想到在otp文檔好像提過(guò)另一個(gè)limit,那就是端口數(shù)...在此同時(shí)我們發(fā)現(xiàn)erlang在linux上是用的傳統(tǒng)poll模型. 但查erlang的源代碼發(fā)現(xiàn)是支持epoll的. 在網(wǎng)上搜了半天,終于搜到了個(gè)maillist的帖子.

            代碼
            1. $./configure --enable-kernel-poll  

            由于我們的測(cè)試服務(wù)器是雙核的,我們?cè)谂渲玫臅r(shí)候也打開(kāi)了smp支持.  歡快的make  & make install之后....
            把 /proc/sys/net/ipv4/ip_local_port_range 的內(nèi)容改成了1024到65535.  最多也也能改成65535 :)

            代碼
            1. $echo 1024 65535 > ip_local_port_range  

            另外再添加一個(gè)erl的環(huán)境變量
            代碼
            1. $export ERL_MAX_PORTS=102400  

            于是開(kāi)始跑了,不過(guò)這次跑不一樣了
            echo_server
             
            1. $erl -noshell  +P 102400 +K true +S 2 -smp -s echo_server start  
               
            stress_test
             
            1. $erl -noshell  +P 102400 +K true +S 2 -smp -s stress_test start  
            這里的+K true,表示使用內(nèi)核poll,+S 2 表示兩個(gè)核. 這樣可歡快啦~~~ 10w大關(guān)過(guò)咯! 而且比剛才沒(méi)用epoll的速度快暴多~~
            于是我們又開(kāi)始了204800個(gè)連接發(fā)測(cè)試了~~~

            用top一看cpu占用率極低,服務(wù)器只在5%左右。 內(nèi)存也不是很大~~

            posted @ 2009-09-14 12:25 暗夜教父 閱讀(605) | 評(píng)論 (0)編輯 收藏
            轉(zhuǎn)自http://coderplay.javaeye.com/blog/93403
               1.  -module(echo_server).  
               
            2-export([start/0,stop/0]).  
               
            3.   
               
            4-define(LISTEN_PORT,12345).     % 開(kāi)放端口  
               
            5-define(MAX_CONN, 5000).        % 最大連接數(shù)  
               
            6.   
               
            7. start() ->  
               
            8.     process_flag(trap_exit, true), % 設(shè)置退出陷阱  
               
            9.     tcp_server:start_raw_server(?LISTEN_PORT,  
              
            10.                 fun(Socket) -> socket_handler(Socket,self()) end,  
              
            11.                 ?MAX_CONN,   
              
            12.                 0).  
              
            13.   
              
            14%% 處理數(shù)據(jù)  
              
            15. socket_handler(Socket,Controller) ->  
              
            16.     receive  
              
            17.         {tcp, Socket, Bin} ->  
              
            18.             gen_tcp:send(Socket, Bin); % echo  
              
            19.         {tcp_closed, Socket} ->  
              
            20.             ok;  
              
            21.         _ ->  
              
            22.             socket_handler(Socket,Controller)  
              
            23.     end.  
              
            24.   
              
            25. stop() ->  
              
            26.     tcp_server:stop(?LISTEN_PORT).  

            基于Joe Armstrong 的tcp_server模塊來(lái)做的, 試試先 :)

            編譯
                erl -noshell -s make all -s init stop
            運(yùn)行
                erl -noshell -sname coderplay -s echo_server start
            posted @ 2009-09-14 12:24 暗夜教父 閱讀(406) | 評(píng)論 (0)編輯 收藏
            原文網(wǎng)址:erlang網(wǎng)絡(luò)編程的幾個(gè)性能調(diào)優(yōu)和注意點(diǎn)
            原文作者:coderplay
            前些天給echo_server寫(xiě)了個(gè)非常簡(jiǎn)單的連接壓力測(cè)試程序,
            下載: stress_test.erl
            • -module(stress_test).  

            • -export([start/0, tests/1]).  

            • start() ->  

            • tests(12345).  

            • tests(Port) ->  

            • io:format("starting~n"),  

            • spawn(fun() -> test(Port)
              end),  

            • spawn(fun() -> test(Port)
              end),  

            • spawn(fun() -> test(Port)
              end),  

            • spawn(fun() -> test(Port)
              end).  

            • test(Port) ->  

            • case
              gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}])
              of
            •     {ok, _} ->  

            • test(Port);  

            • _ ->  

            • test(Port)

            • end.

            一開(kāi)始我的這個(gè)stress_test客戶端運(yùn)行在windows上面,echo_server服務(wù)器端運(yùn)行在linux上面。結(jié)果接受了1016個(gè)連接就停止了. 于是我用ulimit -n改了服務(wù)器端的文件描述符數(shù)量為10240. 接著還是如此,折騰了幾天,最終還是沒(méi)有搞明白。
            于是就求助于公司的linux編程牛人,結(jié)果讓我一倒…  客戶端沒(méi)有修改文件描述符個(gè)數(shù). windows上得在注冊(cè)表里面改.

            牛人開(kāi)始對(duì)這東西的性能感興趣了,剛好我摸了一陣子erlang的文檔,于是我倆就走向了erlang網(wǎng)絡(luò)連接的性能調(diào)優(yōu)之旅啦~~過(guò)程真是讓人興奮。 我們很快通過(guò)了1024這一關(guān)~~到了4999個(gè)連接,很興奮.
            但為什么4999個(gè)連接呢, 檢查一下代碼終于發(fā)現(xiàn)echo_server.erl定義了一個(gè)宏, 最大連接數(shù)為5000. 我又倒~~
            修改編譯之后, 連接數(shù)跑到101xx多了, 太哈皮了!
            再測(cè)102400個(gè)連接時(shí),到32767個(gè)連接數(shù)erl掛了~說(shuō)是進(jìn)程開(kāi)得太多了. 好在記得這個(gè)erl的參數(shù)+P,可以定義erlang能生成的進(jìn)程數(shù). 默認(rèn)是32768. 改了!
            后面不知怎么著,在81231個(gè)連接停止了. 新的性能瓶頸又卡了我們. 好在牛人對(duì)linux熟, 用strace(這東西會(huì)莫名地退出),stap查出一些苗頭.我也想到在otp文檔好像提過(guò)另一個(gè)limit,那就是端口數(shù)…在此同時(shí)我們發(fā)現(xiàn) erlang在linux上是用的傳統(tǒng)poll模型.但查erlang的源代碼發(fā)現(xiàn)是支持epoll的. 在網(wǎng)上搜了半天,終于搜到了個(gè)maillist的帖子.
            • $./configure --enable-kernel-poll

            由于我們的測(cè)試服務(wù)器是雙核的,我們?cè)谂渲玫臅r(shí)候也打開(kāi)了smp支持. 歡快的make  & make install之后….
            把 /proc/sys/net/ipv4/ip_local_port_range 的內(nèi)容改成了1024到65535. 最多也也能改成65535
            • $echo 1024 65535 > ip_local_port_range

            另外再添加一個(gè)erl的環(huán)境變量
            • $export ERL_MAX_PORTS=102400

            于是開(kāi)始跑了,不過(guò)這次跑不一樣了
            echo_server
            • $erl -noshell  +P 102400 +K true +S 2 -smp -s echo_server start

            stress_test
            • $erl -noshell  +P 102400 +K true +S 2 -smp -s stress_test start

            這里的+K true,表示使用內(nèi)核poll,+S 2 表示兩個(gè)核. 這樣可歡快啦~~~ 10w大關(guān)過(guò)咯! 而且比剛才沒(méi)用epoll的速度快暴多~~
            于是我們又開(kāi)始了204800個(gè)連接發(fā)測(cè)試了~~~
            用top一看cpu占用率極低,服務(wù)器只在5%左右。內(nèi)存也不是很大~~
            posted @ 2009-09-14 11:20 暗夜教父 閱讀(498) | 評(píng)論 (0)編輯 收藏

            網(wǎng)路的硬件也有限,而人的創(chuàng)造也無(wú)限,在公網(wǎng)平均130ms的Latency下,是不存在“完全的”的同步情況。如何通過(guò)消除/隱藏延時(shí),將用戶帶入快速的交互式實(shí)時(shí)游戲中,體驗(yàn)完美的互動(dòng)娛樂(lè)呢?

            以下六點(diǎn),將助你分清楚哪些我們可以努力,哪些我們不值得努力,弄明白實(shí)時(shí)游戲中同步問(wèn)題關(guān)鍵之所在,巧妙的化解與規(guī)避游戲,最終在適合普遍用戶網(wǎng)絡(luò)環(huán)境中(200ms),實(shí)現(xiàn)實(shí)時(shí)快速互動(dòng)游戲:

            1. 基本情況:
               (A) 網(wǎng)絡(luò)性能指標(biāo)一:帶寬,限制了實(shí)時(shí)游戲的人數(shù)容量
               (B) 網(wǎng)絡(luò)性能指標(biāo)二:延時(shí),決定了實(shí)時(shí)游戲的最低反應(yīng)時(shí)間

            2. 兩個(gè)基本原則:
               (A) 讓所有的用戶屏幕上面表現(xiàn)出完全不同的表象是完全沒(méi)有問(wèn)題的。
               (B) 把這些完全不同表象完全柔和在一個(gè)統(tǒng)一的邏輯中也是完全沒(méi)有問(wèn)題的。

            3. 同步的十二條應(yīng)對(duì)策略:
               (A) 最大可能減少游戲中的數(shù)據(jù)傳輸
               (B) 將阻塞通信放到線程池中實(shí)現(xiàn)
               (C) 永遠(yuǎn)不要為了等待某個(gè)數(shù)據(jù)而不讓游戲進(jìn)行下去
               (D) 利用預(yù)測(cè)和插值改進(jìn)游戲的效果
               (E) 當(dāng)使用預(yù)測(cè)插值的時(shí)候傳送的數(shù)據(jù)不僅包括坐標(biāo),還需要速度和加速度
               (F) 將輸入數(shù)據(jù)枷鎖或者隊(duì)列化(例如鍵盤(pán)消息隊(duì)列),直到下次發(fā)送數(shù)據(jù)的時(shí)刻,傳統(tǒng)的方法是在固定的時(shí)間(發(fā)送數(shù)據(jù)前)檢測(cè)鍵盤(pán),在游戲的原理上隱藏延時(shí)
               (G) 使用事件調(diào)度表,將需要在所有用戶客戶端同時(shí)發(fā)生的事件,提前廣播到所有用戶
               (H) 使用多次攻擊來(lái)殺死一個(gè)精靈,盡量減少一次性的、確定性的、延時(shí)敏感的事件
               (I) 延長(zhǎng)子彈或者火箭在空中飛行的時(shí)間(在其飛行的同時(shí),在所有客戶端進(jìn)行預(yù)測(cè)插值)
               (J) 所有物體從一個(gè)地方移動(dòng)到另外一個(gè)地方都需要時(shí)間,避免諸如“瞬間移動(dòng)”的設(shè)計(jì)
               (K) 盡量使游戲中所有精靈,飛船或者其他物體,都按照可預(yù)測(cè)的軌跡運(yùn)行,比如在移動(dòng)中增加慣性
               (L) 充分發(fā)揮創(chuàng)造力,盡最大可能的合并游戲中前后相關(guān)的事件,合并游戲中存在的延時(shí)此問(wèn)題,需要在技術(shù)上改進(jìn)的同時(shí)也需要策劃有所重視,規(guī)避一些影響較大的設(shè)計(jì),巧妙的隱藏"延時(shí)"

            4. 同步問(wèn)題現(xiàn)狀:
               (A) 重視程度不夠:很多人尚未意識(shí)到此問(wèn)題的存在,曾有公司花半年時(shí)間打算做一款“松鼠大戰(zhàn)”的網(wǎng)絡(luò)版。
               (B) 技術(shù)上無(wú)徹底解決方案:對(duì)于多數(shù)程序員,單機(jī)游戲技術(shù)善未成熟就匆匆步入網(wǎng)絡(luò)時(shí)代。
               (C) 研究這個(gè)技術(shù)需要條件:需要有實(shí)力的公司才能提供,無(wú)此條件,即便有能力的程序員也無(wú)法成功。

            5. 目前網(wǎng)游的三大技術(shù)難題:
               (A) 服務(wù)器的響應(yīng)問(wèn)題:如何使服務(wù)器在支持越來(lái)越多的人數(shù)的情況下提供最高的響應(yīng)。
               (B) 同步問(wèn)題:如何在有限的網(wǎng)絡(luò)響應(yīng)情況下,實(shí)現(xiàn)快速實(shí)時(shí)類游戲,提供最完美的交互。
               (C) 服務(wù)器分布式問(wèn)題:如何在統(tǒng)一用戶數(shù)據(jù)的情況下,利用分部式將各個(gè)分散的“世界”統(tǒng)一到一個(gè)“世界”中。
               誰(shuí)能真正解決好以上三個(gè)問(wèn)題,配合策劃在設(shè)計(jì)上的突破,將使其他人在至少兩年內(nèi)無(wú)法超越。
              
            6. 相關(guān)補(bǔ)充:
               (A) 網(wǎng)格技術(shù)現(xiàn)在還是抄作,真正用到游戲中,還有很多技術(shù)難點(diǎn)需要突破(比如:目前網(wǎng)格的單位計(jì)算時(shí)間是以秒計(jì)算).
               (B) 其實(shí)與很多人想法相反的是現(xiàn)在3D技術(shù)早已不是主要的矛盾。而現(xiàn)在國(guó)內(nèi)外對(duì)于以上三個(gè)問(wèn)題可以說(shuō)處于同一個(gè)起跑線上,完全有機(jī)會(huì)取得先機(jī)。
               (C) 現(xiàn)在解決同步問(wèn)題已經(jīng)很緊迫,而同時(shí)所需要的環(huán)境也已經(jīng)成熟,只要有所關(guān)注,半年之內(nèi)可以得出較成熟的結(jié)論


            那么具體怎么解決呢?再下一步怎么辦?
            這就得自己去實(shí)踐了,我只說(shuō)這么多了,哈哈,不然又教懶了那些成天再網(wǎng)上搜方案的人。



            MMO的同步策略目前已經(jīng)沒(méi)有什么秘密可言了,簡(jiǎn)單敘述下各要素:


            MMO人行走:MMO中如果是鼠標(biāo)點(diǎn)擊行走傳目的地+中間尋路接點(diǎn)就行了,服務(wù)器簡(jiǎn)單驗(yàn)證一下。如果是鍵盤(pán)控制行走的話,需要做簡(jiǎn)單的預(yù)測(cè)插值。

            MMO打怪:要看怎么個(gè)打法,客戶端根本不必管服務(wù)端什么時(shí)候傳來(lái)消息,直接演示動(dòng)畫(huà)就行了,最好把被砍的過(guò)程動(dòng)畫(huà)做長(zhǎng)一點(diǎn),可以在播放動(dòng)畫(huà)的過(guò)程中等待服務(wù)器傳過(guò)來(lái)的延遲敏感事件,比如--該人已死。這樣處理起來(lái)比較容易,最重要的是客戶端看起來(lái)要流暢。

            MMO插值:所謂“把不同表象柔和在一起”算法很多,簡(jiǎn)單的可以參考DR,位置不同的時(shí)候可以做一次線性插值,直接把人拉扯過(guò)去,或者二次線形插值,做一條平滑的曲線修正,看具體項(xiàng)目選擇了。

            時(shí)鐘:就是時(shí)鐘同步,所有時(shí)鐘都是以“貞”為單位的,服務(wù)器主邏輯循環(huán)可以是5-10fps,多則無(wú)益,客戶端同樣維持一個(gè)和服務(wù)端頻率相同的消息同步貞。

            時(shí)間貞:把ping值加到時(shí)間計(jì)算里面是多余的,按貞計(jì)算的話,服務(wù)端告訴客戶端在n貞發(fā)生xx事件的時(shí)候,客戶端收到的時(shí)候如果客戶端貞數(shù)大于n,說(shuō)明是發(fā)生過(guò)了的,那么做插值,如果小于n,說(shuō)明是未來(lái)發(fā)生的,那么加入時(shí)間表。


            1 游戲中的行走,則是一個(gè)需要同步的重頭戲,當(dāng)今游戲分成兩種同步方式,一種是以服務(wù)器端為準(zhǔn),如果發(fā)現(xiàn)客戶端的坐標(biāo)和服務(wù)器的坐標(biāo)不符合的話,則拉回。這點(diǎn)的好處是可以彌補(bǔ)瞬移外掛的出現(xiàn),但在網(wǎng)絡(luò)環(huán)境差的情況下,則出現(xiàn)游戲不流暢。

            所 以,我比較推薦使用以客戶端計(jì)算為準(zhǔn)的方法(當(dāng)然是我因?yàn)槲疫@個(gè)游戲?qū)τ螒蛄鲿承砸蠛芨撸蛻舳讼蚍?wù)器發(fā)送計(jì)算好的數(shù)據(jù),服務(wù)器經(jīng)過(guò)一定的預(yù)測(cè)判 斷,廣播給其他玩家,這個(gè)雖然是能引起和wow一樣的問(wèn)題,瞬移外掛,但完全可以從游戲設(shè)計(jì)方面進(jìn)行根本避免。(這里就不談策劃方面的問(wèn)題了)

            2 游戲的戰(zhàn)斗,戰(zhàn)斗的同步性要求也相當(dāng)高,這里我也引用樓主說(shuō)的,在固定時(shí)間檢測(cè)鍵盤(pán),以一定頻率發(fā)送攻擊消息,這里有一個(gè)關(guān)鍵,就是服務(wù)器和客戶端都進(jìn)行 攻擊判定,即使服務(wù)器中沒(méi)有攻擊判定成功,但在客戶端判定成功,也要播放攻擊效果。不過(guò)一切計(jì)算效果以服務(wù)器為準(zhǔn)。這是一個(gè)欺騙玩家的手段。

            posted @ 2009-09-12 10:39 暗夜教父 閱讀(4490) | 評(píng)論 (2)編輯 收藏
            ErLang語(yǔ)法中充滿了一些約定。大寫(xiě)字母開(kāi)頭的名字(比如Address),表示一個(gè)變量,包括參數(shù)、局部變量等;小寫(xiě)字母開(kāi)頭的單詞(比如ok),表示一個(gè)常量,叫做atom(原子的意思),包括常量名、函數(shù)名、模塊名等。

            ErLang的注釋用%開(kāi)頭。ErLang用下劃線“_”表示任意變量,類似于Java的switch語(yǔ)法里面的default選項(xiàng)。

            ErLang脫胎于Prolog,不過(guò),我覺(jué)得,ErLang語(yǔ)法和Haskell語(yǔ)法比較象,都是采用 -> 定義函數(shù)。

            ErLang語(yǔ)句中的標(biāo)點(diǎn)符號(hào)用法很象文章的標(biāo)點(diǎn)符號(hào)。

            整個(gè)函數(shù)定義結(jié)束用一個(gè)句號(hào)“.”;同一個(gè)函數(shù)中,并列的邏輯分支之間,用分號(hào)“;”分界;順序語(yǔ)句之間,用逗號(hào)“,”分隔。

            ErLang中,{ }不是表示程序塊的開(kāi)頭和結(jié)尾,而是表示一種特殊的數(shù)據(jù)結(jié)構(gòu)類型——Tuple(元組),比如,{12, 3, ok}。我們可以把Tuple理解為定長(zhǎng)數(shù)組。

            [ ] 則表示最基本的函數(shù)式編程的數(shù)據(jù)結(jié)構(gòu)類型——List。List數(shù)據(jù)結(jié)構(gòu)很基本,寫(xiě)法和用法也有一定的復(fù)雜度,不是表面上看起來(lái)那么簡(jiǎn)單,后面講解Closure的章節(jié)會(huì)詳細(xì)介紹List的最基本的構(gòu)造原理。

            下面我們來(lái)看一個(gè)簡(jiǎn)單的例子。

            我們首先定義一個(gè)最簡(jiǎn)單的函數(shù),把一個(gè)參數(shù)乘以10,然后加1。
            times10( Number ) –>
            Temp = 10 * Number,
            Temp + 1.

            為了說(shuō)明問(wèn)題,上面的代碼把乘法操作和加法操作分成兩個(gè)步驟。Temp = 10 * Number語(yǔ)句后面是逗號(hào),因?yàn)檫@是兩條順序執(zhí)行的語(yǔ)句。Temp + 1語(yǔ)句后面是句號(hào),表示整個(gè)函數(shù)定義結(jié)束。而且,可以看出,ErLang沒(méi)有return語(yǔ)句,最后執(zhí)行的那條語(yǔ)句的執(zhí)行結(jié)果就是返回值。

            下面,我們把這個(gè)函數(shù)優(yōu)化一下。當(dāng)參數(shù)等于0的時(shí)候,直接返1;否則,就乘以10,然后加1,然后返回。這時(shí)候,我們就要用到case of邏輯分支語(yǔ)句,相當(dāng)于java的switch語(yǔ)句。

            times10( Number ) –>
            case Number of
            0 -> 1;
            _ ->
            Temp = 10 * Number,
            Temp + 1
            end.

            我們來(lái)仔細(xì)觀察這段ErLang程序。

            當(dāng)Number等于0的時(shí)候,直接返回1。由于這是一條分支語(yǔ)句,和后面的分支是并列的關(guān)系,所以,1的后面的標(biāo)點(diǎn)符號(hào)是分號(hào)。后面這個(gè)分支,下劃線“_”表示任何其它值,這里就表示除了1之外的任何其它數(shù)值。

            需要注意的一點(diǎn)是,case of語(yǔ)句需要用end結(jié)尾,end之前不需要有標(biāo)點(diǎn)符號(hào)。

            上述代碼中的case of 語(yǔ)句,其實(shí)就是Pattern Match的一種。ErLang的Pattern Match很強(qiáng)大,能夠大幅度簡(jiǎn)化程序邏輯,后面進(jìn)行專門(mén)介紹。
            Pattern Match
            Pattern Match主要有兩個(gè)功能——比較分派和變量賦值。
            其中,比較分派是最主要的功能。比較分派的意思是,根據(jù)參數(shù)值進(jìn)行條件分支的分派。可以把比較分派功能看作是一種類似于if, else等條件分支語(yǔ)句的簡(jiǎn)潔強(qiáng)大寫(xiě)法。
            上面的例子中,case Number of 就是根據(jù)Number的值進(jìn)行比較分派。更常見(jiàn)的寫(xiě)法是,可以把Pattern Match部分提到函數(shù)定義分支的高度。于是,上述代碼可以寫(xiě)成下面的形式:
            times10( 0 ) –> 1;
            times10( Number ) –>
            Temp = 10 * Number,
            Temp + 1.

            這段代碼由兩個(gè)函數(shù)定義分支構(gòu)成,由于兩個(gè)函數(shù)分支的函數(shù)名相同,而且參數(shù)個(gè)數(shù)相同,而且兩個(gè)函數(shù)定義分支之間采用分號(hào)“;”分隔,說(shuō)明這是同一個(gè)函數(shù)的定義。函數(shù)式編程語(yǔ)言中,這種定義方式很常見(jiàn),看起來(lái)形式很整齊,宛如數(shù)學(xué)公式。

            這段代碼的含義是,當(dāng)參數(shù)值等于0的時(shí)候,那么,程序走第一個(gè)函數(shù)定義分支(即分號(hào)“;”結(jié)尾的“times10( 0 ) –> 1;”),否則,走下面的函數(shù)定義分支(即“times10( Number ) –>…”)。

            第二個(gè)分支中的參數(shù)不是一個(gè)常數(shù),而是一個(gè)變量Number,表示這個(gè)分支可以接受任何除了0之外的參數(shù)值,比如,1、2、12等等,這些值將賦給變量Number。
            因此,這個(gè)地方也體現(xiàn)了Pattern Match的第二個(gè)功能——變量賦值。

            Pattern Match的形式可以很復(fù)雜,下面舉幾個(gè)典型的例子。
            (1)數(shù)據(jù)結(jié)構(gòu)拆解賦值
            前面將到了ErLang語(yǔ)言有一種相當(dāng)于定長(zhǎng)數(shù)組的Tuple類型,我們可以很方便地根據(jù)元素的位置進(jìn)行并行賦值。比如,
            {First, Second} = {1, 2}
            我們還可以對(duì)復(fù)合Tuple數(shù)據(jù)結(jié)構(gòu)進(jìn)行賦值,比如
            {A, {B, C}, D} = { 1, {2, 3}, 4 }
            List數(shù)據(jù)結(jié)構(gòu)的賦值也是類似。由于List的寫(xiě)法和用法不是那么簡(jiǎn)單,三言兩語(yǔ)也說(shuō)不清楚,還徒增困擾,這里不再贅述。
            (2)assertEquals語(yǔ)句
            在Java等語(yǔ)言中,我們寫(xiě)單元測(cè)試的時(shí)候,會(huì)寫(xiě)一些assert語(yǔ)句,驗(yàn)證程序運(yùn)行結(jié)果。這些assert語(yǔ)句通常是以API的方式提供,比如,assertTrue()、assertEquals()等。
            在ErLang中,可以用簡(jiǎn)單的語(yǔ)句達(dá)到類似于assertTrue()、assertEquals()等API的效果。
            比如,ErLang中,true = testA() 這樣的語(yǔ)句表示testA的返回結(jié)果必須是true,否則就會(huì)拋出異常。這個(gè)用法很巧妙。這里解釋一下。
            前面講過(guò),ErLang語(yǔ)法約定,小寫(xiě)字母開(kāi)頭的名字,都是常量名。這里的true自然也是一個(gè)常量,既然是常量,我們不可能對(duì)它賦值,那么true = testA()的意思就不是賦值,而是進(jìn)行匹配比較。
            (3)匹配和賦值同時(shí)進(jìn)行
            我們來(lái)看這樣一段代碼。
            case Result of
            {ok, Message} -> save(Message);
            {error, ErrorMessage} -> log(ErrorMessage)
            end.

            這段代碼中,Result是一個(gè)Tuple類型,包含兩個(gè)元素,第一個(gè)元素表示成功(ok)或者失敗(error),第二個(gè)元素表示具體的信息。
            可以看到,這兩個(gè)條件分支中,同時(shí)出現(xiàn)了常量和變量。第一個(gè)條件分支中的ok是常量,Message是變量;第二個(gè)條件分支中的error是常量,ErrorMessage是變量。
            這兩個(gè)條件分支都既有比較判斷,也有變量賦值。首先,判斷ResultTuple中的第一個(gè)元素和哪一個(gè)分支的第一個(gè)元素匹配,如果相配,那么把ResultTuple中的第二個(gè)元素賦給這個(gè)分支的第二個(gè)變量元素。即,如果Result的第一個(gè)元素是ok,那么走第一個(gè)條件分支,并且把Result的第二個(gè)元素賦給Message變量;如果Result的第二個(gè)元素是error,那么走第二個(gè)條件分支,并且把Result的第二個(gè)元素賦給ErrorMessage變量。

            在Java等語(yǔ)言中,實(shí)現(xiàn)上述的條件分支邏輯,則需要多寫(xiě)幾條語(yǔ)句ErLang語(yǔ)法可以從形式上美化和簡(jiǎn)化邏輯分支分派復(fù)雜的程序。
            除了支持?jǐn)?shù)相等比較,Pattern Match還可以進(jìn)行范圍比較、大小比較等,需要用到關(guān)鍵字when,不過(guò)用到when的情況,就比if else簡(jiǎn)潔不了多少,這里不再贅述。
            匿名函數(shù)
            ErLang允許在一個(gè)函數(shù)體內(nèi)部定義另一個(gè)匿名函數(shù),這是函數(shù)式編程的最基本的功能。這樣,函數(shù)式語(yǔ)言才可以支持Closure。我們來(lái)看一個(gè)ErLang的匿名函數(shù)的例子。
            outer( C ) –>
            Inner = fun(A, B) -> A + B + C end,
            Inner(2, 3).

            這段代碼首先定義了一個(gè)命名函數(shù)outer,然后在outer函數(shù)內(nèi)部定義了一個(gè)匿名函數(shù)。可以看到,這個(gè)匿名函數(shù)采用關(guān)鍵字fun來(lái)定義。前面講過(guò),函數(shù)式編程的函數(shù)就相當(dāng)于面向?qū)ο缶幊痰念悓?shí)例對(duì)象,匿名函數(shù)自然也是這樣,也相當(dāng)于類實(shí)例,我們可以把這個(gè)匿名函數(shù)賦給一個(gè)變量Inner,然后我們還可以把這個(gè)變量當(dāng)作函數(shù)來(lái)調(diào)用,比如,Inner(2, 3)。
            fun是ErLang用來(lái)定義匿名函數(shù)的關(guān)鍵字。這個(gè)關(guān)鍵字很重要。fun定義匿名函數(shù)的用法不是很復(fù)雜,和命名函數(shù)定義類似。
            函數(shù)分支的定義也是類似,只是需要用end結(jié)尾,而不是用句號(hào)“.”結(jié)尾,而且fun只需要寫(xiě)一次,不需要向命名函數(shù)那樣,每個(gè)分支都要寫(xiě)。比如,
            MyFunction = fun(0) -> 0;
            (Number) -> Number * 10 + 1 end,
            MyFunction(3),
            函數(shù)作為變量
            匿名函數(shù)可以當(dāng)作對(duì)象賦給變量,命名函數(shù)同樣也可以賦給變量。具體用法還是需要借助重要的fun關(guān)鍵字。比如,
            MyFunction = fun outer / 1

            就可以把上述定義的outer函數(shù)賦給MyFunction變量。后面的 / 0表示這個(gè)outer函數(shù)只有一個(gè)參數(shù)。因?yàn)镋rLang允許有多個(gè)同名函數(shù)的定義,只要參數(shù)個(gè)數(shù)不同,就是不同的函數(shù)。
            我們可以看到,任何函數(shù)都可以作為變量,也可以作為參數(shù)和返回值傳來(lái)傳去,這些變量也可以隨時(shí)作為函數(shù)進(jìn)行調(diào)用,于是就具有了一定的動(dòng)態(tài)性。
            函數(shù)的動(dòng)態(tài)調(diào)用
            ErLang有一個(gè)apply函數(shù),可以動(dòng)態(tài)調(diào)用某一個(gè)函數(shù)變量。
            基本用法是 apply( 函數(shù)變量,函數(shù)參數(shù)列表 )。比如,上面的MyFunciton函數(shù)變量,就可以這么調(diào)用,apply( MyFunction, [ 5 ])。
            那么我們能否根據(jù)一個(gè)字符串作為函數(shù)名獲取一個(gè)函數(shù)變量呢?這樣我們就可以根據(jù)一個(gè)字符串來(lái)動(dòng)態(tài)調(diào)用某個(gè)函數(shù)了。
            ErLang中,做到這一點(diǎn)很簡(jiǎn)單。前面講過(guò),函數(shù)名一旦定義了,自然就固定了,這也類似于常量名,屬于不可變的atom(原子)。所有的atom都可以轉(zhuǎn)換成字符串,也可以從字符串轉(zhuǎn)換過(guò)來(lái)。ErLang中的字符串實(shí)質(zhì)上都是List。字符串和atom之間的轉(zhuǎn)換通過(guò)list_to_atom和atom_to_list來(lái)轉(zhuǎn)換。
            于是我們可以這樣獲取MyFunciton:MyFunction = list_to_atom(“outer”)
            如果outer函數(shù)已經(jīng)定義,那么MyFucntion就等于outer函數(shù),如果outer函數(shù)沒(méi)有定義,那么list_to_atom(“outer”)會(huì)產(chǎn)生一個(gè)新的叫做outer的atom,MyFucntion就等于這個(gè)新產(chǎn)生的atom。
            如果需要強(qiáng)制產(chǎn)生一個(gè)已經(jīng)存在的atom,那么我們需要調(diào)用list_to_existing_atom轉(zhuǎn)換函數(shù),這個(gè)函數(shù)不會(huì)產(chǎn)生新的atom,而是返回一個(gè)已經(jīng)存在了的atom。
            Tuple作為數(shù)據(jù)成員集合
            前面講解函數(shù)式編程特性的時(shí)候,提到了函數(shù)式編程沒(méi)有面向?qū)ο缶幊痰某蓡T變量,這是一個(gè)限制。
            ErLang的Tuple類型可以一定程度克服這個(gè)限制。Tuple可以一定程度上擔(dān)當(dāng)容納成員變量的職責(zé)。
            面向?qū)ο蟮念惗x,其實(shí)就是一群數(shù)據(jù)和函數(shù)的集合,只是集合的成員之間都有一個(gè)this指針相關(guān)聯(lián),可以相互找到。
            ErLang的Tuple類型就是數(shù)據(jù)的集合,可以很自然地發(fā)揮成員變量的作用,比如,{Member1, Member2}。
            讀者可能會(huì)說(shuō),ErLang的函數(shù)也可以作為變量,也可以放到Tuple里面,比如, { Memer1, Member2, Funtion1, Function2}。這不就和面向?qū)ο缶幊桃粯恿藛幔?
            遺憾的是,這樣做是得不償失的。因?yàn)楹瘮?shù)式編程沒(méi)有面向?qū)ο蟮哪欠N內(nèi)在的this指針支持,自然也沒(méi)有內(nèi)在的多態(tài)和繼承支持,硬把數(shù)據(jù)和函數(shù)糅合在一個(gè)Tuple里面,一點(diǎn)好處都沒(méi)有,而且還喪失了函數(shù)作為實(shí)例對(duì)象的靈活性。
            所以,函數(shù)式編程的最佳實(shí)踐(Best Practice)應(yīng)該是:Tuple用來(lái)容納成員數(shù)據(jù),函數(shù)操作Tuple。Tuple定義和函數(shù)定義加在一起,就構(gòu)成了松散的數(shù)據(jù)結(jié)構(gòu),功能上類似于面向?qū)ο蟮念惗x。Tuple + 函數(shù)的數(shù)據(jù)結(jié)構(gòu),具有多態(tài)的特性,因?yàn)楹瘮?shù)本身能夠作為變量替換;但是不具有繼承的特性,因?yàn)闆](méi)有this指針的內(nèi)在支持。
            正是因?yàn)門(mén)uple在數(shù)據(jù)類型構(gòu)造方面的重大作用,所以,ErLang專門(mén)引入了一種叫做Record的宏定義,可以對(duì)Tuple的數(shù)組下標(biāo)位置命名。比如,把第一個(gè)元素叫做Address,第二個(gè)元素叫做Zipcode,這樣程序員就可以這些名字訪問(wèn)Tuple里面的元素,而不需要按照數(shù)組下標(biāo)位置來(lái)訪問(wèn)。
            Tuple和Record的具體用法還是有一定復(fù)雜度,限于篇幅,本章沒(méi)有展開(kāi)說(shuō)明,只提了一些原理方面的要點(diǎn)。
            其它
            ErLang還有其它語(yǔ)法特性和細(xì)節(jié),不再一一贅述。有興趣的讀者,可以自行去ErLang網(wǎng)站(www.erlang.org)進(jìn)行研究。
            posted @ 2009-09-11 11:04 暗夜教父 閱讀(722) | 評(píng)論 (0)編輯 收藏
            僅列出標(biāo)題
            共9頁(yè): 1 2 3 4 5 6 7 8 9 

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

            常用鏈接

            留言簿(2)

            隨筆分類

            隨筆檔案

            文章分類

            文章檔案

            搜索

            •  

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            国产激情久久久久影院老熟女 | 久久精品夜色噜噜亚洲A∨| 久久精品国产精品亚洲下载| 久久久无码精品亚洲日韩京东传媒 | 久久久无码精品午夜| 久久精品中文字幕一区| 狠狠色丁香婷婷综合久久来| 蜜臀久久99精品久久久久久| 久久精品一本到99热免费| 久久久久国色AV免费观看| 久久久久久亚洲Av无码精品专口| 久久se精品一区二区影院| 久久亚洲春色中文字幕久久久| 久久国产精品免费一区| 久久久久久午夜成人影院| 亚洲精品久久久www| 久久综合久久综合久久| 伊人久久大香线焦AV综合影院| 丁香五月综合久久激情| 久久久久亚洲av无码专区喷水 | 久久久精品视频免费观看| 久久综合综合久久综合| 久久久久国产亚洲AV麻豆| 99久久成人国产精品免费| 欧美日韩精品久久久免费观看| 狠狠久久综合伊人不卡| 少妇久久久久久被弄高潮| 久久综合亚洲色HEZYO国产| 久久精品国产亚洲麻豆| 日韩人妻无码精品久久久不卡 | 久久综合综合久久综合| 亚洲国产视频久久| 国产成人精品久久综合| 国内精品久久久久久久97牛牛| 精品伊人久久久| 久久九九免费高清视频| 久久久九九有精品国产| 九九久久自然熟的香蕉图片| 久久免费看黄a级毛片| 一本色道久久综合| 人人狠狠综合久久亚洲|