0


高性能的 C++ Web 开发框架 CPPCMS + WebSocket 模拟实现聊天与文件传输案例。

1. 项目结构

在这里插入图片描述

2. config.json

{"service":{"api":"http","port":8080,"ip":"0.0.0.0"},"http":{"script":"","static":"/static"}}

3. CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(c_web)

set(CMAKE_CXX_STANDARD 17)

# 指定源文件
set(SOURCE_FILES src/main.cpp src/blog.cpp)

# 手动设置 CppCMS 和 Booster 的头文件路径
include_directories(/usr/local/include)

# 手动设置库文件路径
link_directories(/usr/local/lib)

# 添加可执行文件
add_executable(c_web ${SOURCE_FILES})

find_package(Boost REQUIRED COMPONENTS system thread)
include_directories(${Boost_INCLUDE_DIRS})

# 添加 nlohmann_json 的头文件路径
include_directories(/usr/include/nlohmann)

# 链接 CppCMS 和 Booster 库
target_link_libraries(c_web cppcms booster ${Boost_LIBRARIES})

4. main.cpp

#include <cppcms/service.h>  // 引入CppCMS服务的头文件
#include <cppcms/applications_pool.h>  // 应用池管理类
#include <cppcms/http_response.h>  // 处理HTTP响应的类
#include <cppcms/url_dispatcher.h>  // URL调度器,用于将请求映射到处理程序
#include <boost/beast/core.hpp>  // Beast库的核心部分,用于处理输入输出操作
#include <boost/beast/websocket.hpp>  // Beast库的WebSocket部分,用于WebSocket处理
#include <boost/asio/ip/tcp.hpp>  // ASIO库的TCP/IP协议部分
#include <boost/asio/io_context.hpp>  // ASIO库的IO上下文,用于管理异步操作
#include <boost/asio/strand.hpp>  // ASIO库的strand,用于确保回调顺序
#include <thread>  // C++标准库的线程支持
#include <mutex>  // C++标准库的互斥锁,用于线程安全操作
#include <set>  // C++标准库的集合容器
#include <vector>  // C++标准库的向量容器
#include <iostream>  // 标准输入输出流
#include <fstream>  // 文件流,用于文件读写操作
#include <map>  // 映射容器,用于键值对存储
#include "blog.h"  // 自定义的博客应用程序头文件
#include <nlohmann/json.hpp>  // 引入nlohmann JSON库,用于处理JSON数据

namespace beast = boost::beast;  // 简化命名空间
namespace websocket = beast::websocket;  // 简化命名空间
namespace net = boost::asio;  // 简化命名空间
using tcp = net::ip::tcp;  // 使用TCP协议
using json = nlohmann::json;  // 使用nlohmann JSON库进行JSON处理

// 用于存储当前所有连接的用户及其对应的WebSocket连接
std::map<std::string, std::shared_ptr<websocket::stream<tcp::socket>>> user_connections;
// 用于确保线程安全地访问user_connections的互斥锁
std::mutex connections_mutex;

