西西軟件下載最安全的下載網(wǎng)站、值得信賴的軟件下載站!

首頁編程開發(fā)VC|VC++ → C++ 異步和類型安全、printf風格的日志庫

C++ 異步和類型安全、printf風格的日志庫

相關(guān)軟件相關(guān)文章發(fā)表評論 來源:西西整理時間:2013/1/20 19:55:14字體大小:A-A+

作者:西西點擊:0次評論:1次標簽: 異步

  • 類型:行業(yè)軟件大小:1.5M語言:中文 評分:5.0
  • 標簽:
立即下載

C++程序的調(diào)試一般有調(diào)試器、printf、日志文件三種。Linux下的調(diào)試器為gdb,關(guān)于gdb的使用甚至可以單獨用一本書來說明,但是本章并不會過度討論gdb,讀者可以尋找相關(guān)的資料閱讀。Gdb是C++程序調(diào)試中非常重要的調(diào)試手段,其有如下特點:

•通過增加斷點,可以觀察重點代碼的執(zhí)行
•若程序出現(xiàn)segmentation fault,gdb可以輸出調(diào)用堆棧,方便找到bug之所在
•有些邏輯代碼段非常不容易觸發(fā),可以在gdb環(huán)境下通過加斷點、修改內(nèi)存來強制進入特定的代碼段
•但是gdb不能用于生產(chǎn)環(huán)境,在幾百上千在線的服務(wù)器程序上執(zhí)行g(shù)db的attach操作,是不可能接受的
      Gdb絕對是調(diào)試期的利器,另外一個調(diào)試期使用的既簡單又實用的方法是printf,就是使用c庫的函數(shù)printf輸出變量到控制臺。其優(yōu)點是直觀,可以完整的、清晰的觀察程序的運行過程,而不需像gdb一樣暫停程序。另外printf也只能用于開發(fā)調(diào)試環(huán)境,上線時服務(wù)器程序都是在后臺運行的,printf將會失去作用。更重要的是因為gdb和printf都不會將數(shù)據(jù)存儲,歷史數(shù)據(jù)或歷史操作都會在重啟程序后消失。日志文件可以彌補gdb和printf的不足,我們需要一個具有如下功能的日志組件:

•用于調(diào)試可以顯示、記錄變量、數(shù)據(jù),即能支持像printf一樣可以實時的在控制臺輸出顯示,又能將記錄存儲文件,方便搜索查看歷史記錄
•日志應(yīng)該擁有良好的格式,即方便開發(fā)和運維人員的閱讀,又要包含足夠多的信息,例如事件記錄時間、線程id、事件類型,事件的嚴重級別
•日志文件應(yīng)該被良好的組織,一方面日志應(yīng)該按照每天單獨文件夾分類,另一方面日志日志文件并應(yīng)該過大,否則使用編輯器打開、搜索將會非常困難。日志內(nèi)容也應(yīng)該組織分類,比如數(shù)據(jù)庫的操作日志和用戶做任務(wù)的日志應(yīng)該有明確的標志,這樣可以對日志文件進行g(shù)rep等進行過濾分類查看。
•日志文件必須非常容易配置,當調(diào)試時期望看到盡可能多的內(nèi)容,但是不關(guān)心的內(nèi)容需要被過濾掉,比如調(diào)試用戶任務(wù)模塊時,可以不顯示數(shù)據(jù)庫相關(guān)日志。在上線后,運維只關(guān)心報錯信息,比警告級別低的日志需要被屏蔽。在調(diào)試時,開發(fā)人員經(jīng)常會盯著控制臺的輸出,相比于普通級別日志內(nèi)容,錯誤級別的日志更應(yīng)該引起開發(fā)注意力,所以重要級別的日志在控制臺輸出時應(yīng)該有彩色高亮顯示。
•日志組件必須有高效的性能,一方面調(diào)用者期望日志組件調(diào)用后立即返回不影響邏輯層的效率,另一方面寫文件屬于io操作,比起內(nèi)存操作慢的多得多。所以要求日志的接口調(diào)用是異步的,日志組件單獨開啟線程執(zhí)行寫文件操作,只有如此才能盡最大程度滿足程序的實時性。
下面來探討一下日志 的實現(xiàn)。

實現(xiàn)類
定義log_t類來封裝對于日志配置、格式化、輸出的操作。log_t主要的功能有:

