当前位置: 首页 > article >正文

C++Linux项目推荐-Web多人聊天+MySQL+Redis+Websocket+Json,可以写简历的C++项目

1 项目地址

项目配套视频简介:程序员老廖的个人空间-程序员老廖个人主页-哔哩哔哩视频 (bilibili.com)

1.1 项目原有功能

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

功能:

  1. 支持HTTP请求,掌握HTTP API + json的请求相应
  2. 支持Websocket,掌握json做序列化和反序列化
  3. 支持多房间聊天
  4. 支持多人聊天
  5. 支持MySQL存储用户信息
  6. 支持Redis缓存token,存储聊天消息
  7. json序列化
  8. 静态网页支持
  9. 支持单元测试
  10. 支持python脚本性能测试

1.2 建议扩展功能

  1. 基于Reactor网络模型构建HTTP服务和Websocket服务,替换现有的协程框架;
  2. 使用rapidjson做序列化和反序列化;
  3. 仿写MySQL/Redis连接池;
  4. 增加房间创建/修改/删除接口,并将房间成员存储到MySQL;
  5. 单元测试替换为gtest;
  6. ........可以不断扩展,总而言之,就是比做单纯的webserver项目强

2 开发环境

对gcc/g++编译版本要求比较高,建议升级到10.0以后的编译器版本。

  • Ubuntu 20.04 ,如果Linux没有基础可以参考:Linux C/C++开发环境搭建(系列视频)教程,vscode远程ubuntu调试多个c++文件,让你少走弯路_哔哩哔哩_bilibili

  • MySQL 8.0

  • Redis 6.0

  • gcc/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

#进入boost
cd boost_1_86_0

#配置boost库
./bootstrap.sh

#编译Boost库
./b2


#安装Boost库
sudo ./b2 install
#这将Boost库安装到系统默认的位置(一般是/usr/local)。

3.2 编译聊天室服务

  1. 下载源码
git clone https://github.com/anarthal/servertech-chat.git

PS:下载时最新的commit 0008f72e9bf7d

  1. 编译源码
cd servertech-chat/
cd server/
mkdir build
cmake .. -DCMAKE_CXX_STANDARD=17
make

在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和Redis

3.3.1 配置MySQL

  1. 启动MySQL

如果MySQL没有启动则需要启动

  1. 修改程序访问MySQL的用户名和密码

/home/lqf/long/servertech-chat/server/src/services/mysql_client.cpp

修改用户和密码,我这里用户名是root,密码123456,所以改成如下所示

  1. 修改程序访问MySQL的地址

host我们用默认的就行,因为当前部署是在MySQL所在机器部署的

3.3.2 配置Redis

以不需要密码的方式启动redis即可。

3.4 重新编译和启动服务程序

  1. 重新编译程序

因为我们重新修改了源码文件,所以需要使用make命令重新编译

#确保此时是在servertech-chat/server/build目录
# 重新编译
make
  1. 启动服务程序

启动服务程序,这里要注意命令格式:

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客户端,这样才能访问服务程序。

  1. 查看数据库情况

(这里只是告诉大家这个服务程序对应的数据库名字,以及有哪些表,表结构是怎么样的)

服务程序启动后,数据库servertech_chat不存在则自动创建,我们使用mysql命令进入MySQL命令行控制台,可以查看到数据库servertech_chat被创建了。

数据库只有一个表,用来存储用户信息。

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命令生效

创建软链接,注意自己的路径,比如我的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. 配置国内的源

国内源速度快一些。

# 设置国内源
npm config set registry https://registry.npmmirror.com
# 查看国内源
npm get registry
  1. 验证安装的版本是否正确
node -v
显示
v21.7.3

npm -v
显示
10.5.0

4.2 部署Web客户端

  1. 使用npm安装web客户端需要的组件

Web客户端程序目录:servertech-chat/client

安装客户端需要的node组件

# 进入Web客户端代码目录
cd client
# 安装web客户端需要的组件
npm install
  1. 启动客户端
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_event

5.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": "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连接的消息

服务器回应客户端的数据

{
    "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),这里主要的流程:

  1. 先把消息存储到std::vector msgs;
  2. 将消息存储到redis ,调用 result_with_message<std::vectorstd::string> store_messages函数
    1. 使用XADD把消息加载到redis,其实是stream模式,使用room_id作为key。参考:Redis Stream | 菜鸟教程 (runoob.com)
    2. redis-cli里,可以使用 XREAD COUNT 3 STREAMS beast 0 来读取beast房间的消息。
  3. 将redis返回的消息id赋值给msgs,并重新封装成消息
  4. 将重新封装后带消息id的消息 发给所有的客户端 st.pubsub().publish(evt.roomId, server_evt.to_json());
    1. chat_websocket_session::on_message
      1.  websocket::write 发送消息给接收端

6 项目建议

如果不打算深入理解,只需要把这个项目的流程梳理清楚,然后基于自己的webserver扩展这些逻辑。

扩展建议在《1.2 建议扩展功能》。

通过扩展增加代码量,这样在面试的时候更游刃有余。

本文由博客一文多发平台 OpenWrite 发布!


http://www.kler.cn/news/343118.html

相关文章:

  • CompletionFormer 点云补全 学习笔记
  • 易泊:精准与高效的车牌识别解决方案
  • 黑马点评(更新中)
  • 网站设计公司怎么评估?2024网站定制公司哪家好
  • 大模型在问答领域的探索和实践
  • zabbix7.0配置中文界面
  • EXCELWPS工作表批量重命名(按照sheet1中A列内容)
  • Python 使用函数归纳判断回文质数
  • React父子组件,父组件状态更新,子组件的渲染状况
  • 浙江省发规院产业发展研究所调研组莅临迪捷软件考察调研
  • GR-ConvNet论文 学习笔记
  • 有什么方法可以保护ppt文件不被随意修改呢?
  • 从容应对DDoS攻击:小网站的防守之战
  • 【大数据】大数据治理的全面解析
  • Python | Leetcode Python题解之第463题岛屿的周长
  • JSON 格式化工具:快速便捷地格式化和查看 JSON 数据
  • 简单理解Python代码的重构
  • 重新学习Mysql数据库3:Mysql存储引擎与数据存储原理
  • 音频响度归一化 - python 实现
  • 自动驾驶系统研发系列—如何选择适合自动驾驶的激光雷达?从基础到高端全解读