Skip to content

meiseayoung/v-hono

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 存储文件元数据
  • 🚀 双请求池系统:活跃请求池 + 待请求池,支持并发控制和智能调度
  • 并发优化:可配置最大并发数,自动管理请求队列
  • 📊 实时状态监控:实时显示活跃请求数和等待请求数
  • 性能优化:分片大小记录文件,避免遍历文件计算总大小

安装

# 克隆仓库
git clone https://github.com/meiseayoung/v-hono.git
cd v-hono

# 运行示例
v run example.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('<h1>Hello V-Hono!</h1>')
    })
    
    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 是请求的核心对象,包含所有请求和响应信息:

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      // 响应体
}

构造函数

所有对象都使用构造函数风格创建:

// 创建 Context
ctx := hono.Context.new(req, params, query, body)

// 创建路由器
router := hono.ContextHybridRouter.new()
trie_router := hono.ContextTrieRouter.new()

// 创建缓存
cache := hono.ContextLRUCache.new(1000)

路由方法

支持所有 HTTP 方法:

// 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('')
})

动态路由

支持路径参数和通配符:

// 单参数
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

// JSON 响应
return c.json('{"message": "Hello"}')

// 文本响应
return c.text('Hello World')

// HTML 响应
return c.html('<h1>Hello</h1>')

// 设置状态码
c.status(404)
return c.json('{"error": "Not Found"}')

中间件

支持洋葱模型中间件:

// 日志中间件
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 中间件:

// 使用默认配置(./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 检测

查询参数和请求体

// 获取查询参数
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}"}')
})

路由统计和缓存

// 获取路由统计信息
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()

示例

基础示例

运行完整示例:

v run example.v

示例包含:

  • 静态路由
  • 动态路由(单参数、多参数、嵌套参数)
  • 所有 HTTP 方法
  • 中间件功能
  • 参数提取和查询参数处理

静态文件服务示例

运行静态文件服务示例:

v run static_example.v

这个示例演示了:

  • 多个静态文件目录配置
  • 自定义缓存策略
  • 安全特性演示
  • 完整的HTML界面

重要提示: 静态文件路径映射

  • ./public 目录 → 访问路径 /
  • ./static 目录 → 访问路径 /assets
  • ./uploads 目录 → 访问路径 /files

例如:

  • ./public/test.txthttp://localhost:8080/test.txt
  • ./static/style.csshttp://localhost:8080/assets/style.css
  • ./uploads/sample.jsonhttp://localhost:8080/files/sample.json

快速测试

运行简单的静态文件测试:

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 框架构建的高性能文件上传解决方案。支持大文件分片上传、断点续传、秒传、自动合并等功能。

相关文档:

主要特性

  • 分片上传:支持大文件分片上传,默认分片大小 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. 启动服务器

# 克隆项目
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): 分片文件数据

响应格式:

上传完成(自动合并):

{
    "success": true,
    "all_chunk_uploaded": true,
    "file_path": ".\\uploads\\files\\xxx.zip",
    "file_uuid": "xxx-xxx-xxx",
    "message": "File merged successfully"
}

上传中:

{
    "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): 分片大小

响应格式:

{
    "exists": true,
    "all_chunk_uploaded": false
}

3. 上传状态查询接口

接口地址: GET /upload/status

请求参数:

  • file_hash (string): 文件哈希值

响应格式:

{
    "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): 文件哈希值

响应格式:

{
    "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 配置项

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 示例

// 请求池管理类
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 示例

# 上传分片
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. 分片合并判断

系统使用以下逻辑判断是否所有分片都已上传:

// 计算理论上需要的分片数量
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,便于追踪和调试
  • 状态实时更新:实时显示活跃请求数和等待请求数
  • 自动清理:上传完成后自动清空请求池

使用示例:

// 创建请求池实例(最大并发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. 日志分析

查看服务器日志获取详细错误信息:

# 启动时查看详细日志
v run chunk_upload_example.v

扩展功能

1. 云存储集成

可以扩展支持:

  • AWS S3
  • 阿里云 OSS
  • 腾讯云 COS

2. 文件处理

可以添加:

  • 图片压缩
  • 视频转码
  • 文档预览

3. 权限控制

可以集成:

  • 用户认证
  • 文件权限
  • 访问控制

4. 双请求池系统

详细文档请参考:

性能特性

  • 混合路由算法:静态路由 O(1) 查找,动态路由正则匹配
  • LRU 缓存:自动缓存路由匹配结果
  • Trie 路由树:支持复杂路径匹配
  • 内存优化:使用指针和堆分配优化性能
  • 分片上传优化:分片大小记录文件,避免遍历文件计算总大小(O(1) vs O(n))

贡献

欢迎提交 Issue 和 Pull Request!

许可证

MIT License

About

Web framework like honojs in vlang

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •