C++Linux項(xiàng)目推薦-Web多人聊天+MySQL+Redis+Websocket+Json咖祭,可以寫簡歷的C++項(xiàng)目

1 項(xiàng)目地址

<font style="color:#DF2A3F;">項(xiàng)目配套視頻簡介</font>程序員老廖的個人空間-程序員老廖個人主頁-嗶哩嗶哩視頻 (bilibili.com)

1.1 項(xiàng)目原有功能

https://github.com/anarthal/servertech-chat.git

功能:

  1. 支持HTTP請求,掌握HTTP API + json的請求相應(yīng)
  2. 支持Websocket纠吴,掌握json做序列化和反序列化
  3. 支持多房間聊天
  4. 支持多人聊天
  5. 支持MySQL存儲用戶信息
  6. 支持Redis緩存token戴已,存儲聊天消息
  7. json序列化
  8. 靜態(tài)網(wǎng)頁支持
  9. 支持單元測試
  10. 支持python腳本性能測試

1.2 建議擴(kuò)展功能

  1. 基于Reactor網(wǎng)絡(luò)模型構(gòu)建HTTP服務(wù)和Websocket服務(wù)伐坏,替換現(xiàn)有的協(xié)程框架桦沉;
  2. 使用rapidjson做序列化和反序列化纯露;
  3. 仿寫MySQL/Redis連接池埠褪;
  4. 增加房間創(chuàng)建/修改/刪除接口钞速,并將房間成員存儲到MySQL渴语;
  5. 單元測試替換為gtest遵班;
  6. <font style="color:#DF2A3F;">........可以不斷擴(kuò)展,總而言之汇在,就是比做單純的webserver項(xiàng)目強(qiáng)</font>

2 開發(fā)環(huán)境

對gcc/g++編譯版本要求比較高亩鬼,建議升級到10.0以后的編譯器版本雳锋。

3 部署服務(wù)端

3.1 安裝boost庫

該項(xiàng)目依賴boost庫墓懂,需要先安裝boost庫捕仔,我們從官網(wǎng)下載(也可以從我提供的百度云鏈接下載)

# 下載
wget https://archives.boost.io/release/1.86.0/source/boost_1_86_0_rc1.zip --no-check-certificate

#解壓
unzip -x boost_1_86_0_rc1.zip

#進(jìn)入boost
cd boost_1_86_0

#配置boost庫
./bootstrap.sh

#編譯Boost庫
./b2


#安裝Boost庫
sudo ./b2 install
#這將Boost庫安裝到系統(tǒng)默認(rèn)的位置(一般是/usr/local)榜跌。

3.2 編譯聊天室服務(wù)

  1. 下載源碼
git clone https://github.com/anarthal/servertech-chat.git

PS:下載時(shí)最新的commit 0008f72e9bf7d

  1. 編譯源碼
cd servertech-chat/
cd server/
mkdir build
cmake .. -DCMAKE_CXX_STANDARD=17
make

在make時(shí)可能會報(bào)錯,我編譯時(shí)遇到的報(bào)錯情況以及修改方法础浮,可以參考以下方法把 ****<font style="color:#DF2A3F;">三處報(bào)錯 </font>****修改后再一起重新編譯:

(1)CMake Error at /usr/lib/x86_64-linux-gnu/cmake/Boost-1.71.0/BoostConfig.cmake:117 (find_package): Could not find a configuration file for package "boost_json" that exactly

<font style="color:#DF2A3F;">解決方法:修改servertech-chat/server/CMakeLists.txt豆同,手動指定boost的路徑: PATHS /usr/local/lib</font>

<font style="color:#DF2A3F;">大約在14行修改:</font>

find_package(Boost REQUIRED COMPONENTS headers context json regex url PATHS /usr/local/lib)

<font style="color:#DF2A3F;"></font>

(2) undefined reference to symbol 'pthread_condattr_setclock@@GLIBC_2.3.3'

