# V-Hono 一个用 V 语言编写的轻量级 Web 框架,灵感来自 Hono.js。 ## 特性 - 🚀 高性能路由系统(混合路由算法) - 🎯 类似 Hono.js 的 Context API - 🔧 洋葱模型中间件支持 - 📦 内置 LRU 缓存系统 - 🛠️ 支持动态路由参数(`:param`) - ⚡ 支持通配符路由(`*` 和 `**`) - 🔄 支持所有 HTTP 方法(GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS) - 🏗️ 构造函数风格的 API 设计 - 📁 静态文件服务中间件(类似 Hono.js 的 serveStatic) - 🛡️ 内置安全防护(路径遍历攻击防护、点文件访问控制) - 💾 智能缓存支持(ETag、Last-Modified、Cache-Control) - 🎯 自动 Content-Type 检测 - 📤 **大文件分片上传**:支持大文件分片上传、断点续传、秒传、自动合并 - 🔄 **断点续传**:支持上传中断后继续上传 - ⚡ **秒传功能**:基于文件哈希的秒传检测 - 🗂️ **文件去重**:基于文件哈希的去重机制 - 💾 **数据库存储**:使用 SQLite 存储文件元数据 - 🚀 **双请求池系统**:活跃请求池 + 待请求池,支持并发控制和智能调度 - ⚡ **并发优化**:可配置最大并发数,自动管理请求队列 - 📊 **实时状态监控**:实时显示活跃请求数和等待请求数 - ⚡ **性能优化**:分片大小记录文件,避免遍历文件计算总大小 ## 安装 ```bash # 克隆仓库 git clone https://github.com/meiseayoung/v-hono.git cd v-hono # 运行示例 v run example.v ``` ## 快速开始 ```v module main import hono import net.http fn main() { mut app := hono.Hono.new() // 基本路由 app.get('/', fn (mut c hono.Context) http.Response { return c.html('

Hello V-Hono!

') }) app.get('/api/users/:id', fn (mut c hono.Context) http.Response { user_id := c.params['id'] return c.json('{"id": "${user_id}", "name": "John Doe"}') }) app.post('/api/users', fn (mut c hono.Context) http.Response { c.status(201) return c.json('{"message": "User created", "data": "${c.body}"}') }) // 中间件 app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { println('[LOG] ${c.path}') return next(mut c) }) app.listen(':3000') } ``` ## API 参考 ### Context Context 是请求的核心对象,包含所有请求和响应信息: ```v pub struct Context { pub: req http.Request // 原始 HTTP 请求 params map[string]string // 路径参数 query map[string]string // 查询参数 url string // 请求 URL pub mut: status_code int = 200 // 响应状态码 headers map[string]string // 响应头 body string // 响应体 } ``` ### 构造函数 所有对象都使用构造函数风格创建: ```v // 创建 Context ctx := hono.Context.new(req, params, query, body) // 创建路由器 router := hono.ContextHybridRouter.new() trie_router := hono.ContextTrieRouter.new() // 创建缓存 cache := hono.ContextLRUCache.new(1000) ``` ### 路由方法 支持所有 HTTP 方法: ```v // GET 请求 app.get('/path', fn (mut c hono.Context) http.Response { return c.json('{"message": "GET response"}') }) // POST 请求 app.post('/path', fn (mut c hono.Context) http.Response { return c.json('{"message": "POST response"}') }) // PUT 请求 app.put('/path', fn (mut c hono.Context) http.Response { return c.json('{"message": "PUT response"}') }) // DELETE 请求 app.delete('/path', fn (mut c hono.Context) http.Response { c.status(204) return c.text('') }) // PATCH 请求 app.patch('/path', fn (mut c hono.Context) http.Response { return c.json('{"message": "PATCH response"}') }) // HEAD 请求 app.head('/path', fn (mut c hono.Context) http.Response { return c.text('') }) // OPTIONS 请求 app.options('/path', fn (mut c hono.Context) http.Response { return c.text('') }) ``` ### 动态路由 支持路径参数和通配符: ```v // 单参数 app.get('/users/:id', fn (mut c hono.Context) http.Response { user_id := c.params['id'] return c.json('{"id": "${user_id}"}') }) // 多参数 app.get('/posts/:id/comments/:comment_id', fn (mut c hono.Context) http.Response { post_id := c.params['id'] comment_id := c.params['comment_id'] return c.json('{"post_id": "${post_id}", "comment_id": "${comment_id}"}') }) // 嵌套参数 app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response { user_id := c.params['user_id'] post_id := c.params['post_id'] return c.json('{"user_id": "${user_id}", "post_id": "${post_id}"}') }) // 通配符 app.get('/files/*', fn (mut c hono.Context) http.Response { file_path := c.params['*'] return c.text('File: ${file_path}') }) // 多级通配符 app.get('/api/**/search', fn (mut c hono.Context) http.Response { return c.json('{"search": "wildcard"}') }) ``` ### 响应方法 所有响应方法直接返回 `http.Response`: ```v // JSON 响应 return c.json('{"message": "Hello"}') // 文本响应 return c.text('Hello World') // HTML 响应 return c.html('