•支持對日志級別的配置
•支持對日志類別的配置
•支持配置日志內(nèi)容是否輸出到文件和控制臺
•格式化日志

class log_t
{
public:
    log_t(int level_, const string& all_class_, const string& path_, const string& file_,
          booprint_file_, booprint_screen_);
    virtua~log_t();

    void mod_level(int level_, booflag_);
    void mod_class(const string& class_, booflag_);
    void mod_print_file(booflag_);
    void mod_print_screen(booflag_);
    boois_level_enabled(int level_);
    const char* find_class_name(const char* class_);

    void log_content(int level_, const char* str_class_, const string& content_);
};

接口log_content 負責格式化和輸出日志內(nèi)容,其主要實現(xiàn)代碼如下:

void log_t::log_content(int level_, const char* str_class_, const string& content_)
{
    struct timevacurtm;
    gettimeofday(&curtm, NULL);
    struct tm tm_va= *localtime(&(curtm.tv_sec));

    char log_buff[512];
    ::snprintf(log_buff, sizeof(log_buff), "%02d:%02d:%02d.%03ld %s [%ld] [%s] ",
            tm_val.tm_hour, tm_val.tm_min, tm_val.tm_sec, curtm.tv_usec/1000,
            g_log_level_desp[level_], gettid(), str_class_);

    if (m_enable_file && check_and_create_dir(&tm_val))
    {
        m_file << log_buff << content_ << endl;
        m_file.flush();
    }

    if (m_enable_screen)
    {
        printf("%s%s%s%s\n", g_log_color_head[level_], log_buff, content_.c_str(), g_log_color_tail[level_]);
    }
}

其執(zhí)行的主要過程如下:
•格式化時間,包括調(diào)用時的時分秒以及毫秒,為什么沒有年月日呢?日志目錄已經(jīng)安裝每天一個文件夾分類了,故這里省略了年月日信息。
•增加日志級別信息,日志級別對應(yīng)一個字符串描述,如debug級別日志每一行會包含DEBUG字符串。
•記錄線程id,這里并沒有直接使用::pthread_self() 獲取線程id,而是獲取線程在系統(tǒng)中分配的“TID”,要知道線程和進程在內(nèi)核中都有唯一的id,可以通過top進行查看,top -H –p [pid] 可以查看進程內(nèi)的所有線程的運行負載情況,如果某個線程運行負載很高,我們需要知道到底是那一部分邏輯是熱點,通過搜索日志,可以知道該線程負責了哪塊邏輯,從而能夠發(fā)現(xiàn)問題。
•記錄日志類別
•若配置允許輸出屏幕,那么利用printf輸出,不同的日志級別會有不同的顯示顏色,如printf("\033[1;33mDEBUG\033[0m"), DEBUG 會以黃色輸出。
•若配置允許輸出文件,那么flush到文件中,若日期發(fā)生變化,重新創(chuàng)建日期目錄,保證每一天一個文件夾,若單個文件內(nèi)容超過5000行,會創(chuàng)建新的文件,避免文件內(nèi)容過大,最終目錄機構(gòu)如下

異步操作
為了保證日志接口盡可能的快,日志接口都是異步完成的其。時序圖如下:

對于用戶層而言,調(diào)用日志組件接口的開銷為日志內(nèi)容格式化和拷貝字符串到隊列,而相對開銷較大的寫文件、輸出屏幕操作則有日志線程完成,這樣可以最大程度的保證用戶層的高效運行。

我們定義log_service_t封裝異步操作,對于格式化和輸出,log_service_t仍然通過log_t實現(xiàn),log_service_t的職責有四:

•封裝異步接口,外部直接調(diào)用log_service_t的接口,一般log_service_t一單件模式使用
•Log_service_t接口模板函數(shù),利用C++的泛型能力確保類型安全,比如當%s參數(shù)本應(yīng)該是user.name()時,卻手誤寫成user,log_service_t的接口保證在編譯器就能報錯。
•Log_service_t創(chuàng)建日志線程和日志任務(wù)隊列,
•Log_service_t在初始化的時候接受配置日志組件的參數(shù),同時它也支持在運行期修改參數(shù),并且線程安全。

關(guān)鍵代碼如下:

