<think>我们使用libhv库来实现WebSocket协议下的文件传输功能。由于WebSocket是双向通信的,我们可以通过将文件分块传输来实现大文件的传输。
方案概述:
1. 服务端和客户端建立WebSocket连接。
2. 发送方(可以是客户端或服务端)将文件分块(chunk)发送,每块数据作为一个WebSocket消息(二进制帧)发送。
3. 接收方接收这些分块,并按顺序组合成完整的文件。
4. 传输过程中可以加入元数据(如文件名、文件大小、分块信息等)以便接收方正确保存文件。
设计考虑:
- 分块大小:通常选择几KB到几十KB,以避免单帧过大。
- 元数据:在传输文件内容前,先发送一个控制消息(如JSON格式)包含文件名、文件大小、分块数等信息。
- 传输确认:每传输一个分块,接收方可以发送一个确认(ACK),但为了简单也可以不确认,除非要求可靠性高。
步骤:
1. 建立连接。
2. 发送方发送一个文件信息(元数据)消息。
3. 接收方收到元数据后,准备接收文件(创建文件等)。
4. 发送方按顺序发送文件分块。
5. 接收方按顺序接收并写入文件。
6. 传输完成后,发送方发送一个结束消息(可选)。
7. 接收方关闭文件,传输结束。
代码示例:
服务端代码(接收文件):
```cpp
#include "hv/WebSocketServer.h"
#include "hv/hv.h"
#include "hv/hfile.h"
#include "hv/hbase.h"
#include "hv/json.hpp"
using namespace hv;
using json = nlohmann::json;
// 定义每个分块的大小(10KB)
#define CHUNK_SIZE 10240
int main() {
WebSocketService ws;
// 存储每个连接的文件传输上下文
std::map<WebSocketChannelPtr, FILE*> file_map;
std::map<WebSocketChannelPtr, std::string> filename_map;
std::map<WebSocketChannelPtr, size_t> filesize_map;
std::map<WebSocketChannelPtr, size_t> received_size_map;
ws.onopen = [&file_map, &filename_map, &filesize_map, &received_size_map](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) {
printf("Client connected\n");
// 初始化上下文
file_map[channel] = nullptr;
filename_map[channel] = "";
filesize_map[channel] = 0;
received_size_map[channel] = 0;
};
ws.onmessage = [&](const WebSocketChannelPtr& channel, const std::string& msg) {
// 文本消息用于传输元数据
if (channel->opcode == WS_OPCODE_TEXT) {
try {
json meta = json::parse(msg);
if (meta.contains("filename") && meta.contains("filesize")) {
std::string filename = meta["filename"];
size_t filesize = meta["filesize"];
// 打开文件准备写入
std::string save_path = "./downloads/" + filename;
FILE* fp = fopen(save_path.c_str(), "wb");
if (!fp) {
perror("fopen");
channel->close();
return;
}
// 保存上下文
file_map[channel] = fp;
filename_map[channel] = filename;
filesize_map[channel] = filesize;
received_size_map[channel] = 0;
// 发送确认
json ack = {
{"status", "started"},
{"filename", filename}
};
channel->send(ack.dump());
}
} catch (const std::exception& e) {
printf("JSON parse error: %s\n", e.what());
}
} else if (channel->opcode == WS_OPCODE_BINARY) {
// 二进制消息为文件分块
FILE* fp = file_map[channel];
if (fp) {
// 写入分块
fwrite(msg.data(), 1, msg.size(), fp);
received_size_map[channel] += msg.size();
// 可选:发送进度
if (filesize_map[channel] > 0) {
int progress = (int)((received_size_map[channel] * 100) / filesize_map[channel]);
printf("Progress: %d%%\r", progress);
fflush(stdout);
}
// 如果接收完成
if (received_size_map[channel] >= filesize_map[channel]) {
fclose(fp);
file_map[channel] = nullptr;
printf("\nFile %s received successfully.\n", filename_map[channel].c_str());
// 发送完成确认
json ack = {
{"status", "completed"},
{"filename", filename_map[channel]}
};
channel->send(ack.dump());
}
}
}
};
ws.onclose = [&](const WebSocketChannelPtr& channel) {
printf("Client disconnected\n");
// 清理资源
if (file_map[channel]) {
fclose(file_map[channel]);
file_map[channel] = nullptr;
}
// 从map中移除
file_map.erase(channel);
filename_map.erase(channel);
filesize_map.erase(channel);
received_size_map.erase(channel);
};
// 创建下载目录
system("mkdir -p downloads");
// 启动WebSocket服务器
WebSocketServer server(&ws);
server.setPort(8888);
server.setThreadNum(4);
server.start();
while (getchar() != '\n');
server.stop();
return 0;
}
```
客户端代码(发送文件):
```cpp
#include "hv/WebSocketClient.h"
#include "hv/hv.h"
#include "hv/hfile.h"
#include "hv/hbase.h"
#include "hv/json.hpp"
using namespace hv;
using json = nlohmann::json;
#define CHUNK_SIZE 10240
int main(int argc, char** argv) {
if (argc < 2) {
printf("Usage: %s <filepath>\n", argv[0]);
return -1;
}
const char* filepath = argv[1];
std::string filename = hv_basename(filepath);
// 获取文件大小
size_t filesize = hv_filesize(filepath);
if (filesize == (size_t)-1) {
printf("File not found: %s\n", filepath);
return -1;
}
WebSocketClient ws;
ws.onopen = [&]() {
printf("Connected to server\n");
// 发送元数据
json meta = {
{"filename", filename},
{"filesize", filesize}
};
ws.send(meta.dump());
};
ws.onmessage = [&](const std::string& msg) {
// 处理服务器返回的确认消息
try {
json ack = json::parse(msg);
if (ack["status"] == "started") {
printf("Start sending file: %s\n", filename.c_str());
// 打开文件并分块发送
FILE* fp = fopen(filepath, "rb");
if (!fp) {
perror("fopen");
ws.close();
return;
}
char buf[CHUNK_SIZE];
size_t nread;
size_t total_sent = 0;
while ((nread = fread(buf, 1, sizeof(buf), fp)) > 0) {
ws.send(buf, nread, WS_OPCODE_BINARY);
total_sent += nread;
int progress = (int)((total_sent * 100) / filesize);
printf("Progress: %d%%\r", progress);
fflush(stdout);
}
fclose(fp);
printf("\nFile sent successfully.\n");
} else if (ack["status"] == "completed") {
printf("Server has received the file: %s\n", ack["filename"].get<std::string>().c_str());
}
} catch (const std::exception& e) {
printf("JSON parse error: %s\n", e.what());
}
};
ws.onclose = []() {
printf("Disconnected\n");
};
// 连接服务器
ws.open("ws://127.0.0.1:8888");
// 等待连接关闭
while (ws.isConnected()) {
hv_delay(1000);
}
return 0;
}
```
代码说明:
1. 服务端:
- 使用`WebSocketServer`启动一个WebSocket服务器。
- 在`onopen`中初始化每个连接的传输上下文。
- 在`onmessage`中,如果是文本消息则解析为元数据,并创建文件;如果是二进制消息则写入文件。
- 在`onclose`中清理资源。
2. 客户端:
- 使用`WebSocketClient`连接服务器。
- 连接建立后发送元数据(文件名和文件大小)。
- 收到服务器确认后开始分块读取文件并发送二进制消息。
- 显示发送进度。
注意事项:
- 该示例没有处理传输错误和重传,实际应用中可能需要增加可靠性。
- 分块大小可以根据网络状况调整。
- 传输大文件时,注意内存使用,避免一次性读取整个文件。
改进点:
1. 增加MD5校验,确保文件完整性。
2. 支持断点续传。
3. 支持多个文件同时传输。
§§相关问题§§:
1. 在WebSocket文件传输中,如何实现断点续传功能?
2. 如何优化WebSocket传输大文件的性能?
3. 在传输过程中如何保证文件的完整性和安全性(加密)?
4. 如何处理多个文件同时传输的情况?</think># 使用 libhv 的 WebSocket 协议实现文件传输
在 libhv 中实现 WebSocket 的文件传输功能需要精心设计协议和分块机制,因为 WebSocket 本身是面向消息的协议,而非面向流的协议。下面我将详细介绍如何在 libhv 中实现 WebSocket 客户端和服务端之间的文件传输功能。
## 整体设计思路
### 文件传输协议设计
1. **元数据交换**:首先传输文件元信息(文件名、大小、类型等)
2. **分块传输**:将大文件分割为多个数据块传输
3. **确认机制**:接收方确认每个数据块的接收
4. **校验机制**:传输完成后进行完整性校验
### 消息格式
```json
// 元数据消息
{
"type": "metadata",
"filename": "example.zip",
"filesize": 10485760,
"chunks": 1024,
"md5": "a1b2c3d4e5f6..."
}
// 数据块消息
{
"type": "data",
"chunk_index": 123,
"total_chunks": 1024,
"data": "<binary data>"
}
// 确认消息
{
"type": "ack",
"chunk_index": 123
}
// 完成消息
{
"type": "complete",
"success": true,
"md5": "a1b2c3d4e5f6..."
}
```
## 服务端实现(接收文件)
```cpp
#include "hv/WebSocketServer.h"
#include "hv/hfile.h"
#include "hv/hlog.h"
#include "hv/md5.h"
#include <fstream>
using namespace hv;
class FileReceiver {
public:
FileReceiver() : received_chunks(0), total_chunks(0), file_size(0) {}
bool prepare(const std::string& filename, size_t size, int chunks) {
// 创建文件
file_stream.open(filename, std::ios::binary | std::ios::out);
if (!file_stream.is_open()) {
hloge("Failed to create file: %s", filename.c_str());
return false;
}
this->filename = filename;
this->file_size = size;
this->total_chunks = chunks;
this->received_chunks = 0;
// 预分配文件空间
file_stream.seekp(size - 1);
file_stream.put('\0');
file_stream.seekp(0);
return true;
}
bool writeChunk(int index, const char* data, size_t size) {
// 计算写入位置
size_t position = (size_t)index * CHUNK_SIZE;
if (position >= file_size) {
hloge("Invalid chunk index: %d", index);
return false;
}
// 写入数据
file_stream.seekp(position);
file_stream.write(data, size);
received_chunks++;
return true;
}
bool complete() {
file_stream.close();
// 验证文件大小
size_t actual_size = hv_filesize(filename.c_str());
if (actual_size != file_size) {
hloge("File size mismatch: expected %zu, actual %zu", file_size, actual_size);
return false;
}
// 计算MD5校验
std::string actual_md5 = hv_md5_file(filename.c_str());
if (actual_md5 != expected_md5) {
hloge("MD5 mismatch: expected %s, actual %s",
expected_md5.c_str(), actual_md5.c_str());
return false;
}
return true;
}
void setExpectedMD5(const std::string& md5) {
expected_md5 = md5;
}
private:
std::ofstream file_stream;
std::string filename;
size_t file_size;
int total_chunks;
int received_chunks;
std::string expected_md5;
static const size_t CHUNK_SIZE = 1024 * 16; // 16KB 分块
};
int main() {
WebSocketService ws;
std::map<WebSocketChannelPtr, FileReceiver> receivers;
ws.onopen = [&](const WebSocketChannelPtr& channel, const HttpRequestPtr& req) {
hlogi("Client connected: %s", channel->peeraddr().c_str());
};
ws.onmessage = [&](const WebSocketChannelPtr& channel, const std::string& msg) {
try {
// 解析JSON消息
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
hlogw("Invalid JSON message: %s", msg.c_str());
return;
}
std::string type = root["type"].asString();
if (type == "metadata") {
// 处理元数据
std::string filename = root["filename"].asString();
size_t filesize = root["filesize"].asUInt64();
int chunks = root["chunks"].asInt();
std::string md5 = root["md5"].asString();
// 创建文件接收器
FileReceiver& receiver = receivers[channel];
if (!receiver.prepare(filename, filesize, chunks)) {
channel->close();
return;
}
receiver.setExpectedMD5(md5);
// 发送准备就绪确认
Json::Value ack;
ack["type"] = "ack";
ack["message"] = "ready";
channel->send(ack.toStyledString());
}
else if (type == "data") {
// 处理数据块
int chunk_index = root["chunk_index"].asInt();
int total_chunks = root["total_chunks"].asInt();
const std::string& data = root["data"].asString();
FileReceiver& receiver = receivers[channel];
if (!receiver.writeChunk(chunk_index, data.data(), data.size())) {
channel->close();
return;
}
// 发送块确认
Json::Value ack;
ack["type"] = "ack";
ack["chunk_index"] = chunk_index;
channel->send(ack.toStyledString());
// 显示进度
if (chunk_index % 100 == 0) {
int progress = (int)((float)chunk_index / total_chunks * 100);
hlogi("Received %d%% of file", progress);
}
}
else if (type == "complete") {
// 完成传输
FileReceiver& receiver = receivers[channel];
bool success = receiver.complete();
// 发送完成确认
Json::Value complete;
complete["type"] = "complete";
complete["success"] = success;
channel->send(complete.toStyledString());
if (success) {
hlogi("File received successfully");
}
}
} catch (const std::exception& e) {
hloge("Error processing message: %s", e.what());
}
};
ws.onclose = [&](const WebSocketChannelPtr& channel) {
hlogi("Client disconnected: %s", channel->peeraddr().c_str());
receivers.erase(channel);
};
// 创建接收目录
system("mkdir -p received_files");
// 启动WebSocket服务器
WebSocketServer server(&ws);
server.setPort(8888);
server.setThreadNum(4);
server.start();
printf("WebSocket file server running at ws://127.0.0.1:8888\n");
printf("Press Enter to stop...\n");
while (getchar() != '\n');
server.stop();
return 0;
}
```
## 客户端实现(发送文件)
```cpp
#include "hv/WebSocketClient.h"
#include "hv/hfile.h"
#include "hv/hlog.h"
#include "hv/md5.h"
#include <fstream>
#include <thread>
using namespace hv;
class FileSender {
public:
FileSender(const std::string& filepath) : filepath(filepath), total_chunks(0) {
filename = hv_basename(filepath.c_str());
file_size = hv_filesize(filepath.c_str());
file_md5 = hv_md5_file(filepath.c_str());
// 计算分块数
total_chunks = (file_size + CHUNK_SIZE - 1) / CHUNK_SIZE;
}
bool sendMetadata(WebSocketClient* ws) {
Json::Value meta;
meta["type"] = "metadata";
meta["filename"] = filename;
meta["filesize"] = (Json::UInt64)file_size;
meta["chunks"] = total_chunks;
meta["md5"] = file_md5;
return ws->send(meta.toStyledString()) == 0;
}
bool sendFile(WebSocketClient* ws) {
std::ifstream file(filepath, std::ios::binary);
if (!file.is_open()) {
hloge("Failed to open file: %s", filepath.c_str());
return false;
}
char buffer[CHUNK_SIZE];
for (int i = 0; i < total_chunks; i++) {
// 读取分块
file.read(buffer, CHUNK_SIZE);
size_t bytes_read = file.gcount();
// 构建数据消息
Json::Value data;
data["type"] = "data";
data["chunk_index"] = i;
data["total_chunks"] = total_chunks;
data["data"] = std::string(buffer, bytes_read);
// 发送分块
if (ws->send(data.toStyledString()) != 0) {
hloge("Failed to send chunk %d", i);
return false;
}
// 显示进度
if (i % 100 == 0) {
int progress = (int)((float)i / total_chunks * 100);
hlogi("Sent %d%% of file", progress);
}
// 限速(可选)
// hv_delay(10);
}
// 发送完成消息
Json::Value complete;
complete["type"] = "complete";
return ws->send(complete.toStyledString()) == 0;
}
private:
std::string filepath;
std::string filename;
size_t file_size;
std::string file_md5;
int total_chunks;
static const size_t CHUNK_SIZE = 1024 * 16; // 16KB 分块
};
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s <filepath>\n", argv[0]);
return -1;
}
const char* filepath = argv[1];
FileSender sender(filepath);
WebSocketClient ws;
ws.onopen = [&]() {
hlogi("Connected to server");
// 发送元数据
if (!sender.sendMetadata(&ws)) {
ws.close();
return;
}
};
ws.onmessage = [&](const std::string& msg) {
try {
Json::Value root;
Json::Reader reader;
if (!reader.parse(msg, root)) {
hlogw("Invalid JSON message: %s", msg.c_str());
return;
}
std::string type = root["type"].asString();
if (type == "ack") {
if (root.isMember("message") && root["message"].asString() == "ready") {
// 开始发送文件
std::thread([&]() {
if (sender.sendFile(&ws)) {
hlogi("File sent successfully");
}
ws.close();
}).detach();
}
else if (root.isMember("chunk_index")) {
// 块确认
int chunk_index = root["chunk_index"].asInt();
// hlogd("Chunk %d acknowledged", chunk_index);
}
}
else if (type == "complete") {
bool success = root["success"].asBool();
if (success) {
hlogi("File transfer completed successfully");
} else {
hloge("File transfer failed");
}
ws.close();
}
} catch (const std::exception& e) {
hloge("Error processing message: %s", e.what());
}
};
ws.onclose = []() {
hlogi("Disconnected from server");
};
// 连接服务器
ws.open("ws://127.0.0.1:8888");
// 运行事件循环
while (ws.isConnected()) {
hv_delay(1000);
}
return 0;
}
```
## 关键实现细节
### 1. 文件分块策略
- 使用固定大小的分块(16KB),平衡传输效率和内存使用
- 分块大小可调整,根据网络状况优化
- 使用JSON封装二进制数据(Base64编码)
### 2. 传输控制机制
- **流量控制**:接收方确认后才发送下一块
- **错误处理**:块传输失败时重试机制
- **完整性校验**:文件大小和MD5双重验证
### 3. 性能优化
- **并行传输**:服务端使用多线程处理多个客户端
- **零拷贝优化**:避免不必要的数据复制
- **内存管理**:使用流式处理避免大内存占用
### 4. 断点续传实现
```cpp
// 在元数据中添加已接收块信息
meta["received_chunks"] = received_chunks_list;
// 发送方跳过已接收的块
for (int i = last_received + 1; i < total_chunks; i++) {
// 发送分块
}
```
### 5. 传输进度显示
```cpp
// 服务端进度显示
int progress = (int)((float)received_chunks / total_chunks * 100);
hlogi("Received %d%% of file", progress);
// 客户端进度显示
int progress = (int)((float)i / total_chunks * 100);
hlogi("Sent %d%% of file", progress);
```
## 编译和运行
```bash
# 编译服务端
g++ websocket_file_server.cpp -o server -lhv -std=c++11
# 编译客户端
g++ websocket_file_client.cpp -o client -lhv -std=c++11
# 启动服务端
./server
# 客户端发送文件
./client /path/to/largefile.zip
```
## 高级功能扩展
### 1. 传输加密
```cpp
// 在发送前加密数据
std::string encrypt_data(const std::string& data, const std::string& key) {
// 使用AES等加密算法
}
// 在接收后解密数据
std::string decrypt_data(const std::string& encrypted, const std::string& key) {
// 解密算法
}
```
### 2. 压缩传输
```cpp
// 发送前压缩数据
std::string compressed = hv_compress(data, HV_COMPRESS_GZIP);
// 接收后解压数据
std::string decompressed = hv_decompress(compressed, HV_COMPRESS_GZIP);
```
### 3. 多文件传输
```cpp
// 元数据中添加文件列表
meta["files"] = Json::Value(Json::arrayValue);
meta["files"].append("file1.txt");
meta["files"].append("file2.jpg");
```
### 4. 传输速度限制
```cpp
// 在发送循环中添加延迟
for (int i = 0; i < total_chunks; i++) {
// 发送分块...
// 限制为100KB/s
hv_delay((CHUNK_SIZE * 1000) / (100 * 1024));
}
```
## 性能优化建议
1. **增大分块大小**:在网络条件好的情况下使用更大的分块(64KB-256KB)
2. **并行传输**:将文件分成多个部分同时传输
3. **内存映射文件**:使用`mmap`提高文件读写效率
4. **零拷贝技术**:避免不必要的数据复制
5. **二进制协议**:使用自定义二进制协议代替JSON提高效率