1 项目地址程序员老廖1.1 项目原有功能https://github.com/anarthal/servertech-chat.git功能:支持HTTP请求,掌握HTTP API + json的请求相应支持Websocket,掌握json做序列化和反序列化支持多房间聊天支持多人聊天支持MySQL存储用户信息支持Redis缓存token,存储聊天消息json序列化静态网页支持支持单元测试支持python脚本性能测试1.2 建议扩展功能基于Reactor网络模型构建HTTP服务和Websocket服务,替换现有的协程框架;使用rapidjson做序列化和反序列化;仿写MySQL/Redis连接池;增加房间创建/修改/删除接口,并将房间成员存储到MySQL;单元测试替换为gtest;........可以不断扩展,总而言之,就是比做单纯的webserver项目强2 开发环境对gcc/g++编译版本要求比较高,建议升级到10.0以后的编译器版本。Ubuntu 20.04 ,如果Linux没有基础可以参考:Linux C/C++开发环境搭建(系列视频)教程,vscode远程ubuntu调试多个c++文件,让你少走弯路_哔哩哔哩_bilibiliMySQL 8.0Redis 6.0gcc/g++ 10.5.0,如果你的编译器版本较低则可以参考【小记】Ubuntu 工具链升级 gcc 流程 - 芯片烤电池 - 博客园 (cnblogs.com) 进行升级。boost库 1.86版本3 部署服务端3.1 安装boost库该项目依赖boost库,需要先安装boost库,我们从官网下载(也可以从我提供的百度云链接下载)# 下载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#进入boostcd boost_1_86_0#配置boost库./bootstrap.sh#编译Boost库./b2#安装Boost库sudo ./b2 install#这将Boost库安装到系统默认的位置(一般是/usr/local)。3.2 编译聊天室服务下载源码git clone https://github.com/anarthal/servertech-chat.gitPS:下载时最新的commit 0008f72e9bf7d编译源码cd servertech-chat/cd server/mkdir buildcmake .. -DCMAKE_CXX_STANDARD=17make**在make时可能会报错,我编译时遇到的报错情况以及修改方法,可以参考以下方法把 ****三处报错 **修改后再一起重新编译:(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解决方法:修改servertech-chat/server/CMakeLists.txt,手动指定boost的路径: PATHS /usr/local/lib大约在14行修改:find_package(Boost REQUIRED COMPONENTS headers context json regex url PATHS /usr/local/lib)(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)'解决方法:修改servertech-chat/server/CMakeLists.txt,增加pthread,boost_charconv两个库大约在67行的target_link_libraries()里添加,如下所示: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库的头文件报错/usr/local/include/boost/redis/adapter/detail/adapters.hpp 报错添加 #define _LIBCPP_VERSION然后重新编译#确保此时是在servertech-chat/server/build目录# 删除之前cmake产生的文件,但要注意你一定是在servertech-chat/server/build目录rm -rf *#重新cmake配置cmake .. -DCMAKE_CXX_STANDARD=17# 重新编译make编译成功后产生一个 main的执行文件,就是我们聊天室的服务程序。现在我们还不能直接运行,还要配置MySQL和Redis。3.3 配置MySQL和Redis3.3.1 配置MySQL启动MySQL如果MySQL没有启动则需要启动修改程序访问MySQL的用户名和密码/home/lqf/long/servertech-chat/server/src/services/mysql_client.cpp修改用户和密码,我这里用户名是root,密码123456,所以改成如下所示修改程序访问MySQL的地址host我们用默认的就行,因为当前部署是在MySQL所在机器部署的3.3.2 配置Redis以不需要密码的方式启动redis即可。3.4 重新编译和启动服务程序重新编译程序因为我们重新修改了源码文件,所以需要使用make命令重新编译#确保此时是在servertech-chat/server/build目录# 重新编译make启动服务程序启动服务程序,这里要注意命令格式:Usage: ./main <address> <port> <doc_root>Example: ./main 0.0.0.0 8080 .doc_root的路径一定要设置对,比如./main 0.0.0.0 8080 ../../doc ,即是要正确给出这个项目自带的doc的目录我目前是在build目录下启动的,因为doc是在servertech-chat目录下,我的启动格式如下所示**(8080端口是web客户端调用http api时访问的端口,这里不要改其他的端口)**:lqf@ubuntu:~/long/servertech-chat/server/build$ ./main 0.0.0.0 8080 ../../doc正常启动后(没有信息输出是正常的):我们光有服务程序还不行,需要在 《4 部署客户端》 继续部署Web客户端,这样才能访问服务程序。查看数据库情况(这里只是告诉大家这个服务程序对应的数据库名字,以及有哪些表,表结构是怎么样的)服务程序启动后,数据库servertech_chat不存在则自动创建,我们使用mysql命令进入MySQL命令行控制台,可以查看到数据库servertech_chat被创建了。数据库只有一个表,用来存储用户信息。4 部署客户端需要安装node 16.14以上的版本4.1 安装node下载nodewget 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命令生效创建软链接,注意自己的路径,比如我的node路径是/home/lqf/long/node-v21.7.3-linux-x64sudo ln -s /home/lqf/long/node-v21.7.3-linux-x64/bin/node /usr/local/bin/nodesudo ln -s /home/lqf/long/node-v21.7.3-linux-x64/bin/npm /usr/local/bin/npm配置国内的源国内源速度快一些。# 设置国内源npm config set registry https://registry.npmmirror.com# 查看国内源npm get registry验证安装的版本是否正确node -v显示v21.7.3npm -v显示10.5.04.2 部署Web客户端使用npm安装web客户端需要的组件Web客户端程序目录:servertech-chat/client安装客户端需要的node组件# 进入Web客户端代码目录cd client# 安装web客户端需要的组件npm install启动客户端npm run dev服务器会将任何匹配 URL http://localhost:3000/api/(.*) 的传入 HTTP 流量路由到位于 http://localhost:8080/api/ 的 C++ 服务器。如果你的 C++ 服务器在不同的端口上运行,请相应地编辑 client/.env.development 文件修改端口。访问web客户端在浏览器访问 http://localhost:3000, 如果是在服务器外部访问,则把localhost改成 服务器的ip地址,比如:http://192.168.1.27:3000进入界面:创建账号登录聊天室在聊天窗口根据提示发送消息就可以了。5 项目架构分析我们主要关注服务端的代码。我们的重点不是学习boost,而是理清楚框架,然后可以改造成自己的聊天室。get_hello_data获取房间的历史消息request_room_history_event5.1 数据存储MySQL:存储用户信息,在servertech_chat数据库对应的users表。Redis:存储房间消息和用户cookie房间消息:使用redis的stream结构,key为房间id,value为房间的聊天消息,更多详情参考:Redis Stream | 菜鸟教程 (runoob.com)用户cookie,使用redis的string结构,key为cookie,value为用户id,cookie默认有效期是7天,超过七天redis就将他删除,就需要用户重新登录。5.2 消息格式5.2.1 HTTP请求消息格式create_account创建账号消息API URL:http:xxx.xxx.xxx.xxx:3000/api/create-account{ "username": "darren", "email": "**********", "password": "xxxxxxx"}测试范例:login登录消息API URL:http:xxx.xxx.xxx.xxx:3000/api/login{ "email": "**********", "password": "xxxxxxx"}测试范例:5.2.2 Websocket交互消息格式刚websocket连接的消息服务器回应客户端的数据{ "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": [] } ] }}聊天消息格式发送端:比如用户名:小鸭子米奇,用户id:5发送的消息,此时会携带cookie{ "type": "clientMessages", "payload": { "roomId": "beast", "messages": [ { "content": "这是小鸭子发送的消息" } ] }}经过服务端处理后转发给其他接收者的消息,此时消息类型type 变为“serverMessages”,message字段增加了消息id,并增加了用户信息 "user": { "id": 5, "username": "小鸭子米奇"},,以及时间戳timestamp。{ "type": "serverMessages", "payload": { "roomId": "beast", "messages": [ { "id": "1726839290525-0", "content": "这是小鸭子发送的消息", "user": { "id": 5, "username": "小鸭子米奇" }, "timestamp": 1726839290524 } ] }}发送端的json数据只所以不带用户信息,是因为其可以通过cookie从redis读取user_id,再根据 user_id去MySQL查询到username,这里这个设计可以了解,但这种做法虽然减少了客户端发送的数据量,但每条消息都访问MySQL对性能有影响的。5.3 HTTP或者Websocket数据处理服务端程序入口servertech-chat/server/src/main.cpp的main函数,重点在于launch_http_listener函数。int main(int argc, char* argv[]){........ // 对外提供服务的入口 auto ec = launch_http_listener(ioc.get_executor(), listening_endpoint, st);........}接下来分析launch_http_listener函数的重点内容,这里就是一套tcp server的操作,我们重点是看accept_loop函数。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 ); ............}继续分析accept_loop(), 我们有tcp server端的基础,应该能理解每个新连接过来,需要通过accept获取新连接,这里我们只关注拿到新连接后怎么处理,即是run_http_session是我们关注的重点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 { //重点在于run_http_session run_http_session(std::move(socket), std::move(state), yield); }, rethrow_handler // Propagate exceptions to the io_context ); } .......}继续分析chat::run_http_session()函数,该函数读取socket数据,然后分析是否是websocket或者http协议,不同的协议调用不同函数处理:handle_chat_websocket 聊天的时候是websockt协议chat_websocket_session::run() 这里负责读取聊天消息,并转发给房间里的其他人本质是调用event_handler_visitor的error_with_message operator()(client_messages_event& evt)handle_http_request 注册和登录是http协议handle_http_request_impl 根据url解析api请求,以http://xxx/api 开头的是http api请求,其他的认为是静态文件请求5.3.1 HTTP请求处理流程handle_http_request_impl函数api/create-account 创建账号,调用chat::handle_create_account将用户信息写入MySQL生成cookie返回给客户端,并且服务端将该cookie存储在redis,以string类型存储,cookie作为key,用户id作为value。api/login 登录账号,调用chat::handle_login:解析json获取邮箱和密码根据邮箱获取用户id,然后校验密码校验成功则生成cookie返回给客户端并存储在服务端。5.3.2 Websocket处理流程servertech-chat/server/src/api/chat_websocket.cpp分析websocket的处理函数event_handler_visitor 的 error_with_message operator()(client_messages_event& evt),这里主要的流程:先把消息存储到std::vector msgs;将消息存储到redis ,调用 result_with_message<std::vectorstd::string> store_messages函数使用XADD把消息加载到redis,其实是stream模式,使用room_id作为key。参考:Redis Stream | 菜鸟教程 (runoob.com)redis-cli里,可以使用 XREAD COUNT 3 STREAMS beast 0 来读取beast房间的消息。将redis返回的消息id赋值给msgs,并重新封装成消息将重新封装后带消息id的消息 发给所有的客户端 st.pubsub().publish(evt.roomId, server_evt.to_json());chat_websocket_session::on_message websocket::write 发送消息给接收端6 项目建议如果不打算深入理解,只需要把这个项目的流程梳理清楚,然后基于自己的webserver扩展这些逻辑。扩展建议在《1.2 建议扩展功能》。通过扩展增加代码量,这样在面试的时候更游刃有余。