• <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>
            Fork me on GitHub
            隨筆 - 215  文章 - 13  trackbacks - 0
            <2016年9月>
            28293031123
            45678910
            11121314151617
            18192021222324
            2526272829301
            2345678


            專注即時(shí)通訊及網(wǎng)游服務(wù)端編程
            ------------------------------------
            Openresty 官方模塊
            Openresty 標(biāo)準(zhǔn)模塊(Opm)
            Openresty 三方模塊
            ------------------------------------
            本博收藏大部分文章為轉(zhuǎn)載,并在文章開頭給出了原文出處,如有再轉(zhuǎn),敬請保留相關(guān)信息,這是大家對原創(chuàng)作者勞動(dòng)成果的自覺尊重!!如為您帶來不便,請于本博下留言,謝謝配合。

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 219199
            • 排名 - 117

            最新評論

            閱讀排行榜

            http://blog.sina.com.cn/s/blog_7e0be1580101r09c.html
            Erlang語言與分布式編程引言

            “哦,怎么又是一門編程語言,夠了,我不需要了!”

            也許你看到本文題目首先想到的就是這個(gè)。我第一次看到Erlang語言時(shí)也是這么想的。

            曾幾何時(shí),面向?qū)ο蟮某绦蛟O(shè)計(jì)語言(C++、JAVA、C#等)一直是我心目中無所不能的“大神”,但是在計(jì)算機(jī)多核化發(fā)展日新月異、“云計(jì)算”“云存儲”火爆異常的今天,這些主流語言對并發(fā)和分布式程序的支持似乎不是那么得心應(yīng)手。越來越多的鎖、信號量、線程、同步、互斥之類的概念總是讓程序員頭痛不已。

            為了適應(yīng)并行計(jì)算和分布式應(yīng)用的大趨勢,Java5.0引入了concurrency庫,C++有了標(biāo)準(zhǔn)化的OpenMP和MPI,微軟發(fā)布了Parrallel FX和MPI.NET等一系列產(chǎn)品。然而,這些亡羊補(bǔ)牢的手段卻不能彌補(bǔ)它們的“先天不足”——即這些語言在創(chuàng)作時(shí)就沒有把并行化的問題放到優(yōu)先的位置上去考慮。

            也許是時(shí)候讓天生面向并發(fā)的Erlang語言大顯身手了。

            Erlang最初應(yīng)用于電信領(lǐng)域,但是現(xiàn)在也遠(yuǎn)不限于此。Facebook(也許中國人不大熟悉)用它重寫了超大型聊天系統(tǒng),Amazon用它開發(fā)了SimpleDB云存儲系統(tǒng),Yahoo!用它開發(fā)了Deliciou2.0,類似的成功項(xiàng)目比比皆是。

            那么,Erlang語言到底是何方神圣呢?且聽下文分解。

            Erlang語言與分布式編程1 Erlang語言的由來

            有人說Erlang語言是“因并發(fā)而生,因云計(jì)算而熱”。與主流語言不同,它是一種“函數(shù)式語言(Functional programming,F(xiàn)P)”,使用的是向并發(fā)編程COP,Concurrency Oriented Programming)的方法。Erlang并不是一門新的語言,它已經(jīng)有二十多年的歷史了。只是由于近些年對高并發(fā)、分布部署、持續(xù)服務(wù)的需求日漸增加,以及多核CPU的全面普及,Erlang才又引起人們的關(guān)注。

            Erlang得名于丹麥數(shù)學(xué)家及統(tǒng)計(jì)學(xué)家Agner Krarup Erlang,同時(shí)Erlang還可以表示Ericsson Language。1987年,Erlang語言創(chuàng)始人Joe Armstrong當(dāng)年在愛立信做電話網(wǎng)絡(luò)方面的開發(fā),他使用Smalltalk,可惜那個(gè)時(shí)候Smalltalk太慢,不能滿足電話網(wǎng)絡(luò)的高性能要求。但Joe實(shí)在喜歡Smalltalk,于是定購了一臺Tektronix Smalltak機(jī)器。但機(jī)器要兩個(gè)月時(shí)間才到,Joe在等待中百無聊賴,就開始使用Prolog,結(jié)果等Tektronix到來的時(shí)候,他已經(jīng)對Prolog更感興趣,Joe當(dāng)然不滿足于精通Prolog,經(jīng)過一段時(shí)間的試驗(yàn),Joe給Prolog加上了并發(fā)處理和錯(cuò)誤恢復(fù),于是Erlang就誕生了。

            Erlang語言與分布式編程2 Erlang語言的特性

            Erlang語言非常有特色,并發(fā)、分布、容錯(cuò)貫穿于程序的始終。在它的語法里,相等其實(shí)不是相等,變量實(shí)際上不能改變,實(shí)在是非常古怪。但是為了不使本文過于冗長,這里不介紹它的語法,只

            Erlang語言與分布式編程2.1 跨平臺性

            Erlang運(yùn)行時(shí)環(huán)境是一個(gè)虛擬機(jī),有點(diǎn)像Java虛擬機(jī),這樣的代碼一經(jīng)編譯,同樣可以隨處運(yùn)行。當(dāng)然,如果你需要更高效的話,字節(jié)代碼也可以編譯成本地代碼運(yùn)行。

            Erlang語言與分布式編程2.2并發(fā)性

            現(xiàn)實(shí)世界是并發(fā)的。我們很多人可以共同協(xié)作完成一項(xiàng)工作。人與人之間不存在任何“Shared Memory(共享的記憶)”,通過傳遞消息(如語言、文字、手勢、表情等)來進(jìn)行交互。這是現(xiàn)實(shí)世界的存在方式,也是Erlang進(jìn)程的工作方式。

            Erlang的并發(fā)特性源自語言本身而非操作系統(tǒng)。它創(chuàng)建了一種新的做法,使進(jìn)程可以非常快地創(chuàng)建和銷毀。因此,Erlang程序可以由多達(dá)幾百萬個(gè)超輕量級的進(jìn)程組成,這些進(jìn)程可以運(yùn)行于單處理器、多核處理器或者處理器網(wǎng)絡(luò)上。Erlang進(jìn)程之間高度隔離,沒有共享任何數(shù)據(jù),只能通過消息傳遞來進(jìn)行交互,并且這種消息傳遞也是非常迅捷的。

            Erlang程序的消息傳遞是通過進(jìn)程的消息郵箱來實(shí)現(xiàn)的。每個(gè)進(jìn)程都有一個(gè)用于接受別的進(jìn)程發(fā)來的消息的郵箱。進(jìn)程可以在任何時(shí)候檢查自己的郵箱有沒有新消息抵達(dá),如果有,進(jìn)程可以按照抵達(dá)的順序依次讀取。

            消除了共享內(nèi)存和進(jìn)程之間的消息通信是Erlang最明顯的優(yōu)勢之一,這一特點(diǎn)為進(jìn)行并行與分布式應(yīng)用的開發(fā)提供了非常好的基礎(chǔ)。

            Erlang語言與分布式編程2.3分布式

            Erlang被設(shè)計(jì)用于運(yùn)行在分布式環(huán)境下。一個(gè)Erlang虛擬機(jī)作為一個(gè)Erlang節(jié)點(diǎn)。一個(gè)分布式Erlang系統(tǒng)是由多個(gè)Erlang節(jié)點(diǎn)組成的網(wǎng)絡(luò)。使用Erlang來編寫分布式應(yīng)用要簡單的多,因?yàn)樗姆植际綑C(jī)制是透明的:對于程序來說并不知道自己是在分布式運(yùn)行。

            Erlang語言與分布式編程2.4健壯性

            Erlang可以說是一門為健壯而生動(dòng)語言。它不像其它語言一樣假設(shè)不會發(fā)生錯(cuò)誤,而是假定錯(cuò)誤隨時(shí)可能發(fā)生,并隨時(shí)準(zhǔn)備修正錯(cuò)誤。

            在一個(gè)分布式系統(tǒng)中,如果一個(gè)進(jìn)程出錯(cuò),讓這個(gè)進(jìn)程自己修復(fù)錯(cuò)誤有時(shí)并不可行,有時(shí)節(jié)點(diǎn)可能徹底崩潰(死機(jī)),因此將錯(cuò)誤交給其他進(jìn)程去處理是更恰當(dāng)?shù)倪x擇。

            在現(xiàn)實(shí)世界中,如果一個(gè)人停止工作,他就會告訴其他人“我頭疼無法工作了”或者“我心臟病發(fā)作了”等。

            Erlang錯(cuò)誤偵測的工作機(jī)制也類似于此,通過進(jìn)程間的相互“鏈接”來實(shí)現(xiàn)進(jìn)程監(jiān)控(這些進(jìn)程可以運(yùn)行于不同節(jié)點(diǎn)之上)機(jī)制,將錯(cuò)誤進(jìn)行分層處理,從而實(shí)現(xiàn)構(gòu)造“容錯(cuò)系統(tǒng)”。Erlang引入了“速錯(cuò)(Fail fast)”的理念,在一個(gè)進(jìn)程出錯(cuò)時(shí)不會試圖自行處理,而是直接退出,并發(fā)出錯(cuò)誤消息如“有人讓我去除0”或者“有人讓我從空列表中讀取數(shù)據(jù)”等,讓其他進(jìn)程進(jìn)行錯(cuò)誤處理、故障恢復(fù)等工作。

            Erlang語言與分布式編程2.5 軟實(shí)時(shí)性

            Erlang支持可編程的“軟”實(shí)時(shí)系統(tǒng),這種系統(tǒng)需要反應(yīng)時(shí)間在毫秒級。而在這種系統(tǒng)中,長時(shí)間的垃圾收集(garbage collection)延遲是無法接受的,因此Erlang使用了遞增式垃圾收集技術(shù)。

            Erlang語言與分布式編程2.6熱部署(Hot swap)

            有些系統(tǒng)是不能夠由于軟件維護(hù)而停止運(yùn)行的。Erlang允許程序代碼在系統(tǒng)運(yùn)行中被修改。舊第代碼能被逐步淘汰而后被新代碼替換。在此過渡期間,新舊代碼是共存的。這也使得安裝bug補(bǔ)丁、系統(tǒng)升級而不干擾系統(tǒng)運(yùn)行成為了可能。

            Erlang語言與分布式編程2.7 遞增式代碼裝載

            用戶能夠控制代碼如何被裝載的細(xì)節(jié)。在嵌入式系統(tǒng)中,所有代碼通常是在啟動(dòng)時(shí)就被完全裝載。而在開發(fā)系統(tǒng)中,代碼是按需裝載的,甚至在系統(tǒng)運(yùn)行時(shí)被裝載的。如果測試到了未覆蓋的bug,那么只有具有bug的代碼需要被替換。

            Erlang語言與分布式編程2.8 外部接口

            Erlang進(jìn)程與外部世界之間的通訊機(jī)制與Erlang進(jìn)程之間相同的消息傳送機(jī)制相同。這種機(jī)制被用于和操作系統(tǒng)通訊、與其它語言編寫的程序交互。如果出于高效率的需要,這種機(jī)制的一個(gè)特殊版本也允許例如C程序這樣的代碼直接鏈接到Erlang運(yùn)行時(shí)系統(tǒng)中來。

            Erlang語言與分布式編程3 Erlang語言的分布式編程

            Erlang語言與分布式編程3.1 分布式系統(tǒng)的特性

            George Coulouris認(rèn)為:分布式系統(tǒng)是由位于網(wǎng)絡(luò)中的一些計(jì)算機(jī)組成的系統(tǒng),這些網(wǎng)絡(luò)中的計(jì)算機(jī)之間只能通過傳遞消息來進(jìn)行溝通和協(xié)調(diào)。Andrew S.Tanenbaum說:一個(gè)分布式系統(tǒng)是一些獨(dú)立計(jì)算機(jī)的集合,但是對這個(gè)系統(tǒng)的用戶來說,系統(tǒng)就像一臺計(jì)算機(jī)一樣。

            盡管分布式系統(tǒng)還沒有一個(gè)公認(rèn)令人滿意的定義,但是Erlang語言與分布式編程所有分布式系統(tǒng)都有以下兩個(gè)特點(diǎn):一、系統(tǒng)的各個(gè)節(jié)點(diǎn)(計(jì)算機(jī))是相互獨(dú)立的;二、各個(gè)節(jié)點(diǎn)之間只能通過消息傳遞來進(jìn)行溝通和協(xié)調(diào)。

            這兩個(gè)特點(diǎn)很耳熟吧?沒錯(cuò),這也正是Erlang程序的特點(diǎn)!各進(jìn)程相互獨(dú)立,且只能通過消息傳遞來進(jìn)行交互。

            Erlang與分布式系統(tǒng)天性互相吻合,決定了使用Erlang開發(fā)分布式系統(tǒng)具有得天獨(dú)厚的優(yōu)勢。Erlang天生面向并發(fā),在Erlang開發(fā)的分布式系統(tǒng)增加和刪除節(jié)點(diǎn)非常簡便,具有良好的伸縮性。

            Erlang語言與分布式編程3.2 Erlang語言分布式編程的兩種基本模型

            3.2.1 分布式Erlang

            這種模型用于在一個(gè)緊密耦合的計(jì)算機(jī)集群上編寫程序。這種模型幾乎不需要有什么額外操作,一切就像在單節(jié)點(diǎn)編程時(shí)一樣:我們可以在任何一個(gè)節(jié)點(diǎn)創(chuàng)建進(jìn)程,所有的消息傳遞和錯(cuò)誤處理原語都可以照搬。

            分布式Erlang運(yùn)行于一個(gè)可信任的環(huán)境中——因?yàn)槿魏喂?jié)點(diǎn)都可以運(yùn)行其他節(jié)點(diǎn)上的操作,這就需要一個(gè)高度信任的網(wǎng)絡(luò)環(huán)境。

            3.2.2 基于套接字的分布式應(yīng)用

            使用TCP/IP套接字,我們可以編寫運(yùn)行在非信任環(huán)境中的分布式應(yīng)用程序。這種編程模型的功能比分布式Erlang要弱一些,但是卻更加可靠。

            Erlang語言與分布式編程3.3分布式編程實(shí)例:名字服務(wù)和遠(yuǎn)程過程調(diào)用

            下面我們用Erlang語言來實(shí)現(xiàn)一個(gè)分布式應(yīng)用實(shí)例:名字服務(wù)(Naming Service)。這里面將會用到遠(yuǎn)程過程調(diào)用(Remote Procedure Call,RPC),你會發(fā)現(xiàn)使用Erlang實(shí)現(xiàn)RPC簡直是易如反掌。

            名字服務(wù)就是一種“Key-Value”類型的服務(wù)。客戶機(jī)向服務(wù)器發(fā)送一個(gè)名字(Key),服務(wù)器返回與這個(gè)名字(Key)相關(guān)聯(lián)的值(Value),這樣的服務(wù)就叫做名字服務(wù)。

            3.3.1 程序源碼

            文件名kvs.erl:

            -module(kvs).

            -export([start/0, store/2, lookup/1]).

             

            start() -> register(kvs, spawn(fun() -> loop() end)).

             

            store(Key, Value) -> rpc({store, Key, Value}).

             

            lookup(Key) -> rpc({lookup, Key}).

             

            rpc(Q) ->

                kvs !{self(), Q},

                receive

                       {kvs, Reply}->

                      Reply

            end.

             

            loop() ->

                receive

                       {From, {store, Key, Value}} ->

                            put(Key,{ok, Value}),

                            From !{kvs, true},

                            loop();

                       {From, {lookup, Key}} ->

                            From !{kvs, get(Key)},

                            loop()

                end.

             可以看到,使用Erlang編寫的代碼非常簡潔。代碼中使用了Erlang編程中常用的尾遞歸。

            3.3.2 在同一個(gè)局域網(wǎng)內(nèi)的不同機(jī)器上運(yùn)行客戶機(jī)和服務(wù)器

            我所使用的系統(tǒng)是Microsoft Windows 7,所以下面的例子都是在Windows操作系統(tǒng)上完成的。應(yīng)首先保證系統(tǒng)上安裝有一個(gè)可運(yùn)行的Erlang版本,我所安裝的版本是5.8.1.1。

            首先準(zhǔn)備使用兩個(gè)節(jié)點(diǎn):第一個(gè)節(jié)點(diǎn)位于名為SERVER計(jì)算機(jī)上充當(dāng)服務(wù)器的ss節(jié)點(diǎn),另外一個(gè)節(jié)點(diǎn)是位于位于名為CLIENT的計(jì)算機(jī)上充當(dāng)客戶端的cc。

            步驟1:在SERVER上啟動(dòng)一個(gè)名為ss的Erlang節(jié)點(diǎn)。kvs模塊部署于ss節(jié)點(diǎn)上。

            C:\ erl –sname ss –setcookie abc

            Eshell V5.8.1.1  (abort with ^G)

            (ss@SERVER)1>

            步驟2:在節(jié)點(diǎn)ss上啟動(dòng)服務(wù)。

            (ss@SERVER)1>kvs:start().

            true

            步驟3:在CLIENT上啟動(dòng)一個(gè)名為cc的Erlang節(jié)點(diǎn),應(yīng)保證與服務(wù)器節(jié)點(diǎn)使用相同的cookie。

            C:\ erl –sname cc –setcookie abc

            Eshell V5.8.1.1  (abort with ^G)

            (cc@CLIENT)1>

            步驟4:現(xiàn)在我們就可以在客戶端節(jié)點(diǎn)cc上進(jìn)行遠(yuǎn)程過程調(diào)用了。首先調(diào)用服務(wù)器節(jié)點(diǎn)ss上kvs模塊的store進(jìn)程,存儲一個(gè)名字 “wheather”,其關(guān)聯(lián)值為“cold”。

            (cc@CLIENT)1>rpc:call(ss@SERVER , kvs, store, [wheather, cold]).

            true

            (cc@CLIENT)2>

            注意:這里的rpc是Erlang標(biāo)準(zhǔn)庫里的一個(gè)模塊,與kvs模塊中的rpc毫無關(guān)系。

            現(xiàn)在就可以遠(yuǎn)程調(diào)用ss上kvs模塊的lookup進(jìn)程取得“wheather”的關(guān)聯(lián)值。

            (cc@CLIENT)2>rpc:call(ss@SERVER , kvs, lookup, [wheather]).

            {ok,cold}

            (cc@CLIENT)3>

            這樣,我們就輕而易舉地實(shí)現(xiàn)了一個(gè)簡單名字服務(wù)。

            Erlang語言與分布式編程4 Erlang語言的應(yīng)用前景

            Erlang語言與分布式編程4.1 Erlang至今沒有廣泛應(yīng)用的原因

            盡管Erlang已經(jīng)有二十多高齡了,利用它也成功地實(shí)現(xiàn)了一批項(xiàng)目。但是,它迄今為止還是一門小眾語言。連Joe Armstrong 也抱怨說,Erlang總是充當(dāng)“救火隊(duì)員”的角色,總是碰到用其它語言難以解決的難題時(shí),人們才會想起Erlang。那么是什么阻礙了Erlang的廣泛應(yīng)用呢?我覺得主要有以下一些原因:

            4.1.1 軟件開發(fā)人員方面的原因。

            傳統(tǒng)程序員的思維方式還不能適應(yīng)并行程序設(shè)計(jì)的理念;經(jīng)過了OO訓(xùn)練的程序員也難以適應(yīng)COP的模式。習(xí)慣了OO(面向?qū)ο螅┱Z言的程序員在剛接觸到Erlang時(shí)一定會覺得很不適應(yīng)。歷史告訴我們,習(xí)慣的力量是強(qiáng)大的,不順應(yīng)大眾習(xí)慣的東西,成長總是會很艱難。此外,Erlang是小眾語言,使用它還存在著維護(hù)上的風(fēng)險(xiǎn)。

            4.1.2 Erlang語法方面的原因

            Erlang的語法的確有些“怪異”,學(xué)習(xí)困難。它實(shí)際上和我們主流的編程的方法、思路是不一樣的。主流編程語言如C++、Java等是基于圖靈機(jī)的命令式語言,而Erlang是基于λ-演算的函數(shù)式語言。

            使用Erlang編寫的代碼非常簡潔(幾乎可以簡潔到極致),代碼量可以非常少。但是我個(gè)人認(rèn)為它并不夠優(yōu)雅(Joe Armstrong一定不同意我的看法),難以讀懂(在沒有注釋的情況下更是如此),并且也難以學(xué)習(xí)。

            Erlang語言對二進(jìn)制數(shù)據(jù)操作支持較好,但是在字符編碼這方面的支持是比較薄弱的。當(dāng)前版本的Erlang(v5.5.2)使用的是Latin-1(ISO-8859-1)編碼的字符。使用Erlang操作多字節(jié)字符存在一定困難。這點(diǎn)對于使用中文的我們來說,非常不便。

            4.1.3 開發(fā)工具方面的原因

            Erlang至今仍沒有一個(gè)優(yōu)秀的集成開發(fā)環(huán)境(IDE),仍然停留在命令行階段。盡管有些人(我覺得他們屬于技術(shù)狂一類)認(rèn)為IDE的功能太強(qiáng)大,會讓程序員越來越懶,但是這的確造成Erlang的吸引力不足。使用Erlang進(jìn)行桌面應(yīng)用的界面設(shè)計(jì)也很麻煩。

            Erlang語言與分布式編程4.2Erlang與其它面向并發(fā)的編程語言

            除了Erlang以外,面向并發(fā)的編程語言還有很多。下面簡單介紹我找到的幾種:

                   Scala語言

            Scala語言近年來的流行度也在不斷提升。它是一種JVM語言,運(yùn)行于Java虛擬機(jī)之上。Scala語言是靜態(tài)類型的,兼具函數(shù)式語言(FP)和面向?qū)ο缶幊蹋∣O)的特點(diǎn)。它比Java有著更簡潔的語法,但運(yùn)行速度不遜于Java甚至超過Java。但是Scala進(jìn)程與Erlang相比還是不夠輕量級。

                   Go語言

            Go語言是谷歌2009發(fā)布的第二款開源編程語言。它專門針對多處理器系統(tǒng)應(yīng)用程序的編程進(jìn)行了優(yōu)化,使用Go編譯的程序可以媲美C或C++代碼的速度,而且更加安全、支持并行進(jìn)程。Go語言一誕生就備受青睞,由于在2009年市場份額增長最多而一舉摘得了TIOBE公布的年度大獎(jiǎng)。

                   F#語言

            F#語言是微軟推出的一門函數(shù)式語言(FP),是一種運(yùn)行在.Erlang語言與分布式編程Net CLR平臺上的新語言。它是OCaml的一個(gè)分支,兼具了函數(shù)式和命令式面向?qū)ο笳Z言的特點(diǎn);同時(shí)它也是一種強(qiáng)類型的編程語言。作為微軟唯一的FP語言,F(xiàn)#的特殊性引起了很多人的關(guān)注,而微軟也致力于將F#打造成為.NET平臺的頂峰語言,我們也期待著F#能夠給函數(shù)式編程語言帶來一次重大革命。

             

            Scala、Go、F#、Erlang各有特點(diǎn),很難分出高下。盡管不能斷定Erlang是不是在并行領(lǐng)域是最優(yōu)的語言,但至少目前來說我覺得Erlang是比較好的。Erlang憑借20多年的發(fā)展經(jīng)驗(yàn),在Erlang的層級上支持高并發(fā)、濃縮性和數(shù)據(jù)的持續(xù)性存儲都有豐富的庫有著相當(dāng)多的成熟庫(OTP)和開源軟件,這些資產(chǎn)使得它有極高的實(shí)用價(jià)值。此外,Erlang還有非常活躍和成熟的社區(qū)為開發(fā)者解疑釋惑,甚至在社區(qū)經(jīng)常能看到。在熟悉了Erlang的思維方法和社區(qū)后,用Erlang開發(fā)將是一件非常輕松、非常高效的事情。如果你想在將來的構(gòu)建多核的應(yīng)用程序或者是健壯的分布式系統(tǒng),你應(yīng)該學(xué)習(xí)Erlang。

            http://blog.csdn.net/slmeng2002/article/details/5532771

            最近關(guān)注erlang游戲服務(wù)器開發(fā)  erlang大牛寫的游戲服務(wù)器值得參考

             

            介紹
            本文以我的OpenPoker項(xiàng)目為例子,講述了一個(gè)構(gòu)建超強(qiáng)伸縮性的在線多游戲玩家系統(tǒng)。
            OpenPoker是一個(gè)超強(qiáng)多玩家紙牌服務(wù)器,具有容錯(cuò)、負(fù)載均衡和無限伸縮性等特性。
            源代碼位于我的個(gè)人站點(diǎn)上,大概10,000行代碼,其中1/3是測試代碼。

            在OpenPoker最終版本敲定之前我做了大量調(diào)研,我嘗試了Delphi、Python、C#、C/C++和Scheme。我還用Common Lisp寫了紙牌引擎。
            雖然我花費(fèi)了9個(gè)月的時(shí)間研究原型,但是最終重寫時(shí)只花了6個(gè)星期的時(shí)間。
            我認(rèn)為我所節(jié)約的大部分時(shí)間都得益于選擇Erlang作為平臺。

            相比之下,舊版本的OpenPoker花費(fèi)了一個(gè)4~5人的團(tuán)隊(duì)9個(gè)月時(shí)間。

            Erlang是什么東東?

            我建議你在繼續(xù)閱讀本文之前瀏覽下Erlang FAQ,這里我給你一個(gè)簡單的總結(jié)...

            Erlang是一個(gè)函數(shù)式動(dòng)態(tài)類型編程語言并自帶并發(fā)支持。它是由Ericsson特別為控制開關(guān)、轉(zhuǎn)換協(xié)議等電信應(yīng)用設(shè)計(jì)的。
            Erlang十分適合構(gòu)建分布式、軟實(shí)時(shí)的并發(fā)系統(tǒng)。

            由Erlang所寫的程序通常由成百上千的輕量級進(jìn)程組成,這些進(jìn)程通過消息傳遞來通訊。
            Erlang進(jìn)程間的上下文切換通常比C程序線程的上下文切換要廉價(jià)一到兩個(gè)數(shù)量級。

            使用Erlang寫分布式程序很簡單,因?yàn)樗姆植际綑C(jī)制是透明的:程序不需要了解它們是否分布。

            Erlang運(yùn)行時(shí)環(huán)境是一個(gè)虛擬機(jī),類似于Java虛擬機(jī)。這意味著在一個(gè)價(jià)格上編譯的代碼可以在任何地方運(yùn)行。
            運(yùn)行時(shí)系統(tǒng)也允許在一個(gè)運(yùn)行著的系統(tǒng)上不間斷的更新代碼。
            如果你需要額外的性能提升,字節(jié)碼也可以編譯成本地代碼。

            請移步Erlang site,參考Getting started、Documentation和Exampes章節(jié)等資源。

            為何選擇Erlang?

            構(gòu)建在Erlang骨子里的并發(fā)模型特別適合寫在線多玩家服務(wù)器。

            一個(gè)超強(qiáng)伸縮性的多玩家Erlang后端構(gòu)建為擁有不同“節(jié)點(diǎn)”的“集群”,不同節(jié)點(diǎn)做不同的任務(wù)。
            一個(gè)Erlang節(jié)點(diǎn)是一個(gè)Erlang VM實(shí)例,你可以在你的桌面、筆記本電腦或服務(wù)器上上運(yùn)行多個(gè)Erlang節(jié)點(diǎn)/VM。
            推薦一個(gè)CPU一個(gè)節(jié)點(diǎn)。

            Erlang節(jié)點(diǎn)會追蹤所有其他和它相連的節(jié)點(diǎn)。向集群里添加一個(gè)新節(jié)點(diǎn)所需要的只是將該新節(jié)點(diǎn)指向一個(gè)已有的節(jié)點(diǎn)。
            一旦這兩個(gè)節(jié)點(diǎn)建立連接,集群里所有其他的節(jié)點(diǎn)都會知曉這個(gè)新節(jié)點(diǎn)。

            Erlang進(jìn)程使用一個(gè)進(jìn)程id來相互發(fā)消息,進(jìn)程id包含了節(jié)點(diǎn)在哪里運(yùn)行的信息。進(jìn)程不需要知道其他進(jìn)程在哪里就可以通訊。
            連接在一起的Erlang節(jié)點(diǎn)集可以看作一個(gè)網(wǎng)格或者超級計(jì)算設(shè)備。

            超多玩家游戲里玩家、NPC和其他實(shí)體最好建模為并行運(yùn)行的進(jìn)程,但是并行很難搞是眾所皆知的。Erlang讓并行變得簡單。

            Erlang的位語法∞讓它在處理結(jié)構(gòu)封裝/拆解的能力上比Perl和Python都要強(qiáng)大。這讓Erlang特別適合處理二進(jìn)制網(wǎng)絡(luò)協(xié)議。

            OpenPoker架構(gòu)
            OpenPoker里的任何東西都是進(jìn)程。玩家、機(jī)器人、游戲等等多是進(jìn)程。
            對于每個(gè)連接到OpenPoker的客戶端都有一個(gè)玩家“代理”來處理網(wǎng)絡(luò)消息。
            根據(jù)玩家是否登錄來決定部分消息忽略,而另一部分消息則發(fā)送給處理紙牌游戲邏輯的進(jìn)程。

            紙牌游戲進(jìn)程是一個(gè)狀態(tài)機(jī),包含了游戲每一階段的狀態(tài)。
            這可以讓我們將紙牌游戲邏輯當(dāng)作堆積木,只需將狀態(tài)機(jī)構(gòu)建塊放在一起就可以添加新的紙牌游戲。
            如果你想了解更多的話可以看看cardgame.erl的start方法。

            紙牌游戲狀態(tài)機(jī)根據(jù)游戲狀態(tài)來決定不同的消息是否通過。
            同時(shí)也使用一個(gè)單獨(dú)的游戲進(jìn)程來處理所有游戲共有的一些東西,如跟蹤玩家、pot和限制等等。
            當(dāng)在我的筆記本電腦上模擬27,000個(gè)紙牌游戲時(shí)我發(fā)現(xiàn)我擁有大約136,000個(gè)玩家以及總共接近800,000個(gè)進(jìn)程。

            下面我將以O(shè)penPoker為例子,專注于講述怎樣基于Erlang讓實(shí)現(xiàn)伸縮性、容錯(cuò)和負(fù)載均衡變簡單。
            我的方式不是特別針對紙牌游戲。同樣的方式可以用在其他地方。

            伸縮性
            我通過多層架構(gòu)來實(shí)現(xiàn)伸縮性和負(fù)載均衡。
            第一層是網(wǎng)關(guān)節(jié)點(diǎn)。
            游戲服務(wù)器節(jié)點(diǎn)組成第二層。
            Mnesia“master”節(jié)點(diǎn)可以認(rèn)為是第三層。

            Mnesia是Erlang實(shí)時(shí)分布式數(shù)據(jù)庫。Mnesia FAQ有一個(gè)很詳細(xì)的解釋。Mnesia基本上是一個(gè)快速的、可備份的、位于內(nèi)存中的數(shù)據(jù)庫。
            Erlang里沒有對象,但是Mnesia可以認(rèn)為是面向?qū)ο蟮模驗(yàn)樗梢源鎯θ魏蜤rlang數(shù)據(jù)。

            有兩種類型的Mnesia節(jié)點(diǎn):寫到硬盤的節(jié)點(diǎn)和不寫到硬盤的節(jié)點(diǎn)。除了這些節(jié)點(diǎn),所有其他的Mnesia節(jié)點(diǎn)將數(shù)據(jù)保存在內(nèi)存中。
            在OpenPoker里Mnesia master節(jié)點(diǎn)會將數(shù)據(jù)寫入硬盤。網(wǎng)關(guān)和游戲服務(wù)器從Mnesia master節(jié)點(diǎn)獲得數(shù)據(jù)庫并啟動(dòng),它們只是內(nèi)存節(jié)點(diǎn)。

            當(dāng)啟動(dòng)Mnesia時(shí),你可以給Erlang VM和解釋器一些命令行參數(shù)來告訴Mnesia master數(shù)據(jù)庫在哪里。
            當(dāng)一個(gè)新的本地Mnesia節(jié)點(diǎn)與master Mnesia節(jié)點(diǎn)建立連接之后,新節(jié)點(diǎn)變成master節(jié)點(diǎn)集群的一部分。

            假設(shè)master節(jié)點(diǎn)位于apple和orange節(jié)點(diǎn)上,添加一個(gè)新的網(wǎng)關(guān)、游戲服務(wù)器等等。OpenPoker集群簡單的如下所示:

            代碼: 全選
            erl -mnesia extra_db_nodes /['db@apple','db@orange'/] -s mnesia start


            -s mnesia start相當(dāng)于這樣在erlang shell里啟動(dòng)Mnedia:

            代碼: 全選
            erl -mnesia extra_db_nodes /['db@apple','db@orange'/]
            Erlang (BEAM) emulator version 5.4.8 [source] [hipe] [threads:0]

            Eshell V5.4.8 (abort with ^G)
            1> mnesia:start().
            ok




            OpenPoker在Mnesia表里保存配置信息,并且這些信息在Mnesia啟動(dòng)后立即自動(dòng)被新的節(jié)點(diǎn)下載。零配置!

            容錯(cuò)
            通過添加廉價(jià)的Linux機(jī)器到我的服務(wù)器集群,OpenPoker讓我隨心所欲的變大。
            將幾架1U的服務(wù)器放在一起,這樣你就可以輕易的處理500,000甚至1,000,000的在線玩家。這對MMORPG也是一樣。

            我讓一些機(jī)器運(yùn)行網(wǎng)關(guān)節(jié)點(diǎn),另一些運(yùn)行數(shù)據(jù)庫master來寫數(shù)據(jù)庫事務(wù)到硬盤,讓其他的機(jī)器運(yùn)行游戲服務(wù)器。
            我限制游戲服務(wù)器接受最多5000個(gè)并發(fā)的玩家,這樣當(dāng)游戲服務(wù)器崩潰時(shí)最多影響5000個(gè)玩家。

            值得注意的是,當(dāng)游戲服務(wù)器崩潰時(shí)沒有任何信息丟失,因?yàn)樗械腗nesia數(shù)據(jù)庫事務(wù)都是實(shí)時(shí)備份到其他運(yùn)行Mnesia以及游戲服務(wù)器的節(jié)點(diǎn)上的。

            為了預(yù)防出錯(cuò),游戲客戶端必須提供一些援助來平穩(wěn)的重連接OpenPoker集群。
            一旦客戶端發(fā)現(xiàn)一個(gè)網(wǎng)絡(luò)錯(cuò)誤,它應(yīng)該連接網(wǎng)關(guān),接受一個(gè)新的游戲服務(wù)器地址,然后重新連接新的游戲服務(wù)器。
            下面發(fā)生的事情需要一定技巧,因?yàn)椴煌愋偷闹剡B接場景需要不同的處理。

            OpenPoker會處理如下幾種重連接的場景:
            1,游戲服務(wù)器崩潰
            2,客戶端崩潰或者由于網(wǎng)絡(luò)原因超時(shí)
            3,玩家在線并且在一個(gè)不同的連接上
            4,玩家在線并且在一個(gè)不同的連接上并在一個(gè)游戲中

            最常見的場景是一個(gè)客戶端由于網(wǎng)絡(luò)出錯(cuò)而重新連接。
            比較少見但仍然可能的場景是客戶端已經(jīng)在一臺機(jī)器上玩游戲,而此時(shí)從另一臺機(jī)器上重連接。

            每個(gè)發(fā)送給玩家的OpenPoker游戲緩沖包和每個(gè)重連接的客戶端將首先接受所有的游戲包,因?yàn)橛螒虿皇窍裢ǔD菢诱?dòng)然后接受包。
            OpenPoker使用TCP連接,這樣我不需要擔(dān)心包的順序——包會按正確的順序到達(dá)。

            每個(gè)客戶端連接由兩個(gè)OpenPoker進(jìn)程來表現(xiàn):socket進(jìn)程和真正的玩家進(jìn)程。
            先使用一個(gè)功能受限的visitor進(jìn)程,直到玩家登錄。例如visitor不能參加游戲。
            在客戶端斷開連接后,socket進(jìn)程死掉,而玩家進(jìn)程仍然活著。

            當(dāng)玩家進(jìn)程嘗試發(fā)送一個(gè)游戲包時(shí)可以通知一個(gè)死掉的socket,并讓它自己進(jìn)入auto-play模式或者掛起。
            在重新連接時(shí)登錄代碼將檢查死掉的socket和活著的玩家進(jìn)程的結(jié)合。代碼如下:

            代碼: 全選
            login({atomic, [Player]}, [_Nick, Pass|_] = Args)
              when is_record(Player, player) ->
                Player1 = Player#player {
                  socket = fix_pid(Player#player.socket),
                  pid = fix_pid(Player#player.pid)
                },
                Condition = check_player(Player1, [Pass],
                  [
                    fun is_account_disabled/2,
                    fun is_bad_password/2,
                    fun is_player_busy/2,
                    fun is_player_online/2,
                    fun is_client_down/2,
                    fun is_offline/2
                  ]),
                ...




            condition本身由如下代碼決定:

            代碼: 全選
            is_player_busy(Player, _) ->
              {Online, _} = is_player_online(Player, []),
              Playing = Player#player.game /= none,
              {Online and Playing, player_busy}.

            is_player_online(Player, _) ->
              SocketAlive = Player#player.socket /= none,
              PlayerAlive = Player#player.pid /= none,
              {SocketAlive and PlayerAlive, player_online}.

            is_client_down(Player, _) ->
              SocketDown = Player#player.socket == none,
              PlayerAlive = Player#player.pid /= none,
              {SocketDown and PlayerAlive, client_down}.

            is_offline(Player, _) ->
              SocketDown = Player#player.socket == none,
              PlayerDown = Player#player.pid == none,
              {SocketDown and PlayerDown, player_offline}.



            注意login方法的第一件事是修復(fù)死掉的進(jìn)程id:

            代碼: 全選
            fix_pid(Pid)
              when is_pid(Pid) ->
                case util:is_process_alive(Pid) of
                true ->
                  Pid;
                _->
                  none
                end;

            fix_pid(Pid) ->
                Pid.




            以及:

            代碼: 全選
            -module(util).

            -export([is_process_alive/1]).

            is_process_alive(Pid)
              when is_pid(Pid) ->
                rpc:call(node(Pid), erlang, is_process_alive, [Pid]).




            Erlang里一個(gè)進(jìn)程id包括正在運(yùn)行的進(jìn)程的節(jié)點(diǎn)的id。
            is_pid(Pid)告訴我它的參數(shù)是否是一個(gè)進(jìn)程id(pid),但是不能告訴我進(jìn)程是活著還是死了。
            Erlang自帶的erlang:is_process_alive(Pid)告訴我一個(gè)本地進(jìn)程(運(yùn)行在同一節(jié)點(diǎn)上)是活著還是死了,但沒有檢查遠(yuǎn)程節(jié)點(diǎn)是或者還是死了的is_process_alive變種。

            還好,我可以使用Erlang rpc工具和node(pid)來在遠(yuǎn)程節(jié)點(diǎn)上調(diào)用is_process_alive()。
            事實(shí)上,這跟在本地節(jié)點(diǎn)上一樣工作,這樣上面的代碼就可以作為全局分布式進(jìn)程檢查器。

            剩下的唯一的事情是在不同的登錄條件上活動(dòng)。
            最簡單的情況是玩家離線,我期待一個(gè)玩家進(jìn)程,連接玩家到socket并更新player record。

            代碼: 全選
            login(Player, player_offline, [Nick, _, Socket]) ->
              {ok, Pid} = player:start(Nick),
              OID = gen_server:call(Pid, 'ID'),
              gen_server:cast(Pid, {'SOCKET', Socket}),
              Player1 = Player#player {
                oid = OID,
                pid = Pid,
                socket = Socket
              },
              {Player1, {ok, Pid}}.




            假如玩家登陸信息不匹配,我可以返回一個(gè)錯(cuò)誤并增加錯(cuò)誤登錄次數(shù)。如果次數(shù)超過一個(gè)預(yù)定義的最大值,我就禁止該帳號:

            代碼: 全選
            login(Player, bad_password, _) ->
              N = Player#player.login_errors + 1,
              {atomic, MaxLoginErrors} =
              db:get(cluster_config, 0, max_login_errors),
              if
              N > MaxLoginErrors ->
                Player1 = Player#player {
                  disabled = true
                },
                {Player1, {error, ?ERR_ACCOUNT_DISABLED}};
              true ->
                Player1 = Player#player {
                  login_errors =N
                },
                {Player1, {error, ?ERR_BAD_LOGIN}}
              end;

            login(Player, account_disabled, _) ->
                {Player, {error, ?ERR_ACCOUNT_DISABLED}};




            注銷玩家包括使用Object ID(只是一個(gè)數(shù)字)找到玩家進(jìn)程id,停止玩家進(jìn)程,然后在數(shù)據(jù)庫更新玩家record:

            代碼: 全選
            logout(OID) ->
              case db:find(player, OID) of
              {atomic, [Player]} ->
                player:stop(Player#player.pid),
                {atomic, ok} = db:set(player, OID,
                  [{pid, none},
                  {socket, none}];
              _->
                oops
              end.




            這樣我就可以完成多種重連接condition,例如從不同的機(jī)器重連接,我只需先注銷再登錄:

            代碼: 全選
            login(Player, player_online, Args) ->
              logout(Player#player.oid),
              login(Player, player_offline, Args);




            如果玩家空閑時(shí)客戶端重連接,我所需要做的只是在玩家record里替換socket進(jìn)程id然后告訴玩家進(jìn)程新的socket:

            代碼: 全選
            login(Player, client_down, [_, _, SOcket]) ->
              gen_server:cast(Player#player.pid, {'SOCKET', Socket}),
              Player1 = Player#player {
                socket = Socket
              },
              {Player1, {ok, Player#player.pid}};




            如果玩家在游戲中,這是我們運(yùn)行上面的代碼,然后告訴游戲重新發(fā)送時(shí)間歷史:

            代碼: 全選
            login(Player, player_busy, Args) ->
              Temp = login(Player, client_down, Args),
              cardgame:cast(Player#player.game,
                {'RESEND UPDATES', Player#player.pid}),
              Temp;




            總體來說,一個(gè)實(shí)時(shí)備份數(shù)據(jù)庫,一個(gè)知道重新建立連接到不同的游戲服務(wù)器的客戶端和一些有技巧的登錄代碼運(yùn)行我提供一個(gè)高級容錯(cuò)系統(tǒng)并且對玩家透明。

            負(fù)載均衡
            我可以構(gòu)建自己的OpenPoker集群,游戲服務(wù)器數(shù)量大小隨心所欲。
            我希望每臺游戲服務(wù)器分配5000個(gè)玩家,然后在集群的活動(dòng)游戲服務(wù)器間分散負(fù)載。
            我可以在任何時(shí)間添加一個(gè)新的游戲服務(wù)器,并且它們將自動(dòng)賦予自己接受新玩家的能力。

            網(wǎng)關(guān)節(jié)點(diǎn)分散玩家負(fù)載到OpenPoker集群里活動(dòng)的游戲服務(wù)器。
            網(wǎng)關(guān)節(jié)點(diǎn)的工作是選擇一個(gè)隨機(jī)的游戲服務(wù)器,詢問它所連接的玩家數(shù)量和它的地址、主機(jī)和端口號。
            一旦網(wǎng)關(guān)找到一個(gè)游戲服務(wù)器并且連接的玩家數(shù)量少于最大值,它將返回該游戲服務(wù)器的地址到連接的客戶端,然后關(guān)閉連接。

            網(wǎng)關(guān)上絕對沒有壓力,網(wǎng)關(guān)的連接都非常短。你可以使用非常廉價(jià)的機(jī)器來做網(wǎng)關(guān)節(jié)點(diǎn)。

            節(jié)點(diǎn)一般都成雙成對出現(xiàn),這樣一個(gè)節(jié)點(diǎn)崩潰后還有另一個(gè)繼續(xù)工作。你可能需要一個(gè)類似于Round-robin DNS的機(jī)制來保證不只一個(gè)單獨(dú)的網(wǎng)關(guān)節(jié)點(diǎn)。

            代碼: 全選
            網(wǎng)關(guān)怎么知曉游戲服務(wù)器?



            OpenPoker使用Erlang Distirbuted Named Process Groups工具來為游戲服務(wù)器分組。
            該組自動(dòng)對所有的節(jié)點(diǎn)全局可見。
            新的游戲服務(wù)器進(jìn)入游戲服務(wù)器后,當(dāng)一個(gè)游戲服務(wù)器節(jié)點(diǎn)崩潰時(shí)它被自動(dòng)刪除。

            這是尋找容量最大為MaxPlayers的游戲服務(wù)器的代碼:

            代碼: 全選
            find_server(MaxPlayers) ->
              case pg2:get_closest_pid(?GAME_SERVER) of
              Pid when is_pid(Pid) ->
                {Time, {Host, Port}} = timer:tc(gen_server, call, [Pid, 'WHERE']),
                Coutn = gen_server:call(Pid, 'USER COUNT'),
                if
                  Count < MaxPlayers ->
                    io:format("~s:~w ~w players~n", [Host, Port, Count]),
                    {Host, Port};
                  true ->
                    io:format("~s:~w is full...~n", [Host, Port]),
                    find_server(MaxPlayers)
                end;
              Any ->
                Any
              end.




            pg2:get_closest_pid()返回一個(gè)隨機(jī)的游戲服務(wù)器進(jìn)程id,因?yàn)榫W(wǎng)關(guān)節(jié)點(diǎn)上不允許跑任何游戲服務(wù)器。
            如果一個(gè)游戲服務(wù)器進(jìn)程id返回,我詢問游戲服務(wù)器的地址(host和port)和連接的玩家數(shù)量。
            只要連接的玩家數(shù)量少于最大值,我返回游戲服務(wù)器地址給調(diào)用者,否則繼續(xù)查找。

            代碼: 全選
            多功能熱插拔中間件


            OpenPoker是一個(gè)開源軟件,我最近也正在將其投向許多棋牌類運(yùn)營商。所有商家都存在容錯(cuò)性和可伸縮性的問題,即使有些已經(jīng)經(jīng)過了長年的開發(fā)維護(hù)。有些已經(jīng)重寫了代碼,而有些才剛剛起步。所有商家都在Java體系上大筆投入,所以他們不愿意換到Erlang也是可以理解的。

            但是,對我來說這是一種商機(jī)。我越是深入研究,越發(fā)現(xiàn)Erlang更適合提供一個(gè)簡單直接卻又高效可靠的解決方案。我把這個(gè)解決方案看成一個(gè)多功能插座,就像你現(xiàn)在電源插頭上連著的一樣。

            你的游戲服務(wù)器可以像簡單的單一套接字服務(wù)器一樣的寫,只用一個(gè)數(shù)據(jù)庫后臺。實(shí)際上,可能比你現(xiàn)在的游戲服務(wù)器寫得還要簡單。你的游戲服務(wù)器就好比一個(gè)電源插頭,多種電源插頭接在我的插線板上,而玩家就從另一端流入。

            你提供游戲服務(wù),而我提供可伸縮性,負(fù)載平衡,還有容錯(cuò)性。我保持玩家連到插線板上并監(jiān)視你的游戲服務(wù)器們,在需要的時(shí)候重啟任何一個(gè)。我還可以在某個(gè)服務(wù)器當(dāng)?shù)舻那闆r下把玩家從一個(gè)服務(wù)器切換到另一個(gè),而你可以隨時(shí)插入新的服務(wù)器。

            這么一個(gè)多功能插線板中間件就像一個(gè)黑匣子設(shè)置在玩家與服務(wù)器之間,而且你的游戲代碼不需要做出任何修改。你可以享用這個(gè)方案帶來的高伸縮性,負(fù)載平衡,可容錯(cuò)性等好處,與此同時(shí)節(jié)約投資并寫僅僅修改一小部分體系結(jié)構(gòu)。

            你可以今天就開始寫這個(gè)Erlang的中間件,在一個(gè)特別為TCP連接數(shù)做了優(yōu)化的Linux機(jī)器上運(yùn)行,把這臺機(jī)器放到公眾網(wǎng)上的同時(shí)保持你的游戲服務(wù)器群組在防火墻背后。就算你不打算用,我也建議你抽空看看Erlang考慮一下如何簡化你的多人在線服務(wù)器架構(gòu)。而且我隨時(shí)愿意幫忙!




            posted on 2016-03-15 14:16 思月行云 閱讀(854) 評論(0)  編輯 收藏 引用 所屬分類: Erlang
            久久中文骚妇内射| 囯产极品美女高潮无套久久久| 69SEX久久精品国产麻豆| 成人精品一区二区久久久| 亚洲成av人片不卡无码久久| 亚洲va久久久噜噜噜久久男同| 久久香蕉一级毛片| 亚洲中文字幕无码久久2020| 一本大道久久a久久精品综合| 久久99这里只有精品国产| 91精品国产综合久久香蕉 | 国产无套内射久久久国产| 亚洲精品美女久久久久99小说| aaa级精品久久久国产片| 四虎国产精品成人免费久久| 91久久福利国产成人精品| 色综合久久久久久久久五月| 伊人久久一区二区三区无码| 国产999精品久久久久久| 久久精品九九亚洲精品| 久久精品国产亚洲av麻豆蜜芽| 日韩AV毛片精品久久久| 国产巨作麻豆欧美亚洲综合久久 | 久久精品国产精品国产精品污 | 午夜精品久久久久久| 国产精品日韩欧美久久综合| 99久久亚洲综合精品网站| 2021精品国产综合久久| 久久精品国产亚洲AV高清热| 亚洲中文字幕久久精品无码APP| 中文字幕精品久久久久人妻| 亚洲精品视频久久久| 久久久久99这里有精品10| 四虎久久影院| 少妇熟女久久综合网色欲| 久久久久国产| 亚洲一区精品伊人久久伊人| 亚洲精品乱码久久久久久蜜桃| 久久久久久久久久久久久久| 亚洲av伊人久久综合密臀性色| 欧美牲交A欧牲交aⅴ久久|