C++Linux项目-Web多人聊天+MySQL+Redis

1 项目地址

程序员老廖

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脚本性能测试 alt alt alt

1.2 建议扩展功能

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

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时可能会报错,我编译时遇到的报错情况以及修改方法,可以参考以下方法把 ****三处报错 **修改后再一起重新编译:

(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

alt

然后重新编译

#确保此时是在servertech-chat/server/build目录
# 删除之前cmake产生的文件,但要注意你一定是在servertech-chat/server/build目录
rm -rf *
#重新cmake配置
cmake .. -DCMAKE_CXX_STANDARD=17
# 重新编译
make

编译成功后产生一个 main的执行文件,就是我们聊天室的服务程序。

alt

现在我们还不能直接运行,还要配置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,所以改成如下所示

alt

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

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

alt

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

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

alt

我们光有服务程序还不行,需要在 《4 部署客户端》 继续部署Web客户端,这样才能访问服务程序。

  1. 查看数据库情况

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

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

alt

alt

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

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

alt

服务器会将任何匹配 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

进入界面:

alt

创建账号

alt

登录聊天室

alt

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

5 项目架构分析

我们主要关注服务端的代码。我们的重点不是学习boost,而是理清楚框架,然后可以改造成自己的聊天室。 alt

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": "**********",
    "password": "xxxxxxx"
}

测试范例:

alt

login登录消息

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

{
    "email": "**********",
    "password": "xxxxxxx"
}

测试范例:

alt

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 建议扩展功能》。

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

#webserver##我的失利项目复盘##C++##校招过来人的经验分享##我的成功项目解析#
C/C++一站式学习知识库 文章被收录于专栏

C/C++学习难度较大且方向较多,设置的开发环境也比较多,为了节省大家的时间,程序员老廖我创建了该知识库,大家可以参考知识库的文章系统学习。

全部评论
点赞 回复 分享
发布于 04-18 12:16 陕西

相关推荐

