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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>File Transfer and Chat</title>
<!-- 引入 Vue.js 和 Element-UI (包含图标支持) -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.15.9/lib/theme-chalk/index.css">
<script src="https://cdn.jsdelivr.net/npm/element-ui@2.15.9/lib/index.js"></script>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h2>实时聊天与文件传输</h2>
</el-header>
<el-main>
<el-row :gutter="20">
<el-col :span="6">
<el-select v-model="selectedUser" placeholder="选择一个用户" style="width: 100%;">
<el-option
v-for="user in users"
:key="user"
:label="user"
:value="user">
</el-option>
</el-select>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<div id="chatbox" class="chatbox">
<el-card v-for="(msg, index) in messages" :key="index" class="box-card">
<p><strong>{{ msg.sender }}:</strong>
<!-- 图片预览 -->
<template v-if="msg.isImage">
{{ msg.content }} <br>
<img :src="msg.url" alt="Image Preview" style="max-width: 100px; max-height: 100px;">
</template>
<!-- 文件下载链接 -->
<template v-else>
{{ msg.content }}
<a :href="msg.url" :download="msg.filename" style="color: blue; text-decoration: underline;">
{{ msg.filename }}
</a>
</template>
</p>
<p style="font-size: 0.85em; color: #888;">{{ formatDate(msg.timestamp) }}</p>
</el-card>
</div>
</el-col>
</el-row>
<el-row>
<el-col :span="18">
<el-input placeholder="输入消息..." v-model="newMessage" @keyup.enter.native="sendMessage"></el-input>
</el-col>
<el-col :span="6">
<el-button type="primary" icon="el-icon-send" @click="sendMessage">发送</el-button>
</el-col>
</el-row>
<el-row style="margin-top: 20px;">
<el-col :span="18">
<el-upload
class="upload-demo"
drag
action=""
:auto-upload="false"
:on-change="handleFileChange"
:file-list="fileList">
<i class="el-icon-upload"></i>
<div class="el-upload__text">拖拽文件到此或点击上传</div>
</el-upload>
</el-col>
<el-col :span="6">
<el-button type="success" icon="el-icon-upload2" @click="sendFile">发送文件</el-button>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
ws: null,
users: [],
selectedUser: null,
currentUser: null,
newMessage: '',
messages: [],
fileList: [],
pendingFileMessages: [] // 用于存储尚未处理的文件元数据
};
},
mounted() {
this.currentUser = prompt("请输入您的用户名:");
this.ws = new WebSocket("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);
} else if (data.type === 'message' || data.type === 'metadata') {
data.timestamp = new Date();
if (data.type === 'metadata') {
this.pendingFileMessages.push(data); // 存储文件元数据
} else {
this.messages.push(data);
}
}
} catch (error) {
// 如果解析失败,可能是Blob数据
if (event.data instanceof Blob) {
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: new Date() // 增加时间戳
};
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 = new FileReader();
reader.onload = (event) => {
const metadata = {
type: "metadata",
sender: this.currentUser,
target: this.selectedUser,
filename: file.name,
filesize: file.size,
timestamp: new Date().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: new Date() // 增加时间戳
};
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 = new Date(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实现简单案例。