// 处理每个WebSocket会话的函数
void do_session(std::shared_ptr<websocket::stream<tcp::socket>> ws) {
    std::string current_user;  // 存储当前用户的用户名

    try {
        ws->accept();  // 接受WebSocket连接

        while(1) {  // 无限循环处理接收的消息
            beast::flat_buffer buffer;  // 创建一个缓冲区来存储接收到的数据
            ws->read(buffer);  // 从WebSocket连接中读取数据
            std::string message = beast::buffers_to_string(buffer.data());  // 将缓冲区中的数据转换为字符串

            auto data = json::parse(message);  // 解析JSON格式的数据

            // 处理用户初始化(登录)请求
            if (data["type"] == "init") {
                current_user = data["username"];  // 获取并保存当前用户的用户名
                {
                    // 使用互斥锁确保线程安全地更新user_connections
                    std::lock_guard<std::mutex> lock(connections_mutex);
                    user_connections[current_user] = ws;  // 将用户与其WebSocket连接关联
                }

                // 构建一个包含所有已连接用户列表的JSON对象
                json user_list = { {"type", "user_list"}, {"users", json::array()} };
                for (const auto& pair : user_connections) {
                    user_list["users"].push_back(pair.first);  // 将每个用户的用户名添加到列表中
                }

                // 将用户列表发送给所有已连接的用户
                for (const auto& pair : user_connections) {
                    pair.second->text(true);  // 设置为文本消息
                    pair.second->write(net::buffer(user_list.dump()));  // 发送用户列表
                }
            }
            // 处理普通消息传递请求
            else if (data["type"] == "message") {
                std::string target = data["target"];  // 获取消息的目标用户
                // 如果目标用户在线,则将消息转发给该用户
                if (user_connections.find(target) != user_connections.end()) {
                    auto target_ws = user_connections[target];
                    target_ws->text(true);  // 设置为文本消息
                    target_ws->write(net::buffer(message));  // 将消息发送给目标用户
                }
            }
            // 处理文件元数据及文件传输请求
            else if (data["type"] == "metadata") {
                std::string target = data["target"];  // 获取文件传输的目标用户
                // 如果目标用户在线,则发送文件元数据,并准备接收文件内容
                if (user_connections.find(target) != user_connections.end()) {
                    auto target_ws = user_connections[target];
                    target_ws->text(true);  // 发送文件元数据
                    target_ws->write(net::buffer(message));
                    beast::flat_buffer file_buffer;  // 准备接收文件内容的缓冲区
                    ws->read(file_buffer);  // 从发送者处读取文件内容
                    target_ws->binary(true);  // 设置为二进制消息
                    target_ws->write(file_buffer.data());  // 将文件内容转发给目标用户
                }
            }
        }
    } catch (std::exception const& e) {  // 捕获并处理异常
        std::cerr << "WebSocket Error: " << e.what() << std::endl;  // 打印错误信息
    }

    // 清理连接,确保在连接断开时从用户列表中移除用户
    std::lock_guard<std::mutex> lock(connections_mutex);
    if (!current_user.empty()) {
        user_connections.erase(current_user);  // 从连接映射中移除当前用户
    }
}

// 程序入口
int main(int argc, char* argv[]) {
    try {
        cppcms::service app(argc, argv);  // 创建CppCMS服务对象

        // 启动WebSocket服务器线程
        std::thread websocket_thread([]() {
            net::io_context ioc{1};  // 创建IO上下文对象
            tcp::acceptor acceptor{ioc, tcp::endpoint(tcp::v4(), 8081)};  // 创建TCP接受器,监听8081端口
            for (;;) {  // 无限循环,处理每个新的连接
                auto socket = std::make_shared<websocket::stream<tcp::socket>>(ioc);
                acceptor.accept(socket->next_layer());  // 接受新的TCP连接,并将其提升为WebSocket连接
                std::thread(&do_session, socket).detach();  // 为每个连接启动一个新的会话线程
            }
        });

        // 启动CppCMS服务
        app.applications_pool().mount(cppcms::applications_factory<blog>());  // 将博客应用挂载到服务池中
        app.run();  // 运行服务

        websocket_thread.join();  // 等待WebSocket线程结束
    } catch (std::exception const &e) {  // 捕获并处理异常
        std::cerr << "Error: " << e.what() << std::endl;  // 打印错误信息
        return EXIT_FAILURE;  // 以失败状态退出程序
    }
}

说明:WebSocket服务端口:8081,CppCMS服务端口:8080

5. blog.h

#ifndef BLOG_H
#define BLOG_H
#include <cppcms/application.h>
#include <cppcms/http_response.h>
#include <cppcms/url_dispatcher.h>
#include <cppcms/url_mapper.h>
#include <fstream>
class blog : public cppcms::application {
public:
    blog(cppcms::service &srv);
    void index();
private:
    void serve_html(const std::string &path);
    
};
#endif

6. blog.cpp

#include "blog.h"

