1 項(xiàng)目地址
<font style="color:#DF2A3F;">項(xiàng)目配套視頻簡介</font>:程序員老廖的個人空間-程序員老廖個人主頁-嗶哩嗶哩視頻 (bilibili.com)
1.1 項(xiàng)目原有功能
https://github.com/anarthal/servertech-chat.git
功能:
- 支持HTTP請求,掌握HTTP API + json的請求相應(yīng)
- 支持Websocket纠吴,掌握json做序列化和反序列化
- 支持多房間聊天
- 支持多人聊天
- 支持MySQL存儲用戶信息
- 支持Redis緩存token戴已,存儲聊天消息
- json序列化
- 靜態(tài)網(wǎng)頁支持
- 支持單元測試
- 支持python腳本性能測試
1.2 建議擴(kuò)展功能
- 基于Reactor網(wǎng)絡(luò)模型構(gòu)建HTTP服務(wù)和Websocket服務(wù)伐坏,替換現(xiàn)有的協(xié)程框架桦沉;
- 使用rapidjson做序列化和反序列化纯露;
- 仿寫MySQL/Redis連接池埠褪;
- 增加房間創(chuàng)建/修改/刪除接口钞速,并將房間成員存儲到MySQL渴语;
- 單元測試替換為gtest遵班;
- <font style="color:#DF2A3F;">........可以不斷擴(kuò)展,總而言之汇在,就是比做單純的webserver項(xiàng)目強(qiáng)</font>
2 開發(fā)環(huán)境
對gcc/g++編譯版本要求比較高亩鬼,建議升級到10.0以后的編譯器版本雳锋。
Ubuntu 20.04 玷过,如果Linux沒有基礎(chǔ)可以參考:Linux C/C++開發(fā)環(huán)境搭建(系列視頻)教程,vscode遠(yuǎn)程ubuntu調(diào)試多個c++文件辛蚊,讓你少走彎路_嗶哩嗶哩_bilibili
MySQL 8.0
Redis 6.0
gcc/g++ 10.5.0初澎,如果你的編譯器版本較低則可以參考【小記】Ubuntu 工具鏈升級 gcc 流程 - 芯片烤電池 - 博客園 (cnblogs.com) 進(jìn)行升級碑宴。
boost庫 1.86版本
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ù)
- 下載源碼
git clone https://github.com/anarthal/servertech-chat.git
PS:下載時(shí)最新的commit 0008f72e9bf7d
- 編譯源碼
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
- 啟動MySQL
<font style="color:#DF2A3F;">如果MySQL沒有啟動則需要啟動</font>
- 修改程序訪問MySQL的用戶名和密碼
/home/lqf/long/<font style="color:#DF2A3F;">servertech-chat/server/src/services/mysql_client.cpp</font>
修改用戶和密碼,我這里用戶名是root颖侄,密碼123456览祖,所以改成如下所示
- 修改程序訪問MySQL的地址
host我們用默認(rèn)的就行又活,因?yàn)楫?dāng)前部署是在MySQL所在機(jī)器部署的
3.3.2 配置Redis
以不需要密碼的方式啟動redis即可柳骄。
3.4 重新編譯和啟動服務(wù)程序
- 重新編譯程序
因?yàn)槲覀冎匦滦薷牧嗽创a文件夹界,所以需要使用make命令重新編譯
#確保此時(shí)是在servertech-chat/server/build目錄
# 重新編譯
make
- 啟動服務(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>
- 查看數(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
- 下載node
wget https://cdn.npmmirror.com/binaries/node/v21.7.3/node-v21.7.3-linux-x64.tar.gz
- 解壓
tar zxf node-v21.7.3-linux-x64.tar.gz
- 使用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
- 配置國內(nèi)的源
國內(nèi)源速度快一些。
# 設(shè)置國內(nèi)源
npm config set registry https://registry.npmmirror.com
# 查看國內(nèi)源
npm get registry
- 驗(yàn)證安裝的版本是否正確
node -v
顯示
v21.7.3
npm -v
顯示
10.5.0
4.2 部署Web客戶端
- 使用npm安裝web客戶端需要的組件
Web客戶端程序目錄:servertech-chat/client
安裝客戶端需要的node組件
# 進(jìn)入Web客戶端代碼目錄
cd client
# 安裝web客戶端需要的組件
npm install
- 啟動客戶端
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地址,比如:
進(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)
- chat_websocket_session::run() 這里負(fù)責(zé)讀取聊天消息炮捧,并轉(zhuǎn)發(fā)給房間里的其他人
- 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)瓤漏,這里主要的流程:
- 先把消息存儲到std::vector<message> msgs;
- 將消息存儲到redis 颊埃,調(diào)用 result_with_message<std::vector<std::string>> store_messages函數(shù)
- 使用XADD把消息加載到redis班利,其實(shí)是stream模式罗标,使用room_id作為key。參考:Redis Stream | 菜鳥教程 (runoob.com)
- redis-cli里彻消,可以使用 XREAD COUNT 3 STREAMS beast 0 來讀取beast房間的消息宾尚。
- 將redis返回的消息id賦值給msgs,并重新封裝成消息
- 將重新封裝后帶消息id的消息 發(fā)給所有的客戶端 st.pubsub().publish(evt.roomId, server_evt.to_json());
- chat_websocket_session::on_message
- websocket::write 發(fā)送消息給接收端
- chat_websocket_session::on_message
6 項(xiàng)目建議
如果不打算深入理解御板,只需要把這個項(xiàng)目的流程梳理清楚怠肋,然后基于自己的webserver擴(kuò)展這些邏輯笙各。
擴(kuò)展建議在《1.2 建議擴(kuò)展功能》酪惭。
通過擴(kuò)展增加代碼量春感,這樣在面試的時(shí)候更游刃有余虏缸。
本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布刽辙!