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

1 项目地址

<font style="color:#DF2A3F;">项目配套视频简介</font>程序员老廖的个人空间-程序员老廖个人主页-哔哩哔哩视频 (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. <font style="color:#DF2A3F;">........可以不断扩展,总而言之,就是比做单纯的webserver项目强</font>

2 开发环境

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

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时可能会报错,我编译时遇到的报错情况以及修改方法,可以参考以下方法把 ****<font style="color:#DF2A3F;">三处报错 </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库的头文件报错

/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

<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我们用默认的就行,因为当前部署是在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

正常启动后(没有信息输出是正常的):

<font style="color:#DF2A3F;">我们光有服务程序还不行,需要在 《4 部署客户端》 继续部署Web客户端,这样才能访问服务程序。</font>

  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

<font style="color:rgba(0, 0, 0, 0.85);">服务器会将任何匹配 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++ 服务器。如果你的 C++ 服务器在不同的端口上运行,请相应地编辑 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, 如果是在服务器外部访问,则把localhost改成 服务器的ip地址,比如:

http://192.168.1.27:3000

进入界面:

创建账号

登录聊天室

在聊天窗口根据提示发送消息就可以了。

5 项目架构分析

我们主要关注服务端的代码。我们的重点不是学习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 数据存储

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<message> msgs;
  2. 将消息存储到redis ,调用 result_with_message<std::vector<std::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 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容