blog::blog(cppcms::service &srv) : cppcms::application(srv) {
    dispatcher().map("GET", "/", &blog::index, this);
}

void blog::index() {
    serve_html("./views/index.html");
}

void blog::serve_html(const std::string &path) {
    std::ifstream file(path);
    if (file.is_open()) {
        std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
        response().out() << content;
    } else {
        response().status(404);
        response().out() << "Page not found";
    }
}

7. index.html

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>File Transfer and Chat</title><!-- 引入 Vue.js 和 Element-UI (包含图标支持) --><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script><linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/[email protected]/lib/theme-chalk/index.css"><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js"></script></head><body><divid="app"><el-container><el-header><h2>实时聊天与文件传输</h2></el-header><el-main><el-row:gutter="20"><el-col:span="6"><el-selectv-model="selectedUser"placeholder="选择一个用户"style="width: 100%;"><el-optionv-for="user in users":key="user":label="user":value="user"></el-option></el-select></el-col></el-row><el-row><el-col:span="24"><divid="chatbox"class="chatbox"><el-cardv-for="(msg, index) in messages":key="index"class="box-card"><p><strong>{{ msg.sender }}:</strong><!-- 图片预览 --><templatev-if="msg.isImage">
                                        {{ msg.content }} <br><img:src="msg.url"alt="Image Preview"style="max-width: 100px;max-height: 100px;"></template><!-- 文件下载链接 --><templatev-else>
                                        {{ msg.content }} 
                                        <a:href="msg.url":download="msg.filename"style="color: blue;text-decoration: underline;">
                                            {{ msg.filename }}
                                        </a></template></p><pstyle="font-size: 0.85em;color: #888;">{{ formatDate(msg.timestamp) }}</p></el-card></div></el-col></el-row><el-row><el-col:span="18"><el-inputplaceholder="输入消息..."v-model="newMessage"@keyup.enter.native="sendMessage"></el-input></el-col><el-col:span="6"><el-buttontype="primary"icon="el-icon-send"@click="sendMessage">发送</el-button></el-col></el-row><el-rowstyle="margin-top: 20px;"><el-col:span="18"><el-uploadclass="upload-demo"dragaction="":auto-upload="false":on-change="handleFileChange":file-list="fileList"><iclass="el-icon-upload"></i><divclass="el-upload__text">拖拽文件到此或点击上传</div></el-upload></el-col><el-col:span="6"><el-buttontype="success"icon="el-icon-upload2"@click="sendFile">发送文件</el-button></el-col></el-row></el-main></el-container></div><script>newVue({el:'#app',data(){return{ws:null,users:[],selectedUser:null,currentUser:null,newMessage:'',messages:[],fileList:[],pendingFileMessages:[]// 用于存储尚未处理的文件元数据};},mounted(){this.currentUser =prompt("请输入您的用户名:");this.ws =newWebSocket("ws://192.168.186.77:8081");this.ws.onopen=()=>{const initMessage ={type:"init",username:this.currentUser
                    };this.ws.send(JSON.stringify(initMessage));};this.ws.onmessage=(event)=>{try{const data =JSON.parse(event.data);if(data.type ==='user_list'){this.users = data.users.filter(user=> user !==this.currentUser);}elseif(data.type ==='message'|| data.type ==='metadata'){
                            data.timestamp =newDate();if(data.type ==='metadata'){this.pendingFileMessages.push(data);// 存储文件元数据}else{this.messages.push(data);}}}catch(error){// 如果解析失败,可能是Blob数据if(event.data instanceofBlob){this.handleFileData(event.data);}else{
                            console.error("消息解析失败:", error);}}};},methods:{sendMessage(){if(this.newMessage.trim()&&this.selectedUser){const message ={type:"message",sender:this.currentUser,target:this.selectedUser,content:this.newMessage,timestamp:newDate()// 增加时间戳};this.ws.send(JSON.stringify(message));this.messages.push(message);this.newMessage ='';}},handleFileChange(file, fileList){this.fileList = fileList;},sendFile(){if(this.fileList.length &&this.selectedUser){const file =this.fileList[0].raw;const reader =newFileReader();
                        reader.onload=(event)=>{const metadata ={type:"metadata",sender:this.currentUser,target:this.selectedUser,filename: file.name,filesize: file.size,timestamp:newDate().toISOString()// 增加时间戳};this.ws.send(JSON.stringify(metadata));// 立即显示文件信息const message ={sender:this.currentUser,content:``,isImage:this.isImageFile(file.name),url:this.isImageFile(file.name)?URL.createObjectURL(file):'',filename: file.name,// 保存文件名timestamp:newDate()// 增加时间戳};this.messages.push(message);this.ws.send(event.target.result);};
                        reader.readAsArrayBuffer(file);this.fileList =[];}},handleFileData(blobData){// 处理文件数据时,检查是否有待处理的文件元数据if(this.pendingFileMessages.length >0){const metadata =this.pendingFileMessages.shift();// 取出第一个元数据const url =URL.createObjectURL(blobData);const lastMessage ={sender: metadata.sender,content:``,isImage:this.isImageFile(metadata.filename),url: url,filename: metadata.filename,// 保存文件名timestamp: metadata.timestamp
                        };// 更新消息中的图片URL或生成下载链接if(lastMessage.isImage){
                            lastMessage.content =``;}else{
                            lastMessage.content =``;}this.messages.push(lastMessage);// 将消息加入消息列表}else{
                        console.error("未找到合适的文件元数据来处理文件数据。");}},isImageFile(filename){return/\.(jpeg|jpg|gif|png|svg)$/i.test(filename);},formatDate(date){const d =newDate(date);const year = d.getFullYear();const month =('0'+(d.getMonth()+1)).slice(-2);const day =('0'+ d.getDate()).slice(-2);const hours =('0'+ d.getHours()).slice(-2);const minutes =('0'+ d.getMinutes()).slice(-2);const seconds =('0'+ d.getSeconds()).slice(-2);return`${year}-${month}-${day}${hours}:${minutes}:${seconds}`;}}});</script><style>.chatbox{height: 300px;overflow-y: scroll;border: 1px solid #ddd;padding: 10px;margin-bottom: 20px;}.el-upload__input{display: none !important;}</style></body></html>

8. 测试验证

8.1 启动项目

cmake ./
make
./c_web -c ./config.json

在这里插入图片描述

说明:如果你想结束,请查询8081的PID,进行端口查杀kill -9 PID。

8.2 测试准备

访问:http://192.168.186.77:8080/

注意:IPv4需要切换为你本机IP。

在这里插入图片描述

说明:模拟用户数量:2,本案例是:admin和guest。

在这里插入图片描述

说明:聊天之前,先选择用户。

8.3 实时聊天

在这里插入图片描述

说明:左边是guest用户,右边是admin用户。

8.4 文件传输

在这里插入图片描述

说明:guest发送了一张图片给admin用户。

在这里插入图片描述

说明:admin发送了一个docx文件给guest。

在这里插入图片描述

说明:用户点击链接可以进行下载,下载不安全的原因是因为这是一个http而不是https。控制台描述:192.168.186.77/:1 The file at ‘blob:http://192.168.186.77:8080/997dc48a-83c2-4992-8ad3-960e7a51e74f’ was loaded over an insecure connection. This file should be served over HTTPS.

9. 总结

​ 只是实现了简单的实时聊天和文件传输,并没有严格的会话窗口区分,比如说如果A发送消息给B,C又发送给B,那么B窗口会同时显示A和C的消息,基于Booster库的Websocket实现简单案例。

标签: c++ websocket CPPCMS

本文转载自: https://blog.csdn.net/qq_71387716/article/details/141161085
版权归原作者 LiberInfo 所有, 如有侵权,请联系我们删除。

“高性能的 C++ Web 开发框架 CPPCMS + WebSocket 模拟实现聊天与文件传输案例。”的评论:

还没有评论