07-31 06:07
已编辑
门头沟学院 C++
历时近五个月的找暑期之旅终于结束了投的基本都是中厂或者大厂&nbsp;总共面了大概二三十面&nbsp;过程充满了坎坷但非常充实&nbsp;总算在最后暑假还未结束的时候找到一份还不错的实习接下来就要边实习边秋招了下面做个总结:腾讯pcg&nbsp;大数据平台&nbsp;后台开发3.18一面&nbsp;3.21二面&nbsp;3.24三面&nbsp;4.1HR面&nbsp;4.2流程结束最难受的一次&nbsp;一上来就来个HR面挂腾讯csig&nbsp;腾讯云&nbsp;后台开发4.18一面&nbsp;4.29二面挂这个时候通过周围人的反映&nbsp;已经意识到后端有多卷了考虑到自己的学历背景加cpp&nbsp;冲后端有点困难了所以五月中旬之后都开始冲客户端了腾讯wxg&nbsp;企业微信&nbsp;移动客户端5.19一面&nbsp;5.23二面&nbsp;5.28面委压力面&nbsp;5.29流程结束也是比较意难平的一次&nbsp;前两面聊的都挺好的&nbsp;面委会因为一些网络等外在因素导致发挥的不是很好&nbsp;最后挂了也是意料之中腾讯csig&nbsp;QQ浏览器&nbsp;移动客户端6.4一面&nbsp;6.6二面挂挂的莫名其妙的&nbsp;手撕也撕出来了&nbsp;最后反馈也挺好&nbsp;可能是因为不匹配吧腾讯wxg&nbsp;企业微信&nbsp;测开6.13号一面挂没什么好说的&nbsp;难度太大纯自己菜字节跳动&nbsp;tiktok社交&nbsp;客户端开发6.24一面&nbsp;6.26二面挂有一说一字节难度确实比腾讯高很多字节跳动&nbsp;飞书办公套件&nbsp;客户端开发7.3一面挂问到了很多自己的漏洞西山居&nbsp;服务器开发&nbsp;7.9笔试&nbsp;7.11一面挂问了很多非常规问题阿里&nbsp;虎鲸文娱&nbsp;客户端开发7.19笔试&nbsp;7.22一面&nbsp;7.25上午二面&nbsp;下午三面&nbsp;7.28HR面&nbsp;两小时后录用马上也要入职了&nbsp;接下来就是尽快landing&nbsp;争取转正同时还得边实习边秋招&nbsp;希望一切顺利
点赞 评论 收藏
分享
已经实习了十几天,闲来无事还是来写个随笔吧。刚拿到实习offer的时候真的特别激动,当时还在宿舍打英雄联盟,一个电话打过来,我还以为是什么骚扰电话,但是电话另一头传来了消息,对我说同学你通过了饿了么的面试,请问你要来这边实习吗,真的挺激动,可能大厂实习好几段的同学不知道我在激动个什么,不就是个实习offer吗,且听我娓娓道来。虽说饿了么不是什么顶级大厂,但是对于我来说,已经算是很不错的了。在这个时间点,我自己手上只有一个合肥的小国企的offer,都打算当天晚上把offer接了。在接收到结果之前的一段时间内都挺煎熬的,从一开始的抱有希望,到后面慢慢接受,一直在搜索浏览阿里hr挂人的帖子,正常看到大家一周内都会有结果,特别是饿了么速度快的hr面完第二天就能收到oc,我面试完之后,都在回忆我哪里做得不好,哪句话可能会给我宣判死刑。在这种自我忏悔和抱有希望夹杂的感情中慢慢度过一天又一天,周四面完,周五焦急等待一天没有消息,希望直接减半,期间我都不知道到看了多少次招聘主页的流程状态有没有变化,每次点开前都忐忑一下。在下周一晚上八九点多的时候,终于看到,状态从面试中到等待面试结果了,从网上看到说,很多是hr面完直接流程结束,到等待面试结果是面试过了但是还在横向对比,也就是大家所谓的泡池子,一说到泡池子我就想到了那下头的华子,后面再说它。我以为状态到这,形势好转,心情好了不少,还开心心的跟女朋友说,看明天能不能收到电话了,但是一天天过去,还是没收到任何消息,期间每一个电话都能让我心跳扑通好几下,最搞人心态的是有一天的高德还是哪里的广告电话,也是八位数来自杭州,我服了。就这样一周过去了,我的大厂梦好像在胜利前的一夜即将看到黎明曙光的时候又逐渐黑了下来。但好在手上至少也还有一个offer,可以直接转正的那种,了解到也不加班之类的,心想也还可以,就直接开摆了。唉,我又幻想了,幻想着去大厂实习,把加入阿里大家庭的截图给我的兄弟们看,幻想着在大厂里每天能学到很多知识,拿着好几百一天的实习薪资美美敲代码,但是这个幻想在这一周过去的时候都破灭了。确实呀,我也能逐渐接受,现在学历为王的时代,我的双非本科真的好像是案底一样,每当失败的时候我都会这样安慰自己,我不是实力不够,只是学历不行。是呀,看到大家都是211本985硕起步的,我自己毫无竞争力可言,能有一个国企offer都已经很不错了。经历了很多次失败,逐渐也能够接受了,一周过去,这期间也面试了很多家,打算回老家休息几天,其中经过我大学同学家顺便去玩了一天,聊了很多,对于双非本科也逐渐认命了,没办法双非就是坐过牢。我总是这样安慰自己。我大概是从五月份才开始投递的,原因是在四五月份还在老师那边干活,也没时间准备和投递,时间比较晚了,投递过很多家但是都基本是简历直接没过,或者一面就直接挂了。当时投华子的时候,其实挺想去华子的,面试真的体验很不错面试官都很温柔,三月底就投递了,那热情的hr让我以为去华子挺有希望,但是最终泡池子被hr的我们通过面试之后是按学历一层层筛的这样一句话拒之门外,真的不知道这是看部门hc还有地方吗,明明我有学历比我低的同学也能去华子,后来我把怒火归结于我的hr,我以为她不一样,跟网上看到的华子hr不一样,但真还是一样(怎么跟谈恋爱似的)。还有讯飞的面试,是的讯飞的我也挂了,主要是我上学期投递的,这学期开学联系我面试期间只有两天的复习时间,经历了一个假期我完全记不太多了,当时就是怪自己太菜,这个确实是,而且给我的打击不小,觉得怎么别人口中的随便去怎么到我这这么困难,想着这么简单的大厂(毕竟也就一面二三十分钟样子)都过不了,那别的更没机会了,经历了几天的昏昏沉沉后面就跟老师来到了苏州干活。也还有面试感觉还行但是秒挂,还有评测完杳无音讯的,这种找工作的经历在我暑期的时候还好都经历了一遍,要是我直接秋招可能心态直接爆炸吧,秋招难度肯定比实习难很多了。要说好事还真是都一起来,后面还通过了一家银行的科技岗暑期,但还是想去大厂实习,想多体验体验。最终的结果也还行吧,虽然这个时间点比较晚了,但好过颗粒无收,无论能不能转正,我都想着在这多学点东西,多体验体验,没有去过大厂实习也没在上海生活过,这样起码有能说得出口的实习经历了。而且不知道为什么,觉得大家都好厉害,offershow上看到很多高薪但是好像下面评论都是说大白菜低了什么的,比如饿了么实习转正压价都是24x16,这一算下来有38w呀,到手能有小30w,我都觉得这已经算是相当不错了,但是怎么觉得大家都对40w以下的offer不正眼看呢?目前实习了两周,整体来说还算可以,没有很push,前几天就是配置环境还有看文档,后面就开始改bug和写需求了,我们组氛围还不错,这边上班没有打卡,早上十点前到,晚上大概八点左右走吧,我一般是早上九点四十多到,坐在工位上吃饭,差不多到吃到十点,一天大概就是9:40-12:00,然后中午在睡到一点多,有沙发或者折叠椅,然后随便吃点吧,下午是13:30-18:00,一小时吃个饭加散步,跟同组的人瞎溜达哈哈,然后19:00-20:00左右走了,周五是早下班下午六点就可以溜了。一天四百+中午25餐补(打工资卡)+晚上25餐补(券不用就没了),还有2k房补,感觉实习待遇也是相当不错了,不过上海的东西真贵,房子3k房租加上水电费3.3k吧,晚上也得三十多的饭。不知道各位友友们实习的时候都在干嘛?menter就在背后,也不敢打开leetcode和面试题网站呀
双非能在秋招上岸吗?
点赞 评论 收藏
分享
评论
2
27
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务