• <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>

            陳碩的Blog

            Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)

            陳碩 (giantchen_AT_gmail)

            Blog.csdn.net/Solstice  t.sina.com.cn/giantchen

            Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx

             

            本文介紹 Muduo 中輸入輸出緩沖區(qū)的設(shè)計(jì)與實(shí)現(xiàn)。

            本文中 buffer 指一般的應(yīng)用層緩沖區(qū)、緩沖技術(shù),Buffer 特指 muduo::net::Buffer class。

            本文前兩節(jié)的內(nèi)容已事先發(fā)表在 muduo 英文博客 http://muduo.chenshuo.com/2011/04/essentials-of-non-blocking-tcp-network.html

            Muduo 的 IO 模型

            UNPv1 第 6.2 節(jié)總結(jié)了 Unix/Linux 上的五種 IO 模型:阻塞(blocking)、非阻塞(non-blocking)、IO 復(fù)用(IO multiplexing)、信號(hào)驅(qū)動(dòng)(signal-driven)、異步(asynchronous)。這些都是單線程下的 IO 模型。

            C10k 問題的頁面介紹了五種 IO 策略,把線程也納入考量。(現(xiàn)在 C10k 已經(jīng)不是什么問題,C100k 也不是大問題,C1000k 才算得上挑戰(zhàn))。

            在這個(gè)多核時(shí)代,線程是不可避免的。那么服務(wù)端網(wǎng)絡(luò)編程該如何選擇線程模型呢?我贊同 libev 作者的觀點(diǎn):one loop per thread is usually a good model。之前我也不止一次表述過這個(gè)觀點(diǎn),見《多線程服務(wù)器的常用編程模型》《多線程服務(wù)器的適用場(chǎng)合》。

            如果采用 one loop per thread 的模型,多線程服務(wù)端編程的問題就簡化為如何設(shè)計(jì)一個(gè)高效且易于使用的 event loop,然后每個(gè)線程 run 一個(gè) event loop 就行了(當(dāng)然、同步和互斥是不可或缺的)。在“高效”這方面已經(jīng)有了很多成熟的范例(libev、libevent、memcached、varnish、lighttpd、nginx),在“易于使用”方面我希望 muduo 能有所作為。(muduo 可算是用現(xiàn)代 C++ 實(shí)現(xiàn)了 Reactor 模式,比起原始的 Reactor 來說要好用得多。)

            event loop 是 non-blocking 網(wǎng)絡(luò)編程的核心,在現(xiàn)實(shí)生活中,non-blocking 幾乎總是和 IO-multiplexing 一起使用,原因有兩點(diǎn):

            • 沒有人真的會(huì)用輪詢 (busy-pooling) 來檢查某個(gè) non-blocking IO 操作是否完成,這樣太浪費(fèi) CPU cycles。
            • IO-multiplex 一般不能和 blocking IO 用在一起,因?yàn)?blocking IO 中 read()/write()/accept()/connect() 都有可能阻塞當(dāng)前線程,這樣線程就沒辦法處理其他 socket 上的 IO 事件了。見 UNPv1 第 16.6 節(jié)“nonblocking accept”的例子。

            所以,當(dāng)我提到 non-blocking 的時(shí)候,實(shí)際上指的是 non-blocking + IO-muleiplexing,單用其中任何一個(gè)是不現(xiàn)實(shí)的。另外,本文所有的“連接”均指 TCP 連接,socket 和 connection 在文中可互換使用。

            當(dāng)然,non-blocking 編程比 blocking 難得多,見陳碩在《Muduo 網(wǎng)絡(luò)編程示例之零:前言》中“TCP 網(wǎng)絡(luò)編程本質(zhì)論”一節(jié)列舉的難點(diǎn)。基于 event loop 的網(wǎng)絡(luò)編程跟直接用 C/C++ 編寫單線程 Windows 程序頗為相像:程序不能阻塞,否則窗口就失去響應(yīng)了;在 event handler 中,程序要盡快交出控制權(quán),返回窗口的事件循環(huán)。

            為什么 non-blocking 網(wǎng)絡(luò)編程中應(yīng)用層 buffer 是必須的?

            Non-blocking IO 的核心思想是避免阻塞在 read() 或 write() 或其他 IO 系統(tǒng)調(diào)用上,這樣可以最大限度地復(fù)用 thread-of-control,讓一個(gè)線程能服務(wù)于多個(gè) socket 連接。IO 線程只能阻塞在 IO-multiplexing 函數(shù)上,如 select()/poll()/epoll_wait()。這樣一來,應(yīng)用層的緩沖是必須的,每個(gè) TCP socket 都要有 stateful 的 input buffer 和 output buffer。

            TcpConnection 必須要有 output buffer

            考慮一個(gè)常見場(chǎng)景:程序想通過 TCP 連接發(fā)送 100k 字節(jié)的數(shù)據(jù),但是在 write() 調(diào)用中,操作系統(tǒng)只接受了 80k 字節(jié)(受 TCP advertised window 的控制,細(xì)節(jié)見 TCPv1),你肯定不想在原地等待,因?yàn)椴恢罆?huì)等多久(取決于對(duì)方什么時(shí)候接受數(shù)據(jù),然后滑動(dòng) TCP 窗口)。程序應(yīng)該盡快交出控制權(quán),返回 event loop。在這種情況下,剩余的 20k 字節(jié)數(shù)據(jù)怎么辦?

            對(duì)于應(yīng)用程序而言,它只管生成數(shù)據(jù),它不應(yīng)該關(guān)心到底數(shù)據(jù)是一次性發(fā)送還是分成幾次發(fā)送,這些應(yīng)該由網(wǎng)絡(luò)庫來操心,程序只要調(diào)用 TcpConnection::send() 就行了,網(wǎng)絡(luò)庫會(huì)負(fù)責(zé)到底。網(wǎng)絡(luò)庫應(yīng)該接管這剩余的 20k 字節(jié)數(shù)據(jù),把它保存在該 TCP connection 的 output buffer 里,然后注冊(cè) POLLOUT 事件,一旦 socket 變得可寫就立刻發(fā)送數(shù)據(jù)。當(dāng)然,這第二次 write() 也不一定能完全寫入 20k 字節(jié),如果還有剩余,網(wǎng)絡(luò)庫應(yīng)該繼續(xù)關(guān)注 POLLOUT 事件;如果寫完了 20k 字節(jié),網(wǎng)絡(luò)庫應(yīng)該停止關(guān)注 POLLOUT,以免造成 busy loop。(Muduo EventLoop 采用的是 epoll level trigger,這么做的具體原因我以后再說。)

            如果程序又寫入了 50k 字節(jié),而這時(shí)候 output buffer 里還有待發(fā)送的 20k 數(shù)據(jù),那么網(wǎng)絡(luò)庫不應(yīng)該直接調(diào)用 write(),而應(yīng)該把這 50k 數(shù)據(jù) append 在那 20k 數(shù)據(jù)之后,等 socket 變得可寫的時(shí)候再一并寫入。

            如果 output buffer 里還有待發(fā)送的數(shù)據(jù),而程序又想關(guān)閉連接(對(duì)程序而言,調(diào)用 TcpConnection::send() 之后他就認(rèn)為數(shù)據(jù)遲早會(huì)發(fā)出去),那么這時(shí)候網(wǎng)絡(luò)庫不能立刻關(guān)閉連接,而要等數(shù)據(jù)發(fā)送完畢,見我在《為什么 muduo 的 shutdown() 沒有直接關(guān)閉 TCP 連接?》一文中的講解。

            綜上,要讓程序在 write 操作上不阻塞,網(wǎng)絡(luò)庫必須要給每個(gè) tcp connection 配置 output buffer。

            TcpConnection 必須要有 input buffer

            TCP 是一個(gè)無邊界的字節(jié)流協(xié)議,接收方必須要處理“收到的數(shù)據(jù)尚不構(gòu)成一條完整的消息”和“一次收到兩條消息的數(shù)據(jù)”等等情況。一個(gè)常見的場(chǎng)景是,發(fā)送方 send 了兩條 10k 字節(jié)的消息(共 20k),接收方收到數(shù)據(jù)的情況可能是:

            • 一次性收到 20k 數(shù)據(jù)
            • 分兩次收到,第一次 5k,第二次 15k
            • 分兩次收到,第一次 15k,第二次 5k
            • 分兩次收到,第一次 10k,第二次 10k
            • 分三次收到,第一次 6k,第二次 8k,第三次 6k
            • 其他任何可能

            網(wǎng)絡(luò)庫在處理“socket 可讀”事件的時(shí)候,必須一次性把 socket 里的數(shù)據(jù)讀完(從操作系統(tǒng) buffer 搬到應(yīng)用層 buffer),否則會(huì)反復(fù)觸發(fā) POLLIN 事件,造成 busy-loop。(Again, Muduo EventLoop 采用的是 epoll level trigger,這么做的具體原因我以后再說。)

            那么網(wǎng)絡(luò)庫必然要應(yīng)對(duì)“數(shù)據(jù)不完整”的情況,收到的數(shù)據(jù)先放到 input buffer 里,等構(gòu)成一條完整的消息再通知程序的業(yè)務(wù)邏輯。這通常是 codec 的職責(zé),見陳碩《Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器》一文中的“TCP 分包”的論述與代碼。

            所以,在 tcp 網(wǎng)絡(luò)編程中,網(wǎng)絡(luò)庫必須要給每個(gè) tcp connection 配置 input buffer。

             

            所有 muduo 中的 IO 都是帶緩沖的 IO (buffered IO),你不會(huì)自己去 read() 或 write() 某個(gè) socket,只會(huì)操作 TcpConnection 的 input buffer 和 output buffer。更確切的說,是在 onMessage() 回調(diào)里讀取 input buffer;調(diào)用 TcpConnection::send() 來間接操作 output buffer,一般不會(huì)直接操作 output buffer。

            btw, muduo 的 onMessage() 的原型如下,它既可以是 free function,也可以是 member function,反正 muduo TcpConnection 只認(rèn) boost::function<>。

            void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime);

            對(duì)于網(wǎng)絡(luò)程序來說,一個(gè)簡單的驗(yàn)收測(cè)試是:輸入數(shù)據(jù)每次收到一個(gè)字節(jié)(200 字節(jié)的輸入數(shù)據(jù)會(huì)分 200 次收到,每次間隔 10 ms),程序的功能不受影響。對(duì)于 Muduo 程序,通常可以用 codec 來分離“消息接收”與“消息處理”,見陳碩《在 muduo 中實(shí)現(xiàn) protobuf 編解碼器與消息分發(fā)器》一文中對(duì)“編解碼器 codec”的介紹。

            如果某個(gè)網(wǎng)絡(luò)庫只提供相當(dāng)于 char buf[8192] 的緩沖,或者根本不提供緩沖區(qū),而僅僅通知程序“某 socket 可讀/某 socket 可寫”,要程序自己操心 IO buffering,這樣的網(wǎng)絡(luò)庫用起來就很不方便了。(我有所指,你懂得。)

             

            Buffer 的要求

            http://code.google.com/p/muduo/source/browse/trunk/muduo/net/Buffer.h

            Muduo Buffer 的設(shè)計(jì)考慮了常見的網(wǎng)絡(luò)編程需求,我試圖在易用性和性能之間找一個(gè)平衡點(diǎn),目前這個(gè)平衡點(diǎn)更偏向于易用性。

            Muduo Buffer 的設(shè)計(jì)要點(diǎn):

            • 對(duì)外表現(xiàn)為一塊連續(xù)的內(nèi)存(char*, len),以方便客戶代碼的編寫。
            • 其 size() 可以自動(dòng)增長,以適應(yīng)不同大小的消息。它不是一個(gè) fixed size array (即 char buf[8192])。
            • 內(nèi)部以 vector of char 來保存數(shù)據(jù),并提供相應(yīng)的訪問函數(shù)。

            Buffer 其實(shí)像是一個(gè) queue,從末尾寫入數(shù)據(jù),從頭部讀出數(shù)據(jù)。

            誰會(huì)用 Buffer?誰寫誰讀?根據(jù)前文分析,TcpConnection 會(huì)有兩個(gè) Buffer 成員,input buffer 與 output buffer。

            • input buffer,TcpConnection 會(huì)從 socket 讀取數(shù)據(jù),然后寫入 input buffer(其實(shí)這一步是用 Buffer::readFd() 完成的);客戶代碼從 input buffer 讀取數(shù)據(jù)。
            • output buffer,客戶代碼會(huì)把數(shù)據(jù)寫入 output buffer(其實(shí)這一步是用 TcpConnection::send() 完成的);TcpConnection 從 output buffer 讀取數(shù)據(jù)并寫入 socket。

            其實(shí),input 和 output 是針對(duì)客戶代碼而言,客戶代碼從 input 讀,往 output 寫。TcpConnection 的讀寫正好相反。

            以下是 muduo::net::Buffer 的類圖。請(qǐng)注意,為了后面畫圖方便,這個(gè)類圖跟實(shí)際代碼略有出入,但不影響我要表達(dá)的觀點(diǎn)。

            bc

            這里不介紹每個(gè)成員函數(shù)的作用,留給《Muduo 網(wǎng)絡(luò)編程示例》系列。下文會(huì)仔細(xì)介紹 readIndex 和 writeIndex 的作用。

            Buffer::readFd()

            我在《Muduo 網(wǎng)絡(luò)編程示例之零:前言》中寫道

            • 在非阻塞網(wǎng)絡(luò)編程中,如何設(shè)計(jì)并使用緩沖區(qū)?一方面我們希望減少系統(tǒng)調(diào)用,一次讀的數(shù)據(jù)越多越劃算,那么似乎應(yīng)該準(zhǔn)備一個(gè)大的緩沖區(qū)。另一方面,我們系統(tǒng)減少內(nèi)存占用。如果有 10k 個(gè)連接,每個(gè)連接一建立就分配 64k 的讀緩沖的話,將占用 640M 內(nèi)存,而大多數(shù)時(shí)候這些緩沖區(qū)的使用率很低。muduo 用 readv 結(jié)合棧上空間巧妙地解決了這個(gè)問題。

            具體做法是,在棧上準(zhǔn)備一個(gè) 65536 字節(jié)的 stackbuf,然后利用 readv() 來讀取數(shù)據(jù),iovec 有兩塊,第一塊指向 muduo Buffer 中的 writable 字節(jié),另一塊指向棧上的 stackbuf。這樣如果讀入的數(shù)據(jù)不多,那么全部都讀到 Buffer 中去了;如果長度超過 Buffer 的 writable 字節(jié)數(shù),就會(huì)讀到棧上的 stackbuf 里,然后程序再把 stackbuf 里的數(shù)據(jù) append 到 Buffer 中。

            代碼見 http://code.google.com/p/muduo/source/browse/trunk/muduo/net/Buffer.cc#36

            這么做利用了臨時(shí)棧上空間,避免開巨大 Buffer 造成的內(nèi)存浪費(fèi),也避免反復(fù)調(diào)用 read() 的系統(tǒng)開銷(通常一次 readv() 系統(tǒng)調(diào)用就能讀完全部數(shù)據(jù))。

            這算是一個(gè)小小的創(chuàng)新吧。

            線程安全?

            muduo::net::Buffer 不是線程安全的,這么做是有意的,原因如下:

            • 對(duì)于 input buffer,onMessage() 回調(diào)始終發(fā)生在該 TcpConnection 所屬的那個(gè) IO 線程,應(yīng)用程序應(yīng)該在 onMessage() 完成對(duì) input buffer 的操作,并且不要把 input buffer 暴露給其他線程。這樣所有對(duì) input buffer 的操作都在同一個(gè)線程,Buffer class 不必是線程安全的。
            • 對(duì)于 output buffer,應(yīng)用程序不會(huì)直接操作它,而是調(diào)用 TcpConnection::send() 來發(fā)送數(shù)據(jù),后者是線程安全的。

            如果 TcpConnection::send() 調(diào)用發(fā)生在該 TcpConnection 所屬的那個(gè) IO 線程,那么它會(huì)轉(zhuǎn)而調(diào)用 TcpConnection::sendInLoop(),sendInLoop() 會(huì)在當(dāng)前線程(也就是 IO 線程)操作 output buffer;如果 TcpConnection::send() 調(diào)用發(fā)生在別的線程,它不會(huì)在當(dāng)前線程調(diào)用 sendInLoop() ,而是通過 EventLoop::runInLoop() 把 sendInLoop() 函數(shù)調(diào)用轉(zhuǎn)移到 IO 線程(聽上去頗為神奇?),這樣 sendInLoop() 還是會(huì)在 IO 線程操作 output buffer,不會(huì)有線程安全問題。當(dāng)然,跨線程的函數(shù)轉(zhuǎn)移調(diào)用涉及函數(shù)參數(shù)的跨線程傳遞,一種簡單的做法是把數(shù)據(jù)拷一份,絕對(duì)安全(不明白的同學(xué)請(qǐng)閱讀代碼)。

            另一種更為高效做法是用 swap()。這就是為什么 TcpConnection::send() 的某個(gè)重載以 Buffer* 為參數(shù),而不是 const Buffer&,這樣可以避免拷貝,而用 Buffer::swap() 實(shí)現(xiàn)高效的線程間數(shù)據(jù)轉(zhuǎn)移。(最后這點(diǎn),僅為設(shè)想,暫未實(shí)現(xiàn)。目前仍然以數(shù)據(jù)拷貝方式在線程間傳遞,略微有些性能損失。)

             

            Muduo Buffer 的數(shù)據(jù)結(jié)構(gòu)

            Buffer 的內(nèi)部是一個(gè) vector of char,它是一塊連續(xù)的內(nèi)存。此外,Buffer 有兩個(gè) data members,指向該 vector 中的元素。這兩個(gè) indices 的類型是 int,不是 char*,目的是應(yīng)對(duì)迭代器失效。muduo Buffer 的設(shè)計(jì)參考了 Netty 的 ChannelBuffer 和 libevent 1.4.x 的 evbuffer。不過,其 prependable 可算是一點(diǎn)“微創(chuàng)新”。

            Muduo Buffer 的數(shù)據(jù)結(jié)構(gòu)如下:

            圖 1buffer0

            兩個(gè) indices 把 vector 的內(nèi)容分為三塊:prependable、readable、writable,各塊的大小是(公式一):

            prependable = readIndex

            readable = writeIndex - readIndex

            writable = size() - writeIndex

            (prependable 的作用留到后面討論。)

            readIndex 和 writeIndex 滿足以下不變式(invariant):

            0 ≤ readIndex ≤ writeIndex ≤ data.size()

            Muduo Buffer 里有兩個(gè)常數(shù) kCheapPrepend 和 kInitialSize,定義了 prependable 的初始大小和 writable 的初始大小。(readable 的初始大小為 0。)在初始化之后,Buffer 的數(shù)據(jù)結(jié)構(gòu)如下:括號(hào)里的數(shù)字是該變量或常量的值。

            圖 2buffer1

            根據(jù)以上(公式一)可算出各塊的大小,剛剛初始化的 Buffer 里沒有 payload 數(shù)據(jù),所以 readable == 0。

            Muduo Buffer 的操作

            1. 基本的 read-write cycle

            Buffer 初始化后的情況見圖 1,如果有人向 Buffer 寫入了 200 字節(jié),那么其布局是:

            圖 3buffer2

            圖 3 中 writeIndex 向后移動(dòng)了 200 字節(jié),readIndex 保持不變,readable 和 writable 的值也有變化。

             

            如果有人從 Buffer read() & retrieve() (下稱“讀入”)了 50 字節(jié),結(jié)果見圖 4。與上圖相比,readIndex 向后移動(dòng) 50 字節(jié),writeIndex 保持不變,readable 和 writable 的值也有變化(這句話往后從略)。

            圖 4buffer3

             

            然后又寫入了 200 字節(jié),writeIndex 向后移動(dòng)了 200 字節(jié),readIndex 保持不變,見圖 5

            圖 5buffer4

             

            接下來,一次性讀入 350 字節(jié),請(qǐng)注意,由于全部數(shù)據(jù)讀完了,readIndex 和 writeIndex 返回原位以備新一輪使用,見圖 6,這和圖 2 是一樣的。

            圖 6buffer5

             

            以上過程可以看作是發(fā)送方發(fā)送了兩條消息,長度分別為 50 字節(jié)和 350 字節(jié),接收方分兩次收到數(shù)據(jù),每次 200 字節(jié),然后進(jìn)行分包,再分兩次回調(diào)客戶代碼。

             

            自動(dòng)增長

            Muduo Buffer 不是固定長度的,它可以自動(dòng)增長,這是使用 vector 的直接好處。

            假設(shè)當(dāng)前的狀態(tài)如圖 7 所示。(這和前面圖 5 是一樣的。)

            圖 7buffer4

             

            客戶代碼一次性寫入 1000 字節(jié),而當(dāng)前可寫的字節(jié)數(shù)只有 624,那么 buffer 會(huì)自動(dòng)增長以容納全部數(shù)據(jù),得到的結(jié)果是圖 8。注意 readIndex 返回到了前面,以保持 prependable 等于 kCheapPrependable。由于 vector 重新分配了內(nèi)存,原來指向它元素的指針會(huì)失效,這就是為什么 readIndex 和 writeIndex 是整數(shù)下標(biāo)而不是指針。

            圖 8buffer6

             

            然后讀入 350 字節(jié),readIndex 前移,見圖 9

            圖 9buffer7

             

            最后,讀完剩下的 1000 字節(jié),readIndex 和 writeIndex 返回 kCheapPrependable,見圖 10。

            圖 10buffer8

            注意 buffer 并沒有縮小大小,下次寫入 1350 字節(jié)就不會(huì)重新分配內(nèi)存了。換句話說,Muduo Buffer 的 size() 是自適應(yīng)的,它一開始的初始值是 1k,如果程序里邊經(jīng)常收發(fā) 10k 的數(shù)據(jù),那么用幾次之后它的 size() 會(huì)自動(dòng)增長到 10k,然后就保持不變。這樣一方面避免浪費(fèi)內(nèi)存(有的程序可能只需要 4k 的緩沖),另一方面避免反復(fù)分配內(nèi)存。當(dāng)然,客戶代碼可以手動(dòng) shrink() buffer size()。

            size() 與 capacity()

            使用 vector 的另一個(gè)好處是它的 capcity() 機(jī)制減少了內(nèi)存分配的次數(shù)。比方說程序反復(fù)寫入 1 字節(jié),muduo Buffer 不會(huì)每次都分配內(nèi)存,vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。比方說經(jīng)過第一次增長,size() 剛好滿足寫入的需求,如圖 11。但這個(gè)時(shí)候 vector 的 capacity() 已經(jīng)大于 size(),在接下來寫入 capacity()-size() 字節(jié)的數(shù)據(jù)時(shí),都不會(huì)重新分配內(nèi)存,見圖 12

            圖 11buffer6

            圖 12buffer9

             

            細(xì)心的讀者可能會(huì)發(fā)現(xiàn)用 capacity() 也不是完美的,它有優(yōu)化的余地。具體來說,vector::resize() 會(huì)初始化(memset/bzero)內(nèi)存,而我們不需要它初始化,因?yàn)榉凑⒖叹鸵钊霐?shù)據(jù)。比如,在圖 12 的基礎(chǔ)上寫入 200 字節(jié),由于 capacity() 足夠大,不會(huì)重新分配內(nèi)存,這是好事;但是 vector::resize() 會(huì)先把那 200 字節(jié)設(shè)為 0 (圖 13),然后 muduo buffer 再填入數(shù)據(jù)(圖 14)。這么做稍微有點(diǎn)浪費(fèi),不過我不打算優(yōu)化它,除非它確實(shí)造成了性能瓶頸。(精通 STL 的讀者可能會(huì)說用 vector::append() 以避免浪費(fèi),但是 writeIndex 和 size() 不一定是對(duì)齊的,會(huì)有別的麻煩。)

             

            圖 13buffer9a

            圖 14buffer9b

            google protobuf 中有一個(gè) STLStringResizeUninitialized 函數(shù),干的就是這個(gè)事情。

            內(nèi)部騰挪

            有時(shí)候,經(jīng)過若干次讀寫,readIndex 移到了比較靠后的位置,留下了巨大的 prependable 空間,見圖 14

            圖 14buffer10

             

            這時(shí)候,如果我們想寫入 300 字節(jié),而 writable 只有 200 字節(jié),怎么辦?muduo Buffer 在這種情況下不會(huì)重新分配內(nèi)存,而是先把已有的數(shù)據(jù)移到前面去,騰出 writable 空間,見圖 15

            圖 15buffer11

             

            然后,就可以寫入 300 字節(jié)了,見圖 16

            圖 16buffer12

            這么做的原因是,如果重新分配內(nèi)存,反正也是要把數(shù)據(jù)拷到新分配的內(nèi)存區(qū)域,代價(jià)只會(huì)更大。

            prepend

            前面說 muduo Buffer 有個(gè)小小的創(chuàng)新(或許不是創(chuàng)新,我記得在哪兒看到過類似的做法,忘了出處),即提供 prependable 空間,讓程序能以很低的代價(jià)在數(shù)據(jù)前面添加幾個(gè)字節(jié)。

            比方說,程序以固定的4個(gè)字節(jié)表示消息的長度(即《Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器》中的 LengthHeaderCodec),我要序列化一個(gè)消息,但是不知道它有多長,那么我可以一直 append() 直到序列化完成(圖 17,寫入了 200 字節(jié)),然后再在序列化數(shù)據(jù)的前面添加消息的長度(圖 18,把 200 這個(gè)數(shù) prepend 到首部)。

             

            圖 17buffer13

             

            圖 18buffer14

            通過預(yù)留 kCheapPrependable 空間,可以簡化客戶代碼,一個(gè)簡單的空間換時(shí)間思路。

            其他設(shè)計(jì)方案

            這里簡單談?wù)勂渌赡艿膽?yīng)用層 buffer 設(shè)計(jì)方案。

            不用 vector<char>?

            如果有 STL 潔癖,那么可以自己管理內(nèi)存,以 4 個(gè)指針為 buffer 的成員,數(shù)據(jù)結(jié)構(gòu)見圖 19。

            圖 19alternative

            說實(shí)話我不覺得這種方案比 vector 好。代碼變復(fù)雜,性能也未見得有 noticeable 的改觀。

            如果放棄“連續(xù)性”要求,可以用 circular buffer,這樣可以減少一點(diǎn)內(nèi)存拷貝(沒有“內(nèi)部騰挪”)。

            Zero copy ?

            如果對(duì)性能有極高的要求,受不了 copy() 與 resize(),那么可以考慮實(shí)現(xiàn)分段連續(xù)的 zero copy buffer 再配合 gather scatter IO,數(shù)據(jù)結(jié)構(gòu)如圖 20,這是 libevent 2.0.x 的設(shè)計(jì)方案。TCPv2介紹的 BSD TCP/IP 實(shí)現(xiàn)中的 mbuf 也是類似的方案,Linux 的 sk_buff 估計(jì)也差不多。細(xì)節(jié)有出入,但基本思路都是不要求數(shù)據(jù)在內(nèi)存中連續(xù),而是用鏈表把數(shù)據(jù)塊鏈接到一起。

            圖 20evbuf0

            當(dāng)然,高性能的代價(jià)是代碼變得晦澀難讀,buffer 不再是連續(xù)的,parse 消息會(huì)稍微麻煩。如果你的程序只處理 protobuf Message,這不是問題,因?yàn)?protobuf 有 ZeroCopyInputStream 接口,只要實(shí)現(xiàn)這個(gè)接口,parsing 的事情就交給 protobuf Message 去操心了。

            性能是不是問題?看跟誰比

            看到這里,有的讀者可能會(huì)嘀咕,muduo Buffer 有那么多可以優(yōu)化的地方,其性能會(huì)不會(huì)太低?對(duì)此,我的回應(yīng)是“可以優(yōu)化,不一定值得優(yōu)化。”

            Muduo 的設(shè)計(jì)目標(biāo)是用于開發(fā)公司內(nèi)部的分布式程序。換句話說,它是用來寫專用的 Sudoku server 或者游戲服務(wù)器,不是用來寫通用的 httpd 或 ftpd 或 www proxy。前者通常有業(yè)務(wù)邏輯,后者更強(qiáng)調(diào)高并發(fā)與高吞吐。

            以 Sudoku 為例,假設(shè)求解一個(gè) Sudoku 問題需要 0.2ms,服務(wù)器有 8 個(gè)核,那么理想情況下每秒最多能求解 40,000 個(gè)問題。每次 Sudoku 請(qǐng)求的數(shù)據(jù)大小低于 100 字節(jié)(一個(gè) 9x9 的數(shù)獨(dú)只要 81 字節(jié),加上 header 也可以控制在 100 bytes 以下),就是說 100 x 40000 = 4 MB per second 的吞吐量就足以讓服務(wù)器的 CPU 飽和。在這種情況下,去優(yōu)化 Buffer 的內(nèi)存拷貝次數(shù)似乎沒有意義。

            再舉一個(gè)例子,目前最常用的千兆以太網(wǎng)的裸吞吐量是 125MB/s,扣除以太網(wǎng) header、IP header、TCP header之后,應(yīng)用層的吞吐率大約在 115 MB/s 上下。而現(xiàn)在服務(wù)器上最常用的 DDR2/DDR3 內(nèi)存的帶寬至少是 4GB/s,比千兆以太網(wǎng)高 40 倍以上。就是說,對(duì)于幾 k 或幾十 k 大小的數(shù)據(jù),在內(nèi)存里邊拷幾次根本不是問題,因?yàn)槭芤蕴W(wǎng)延遲和帶寬的限制,跟這個(gè)程序通信的其他機(jī)器上的程序不會(huì)覺察到性能差異。

            最后舉一個(gè)例子,如果你實(shí)現(xiàn)的服務(wù)程序要跟數(shù)據(jù)庫打交道,那么瓶頸常常在 DB 上,優(yōu)化服務(wù)程序本身不見得能提高性能(從 DB 讀一次數(shù)據(jù)往往就抵消了你做的全部 low-level 優(yōu)化),這時(shí)不如把精力投入在 DB 調(diào)優(yōu)上。

            專用服務(wù)程序與通用服務(wù)程序的另外一點(diǎn)區(qū)別是 benchmark 的對(duì)象不同。如果你打算寫一個(gè) httpd,自然有人會(huì)拿來和目前最好的 nginx 對(duì)比,立馬就能比出性能高低。然而,如果你寫一個(gè)實(shí)現(xiàn)公司內(nèi)部業(yè)務(wù)的服務(wù)程序(比如分布式存儲(chǔ)或者搜索或者微博或者短網(wǎng)址),由于市面上沒有同等功能的開源實(shí)現(xiàn),你不需要在優(yōu)化上投入全部精力,只要一版做得比一版好就行。先正確實(shí)現(xiàn)所需的功能,投入生產(chǎn)應(yīng)用,然后再根據(jù)真實(shí)的負(fù)載情況來做優(yōu)化,這恐怕比在編碼階段就盲目調(diào)優(yōu)要更 effective 一些。

            Muduo 的設(shè)計(jì)目標(biāo)之一是吞吐量能讓千兆以太網(wǎng)飽和,也就是每秒收發(fā) 120 兆字節(jié)的數(shù)據(jù)。這個(gè)很容易就達(dá)到,不用任何特別的努力。

            如果確實(shí)在內(nèi)存帶寬方面遇到問題,說明你做的應(yīng)用實(shí)在太 critical,或許應(yīng)該考慮放到 Linux kernel 里邊去,而不是在用戶態(tài)嘗試各種優(yōu)化。畢竟只有把程序做到 kernel 里才能真正實(shí)現(xiàn) zero copy,否則,核心態(tài)和用戶態(tài)之間始終是有一次內(nèi)存拷貝的。如果放到 kernel 里還不能滿足需求,那么要么自己寫新的 kernel,或者直接用 FPGA 或 ASIC 操作 network adapter 來實(shí)現(xiàn)你的高性能服務(wù)器。

            (待續(xù))

            posted on 2011-04-17 12:24 陳碩 閱讀(9821) 評(píng)論(28)  編輯 收藏 引用

            評(píng)論

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-17 16:54 by

            我用的定長BUFFER組來做的這個(gè)BUFFER。
            另外你提到的ZEROCOPY的那個(gè),我以前在群里也提過,就是底層分配一個(gè)STREAM,讓應(yīng)用層來進(jìn)行讀寫操作。
            不過你的BUFFER的prepend確實(shí)是個(gè)很好的東西。我這邊的這個(gè),暫時(shí)是放在應(yīng)用層的。應(yīng)用層數(shù)據(jù)進(jìn)入底層,全部做了一次拷貝,成為底層的BUFFER。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-17 16:58 by

            關(guān)于協(xié)議解析的問題,我直接把CODEC做成底層插件,應(yīng)用層實(shí)現(xiàn)后,就插進(jìn)去,而且目前是靠單條消息緩沖實(shí)現(xiàn)的。
            以后要做的事情,就是結(jié)合到零拷貝那里面,做一個(gè)ZEROCOPY的STREAM出來,讓應(yīng)用層全部使用這種消息解析和處理的方式。
              回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-17 21:14 vincent

            有木有實(shí)際的測(cè)試數(shù)據(jù)可供參考啊

            另外我個(gè)人覺得對(duì)于這個(gè)input buffer,我覺得它可一直擴(kuò)展不見得是好事
            如果應(yīng)用層真處理的很慢,造成buffer增長到內(nèi)存吃不消的時(shí)候還是很尷尬的
            個(gè)人覺得還是有一個(gè)上限,把大于上限的阻塞于TCP的好  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-17 21:33 陳碩

            @vincent
            實(shí)際的測(cè)試數(shù)據(jù):
            http://www.shnenglu.com/Solstice/archive/2010/09/04/muduo_vs_asio.html
            http://www.shnenglu.com/Solstice/archive/2010/09/05/muduo_vs_libevent.html  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-18 09:26 zuhd

            想問下 你寫這樣一篇博客是如何排版的?用了哪些工具?大概需要多長時(shí)間?謝謝  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-18 16:37 城市男人

            還是很不錯(cuò)的  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-18 19:25 陳碩

            @zuhd
            Live Writer 和 gpic。
            畫圖一天,碼字一天。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-19 09:02 打擊裝B犯

            樓主雖然長的憨了一點(diǎn), 但水平還是不錯(cuò)  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-20 00:26 so

            在之前項(xiàng)目中,也有你在prepend提到的問題,我是pop一個(gè)引用(會(huì)移動(dòng)寫位置),然后寫入數(shù)據(jù)。最后再最之前pop的那個(gè)引用進(jìn)行賦值。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-24 23:49 楊粼波

            自己先好好學(xué)一學(xué)STL。
            還capacity()機(jī)制,寒……
            先仔細(xì)看看std::vector吧,
            調(diào)用reserve()才會(huì)預(yù)分配內(nèi)存。
            這些都是STL的內(nèi)存分配機(jī)制的問題。
            自己先多學(xué)點(diǎn)東西吧。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-25 07:30 陳碩

            @楊粼波
            “調(diào)用reserve()才會(huì)預(yù)分配內(nèi)存”?!這是哪家的 STL?
            我原文說了,“vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。”
            如果“調(diào)用reserve()才會(huì)預(yù)分配內(nèi)存”如何達(dá)到 push_back 的平均復(fù)雜度要求?

            幾行代碼就能驗(yàn)證的事情:

            vector<char> vec;
            printf("%zd %zd\n", vec.size(), vec.capacity());
            vec.resize(1024);
            printf("%zd %zd\n", vec.size(), vec.capacity());
            vec.resize(1300);
            printf("%zd %zd\n", vec.size(), vec.capacity());

            運(yùn)行結(jié)果:

            0 0
            1024 1024
            1300 2048


            原話奉還:“自己先好好學(xué)一學(xué)STL。這些都是STL的內(nèi)存分配機(jī)制的問題。
            自己先多學(xué)點(diǎn)東西吧。”
              回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-25 08:01 halida

            “vector 的 capacity() 以指數(shù)方式增長,讓 push_back() 的平均復(fù)雜度是常數(shù)。”

            這是可變長vector的典型實(shí)現(xiàn)方式, 讓push操作變成O(1)的, 算法導(dǎo)論上面有寫.  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-25 14:11 路人甲

            看了一下@楊粼波的博客,醒目處寫著 “嚴(yán)以律己,寬以待人. 三思而后行....每日自省,慎言敏行...", 這讓我想起了0bug老師,技術(shù)對(duì)錯(cuò)無關(guān)緊要(雖技術(shù)高低可以一眼看出),可以討論,惡語相向,實(shí)不可取也!  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-25 14:23 路人甲

            如果output buffer堆積的數(shù)據(jù)太多,在remalloc或者平臺(tái)根本不提供remalloc的情況下, append操作開銷還是比較大的,可以考慮采用一個(gè)隊(duì)列,隊(duì)列保存的基本單元是 memory block或者vector, 但這無形中會(huì)增加小內(nèi)存的分配,也導(dǎo)致更多的 send操作,即更多的系統(tǒng)調(diào)用,采用混合的方法可能好一點(diǎn),根據(jù) Queue.last_in_vector_item的長度來決定是對(duì) Queue.last_in_vector_item執(zhí)行 append操作,還是對(duì)Queue執(zhí)行 putItem操作。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-25 14:58 陳碩

            @路人甲
            確實(shí),output buffer 不必是連續(xù)的,反正復(fù)雜性丟給 TcpConnection 唄,可以用 writev 來減少系統(tǒng)調(diào)用次數(shù)。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2011-04-26 00:13 楊粼波

            std::vector進(jìn)行一次內(nèi)存分配的成本是很高的,需要銷毀掉原有的內(nèi)存塊,創(chuàng)建一塊新的內(nèi)存塊,然后還有進(jìn)行一次拷貝。啟動(dòng)的時(shí)候,使用std::vector::reserve()進(jìn)行預(yù)先分配,成本將會(huì)低上許多,即使是超支了,再分配的次數(shù)也是很微小的。這是一種空間換時(shí)間的做法。

            像Buffer這種可以預(yù)先知道大致大小的場(chǎng)景,可以使用std::vector::reserve()預(yù)先分配好內(nèi)存塊。而非在使用時(shí)實(shí)時(shí)的進(jìn)行多次的動(dòng)態(tài)分配,雖然時(shí)間復(fù)雜度不大,但是代價(jià)還是是很大的。

            std::vector::capacity() 返回vector所能容納的元素?cái)?shù)量(在不重新分配內(nèi)存的情況下)

            std::vector::reserve() 設(shè)置Vector最小的元素容納數(shù)量。

            做內(nèi)存分配的是STL內(nèi)置的allocator,capacity()本身所取得的是allocator所預(yù)先分配的大小。

            vector::push_back導(dǎo)致vector大小增長過程是這樣的,0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 ...
            既是以2的指數(shù)增長,雖然時(shí)間復(fù)雜度是o(1),但是還是比較費(fèi),因?yàn)橛袃?nèi)存銷毀,因?yàn)橛袃?nèi)存拷貝,如果是原生數(shù)據(jù)還稍微好一點(diǎn),如果是struct或者class,這是一個(gè)惡夢(mèng)。

            另,std::vector::append()這個(gè)方法是不存在的。

            @路人甲
            我并非惡語相向,而是如此的誤導(dǎo)性的東西,是在誤人子弟。此類型文章是面向初學(xué)者的,如果一個(gè)引路者誤導(dǎo)他們,這是一件很糟糕的事情。
            還有,0bug是誰?我還真不認(rèn)識(shí)。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-26 17:18 jigsaw

            陳碩所展示的編程能力全都是toy級(jí)的。

            從難易程度上來說,可以給初學(xué)者當(dāng)入門資料;但從正確性上來說,充其量只能敝帚自珍了。

            不過以前陳碩跟我辯論的時(shí)候,我發(fā)現(xiàn)他有一特長。就是會(huì)悄悄地把我說的話嫁接到他的話里;本來是反駁他的話,結(jié)果變成支持他。并且還一而再再而三的重復(fù),以至于好像變成了他的原創(chuàng)。 吐血~~  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-26 17:24 jigsaw

            當(dāng)然濫竽充數(shù)的人很多,比如0bug老師,也不多陳碩一個(gè)了。

            我只是覺得對(duì)0bug老師不公平啊。都是南郭先生,為什么0bug被釘上了恥辱柱,而陳碩(還有其他一些了,就不一一點(diǎn)名了)卻成了偶像呢?

            嗯,看來還是因?yàn)?bug的廣告效果不如陳碩的好。現(xiàn)在的市場(chǎng),會(huì)包裝就行,管你裝的是酒還是醋呢。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-04-27 09:53 陳碩

            @楊粼波
            思考題:為什么 muduo::net::Buffer 不需要 reserve()?  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2011-05-16 11:21 yrj

            "prepend' 有 Pascal String 的味道  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2012-02-13 15:08 fztcjjl@gmail.com

            這個(gè)函數(shù)好像有個(gè)BUG
            void shrink(size_t reserve)
            {
            std::vector<char> buf(kCheapPrepend+readableBytes()+reserve);
            std::copy(peek(), peek()+readableBytes(), buf.begin()+kCheapPrepend);
            buf.swap(buffer_);
            }

            buf.swap(buffer_)進(jìn)行交換,將first last end指針交換到buffer_對(duì)象中,但是函數(shù)返回的時(shí)候buf對(duì)象被析構(gòu)了,這些指針?biāo)赶虻膬?nèi)存也跟著被釋放了。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2012-02-13 15:34 solstice

            @fztcjjl@gmail.com
            No. swap 是安全的,析構(gòu)的是原來 buffer_ 里的內(nèi)容。
            你實(shí)驗(yàn)一下就知道。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2012-02-13 16:17 fztcjjl@gmail.com

            可能是我對(duì)這個(gè)函數(shù)的功能理解錯(cuò)了  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2012-02-13 16:36 solstice

            @fztcjjl@gmail.com
            retrieveAsString() 會(huì)清空 buffer,第二次調(diào)用自然就沒有內(nèi)容了。  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì) 2012-02-13 18:13 fztcjjl@gmail.com

            @solstice
            終于明白這句話的意思了  回復(fù)  更多評(píng)論   

            # re: Muduo 設(shè)計(jì)與實(shí)現(xiàn)之一:Buffer 類的設(shè)計(jì)[未登錄] 2015-08-29 15:02 name

            求大神,libevent的evbuffer 接收大數(shù)據(jù) 怎么接收? 如果接收N兆的數(shù)據(jù) 是分n次,每次調(diào)用回調(diào)去接收,這樣我就做一個(gè)狀態(tài)機(jī)去讀取,可是,到最后一點(diǎn)數(shù)據(jù),又要用while把每次回調(diào)的buffer數(shù)據(jù)全部讀出來(因?yàn)椴粫?huì)在次調(diào)用回調(diào)了,所謂的回調(diào),是在每次buffer添加數(shù)據(jù)才進(jìn)行回調(diào),所以在回調(diào)函數(shù)一定要全部讀取完數(shù)據(jù)),這樣感覺太不好了,有沒有更好的方案  回復(fù)  更多評(píng)論   


            只有注冊(cè)用戶登錄后才能發(fā)表評(píng)論。
            網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


            <2011年4月>
            272829303112
            3456789
            10111213141516
            17181920212223
            24252627282930
            1234567

            導(dǎo)航

            統(tǒng)計(jì)

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊(cè)

            搜索

            最新評(píng)論

            閱讀排行榜

            評(píng)論排行榜

            精品久久久无码中文字幕天天| 国产精品99久久精品| 久久精品国产半推半就| 一本色道久久综合亚洲精品| 色8激情欧美成人久久综合电| 伊人久久大香线蕉精品| 91精品国产高清91久久久久久| 少妇久久久久久被弄高潮| 久久亚洲AV成人无码软件| 亚洲日本久久久午夜精品| 天天综合久久一二三区| 性高湖久久久久久久久AAAAA| 久久久精品波多野结衣| 久久性精品| 久久久精品国产| 日韩精品久久无码中文字幕| 狠狠色婷婷久久综合频道日韩| 国产成人精品综合久久久久 | 精品免费久久久久久久| 欧洲人妻丰满av无码久久不卡| 久久综合给合久久国产免费| 久久亚洲精品成人av无码网站| 精品无码久久久久久尤物| 精品久久久久久久久中文字幕| 久久久久国产精品| 久久九色综合九色99伊人| 噜噜噜色噜噜噜久久| 亚洲精品国产美女久久久| 久久国产精品无码HDAV| 91精品国产综合久久香蕉| 午夜精品久久久久成人| 日韩久久久久久中文人妻 | 亚洲乱亚洲乱淫久久| 久久久网中文字幕| 思思久久精品在热线热| 久久精品99久久香蕉国产色戒| 日本精品久久久久中文字幕8| 久久久亚洲精品蜜桃臀| 性欧美大战久久久久久久久| 精品综合久久久久久97超人| 美女久久久久久|