• <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 問題的頁(yè)面介紹了五種 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ù)端編程的問題就簡(jiǎn)化為如何設(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ò)庫(kù)來操心,程序只要調(diào)用 TcpConnection::send() 就行了,網(wǎng)絡(luò)庫(kù)會(huì)負(fù)責(zé)到底。網(wǎng)絡(luò)庫(kù)應(yīng)該接管這剩余的 20k 字節(jié)數(shù)據(jù),把它保存在該 TCP connection 的 output buffer 里,然后注冊(cè) POLLOUT 事件,一旦 socket 變得可寫就立刻發(fā)送數(shù)據(jù)。當(dāng)然,這第二次 write() 也不一定能完全寫入 20k 字節(jié),如果還有剩余,網(wǎng)絡(luò)庫(kù)應(yīng)該繼續(xù)關(guān)注 POLLOUT 事件;如果寫完了 20k 字節(jié),網(wǎng)絡(luò)庫(kù)應(yīng)該停止關(guān)注 POLLOUT,以免造成 busy loop。(Muduo EventLoop 采用的是 epoll level trigger,這么做的具體原因我以后再說。)

            如果程序又寫入了 50k 字節(jié),而這時(shí)候 output buffer 里還有待發(fā)送的 20k 數(shù)據(jù),那么網(wǎng)絡(luò)庫(kù)不應(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ò)庫(kù)不能立刻關(guān)閉連接,而要等數(shù)據(jù)發(fā)送完畢,見我在《為什么 muduo 的 shutdown() 沒有直接關(guān)閉 TCP 連接?》一文中的講解。

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

            TcpConnection 必須要有 input buffer

            TCP 是一個(gè)無(wú)邊界的字節(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ò)庫(kù)在處理“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ò)庫(kù)必然要應(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ò)庫(kù)必須要給每個(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è)簡(jiǎn)單的驗(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ò)庫(kù)只提供相當(dāng)于 char buf[8192] 的緩沖,或者根本不提供緩沖區(qū),而僅僅通知程序“某 socket 可讀/某 socket 可寫”,要程序自己操心 IO buffering,這樣的網(wǎng)絡(luò)庫(kù)用起來就很不方便了。(我有所指,你懂得。)

             

            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)增長(zhǎ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ù)。

            誰(shuí)會(huì)用 Buffer?誰(shuí)寫誰(shuí)讀?根據(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 中去了;如果長(zhǎng)度超過 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ù)的跨線程傳遞,一種簡(jiǎn)單的做法是把數(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ā)送了兩條消息,長(zhǎng)度分別為 50 字節(jié)和 350 字節(jié),接收方分兩次收到數(shù)據(jù),每次 200 字節(jié),然后進(jìn)行分包,再分兩次回調(diào)客戶代碼。

             

            自動(dòng)增長(zhǎng)

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

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

            圖 7buffer4

             

            客戶代碼一次性寫入 1000 字節(jié),而當(dāng)前可寫的字節(jié)數(shù)只有 624,那么 buffer 會(huì)自動(dòng)增長(zhǎ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)增長(zhǎ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ù)方式增長(zhǎng),讓 push_back() 的平均復(fù)雜度是常數(shù)。比方說經(jīng)過第一次增長(zhǎ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é)表示消息的長(zhǎng)度(即《Muduo 網(wǎng)絡(luò)編程示例之二:Boost.Asio 的聊天服務(wù)器》中的 LengthHeaderCodec),我要序列化一個(gè)消息,但是不知道它有多長(zhǎng),那么我可以一直 append() 直到序列化完成(圖 17,寫入了 200 字節(jié)),然后再在序列化數(shù)據(jù)的前面添加消息的長(zhǎng)度(圖 18,把 200 這個(gè)數(shù) prepend 到首部)。

             

            圖 17buffer13

             

            圖 18buffer14

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

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

            這里簡(jiǎn)單談?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 去操心了。

            性能是不是問題?看跟誰(shuí)比

            看到這里,有的讀者可能會(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ù)庫(kù)打交道,那么瓶頸常常在 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 陳碩 閱讀(9824) 評(píng)論(28)  編輯 收藏 引用

            評(píng)論

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

            我用的定長(zhǎng)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增長(zhǎng)到內(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://m.shnenglu.com/Solstice/archive/2010/09/04/muduo_vs_asio.html
            http://m.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

            想問下 你寫這樣一篇博客是如何排版的?用了哪些工具?大概需要多長(zhǎng)時(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犯

            樓主雖然長(zhǎng)的憨了一點(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ù)方式增長(zhǎng),讓 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ù)方式增長(zhǎng),讓 push_back() 的平均復(fù)雜度是常數(shù)。”

            這是可變長(zhǎng)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ò)無(wú)關(guān)緊要(雖技術(shù)高低可以一眼看出),可以討論,惡語(yǔ)相向,實(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, 但這無(wú)形中會(huì)增加小內(nèi)存的分配,也導(dǎo)致更多的 send操作,即更多的系統(tǒng)調(diào)用,采用混合的方法可能好一點(diǎn),根據(jù) Queue.last_in_vector_item的長(zhǎng)度來決定是對(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大小增長(zhǎng)過程是這樣的,0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32 ...
            既是以2的指數(shù)增長(zhǎng),雖然時(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è)方法是不存在的。

            @路人甲
            我并非惡語(yǔ)相向,而是如此的誤導(dǎo)性的東西,是在誤人子弟。此類型文章是面向初學(xué)者的,如果一個(gè)引路者誤導(dǎo)他們,這是一件很糟糕的事情。
            還有,0bug是誰(shuí)?我還真不認(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)他有一特長(zhǎng)。就是會(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)論排行榜

            精品人妻久久久久久888| 色综合久久中文色婷婷| 久久久免费精品re6| 成人妇女免费播放久久久| 国产毛片久久久久久国产毛片| 久久综合一区二区无码| 精品久久久无码人妻中文字幕豆芽| 久久精品国产91久久麻豆自制| 日韩电影久久久被窝网| 精品久久久久久国产91| 国产成人精品综合久久久久| 99久久免费只有精品国产| 亚洲欧美成人综合久久久| 99精品久久久久久久婷婷| 国产成人精品三上悠亚久久| 国产精品熟女福利久久AV| 久久精品无码专区免费东京热| 四虎久久影院| 久久综合成人网| 久久精品国产亚洲麻豆| 久久久国产乱子伦精品作者| 久久人做人爽一区二区三区| 久久99精品国产麻豆婷婷| 狠狠色噜噜狠狠狠狠狠色综合久久| 亚洲国产精品一区二区久久hs| 亚洲国产成人久久综合区| 91精品久久久久久无码| 丰满少妇人妻久久久久久| 精品久久无码中文字幕| 国内精品久久人妻互换| 久久久av波多野一区二区| 久久精品蜜芽亚洲国产AV| 久久久久国产精品人妻| 综合久久给合久久狠狠狠97色 | 精品国产乱码久久久久软件| 久久精品国产清自在天天线| 93精91精品国产综合久久香蕉| 亚洲国产成人久久精品影视| 久久精品国产福利国产秒| 国产香蕉97碰碰久久人人| 精品久久久久久无码人妻热|