Hello

') // 设置状态码 c.status(404) return c.json('{"error": "Not Found"}') ``` ### 中间件 支持洋葱模型中间件: ```v // 日志中间件 app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { println('[${time.now()}] ${c.req.method} ${c.path}') return next(mut c) }) // 认证中间件 app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { token := c.query['token'] if token == '' { c.status(401) return c.json('{"error": "Unauthorized"}') } return next(mut c) }) // 性能监控中间件 app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { start := time.now() response := next(mut c) duration := time.now() - start println('请求处理时间: ${duration.milliseconds()}ms') return response }) ``` ### 静态文件服务 提供类似 Hono.js 的 `serveStatic` 中间件: ```v // 使用默认配置(./public 目录) app.use(hono.serve_static_default()) // 指定根目录 app.use(hono.serve_static_root('./static')) // 指定路径前缀和根目录 app.use(hono.serve_static_path('/assets', './static')) // 使用自定义配置 options := hono.StaticOptions{ root: './public' path: '/static' index: 'index.html' dotfiles: false etag: true last_modified: true max_age: 3600 // 1小时缓存 headers: { 'X-Custom-Header': 'value' } } app.use(hono.serve_static(options)) ``` **特性:** - 🛡️ 路径遍历攻击防护 - 📁 自动索引文件支持(index.html) - 🔒 点文件访问控制 - 🏷️ ETag 和 Last-Modified 支持 - 💾 可配置缓存策略 - 📋 自定义响应头 - 🎯 智能 Content-Type 检测 ### 查询参数和请求体 ```v // 获取查询参数 app.get('/search', fn (mut c hono.Context) http.Response { query := c.query['q'] page := c.query['page'] return c.json('{"query": "${query}", "page": "${page}"}') }) // 获取请求体 app.post('/api/data', fn (mut c hono.Context) http.Response { println('请求体: ${c.body}') return c.json('{"received": "${c.body}"}') }) ``` ### 路由统计和缓存 ```v // 获取路由统计信息 static_count, dynamic_count, cache_size, cache_capacity := app.get_router_stats() println('静态路由: ${static_count}, 动态路由: ${dynamic_count}') println('缓存: ${cache_size}/${cache_capacity}') // 清理缓存 app.clear_cache() ``` ## 示例 ### 基础示例 运行完整示例: ```bash v run example.v ``` 示例包含: - 静态路由 - 动态路由(单参数、多参数、嵌套参数) - 所有 HTTP 方法 - 中间件功能 - 参数提取和查询参数处理 ### 静态文件服务示例 运行静态文件服务示例: ```bash v run static_example.v ``` 这个示例演示了: - 多个静态文件目录配置 - 自定义缓存策略 - 安全特性演示 - 完整的HTML界面 **重要提示**: 静态文件路径映射 - `./public` 目录 → 访问路径 `/` - `./static` 目录 → 访问路径 `/assets` - `./uploads` 目录 → 访问路径 `/files` 例如: - `./public/test.txt` → `http://localhost:8080/test.txt` - `./static/style.css` → `http://localhost:8080/assets/style.css` - `./uploads/sample.json` → `http://localhost:8080/files/sample.json` ### 快速测试 运行简单的静态文件测试: ```bash v run test_static.v ``` 测试文件结构: ``` v-hono/ ├── public/ │ ├── index.html # 默认索引文件 │ └── test.txt # 测试文本文件 ├── static/ │ ├── style.css # 样式文件 │ └── app.js # JavaScript文件 └── uploads/ └── sample.json # JSON文件 ``` ## 大文件分片上传系统 ### 概述 V-Hono 大文件分片上传系统是一个基于 V 语言和 Hono 框架构建的高性能文件上传解决方案。支持大文件分片上传、断点续传、秒传、自动合并等功能。 **相关文档:** - [分片上传系统详解](README_CHUNK_UPLOAD.md) - 完整的分片上传功能文档 - [双请求池系统详解](README_REQUEST_POOL.md) - 请求池系统的设计和实现 ### 主要特性 - ✅ **分片上传**:支持大文件分片上传,默认分片大小 1MB - ✅ **断点续传**:支持上传中断后继续上传 - ✅ **秒传功能**:基于文件哈希的秒传检测 - ✅ **自动合并**:所有分片上传完成后自动合并文件 - ✅ **文件去重**:基于文件哈希的去重机制 - ✅ **数据库存储**:使用 SQLite 存储文件元数据 - ✅ **RESTful API**:完整的 REST API 接口 - ✅ **Web 界面**:提供友好的 Web 上传界面 - ✅ **双请求池系统**:活跃请求池 + 待请求池,智能并发控制 - ✅ **实时状态监控**:实时显示上传进度和请求池状态 ### 系统架构 ``` 前端 (Web/移动端) ↓ V-Hono 服务器 ↓ 分片存储 (./uploads/chunks/) ↓ 文件合并 (./uploads/files/) ↓ 数据库 (SQLite) ``` ### 安装和运行 #### 1. 环境要求 - V 语言 0.4.x 或更高版本 - Windows/Linux/macOS #### 2. 启动服务器 ```bash # 克隆项目 git clone https://github.com/meiseayoung/v-hono.git cd v-hono # 启动分片上传服务器 v run chunk_upload_example.v ``` 服务器将在 `http://localhost:8080` 启动。 #### 3. 访问 Web 界面 打开浏览器访问 `http://localhost:8080` 即可使用 Web 上传界面。 ### API 接口文档 #### 1. 分片上传接口 **接口地址:** `POST /upload/chunk` **请求参数:** - `file_hash` (string): 文件哈希值 - `chunk_index` (int): 分片索引,从 0 开始 - `filename` (string): 原始文件名 - `file_size` (int): 文件总大小(字节) - `chunk_size` (int): 分片大小(字节) - `chunk_hash` (string): 分片哈希值 - `chunk` (file): 分片文件数据 **响应格式:** 上传完成(自动合并): ```json { "success": true, "all_chunk_uploaded": true, "file_path": ".\\uploads\\files\\xxx.zip", "file_uuid": "xxx-xxx-xxx", "message": "File merged successfully" } ``` 上传中: ```json { "success": true, "chunk_index": 1, "all_chunk_uploaded": false, "message": "Chunk uploaded successfully" } ``` #### 2. 分片存在检查接口 **接口地址:** `GET /upload/chunk_exists` **请求参数:** - `file_hash` (string): 文件哈希值 - `chunk_index` (int): 分片索引 - `chunk_hash` (string): 分片哈希值 - `file_size` (int): 文件总大小 - `trunk_size` (int): 分片大小 **响应格式:** ```json { "exists": true, "all_chunk_uploaded": false } ``` #### 3. 上传状态查询接口 **接口地址:** `GET /upload/status` **请求参数:** - `file_hash` (string): 文件哈希值 **响应格式:** ```json { "file_hash": "xxx", "filename": "example.zip", "total_chunks": 0, "file_size": 1048576, "chunk_size": 2097152, "uploaded_chunks": [0, 1, 2], "status": "uploading", "created_at": 1640995200, "updated_at": 1640995300 } ``` #### 4. 已上传分片查询接口 **接口地址:** `GET /upload/chunks` **请求参数:** - `file_hash` (string): 文件哈希值 **响应格式:** ```json { "uploaded_chunks": [0, 1, 2], "total_chunks": 5, "completed": false } ``` #### 5. 文件管理接口 ##### 获取所有文件 **接口地址:** `GET /api/files` ##### 根据 UUID 获取文件 **接口地址:** `GET /api/files/{uuid}` ##### 根据哈希获取文件 **接口地址:** `GET /api/files/hash/{hash}` ##### 删除文件 **接口地址:** `DELETE /api/files/{uuid}` ### 配置说明 #### ChunkUploadConfig 配置项 ```v pub struct ChunkUploadConfig { chunk_size: int = 1024 * 1024 // 默认分片大小 1MB max_file_size: int = 1024 * 1024 * 1024 // 最大文件大小 1GB temp_dir: string = './uploads/chunks' // 临时分片目录 upload_dir: string = './uploads/files' // 最终文件目录 cleanup_delay: int = 3600 // 清理延迟时间(秒) clear_chunks_on_complete: bool = false // 完成后是否清理分片 db_path: string = './uploads/files.db' // 数据库文件路径 } ``` ### 文件存储结构 ``` uploads/ ├── chunks/ # 分片文件目录 │ └── {file_hash}/ # 按文件哈希分组 │ └── {chunk_size}/ # 按分片大小分组 │ ├── chunk_0.part # 分片文件 │ ├── chunk_1.part │ └── ... ├── files/ # 最终文件目录 │ ├── {file_hash}.{ext} # 合并后的文件 │ └── ... └── files.db # SQLite 数据库文件 ``` ### 使用示例 #### 1. 前端 JavaScript 示例 ```javascript // 请求池管理类 class RequestPool { constructor(maxConcurrent = 6) { this.maxConcurrent = maxConcurrent; this.activePool = new Map(); // 活跃请求池 {requestId: Promise} this.pendingPool = []; // 待请求池 [{requestId, task, resolve, reject}] this.requestIdCounter = 0; } // 生成请求ID generateRequestId() { return ++this.requestIdCounter; } // 添加请求到池中 async addRequest(task) { const requestId = this.generateRequestId(); return new Promise((resolve, reject) => { const request = { requestId, task, resolve, reject }; // 如果活跃池未满,直接执行 if (this.activePool.size < this.maxConcurrent) { this.executeRequest(request); } else { // 否则加入待请求池 this.pendingPool.push(request); console.log(`请求 ${requestId} 加入待请求池,当前待请求数: ${this.pendingPool.length}`); } }); } // 执行请求 async executeRequest(request) { const { requestId, task, resolve, reject } = request; console.log(`开始执行请求 ${requestId},当前活跃请求数: ${this.activePool.size + 1}`); // 将请求添加到活跃池 const promise = task() .then(result => { console.log(`请求 ${requestId} 执行成功`); resolve(result); return result; }) .catch(error => { console.log(`请求 ${requestId} 执行失败:`, error); reject(error); throw error; }) .finally(() => { // 请求完成后从活跃池移除 this.activePool.delete(requestId); console.log(`请求 ${requestId} 完成,从活跃池移除,当前活跃请求数: ${this.activePool.size}`); // 检查待请求池,如果有待请求则执行 this.processPendingRequests(); }); this.activePool.set(requestId, promise); } // 处理待请求池中的请求 processPendingRequests() { while (this.pendingPool.length > 0 && this.activePool.size < this.maxConcurrent) { const request = this.pendingPool.shift(); this.executeRequest(request); console.log(`从待请求池取出请求 ${request.requestId} 执行,剩余待请求数: ${this.pendingPool.length}`); } } // 获取池状态 getStatus() { return { activeCount: this.activePool.size, pendingCount: this.pendingPool.length, maxConcurrent: this.maxConcurrent }; } // 清空所有池 clear() { this.activePool.clear(); this.pendingPool = []; console.log('请求池已清空'); } } // 计算文件哈希 async function calculateFileHash(file) { return new Promise(resolve => { const spark = new SparkMD5.ArrayBuffer(); const reader = new FileReader(); reader.onload = e => { spark.append(e.target.result); resolve(spark.end()); }; reader.readAsArrayBuffer(file); }); } // 上传分片 async function uploadChunk(file, chunk, { fileHash, chunkIndex, chunkHash }) { const form = new FormData(); form.append('file_hash', fileHash); form.append('chunk_index', chunkIndex); form.append('filename', file.name); form.append('file_size', file.size); form.append('chunk_size', CHUNK_SIZE); form.append('chunk_hash', chunkHash); form.append('chunk', chunk); const response = await fetch('/upload/chunk', { method: 'POST', body: form }); const result = await response.json(); if (result.all_chunk_uploaded) { console.log('上传完成,文件已合并:', result.file_path); return true; // 上传完成 } return false; // 继续上传 } // 使用请求池的分片上传主函数 async function uploadFileWithPool(file) { const fileHash = await calculateFileHash(file); const chunkSize = 2 * 1024 * 1024; // 2MB const totalChunks = Math.ceil(file.size / chunkSize); // 创建请求池实例 const requestPool = new RequestPool(6); // 创建所有分片的上传任务并添加到请求池 const uploadPromises = []; for (let i = 0; i < totalChunks; i++) { const chunkIndex = i; const promise = requestPool.addRequest(async () => { const chunk = file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize); const chunkHash = await calculateFileHash(chunk); return await uploadChunk(file, chunk, { fileHash, chunkIndex, chunkHash }); }); uploadPromises.push(promise); } // 等待所有上传任务完成 const results = await Promise.all(uploadPromises); // 清空请求池 requestPool.clear(); return results; } ``` #### 2. cURL 示例 ```bash # 上传分片 curl -X POST http://localhost:8080/upload/chunk \ -F "file_hash=abc123" \ -F "chunk_index=0" \ -F "filename=large_file.zip" \ -F "file_size=10485760" \ -F "chunk_size=2097152" \ -F "chunk_hash=def456" \ -F "chunk=@chunk_0.part" # 检查分片是否存在 curl "http://localhost:8080/upload/chunk_exists?file_hash=abc123&chunk_index=0&chunk_hash=def456&file_size=10485760&trunk_size=2097152" ``` ### 核心算法 #### 1. 分片合并判断 系统使用以下逻辑判断是否所有分片都已上传: ```v // 计算理论上需要的分片数量 expected_chunks := (expected_file_size + u64(chunk_size) - 1) / u64(chunk_size) // 只有当分片数量达到预期且总大小 >= 文件大小时,才认为上传完成 if chunk_count >= int(expected_chunks) && total_chunk_size >= expected_file_size { all_chunk_uploaded = true } ``` #### 2. 文件去重机制 - 基于文件哈希进行去重 - 相同哈希的文件只存储一份 - 支持同一文件的不同文件名 #### 3. 秒传检测 - 前端计算文件哈希 - 后端检查文件是否已存在 - 如果存在则直接返回文件信息,无需上传 ### 性能优化 #### 1. 内存管理 - 分片文件存储在磁盘,不占用大量内存 - 上传状态使用内存缓存,提高查询速度 - 支持配置清理策略,自动清理临时文件 #### 2. 双请求池系统 **架构设计:** - **活跃请求池**:存储正在执行的请求,使用 Map 结构 `{requestId: Promise}` - **待请求池**:存储等待执行的请求,使用数组结构 `[{requestId, task, resolve, reject}]` - **智能调度**:当活跃池未满时直接执行,满时加入待请求池 - **自动管理**:请求完成后自动从活跃池移除,并处理待请求池中的请求 **核心特性:** - **可配置并发数**:默认最大并发数为 6,可根据服务器性能调整 - **请求ID管理**:每个请求都有唯一ID,便于追踪和调试 - **状态实时更新**:实时显示活跃请求数和等待请求数 - **自动清理**:上传完成后自动清空请求池 **使用示例:** ```javascript // 创建请求池实例(最大并发6个) const requestPool = new RequestPool(6); // 添加请求到池中 const promise = requestPool.addRequest(async () => { // 上传分片的逻辑 return await uploadChunk(chunk); }); // 获取池状态 const status = requestPool.getStatus(); console.log(`活跃: ${status.activeCount}/${status.maxConcurrent}, 等待: ${status.pendingCount}`); ``` #### 3. 并发处理 - 支持多用户同时上传 - 分片级别的并发控制 - 文件级别的锁机制 - 基于请求池的智能并发管理 #### 4. 错误处理 - 网络异常自动重试 - 分片损坏自动重新上传 - 完整的错误日志记录 - 请求池级别的错误隔离 ### 监控和日志 #### 1. 调试日志 系统提供详细的调试日志,包括: - 分片上传状态 - 文件合并过程 - 错误信息追踪 #### 2. 性能监控 - 上传速度统计 - 分片成功率 - 系统资源使用情况 ### 故障排除 #### 1. 常见问题 **Q: 上传大文件时出现内存不足** A: 检查分片大小配置,建议设置为 1-5MB **Q: 分片上传后文件合并失败** A: 检查磁盘空间和文件权限 **Q: 秒传功能不工作** A: 确认前端哈希算法与后端一致 #### 2. 日志分析 查看服务器日志获取详细错误信息: ```bash # 启动时查看详细日志 v run chunk_upload_example.v ``` ### 扩展功能 #### 1. 云存储集成 可以扩展支持: - AWS S3 - 阿里云 OSS - 腾讯云 COS #### 2. 文件处理 可以添加: - 图片压缩 - 视频转码 - 文档预览 #### 3. 权限控制 可以集成: - 用户认证 - 文件权限 - 访问控制 #### 4. 双请求池系统 详细文档请参考: - [双请求池系统详解](README_REQUEST_POOL.md) - 完整的系统设计和实现指南 - [分片上传系统](README_CHUNK_UPLOAD.md) - 包含请求池的分片上传完整方案 ## 性能特性 - **混合路由算法**:静态路由 O(1) 查找,动态路由正则匹配 - **LRU 缓存**:自动缓存路由匹配结果 - **Trie 路由树**:支持复杂路径匹配 - **内存优化**:使用指针和堆分配优化性能 - **分片上传优化**:分片大小记录文件,避免遍历文件计算总大小(O(1) vs O(n)) ## 贡献 欢迎提交 Issue 和 Pull Request! ## 许可证 MIT License