class log_service_t
{
public:
    log_service_t();
    ~log_service_t();
    int start(const string& opt_);
    int stop();

LOG_IMPL_MACRO(async_logdebug, LOG_DEBUG);
    LOG_IMPL_MACRO(async_logtrace, LOG_TRACE);
    LOG_IMPL_MACRO(async_loginfo, LOG_INFO);
    LOG_IMPL_MACRO(async_logwarn, LOG_WARN);
    LOG_IMPL_MACRO(async_logerror, LOG_ERROR);
    LOG_IMPL_MACRO(async_logfatal, LOG_FATAL);
};

由于各個日志級別的接口代碼都是相似的,使用了LOG_IMPL_MACRO簡化代碼,LOG_IMPL_MACRO定義為:

#define LOG_IMPL_NONE_ARG(func, LOG_LEVEL)      \
    inline void func(const char* class_, const char* fmt_)  \
    {     \
        if (m_log->is_level_enabled(LOG_LEVEL))  \
        {     \
            const char* class_name_str = m_log->find_class_name(class_); \
            if (class_name_str)     \
            { \
                m_task_queue.produce(task_binder_t::gen(&log_t::log_content, m_log, LOG_LEVEL, \
                class_name_str, string(fmt_)));     \
            }  \
        }      \
    }

#define LOG_IMPL_ARG1(func, LOG_LEVEL)      \
    template <typename ARG1>    \
    inline void func(const char* class_, const char* fmt_, const ARG1& arg1_) \
    { \
        if (m_log->is_level_enabled(LOG_LEVEL)) \
        {     \
            const char* class_name_str = m_log->find_class_name(class_); \
            if (class_name_str)     \
            {  \
                str_format_t dest(fmt_);  \
                dest.append(arg1_);     \
                m_task_queue.produce(task_binder_t::gen(&log_t::log_content, m_log, LOG_LEVEL, \
                 class_name_str, dest.gen_result()));     \
            } \
        } \
    }

#define LOG_IMPL_MACRO(async_logdebug, LOG_DEBUG)     \
    LOG_IMPL_NONE_ARG(async_logdebug, LOG_DEBUG)      \
    LOG_IMPL_ARG1(async_logdebug, LOG_DEBUG)          \
    LOG_IMPL_ARG2(async_logdebug, LOG_DEBUG)          \
    LOG_IMPL_ARG3(async_logdebug, LOG_DEBUG)        \
    LOG_IMPL_ARG4(async_logdebug, LOG_DEBUG)        \
    LOG_IMPL_ARG5(async_logdebug, LOG_DEBUG)        \
    LOG_IMPL_ARG6(async_logdebug, LOG_DEBUG)

受篇幅所限,沒有附上所有宏展開的代碼,log_service_t初始化的代碼如下:

int log_service_t::start(const string& opt_)
{
    if (m_log) return 0;

    int leve= 2;
    string path = "./log";
    string filename = "log";
    booprint_file = true;
    booprint_screen = false;

    arg_helper_t arg(opt_);
    if (!arg.get_option_value("-log_level").empty()) leve= ::atoi(arg.get_option_value("-log_level").c_str());
    if (!arg.get_option_value("-log_path").empty()) path = arg.get_option_value("-log_level");
    if (!arg.get_option_value("-log_filename").empty()) path = arg.get_option_value("-log_filename");

    if (arg.get_option_value("-log_print_file") == "false" || arg.get_option_value("-log_print_file") == "0")
    {
        print_file = false;
    }
    if (arg.get_option_value("-log_print_screen") == "true" || arg.get_option_value("-log_print_screen") == "1")
    {
        print_screen = true;
    }

    m_log = new log_t(level, arg.get_option_value("-log_class"), path, filename, print_file, print_screen);
    m_thread.create_thread(task_binder_t::gen(&task_queue_t::run, &m_task_queue), 1);

    return 0;
}

代碼很簡單,也很直觀,需要對讀者解釋的是,初始化接口start的參數(shù)為字符串,這樣做的好處是可以從配置文件中讀入日志配置參數(shù)后直接傳給log_service_t的start接口,而用戶層完全不需要關(guān)心日志配置語法的細節(jié)。Start函數(shù)創(chuàng)建log_t實例后,創(chuàng)建單獨線程執(zhí)行任務(wù)隊列,而任務(wù)隊列中的所有任務(wù)就是寫日志內(nèi)容或輸出日志內(nèi)容。