undefined reference to `boost::charconv::to_chars(char, char, double, boost::charconv::chars_format)'

<font style="color:#DF2A3F;">解決方法:修改servertech-chat/server/CMakeLists.txt影锈,增加pthread鸭廷,boost_charconv兩個庫</font>

<font style="color:#DF2A3F;">大約在67行的target_link_libraries()里添加辆床,如下所示:</font>

target_link_libraries(
    servertech_chat
    PUBLIC
    Boost::headers
    Boost::context
    Boost::json
    Boost::regex
    Boost::url
    OpenSSL::Crypto
    OpenSSL::SSL
    ICU::data
    ICU::i18n
    ICU::uc
    boost_charconv
    pthread
)

(3)boost庫的頭文件報(bào)錯

/usr/local/include/boost/redis/adapter/detail/adapters.hpp 報(bào)錯

添加 #define _LIBCPP_VERSION

然后重新編譯

#確保此時(shí)是在servertech-chat/server/build目錄
# 刪除之前cmake產(chǎn)生的文件宵晚,但要注意你一定是在servertech-chat/server/build目錄
rm -rf *
#重新cmake配置
cmake .. -DCMAKE_CXX_STANDARD=17
# 重新編譯
make

編譯成功后產(chǎn)生一個 main的執(zhí)行文件,就是我們聊天室的服務(wù)程序吱型。

現(xiàn)在我們還不能直接運(yùn)行津滞,還要配置MySQL和Redis触徐。

3.3 配置MySQL和Redis

3.3.1 配置MySQL

  1. 啟動MySQL

<font style="color:#DF2A3F;">如果MySQL沒有啟動則需要啟動</font>

  1. 修改程序訪問MySQL的用戶名和密碼

/home/lqf/long/<font style="color:#DF2A3F;">servertech-chat/server/src/services/mysql_client.cpp</font>

修改用戶和密碼,我這里用戶名是root颖侄,密碼123456览祖,所以改成如下所示

  1. 修改程序訪問MySQL的地址

host我們用默認(rèn)的就行又活,因?yàn)楫?dāng)前部署是在MySQL所在機(jī)器部署的

3.3.2 配置Redis

以不需要密碼的方式啟動redis即可柳骄。

3.4 重新編譯和啟動服務(wù)程序

  1. 重新編譯程序

因?yàn)槲覀冎匦滦薷牧嗽创a文件夹界,所以需要使用make命令重新編譯

#確保此時(shí)是在servertech-chat/server/build目錄
# 重新編譯
make
  1. 啟動服務(wù)程序

啟動服務(wù)程序,這里要注意命令格式:

Usage: ./main <address> <port> <doc_root>
Example:
    ./main 0.0.0.0 8080 .

doc_root的路徑一定要設(shè)置對丙者,比如./main 0.0.0.0 8080 ../../doc 目锭,即是要正確給出這個項(xiàng)目自帶的doc的目錄

我目前是在build目錄下啟動的,因?yàn)閐oc是在servertech-chat目錄下被去,我的啟動格式如下所示(8080端口是web客戶端調(diào)用http api時(shí)訪問的端口惨缆,這里不要改其他的端口)

lqf@ubuntu:~/long/servertech-chat/server/build$ ./main 0.0.0.0 8080 ../../doc

正常啟動后(沒有信息輸出是正常的):

<font style="color:#DF2A3F;">我們光有服務(wù)程序還不行坯墨,需要在 《4 部署客戶端》 繼續(xù)部署Web客戶端捣染,這樣才能訪問服務(wù)程序耍攘。</font>

  1. 查看數(shù)據(jù)庫情況

(這里只是告訴大家這個服務(wù)程序?qū)?yīng)的數(shù)據(jù)庫名字少漆,以及有哪些表示损,表結(jié)構(gòu)是怎么樣的)

服務(wù)程序啟動后检访,數(shù)據(jù)庫servertech_chat不存在則自動創(chuàng)建,我們使用mysql命令進(jìn)入MySQL命令行控制臺起暮,可以查看到數(shù)據(jù)庫servertech_chat被創(chuàng)建了负懦。

數(shù)據(jù)庫只有一個表系吭,用來存儲用戶信息肯尺。

4 部署客戶端

需要安裝node 16.14以上的版本

4.1 安裝node

  1. 下載node
wget https://cdn.npmmirror.com/binaries/node/v21.7.3/node-v21.7.3-linux-x64.tar.gz
  1. 解壓
tar zxf node-v21.7.3-linux-x64.tar.gz
  1. 使用node /npm命令生效

創(chuàng)建軟鏈接则吟,注意自己的路徑氓仲,比如我的node路徑是/home/lqf/long/node-v21.7.3-linux-x64

sudo ln -s /home/lqf/long/node-v21.7.3-linux-x64/bin/node /usr/local/bin/node
sudo ln -s /home/lqf/long/node-v21.7.3-linux-x64/bin/npm /usr/local/bin/npm

  1. 配置國內(nèi)的源

國內(nèi)源速度快一些。

# 設(shè)置國內(nèi)源
npm config set registry https://registry.npmmirror.com
# 查看國內(nèi)源
npm get registry
  1. 驗(yàn)證安裝的版本是否正確
node -v
顯示
v21.7.3

npm -v
顯示
10.5.0

4.2 部署Web客戶端

  1. 使用npm安裝web客戶端需要的組件

Web客戶端程序目錄:servertech-chat/client

安裝客戶端需要的node組件

# 進(jìn)入Web客戶端代碼目錄
cd client
# 安裝web客戶端需要的組件
npm install
  1. 啟動客戶端
npm run dev

<font style="color:rgba(0, 0, 0, 0.85);">服務(wù)器會將任何匹配 URL </font>http://localhost:3000/api/(.*)<font style="color:rgba(0, 0, 0, 0.85);"> 的傳入 HTTP 流量路由到位于 </font>http://localhost:8080/api/<font style="color:rgba(0, 0, 0, 0.85);"> 的 C++ 服務(wù)器舔哪。如果你的 C++ 服務(wù)器在不同的端口上運(yùn)行捉蚤,請相應(yīng)地編輯 client/.env.development 文件修改端口缆巧。</font>

<font style="color:rgba(0, 0, 0, 0.85);"></font>

<font style="color:rgba(0, 0, 0, 0.85);">訪問web客戶端</font>

在瀏覽器訪問 http://localhost:3000, 如果是在服務(wù)器外部訪問按傅,則把localhost改成 服務(wù)器的ip地址,比如:

http://192.168.1.27:3000

進(jìn)入界面:

創(chuàng)建賬號

登錄聊天室

在聊天窗口根據(jù)提示發(fā)送消息就可以了。

5 項(xiàng)目架構(gòu)分析

我們主要關(guān)注服務(wù)端的代碼惜纸。我們的重點(diǎn)不是學(xué)習(xí)boost耐版,而是理清楚框架椭更,然后可以改造成自己的聊天室虑瀑。

<img src="https://cdn.nlark.com/yuque/0/2024/svg/708652/1726841723212-1272a97d-4c49-4de4-847d-f8e4629caa36.svg" style="zoom:150%;" />

get_hello_data獲取房間的歷史消息

request_room_history_event

5.1 數(shù)據(jù)存儲

MySQL:存儲用戶信息舌狗,在servertech_chat數(shù)據(jù)庫對應(yīng)的users表痛侍。

Redis:存儲房間消息和用戶cookie

  • 房間消息:使用redis的stream結(jié)構(gòu)主届,key為房間id君丁,value為房間的聊天消息绘闷,更多詳情參考:Redis Stream | 菜鳥教程 (runoob.com)
  • 用戶cookie印蔗,使用redis的string結(jié)構(gòu)华嘹,key為cookie,value為用戶id再菊,cookie默認(rèn)有效期是7天纠拔,超過七天redis就將他刪除稠诲,就需要用戶重新登錄臀叙。

5.2 消息格式

5.2.1 HTTP請求消息格式

create_account創(chuàng)建賬號消息

API URL:http:xxx.xxx.xxx.xxx:3000/api/create-account

{
    "username": "darren",
    "email": "326873713@qq.com",
    "password": "xxxxxxx"
}

測試范例:

login登錄消息

API URL:http:xxx.xxx.xxx.xxx:3000/api/login

{
    "email": "326873713@qq.com",
    "password": "xxxxxxx"
}

測試范例:

5.2.2 Websocket交互消息格式

剛websocket連接的消息

服務(wù)器回應(yīng)客戶端的數(shù)據(jù)

{
    "type": "hello",
    "payload": {
        "me": {
            "id": 5,
            "username": "小鴨子米奇"
        },
        "rooms": [
            {
                "id": "beast",
                "name": "程序員老廖",
                "hasMoreMessages": false,
                "messages": [
                    {
                        "id": "1726840364728-0",
                        "content": "222222",
                        "user": {
                            "id": 5,
                            "username": "小鴨子米奇"
                        },
                        "timestamp": 1726840364726
                    },
                    {
                        "id": "1726840317055-0",
                        "content": "222",
                        "user": {
                            "id": 5,
                            "username": "小鴨子米奇"
                        },
                        "timestamp": 1726840317055
                    } 
                  .......
                ]
            },
            {
                "id": "async",
                "name": "Boost.Async",
                "hasMoreMessages": false,
                "messages": [
                    {
                        "id": "1726839255147-0",
                        "content": "2",
                        "user": {
                            "id": 5,
                            "username": "小鴨子米奇"
                        },
                        "timestamp": 1726839255146
                    },
                    {
                        "id": "1726836482227-0",
                        "content": "22222222",
                        "user": {
                            "id": 5,
                            "username": "小鴨子米奇"
                        },
                        "timestamp": 1726836482218
                    }
                ]
            },
            {
                "id": "db",
                "name": "Database connectors",
                "hasMoreMessages": false,
                "messages": []
            },
            {
                "id": "wasm",
                "name": "Web assembly",
                "hasMoreMessages": false,
                "messages": []
            }
        ]
    }
}

聊天消息格式

發(fā)送端:比如用戶名:小鴨子米奇床嫌,用戶id:5發(fā)送的消息,此時(shí)會攜帶cookie

{
  "type": "clientMessages",
  "payload": {
    "roomId": "beast",
    "messages": [
      {
        "content": "這是小鴨子發(fā)送的消息"
      }
    ]
  }
}

經(jīng)過服務(wù)端處理后轉(zhuǎn)發(fā)給其他接收者的消息鳖谈,此時(shí)消息類型type 變?yōu)椤皊erverMessages”缆娃,message字段增加了消息id贯要,并增加了用戶信息 "user": { "id": 5, "username": "小鴨子米奇"},崇渗,以及時(shí)間戳timestamp。

{
    "type": "serverMessages",
    "payload": {
        "roomId": "beast",
        "messages": [
            {
                "id": "1726839290525-0",
                "content": "這是小鴨子發(fā)送的消息",
                "user": {
                    "id": 5,
                    "username": "小鴨子米奇"
                },
                "timestamp": 1726839290524
            }
        ]
    }
}

發(fā)送端的json數(shù)據(jù)只所以不帶用戶信息傻挂,是因?yàn)槠淇梢酝ㄟ^cookie從redis讀取user_id金拒,再根據(jù) user_id去MySQL查詢到username套腹,這里這個設(shè)計(jì)可以了解电禀,但這種做法雖然減少了客戶端發(fā)送的數(shù)據(jù)量,但每條消息都訪問MySQL對性能有影響的症副。

5.3 HTTP或者Websocket數(shù)據(jù)處理

服務(wù)端程序入口servertech-chat/server/src/main.cpp的main函數(shù)贞铣,重點(diǎn)在于launch_http_listener函數(shù)辕坝。

int main(int argc, char* argv[])
{
........
    // 對外提供服務(wù)的入口
    auto ec = launch_http_listener(ioc.get_executor(), listening_endpoint, st);
........
}

接下來分析launch_http_listener函數(shù)的重點(diǎn)內(nèi)容酱畅,這里就是一套tcp server的操作,我們重點(diǎn)是看accept_loop函數(shù)挚歧。

error_code chat::launch_http_listener(
    boost::asio::any_io_executor ex,
    boost::asio::ip::tcp::endpoint listening_endpoint,
    std::shared_ptr<shared_state> state
)
{
    .........
    boost::asio::spawn(
        std::move(ex),
        [acceptor = std::move(acceptor), st = std::move(state)](boost::asio::yield_context yield) mutable {
            accept_loop(std::move(acceptor), std::move(st), yield);
        },
        rethrow_handler  // Propagate exceptions to the io_context
    );
    ............
}

繼續(xù)分析accept_loop(), 我們有tcp server端的基礎(chǔ)滑负,應(yīng)該能理解每個新連接過來矮慕,需要通過accept獲取新連接痴鳄,這里我們只關(guān)注拿到新連接后怎么處理痪寻,即是run_http_session是我們關(guān)注的重點(diǎn)

static void accept_loop(
    boost::asio::ip::tcp::acceptor acceptor,
    std::shared_ptr<chat::shared_state> st,
    boost::asio::yield_context yield
)
{
    ........
    while (true)
    {
        // Accept a new connection
        auto sock = acceptor.async_accept(yield[ec]);
        if (ec)
            return chat::log_error(ec, "accept");

        // Launch a new session for this connection. Each session gets its
        // own stackful coroutine, so we can get back to listening for new connections.
        boost::asio::spawn(
            sock.get_executor(),
            [state = st, socket = std::move(sock)](boost::asio::yield_context yield) mutable {
                //重點(diǎn)在于run_http_session
                run_http_session(std::move(socket), std::move(state), yield);
            },
            rethrow_handler  // Propagate exceptions to the io_context
        );
    }
    .......
}

繼續(xù)分析chat::run_http_session()函數(shù),該函數(shù)讀取socket數(shù)據(jù)芽唇,然后分析是否是websocket或者h(yuǎn)ttp協(xié)議匆笤,不同的協(xié)議調(diào)用不同函數(shù)處理:

  • handle_chat_websocket 聊天的時(shí)候是websockt協(xié)議
    • chat_websocket_session::run() 這里負(fù)責(zé)讀取聊天消息炮捧,并轉(zhuǎn)發(fā)給房間里的其他人
      • 本質(zhì)是調(diào)用event_handler_visitor的error_with_message operator()(client_messages_event& evt)
  • handle_http_request 注冊和登錄是http協(xié)議
    • handle_http_request_impl 根據(jù)url解析api請求咆课,以http://xxx/api 開頭的是http api請求傀蚌,其他的認(rèn)為是靜態(tài)文件請求

5.3.1 HTTP請求處理流程

handle_http_request_impl函數(shù)

  • api/create-account 創(chuàng)建賬號善炫,調(diào)用chat::handle_create_account
    • 將用戶信息寫入MySQL
    • 生成cookie返回給客戶端,并且服務(wù)端將該cookie存儲在redis宪萄,以string類型存儲榨惰,cookie作為key琅催,用戶id作為value藤抡。
  • api/login 登錄賬號缠黍,調(diào)用chat::handle_login:
    • 解析json獲取郵箱和密碼
    • 根據(jù)郵箱獲取用戶id瓷式,然后校驗(yàn)密碼
    • 校驗(yàn)成功則生成cookie返回給客戶端并存儲在服務(wù)端贸典。

5.3.2 Websocket處理流程

servertech-chat/server/src/api/chat_websocket.cpp

分析websocket的處理函數(shù)event_handler_visitor 的 error_with_message operator()(client_messages_event& evt)瓤漏,這里主要的流程:

  1. 先把消息存儲到std::vector<message> msgs;
  2. 將消息存儲到redis 颊埃,調(diào)用 result_with_message<std::vector<std::string>> store_messages函數(shù)
    1. 使用XADD把消息加載到redis班利,其實(shí)是stream模式罗标,使用room_id作為key。參考:Redis Stream | 菜鳥教程 (runoob.com)
    2. redis-cli里彻消,可以使用 XREAD COUNT 3 STREAMS beast 0 來讀取beast房間的消息宾尚。
  3. 將redis返回的消息id賦值給msgs,并重新封裝成消息
  4. 將重新封裝后帶消息id的消息 發(fā)給所有的客戶端 st.pubsub().publish(evt.roomId, server_evt.to_json());
    1. chat_websocket_session::on_message
      1. websocket::write 發(fā)送消息給接收端

6 項(xiàng)目建議

如果不打算深入理解御板,只需要把這個項(xiàng)目的流程梳理清楚怠肋,然后基于自己的webserver擴(kuò)展這些邏輯笙各。

擴(kuò)展建議在《1.2 建議擴(kuò)展功能》酪惭。

通過擴(kuò)展增加代碼量春感,這樣在面試的時(shí)候更游刃有余虏缸。

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布刽辙!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宰缤,一起剝皮案震驚了整個濱河市慨灭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌呻疹,老刑警劉巖刽锤,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件并思,死亡現(xiàn)場離奇詭異宋彼,居然都是意外死亡输枯,警方通過查閱死者的電腦和手機(jī)桃熄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門瞳收,熙熙樓的掌柜王于貴愁眉苦臉地迎上來螟深,“玉大人烫葬,你說我怎么就攤上這事搭综。” “怎么了条获?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵帅掘,是天一觀的道長堂油。 經(jīng)常有香客問我称诗,道長寓免,這世上最難降的妖魔是什么袜香? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任蜈首,我火速辦了婚禮欢策,結(jié)果婚禮上踩寇,老公的妹妹穿的比我還像新娘俺孙。我一直安慰自己睛榄,他們只是感情好场靴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布票罐。 她就那樣靜靜地躺著泞边,像睡著了一般蚕礼。 火紅的嫁衣襯著肌膚如雪梢什。 梳的紋絲不亂的頭發(fā)上嗡午,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音狸演,去河邊找鬼宵距。 笑死满哪,一個胖子當(dāng)著我的面吹牛哨鸭,可吹牛的內(nèi)容都是我干的像鸡。 我是一名探鬼主播坟桅,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼赖舟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了夸楣?” 一聲冷哼從身側(cè)響起宾抓,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豫喧,沒想到半個月后石洗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡紧显,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年讲衫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孵班。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖拥诡,靈堂內(nèi)的尸體忽然破棺而出折柠,到底是詐尸還是另有隱情前塔,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響考廉,放射性物質(zhì)發(fā)生泄漏啄刹。R本人自食惡果不足惜袱讹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一非区、第九天 我趴在偏房一處隱蔽的房頂上張望管怠。 院中可真熱鬧,春花似錦她肯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘪贱。三九已至甜害,卻和暖如春嚣州,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工挑秉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夜惭,地道東北人木蹬。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓晦譬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容