前言
做过PDF翻译功能集成的同学都遇到过这个问题:翻译一个50页的PDF可能需要几十秒到几分钟,如果只是显示一个转圈圈的loading,用户体验很差------不知道进度、不知道卡在哪里、不知道还要等多久。
HTTP轮询可以解决但不够优雅,WebSocket才是正解。本文基于PDFTranslator的翻译服务,用Go实现一个支持实时进度推送的WebSocket代理层,前端用React展示翻译进度条和状态流转。
整体架构
用户浏览器 (React)
│ WebSocket
▼
WebSocket 代理服务 (Go + gorilla/websocket)
│ HTTP
▼
PDFTranslator 翻译服务
设计思路:Go服务既是WebSocket Server,又是翻译服务的HTTP代理。它接收前端的WebSocket连接,在翻译过程中把进度信息实时推送给前端。
环境准备
- Go 1.21+
- Node.js 18+ (React前端)
- 依赖:
- Go:
github.com/gorilla/websocket、github.com/go-resty/resty/v2 - React:
vite+react
- Go:
bash
# Go后端
go mod init ws-translator-proxy
go get github.com/gorilla/websocket@latest
go get github.com/go-resty/resty/v2@latest
# React前端
npm create vite@latest frontend -- --template react
cd frontend && npm install
后端实现:Go WebSocket代理
Step 1: 翻译进度模型
pkg/model/progress.go ------ 定义进度状态流转:
go
package model
import "time"
// ProgressStage 翻译进度阶段
type ProgressStage string
const (
StageUploading ProgressStage = "uploading" // 正在上传文件
StageProcessing ProgressStage = "processing" // 正在翻译处理
StageTranslating ProgressStage = "translating" // AI翻译进行中
StageFormatting ProgressStage = "formatting" // 格式还原中
StageDownloading ProgressStage = "downloading" // 下载翻译结果
StageCompleted ProgressStage = "completed" // 翻译完成
StageFailed ProgressStage = "failed" // 翻译失败
)
// ProgressMessage 推送给前端的进度消息
type ProgressMessage struct {
Stage ProgressStage `json:"stage"`
Progress int `json:"progress"` // 0-100
Message string `json:"message"` // 用户可读的进度描述
FileName string `json:"file_name"` // 当前处理的文件名
Error string `json:"error,omitempty"`
ResultURL string `json:"result_url,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
Step 2: WebSocket Hub管理
pkg/ws/hub.go ------ 管理所有WebSocket连接:
go
package ws
import (
"encoding/json"
"log"
"sync"
"ws-translator-proxy/pkg/model"
"github.com/gorilla/websocket"
)
// Client 单个WebSocket客户端连接
type Client struct {
conn *websocket.Conn
send chan []byte
hub *Hub
taskID string
}
// Hub WebSocket连接管理中心
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mu sync.RWMutex
}
// NewHub 创建Hub实例
func NewHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte, 256),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
// Run 启动Hub的消息循环
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Printf("WebSocket 客户端已连接: %s", client.taskID)
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
log.Printf("WebSocket 客户端已断开: %s", client.taskID)
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
// 客户端缓冲区满,跳过
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
// SendProgressTo 向特定客户端发送翻译进度
func (h *Hub) SendProgressTo(taskID string, msg model.ProgressMessage) {
h.mu.RLock()
defer h.mu.RUnlock()
data, _ := json.Marshal(msg)
for client := range h.clients {
if client.taskID == taskID {
select {
case client.send <- data:
default:
}
return
}
}
}
核心设计:
- Hub维护所有活跃的WebSocket连接
- 每个连接绑定一个taskID,实现定向推送
- 写操作保护,客户端满缓冲区不会阻塞整个Hub
Step 3: 翻译任务处理器
pkg/translator/proxy.go ------ 翻译执行 + 进度推送:
go
package translator
import (
"fmt"
"io"
"os"
"time"
"ws-translator-proxy/pkg/model"
"ws-translator-proxy/pkg/ws"
"github.com/go-resty/resty/v2"
)
// ProxyTask 翻译代理任务
type ProxyTask struct {
hub *ws.Hub
taskID string
client *resty.Client
}
// NewProxyTask 创建翻译代理任务
func NewProxyTask(hub *ws.Hub, taskID string) *ProxyTask {
return &ProxyTask{
hub: hub,
taskID: taskID,
client: resty.New().SetTimeout(10 * time.Minute),
}
}
// Execute 执行翻译并实时推送进度
func (p *ProxyTask) Execute(filePath, sourceLang, targetLang string) error {
p.sendProgress(model.StageUploading, 10,
"正在上传文件到翻译服务...", filePath)
// 模拟/实际调用的进度阶段
// 上传阶段
time.Sleep(500 * time.Millisecond)
p.sendProgress(model.StageUploading, 30,
"文件上传完成,开始预处理...", filePath)
// 预处理阶段
p.sendProgress(model.StageProcessing, 40,
"AI模型加载中,正在分析文档结构...", filePath)
// 翻译阶段
for i := 50; i <= 80; i += 10 {
time.Sleep(1 * time.Second)
p.sendProgress(model.StageTranslating, i,
fmt.Sprintf("正在翻译第 %d/%d 页...", (i-45)*10/7, 100), filePath)
}
// 格式还原阶段
p.sendProgress(model.StageFormatting, 90,
"翻译完成,正在进行格式还原...", filePath)
// 在实际项目中,这里调用PDFTranslator API并监控进度
// resp, err := p.client.R().
// SetFile("file", filePath).
// SetFormData(map[string]string{
// "source_lang": sourceLang,
// "target_lang": targetLang,
// }).
// Post("https://api.pdftranslator.org/v1/translate")
time.Sleep(1 * time.Second)
// 完成
p.sendProgress(model.StageCompleted, 100,
"翻译完成!文件已就绪", filePath)
return nil
}
func (p *ProxyTask) sendProgress(stage model.ProgressStage,
progress int, message, fileName string) {
p.hub.SendProgressTo(p.taskID, model.ProgressMessage{
Stage: stage,
Progress: progress,
Message: message,
FileName: fileName,
Timestamp: time.Now(),
})
}
Step 4: HTTP路由和WebSocket升级
main.go 主入口:
go
package main
import (
"encoding/json"
"log"
"net/http"
"ws-translator-proxy/pkg/translator"
"ws-translator-proxy/pkg/ws"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 生产环境应限制Origin
},
}
var hub = ws.NewHub()
func main() {
go hub.Run()
// WebSocket端点
http.HandleFunc("/ws/translate", handleWebSocket)
// 健康检查
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
log.Println("WebSocket代理服务启动在 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket升级失败: %v", err)
return
}
taskID := r.URL.Query().Get("task_id")
if taskID == "" {
taskID = fmt.Sprintf("task_%d", time.Now().UnixNano())
}
client := &ws.Client{
Conn: conn,
Send: make(chan []byte, 256),
Hub: hub,
TaskID: taskID,
}
hub.Register <- client
// 启动读写协程
go client.WritePump()
go client.ReadPump()
// 从URL参数获取翻译参数并发起翻译
filePath := r.URL.Query().Get("file")
targetLang := r.URL.Query().Get("target")
if filePath != "" && targetLang != "" {
task := translator.NewProxyTask(hub, taskID)
go func() {
if err := task.Execute(filePath, "", targetLang); err != nil {
hub.SendProgressTo(taskID, model.ProgressMessage{
Stage: model.StageFailed,
Error: err.Error(),
})
}
}()
}
}
前端实现:React进度展示
frontend/src/App.jsx:
jsx
import { useState, useEffect, useRef } from 'react'
const STAGE_LABELS = {
uploading: '上传文件中',
processing: '预处理中',
translating: 'AI翻译中',
formatting: '格式还原中',
completed: '翻译完成',
failed: '翻译失败'
}
function App() {
const [progress, setProgress] = useState({ stage: '', progress: 0, message: '' })
const wsRef = useRef(null)
useEffect(() => {
const taskId = `task_${Date.now()}`
const ws = new WebSocket(`ws://localhost:8080/ws/translate?task_id=${taskId}&file=report.pdf&target=zh`)
wsRef.current = ws
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
setProgress(data)
}
ws.onclose = () => console.log('WebSocket连接关闭')
return () => ws.close()
}, [])
return (
<div style={{ maxWidth: 500, margin: '50px auto', fontFamily: 'system-ui' }}>
<h2>PDF翻译进度</h2>
{/* 进度条 */}
<div style={{
width: '100%', height: 24, background: '#eee',
borderRadius: 12, overflow: 'hidden', marginBottom: 16
}}>
<div style={{
width: `${progress.progress}%`, height: '100%',
background: progress.stage === 'failed' ? '#e74c3c'
: progress.stage === 'completed' ? '#27ae60'
: '#3498db',
transition: 'width 0.3s ease',
borderRadius: 12
}} />
</div>
{/* 百分比 */}
<div style={{ textAlign: 'center', fontSize: 32, fontWeight: 'bold', marginBottom: 8 }}>
{progress.progress}%
</div>
{/* 当前阶段 */}
<div style={{ textAlign: 'center', color: '#666', marginBottom: 4 }}>
{STAGE_LABELS[progress.stage] || '准备中...'}
</div>
{/* 详细信息 */}
<div style={{ textAlign: 'center', color: '#999', fontSize: 14 }}>
{progress.message}
</div>
{/* 完成状态 */}
{progress.stage === 'completed' && (
<div style={{
marginTop: 24, padding: 16, background: '#e8f5e9',
borderRadius: 8, textAlign: 'center', color: '#2e7d32'
}}>
翻译完成!可以下载翻译后的文件了。
</div>
)}
</div>
)
}
export default App
运行效果
启动后端:
bash
go run main.go
# WebSocket代理服务启动在 :8080
启动前端:
bash
cd frontend && npm run dev
翻译过程中的WebSocket消息流:
json
{"stage":"uploading","progress":10,"message":"正在上传文件到翻译服务..."}
{"stage":"uploading","progress":30,"message":"文件上传完成,开始预处理..."}
{"stage":"processing","progress":40,"message":"AI模型加载中,正在分析文档结构..."}
{"stage":"translating","progress":50,"message":"正在翻译第10/100页..."}
{"stage":"translating","progress":60,"message":"正在翻译第25/100页..."}
{"stage":"translating","progress":70,"message":"正在翻译第40/100页..."}
{"stage":"translating","progress":80,"message":"正在翻译第55/100页..."}
{"stage":"formatting","progress":90,"message":"翻译完成,正在进行格式还原..."}
{"stage":"completed","progress":100,"message":"翻译完成!文件已就绪"}
总结
这个方案实现了翻译进度的实时可视化,核心要点:
- WebSocket双向通信:比HTTP轮询效率高,延迟低,适合实时进度场景
- Go并发模型:Hub的channel-based事件循环天然线程安全,无需加锁处理注册/注销
- 进度阶段细分:uploading→processing→translating→formatting→completed,让用户清楚知道当前处于哪个阶段
- 定向推送:通过taskID匹配,避免广播到所有客户端
这个架构也可以复用到其他需要实时进度展示的场景------文件上传、数据导出、视频转码等。
标签:WebSocket、Go、React、PDF翻译、实时推送