格式化
關(guān)于格式化輸出,使用使用了模板函數(shù)和多態(tài)機制保證了絕對的類型安全,這也是相對于sprintf的巨大優(yōu)越點。class str_format_t 是用來格式化字符串的工具類,它使用sprintf的格式化語法,但是額外提供了排錯和糾錯功能:

•使用sprintf格式化語法,最基本的格式化參數(shù)都支持如%d,%u,%ld,%lu,%s,%c,%x,%p,%f甚至形如%04d設(shè)置字符串寬度的語法也是支持的
•str_format_t 類型安全,格式化參數(shù)支持整型、浮點數(shù)、字符串cost char*、指針、string,若賦值其他類型參數(shù),則編譯不能通過。
•str_format_t 擁有自動糾錯功能,使用sprintf的時候除了類型不安全導(dǎo)致出錯外,最常見的就是sprintf的格式化參數(shù)與賦值的參數(shù)個數(shù)不一致,如sprintf(buff, “%s,%s”, 100);這樣的代碼編譯能夠通過只有運行期才能發(fā)現(xiàn)出錯,str_format_t 可以容忍這樣的失誤,當模板字符串中%比賦值的參數(shù)多時,str_format_t自動忽略多余的%,若%比后邊的值參數(shù)少時,值參數(shù)自動追加到字符串尾部,這樣最大程度的避免了出錯和信息丟失。

關(guān)于基本類型的格式化模板函數(shù):
template<typename T>
    void append(T content_)
    {
        if (move_to_next_wildcard())
        {
            if (m_fmt_type.type == 'x')
            {
                char buff[64];
                snprintf(buff, sizeof(buff), "0x%x", (unsigned int)content_);
                m_num_buff = buff;
            }
            else
            {
                m_strstream << content_;
                m_strstream >> m_num_buff;
            }
            int width = m_fmt_type.min_len > m_num_buff.length()? m_fmt_type.min_len - m_num_buff.length(): 0;
            for (; width > 0; -- width)
            {
                m_result += m_fmt_type.fill_char;
            }
        }
        else
        {
            m_strstream << content_;
            m_strstream >> m_num_buff;
        }

        m_result += m_num_buff;
        m_strstream.clear();//! clear error bit,not content
        m_num_buff.clear();
    }

關(guān)于字符串的特化函數(shù):

void str_format_t::append(const string& str_)
{
    if (move_to_next_wildcard())
    {
        int width = m_fmt_type.min_len > str_.length()? m_fmt_type.min_len -str_.length(): 0;
        for (; width > 0; -- width)
        {
            m_result += m_fmt_type.fill_char;
        }
    }

    m_result += str_;
}move_to_next_wildcard 每次嘗試移動到下一個%所在的位置,然后用值參數(shù)替換%的格式化。move_to_next_wildcard的整個開銷是遍歷字符串的開銷:

boostr_format_t::move_to_next_wildcard()
{
    m_fmt_type.clear();
    char tmp = '\0';

    for (; cur_format_index < m_fmt_len; ++ cur_format_index)
    {
        tmp = m_fmt[cur_format_index];
        if (tmp != '%')
        {
            m_result += tmp;
            continue;
        }

        char next = m_fmt[cur_format_index + 1];
        if (next == '%')
        {
            cur_format_index += 1;
            m_result += next;
            continue;
        }

        //! 支持多種格式化 %c %s, %d, %ld, %u, %lu, %x, %X, 找到格式化的類型
        //for (++cur_format_index; cur_format_index < m_fmt_len; ++ cur_format_index)
        for (unsigned int i = 1 ; i <= 5; ++i)
        {
            char cur = m_fmt[cur_format_index + i];

            if (cur == '\0' || cur == '%')
            {
                break;
            }
            else if (cur == 'c' || cur == 'd' || cur == 'u' || cur == 'x' ||
                     cur == 'f' || cur == 's')
            {
                m_fmt_type.type    = cur;
                m_fmt_type.min_len = ::atoi(m_fmt + cur_format_index + 1);
                cur_format_index   = cur_format_index + i + 1;
                if (next == '0')
                {
                    m_fmt_type.fill_char = next;
                }
                return true;
            }
            else if (cur == 'l')
            {
                char c_num = m_fmt[cur_format_index + i + 1];
                if ('d' == c_num || 'u' == c_num)
                {
                    m_fmt_type.type    = c_num;
                    m_fmt_type.min_len = ::atoi(m_fmt + cur_format_index + 1);
                    cur_format_index   = cur_format_index + i + 2;
                    if (next == '0')
                    {
                        m_fmt_type.fill_char = next;
                    }
                    return true;
                }
            }
        }
        m_result += tmp;

    }
    return false;
}

配置
最基本的log_service_t的start接口提供了初始化日志組件時的配置,配置參數(shù):

enum log_level_e
{
    LOG_FATA= 0,
    LOG_ERROR,
    LOG_WARN,
    LOG_INFO,
    LOG_TRACE,
    LOG_DEBUG,
    LOG_LEVEL_NUM
};•-log_leve配置日志級別,0-5代表不同的日志級別,枚舉定義如下:
•-log_path 配置日志文件存儲的根目錄
•-log_filename 配置文件名稱
•-log_print_file" 配置日志是否輸出到文件
•log_print_screen 配置日志是否輸出到屏幕
•- log_class 配置哪些日志類別是開啟的,只有 開啟的類別日志才會被記錄
這其中除了log_path和log_filename不需要運行期配置外,其他的配置都有運行期修改的需求,比如運行期某個類別的日志被關(guān)閉了,但是為了跟蹤某問題必須開啟,如果不能動態(tài)修改日志配置往往會是開發(fā)人員面對問題時束手無策。對于-log_print_file 和-log_print_screen 都是用bool記錄的,-log_leve是整型記錄的,都是直接支持運行期修改的。有的讀者可能指出日志不是有單獨線程嗎,而且使用線程組件的用戶層也可能是多線程的,不就設(shè)計到了多線程競爭了嗎?在明白此答案之前,先讓我們縷一縷log中的結(jié)構(gòu):

•日志的接口會被多線程調(diào)用
•異步日志接口會訪問日志的配置,判斷該日志類別或級別是否已開啟,由于只有讀取操作,不需要加鎖。
•日志格式化后投遞到隊列,隊列是線程安全的,只有日志線程會從任務(wù)隊列中消費任務(wù)。
•運行修改日志配置的操作會投遞到日志線程完成,保證任一時刻只有一個線程修改日志配置
對于-log_print_file 、-log_print_screen 和-log_leve都是多線程讀取訪問,某一時刻一個線程修改,并且三者都是基本類型的,不存在內(nèi)存地址變化的問題,這樣日志線程修改后會立即生效。但是對于-log_class,被開啟的日志類別都被保存到set<string> 的結(jié)構(gòu)中,多線程對其執(zhí)行find操作是安全的,這個stl的多線程特性是明確支持的。但是若對set<string> 在運行期執(zhí)行insert或erase后會使set<string>中的迭代器失效,被壞的情況是會引起讀操作的線程崩潰,所以在運行期絕對不能對老的日志類別容器進行修改。難道日志類別就沒辦法運行期修改了嗎?腦筋急轉(zhuǎn)彎一下,既然不能修改老的,為什么不創(chuàng)建一個新的,然后用新的替換老的?為了使用這個方法,需要一些小技巧:

使用一個指針引用當前日志級別的容器,度線程總是獲取該指針,然后執(zhí)行find操作find_class_name 根據(jù)類別字符串去容器中查找是否存在。這里使用了原子操作ATOMIC_FETCH,在gcc的環(huán)境下可以把它定義為:

#define ATOMIC_FETCH(src_ptr)             __sync_add_and_fetch(src_ptr, 0)
typedef set<string>            str_set_t;
    typedef vector<str_set_t*>    ptr_vt_t;
    str_set_t*                    m_enable_class_set;

const char* log_t::find_class_name(const char* class_)
{
    str_set_t* pset = ATOMIC_FETCH(&m_enable_class_set);
    str_set_t::iterator it = pset->find(class_);
    if (it != pset->end())
    {
        return (*it).c_str();
    }
    return NULL;
}

find_class_name對于存儲日志類別的容器指針使用了原子操作,所以要保證指針對應(yīng)的數(shù)據(jù)永遠不被修改,即使有新的配置產(chǎn)生時,老的配置仍然不會被銷毀,利用數(shù)據(jù)冗余保證無鎖操作的線程安全,最后將新配置容器的指針替換老配置的指針即可,同樣使用原子操作完成

#define ATOMIC_SET(src_ptr, v)            (void)__sync_bool_compare_and_swap(src_ptr, *(src_ptr), v)


void log_t::mod_class(const string& class_, booflag_)
{
    str_set_t* pset = new str_set_t(m_enable_class_set->begin(), m_enable_class_set->end());
    if (flag_)
    {
        pset->insert(class_);
    }
    else
    {
        pset->erase(class_);
    }

    m_class_set_history.push_back(pset);
    ATOMIC_SET(&m_enable_class_set, pset);
}

總結(jié)以上無鎖編程的技巧有:

對于stl的對象的多線程讀操作是安全的

對于指針可以使用原子操作進行讀取、更新、比較等操作

老的容器被保存而不是銷毀,從而保證了獲取了老數(shù)據(jù)的線程仍然能夠工作正常,日志的類別字符串只有十幾頂多幾十個,這里做數(shù)據(jù)冗余的內(nèi)存開銷是可以忽略的。

使用
日志的異步接口是由log_service_t定義的,前邊介紹log_service_t的時候也提到了log_service_t一般是被單件使用的,如果每次調(diào)用singleton_t<log_service_t>::instance().async_logdebug(“XX”, “OhNice”);代碼太長了,使用宏封裝單件的操作:

#define LOG singleton_t<log_service_t>::instance()
#define LOGDEBUG(content)  singleton_t<log_service_t>::instance().async_logdebug content
#define LOGTRACE(content)  singleton_t<log_service_t>::instance().async_logtrace content
#define LOGINFO(content)   singleton_t<log_service_t>::instance().async_loginfo  content
#define LOGWARN(content)   singleton_t<log_service_t>::instance().async_logwarn  content
#define LOGERROR(content)  singleton_t<log_service_t>::instance().async_logerror content
#define LOGFATAL(content)  singleton_t<log_service_t>::instance().async_logfatacontent使用宏的好處是,比如logtrace可能會被到處使用了,而有可能其只在調(diào)試器有用,那么在release版本時候可以把宏LOGTRACE定義成空操作,當然若日志級別不開啟的情況下,調(diào)用LOGTRACE的開銷是很小的,一般情況可以忽略其影響。

使用示例:

int main(int argc, char* argv[])
{

    LOG.start("-log_path ./log -log_filename log -log_class FF,XX -log_print_screen true -log_print_file true -log_leve6");
    LOGDEBUG(("XX", "FFFFF"));
    LOGTRACE(("XX", "FFFFF"));
    LOGINFO(("XX", "FFFFF"));
    LOGWARN(("XX", "FFFFF"));
    LOGERROR(("XX", "FFFFF"));
    LOGFATAL(("XX", "FFFFF"));
    LOG.mod_class("TT", true);

    sleep(1);
    LOGFATAL(("TT", "FFFFF"));

    LOGFATAL(("FF", "DSDFFFFF%s", string("SFWEGGGGGGGGG")));

    LOG.stop();
    return 0;
}

總結(jié):
•日志組件需要盡可能的快從而對于調(diào)用者的影響降到最低,使用異步接口可以使日志接口調(diào)用后立即返回
•日志的文件組織需要較好的被分類,目錄首先按照時間分類,每天生成一個目錄存儲當天的日志,并且日志文件對大小做了上限,超過限制會重新創(chuàng)建新的文件,保證單個日志文件不會過大
•日志組件被設(shè)計成printf的格式化風格,但是增加了類型安全和參數(shù)糾錯,不支持的類型會在編譯期發(fā)現(xiàn),值參數(shù)數(shù)目過多會被追加到字符串尾部,過少則忽略相應(yīng)的%。
•日志組件是線程安全的,對于日志的配置雖然是多線程無鎖訪問的,仍然支持運行期動態(tài)的修改配置,其中使用了原子操作既保證了無鎖編程的高效,又滿足了多線程的穩(wěn)定。
•源碼:https://github.com/fanchy/fflib

    相關(guān)評論

    閱讀本文后您有什么感想? 已有人給出評價!

    • 8 喜歡喜歡
    • 3 頂
    • 1 難過難過
    • 5 囧
    • 3 圍觀圍觀
    • 2 無聊無聊

    熱門評論

    最新評論

    發(fā)表評論 查看所有評論(1)

    昵稱:
    表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
    字數(shù): 0/500 (您的評論需要經(jīng)過審核才能顯示)
    推薦文章

    沒有數(shù)據(jù)

      沒有數(shù)據(jù)
    最新文章
      沒有數(shù)據(jù)