Hono 生产实战 06:SSE、WebSocket 和流式响应
wxk1991 Lv5

Hono 生产实战 06:SSE、WebSocket 和流式响应

前后端分离项目里,经常会遇到“后端需要持续告诉前端一些东西”的场景。

比如:

1
2
3
4
5
6
7
AI 对话逐字输出
任务进度实时更新
订单状态变化
日志实时滚动
协作文档在线状态
聊天室消息
监控大屏刷新

很多人第一反应是 WebSocket。

但生产里并不是所有实时需求都该上 WebSocket。

Hono 支持普通 HTTP、流式响应、SSE,也可以配合不同运行时使用 WebSocket。真正重要的不是“选最酷的协议”,而是选和业务复杂度匹配的通信方式。

这篇文章讲清楚 Hono 项目里如何选择 SSE、WebSocket 和 streaming。

技术栈快照时间:2026-06-23。Hono 的 streaming helper、WebSocket helper 和各运行时支持情况会更新,生产使用前以官方文档为准。


一、先别急着 WebSocket

实时通信有四种常见选择:

1
2
3
4
轮询
流式响应
SSE
WebSocket

可以这样理解:

1
2
3
4
轮询:前端隔几秒问一次
流式响应:一次 HTTP 请求里持续返回数据
SSE:服务端单向推送事件
WebSocket:客户端和服务端双向长连接

选择建议:

1
2
3
4
任务状态偶尔变化:轮询
AI 文本逐步输出:流式响应
服务端持续通知前端:SSE
前后端需要高频双向通信:WebSocket

不要为了“实时”就默认 WebSocket。

如果只是导出任务进度,从 0% 到 100%,轮询或 SSE 通常更简单。

如果是在线聊天、多人协作、实时游戏,那 WebSocket 才更合适。


二、流式响应适合 AI 输出

AI 应用最常见的是流式文本。

用户问一个问题,后端不是等模型完整生成后再返回,而是边生成边返回。

Hono 提供了 streaming helper,可以写出很直观的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Hono } from 'hono'
import { streamText } from 'hono/streaming'

const app = new Hono()

app.post('/api/chat', async (c) => {
const { message } = await c.req.json()

return streamText(c, async (stream) => {
await stream.write('正在思考:')

for await (const chunk of callModel(message)) {
await stream.write(chunk)
}
})
})

前端可以用 fetch 读取 ReadableStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: input }),
})

const reader = res.body!.getReader()
const decoder = new TextDecoder()

while (true) {
const { done, value } = await reader.read()
if (done) break

const text = decoder.decode(value, { stream: true })
appendMessage(text)
}

这种方式非常适合:

1
2
3
4
AI 文本生成
命令执行日志
大文件逐块返回
长耗时计算的阶段性输出

它仍然是 HTTP 请求,不需要维护双向连接模型。


三、SSE 适合服务端单向推送

SSE,全称 Server-Sent Events。

它适合服务端持续向浏览器推送事件。

典型场景:

1
2
3
4
5
任务进度
系统通知
监控数据
订单状态
后台作业日志

Hono 的 streaming helper 也支持 SSE。

一个任务进度接口可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { Hono } from 'hono'
import { streamSSE } from 'hono/streaming'

const app = new Hono<{ Bindings: Env }>()

app.get('/api/jobs/:id/events', async (c) => {
const jobId = c.req.param('id')
const user = c.get('user')

return streamSSE(c, async (stream) => {
while (true) {
const job = await getUserJob(c.env.DB, jobId, user.id)

if (!job) {
await stream.writeSSE({
event: 'error',
data: JSON.stringify({ code: 'JOB_NOT_FOUND' }),
})
break
}

await stream.writeSSE({
event: 'progress',
data: JSON.stringify({
id: job.id,
status: job.status,
progress: job.progress,
}),
})

if (job.status === 'succeeded' || job.status === 'failed') {
break
}

await stream.sleep(1000)
}
})
})

前端使用浏览器原生 EventSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const source = new EventSource(`/api/jobs/${jobId}/events`)

source.addEventListener('progress', (event) => {
const data = JSON.parse(event.data)
renderProgress(data)

if (data.status === 'succeeded' || data.status === 'failed') {
source.close()
}
})

source.addEventListener('error', () => {
source.close()
})

SSE 的优点是简单。

浏览器会自动处理断线重连,协议也比 WebSocket 更接近普通 HTTP。


四、SSE 的生产注意点

SSE 简单,但不是没有坑。

第一,连接时间要受控。

不要让一个 SSE 连接永远挂着。

比如任务完成后立刻结束:

1
2
3
4
5
6
7
if (job.status === 'succeeded' || job.status === 'failed') {
await stream.writeSSE({
event: 'done',
data: JSON.stringify(job),
})
break
}

第二,要有心跳。

如果中间代理长时间没有数据,连接可能被断开。

可以定期发送 ping:

1
2
3
4
await stream.writeSSE({
event: 'ping',
data: JSON.stringify({ now: Date.now() }),
})

第三,要考虑浏览器连接数。

不要给一个页面开十几个 SSE。

如果页面需要多个事件源,尽量合并成一个:

1
2
3
4
/api/events
-> job.progress
-> notification.created
-> billing.updated

第四,鉴权要谨慎。

EventSource 原生不方便设置自定义 header。很多团队会用 cookie 鉴权,或者用短期 token 放在查询参数里。

如果用查询参数 token,必须短有效期,并避免写入日志。


五、WebSocket 适合双向实时

WebSocket 适合前后端都需要主动发送消息的场景。

比如:

1
2
3
4
5
6
聊天室
多人协作
实时白板
终端会话
在线游戏
低延迟状态同步

在 Hono 里,WebSocket 支持和运行时有关。

Node、Bun、Deno、Cloudflare Workers 的底层模型不完全一样,所以不要只看一段 demo 就直接搬到生产。

一个概念上的 Hono WebSocket 写法大概是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Hono } from 'hono'
import { createBunWebSocket } from 'hono/bun'

const app = new Hono()
const { upgradeWebSocket, websocket } = createBunWebSocket()

app.get('/ws', upgradeWebSocket((c) => {
return {
onOpen(_event, ws) {
ws.send('connected')
},

onMessage(event, ws) {
ws.send(`echo: ${event.data}`)
},

onClose() {
console.log('closed')
},
}
}))

export default {
fetch: app.fetch,
websocket,
}

具体 import 和 adapter 要按运行时选择。

如果你部署在 Cloudflare Workers,并且需要维护房间、连接列表、广播状态,通常要认真评估 Durable Objects。

因为普通无状态 Worker 不适合保存长连接之间的共享状态。


六、WebSocket 不只是连接

很多 WebSocket 项目真正难的不是“握手成功”。

难的是后面的协议设计:

1
2
3
4
5
6
7
8
9
10
11
消息类型
鉴权
心跳
重连
房间
广播
顺序
幂等
限流
状态恢复
断线清理

不要直接发送散乱 JSON。

建议统一消息格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
type ClientMessage =
| {
type: 'chat.send'
id: string
payload: {
roomId: string
text: string
}
}
| {
type: 'room.join'
id: string
payload: {
roomId: string
}
}

type ServerMessage =
| {
type: 'chat.created'
id: string
payload: {
roomId: string
messageId: string
text: string
}
}
| {
type: 'error'
id?: string
payload: {
code: string
message: string
}
}

每条客户端消息都带 id

服务端返回时可以带同一个 id,方便前端做确认、失败提示和重试。


七、WebSocket 鉴权怎么做

WebSocket 鉴权通常在升级连接时完成。

常见方式:

1
2
3
Cookie session
短期 access token
一次性连接 token

连接 token 是比较干净的做法:

1
2
3
4
5
1. 前端先调用 /api/ws-token
2. 后端校验用户身份,签发一个 30 秒有效的 token
3. 前端连接 /ws?token=<short-lived-token>
4. WebSocket 服务端校验 token
5. 校验通过后建立连接

示例:

1
2
3
4
5
6
7
8
9
10
11
12
app.post('/api/ws-token', async (c) => {
const user = c.get('user')

const token = await signWsToken({
sub: user.id,
exp: Math.floor(Date.now() / 1000) + 30,
}, c.env.JWT_SECRET)

return c.json({
data: { token },
})
})

注意:如果 token 放在 URL 里,要避免被访问日志完整记录。

也可以把 token 放在协议字段或 cookie 里,具体取决于运行时和客户端能力。


八、长连接要有限流

实时接口也要限流。

至少限制三类东西:

1
2
3
每个用户最大连接数
每个连接每秒消息数
每个房间广播频率

否则一个异常客户端就可能把系统打爆。

可以先做最简单的内存计数。

但如果是多实例、多区域、多房间,应该把连接状态放进更适合的地方,比如 Durable Objects。

限流策略不一定复杂:

1
2
3
普通用户:每个账号最多 3 个连接
单连接:每秒最多 10 条客户端消息
房间广播:合并 100ms 内的状态变更

关键是要明确边界。

实时系统没有边界,就很难稳定。


九、流式响应、SSE、WebSocket 怎么选

可以用这个表快速判断:

场景 推荐方案 原因
AI 文本逐字输出 流式响应 一次请求,一路返回,模型简单
报表导出进度 轮询或 SSE 服务端单向通知即可
系统通知 SSE 低频推送,浏览器支持好
日志实时查看 SSE 或 streaming 单向输出为主
聊天室 WebSocket 双向消息,高频交互
多人协作 WebSocket + 状态服务 需要连接状态和广播
后台任务完成提醒 轮询/SSE 不需要双向协议

我的建议是:

1
2
3
能轮询就先轮询
需要服务端推送就用 SSE
需要双向实时再用 WebSocket

这不是保守。

这是为了让复杂度跟业务价值匹配。


十、一个实用组合

在 Hono 生产项目里,我最常用的组合是:

1
2
3
4
5
6
普通 CRUD:HTTP JSON API
AI 输出:streamText
后台任务进度:SSE
低频状态刷新:轮询
聊天/协作:WebSocket
连接状态管理:Durable Objects 或专门实时服务

这样每类需求都有合适边界。

前后端也更容易协作。

前端同学知道:

1
2
3
4
这个接口是一次性 JSON
这个接口是 stream
这个接口是 SSE
这个接口是 WebSocket

后端同学也知道:

1
2
3
4
哪些接口走请求响应
哪些接口要处理断线
哪些接口要处理连接状态
哪些接口要处理广播和重连

Hono 的价值在这里很明显。

它不逼你把所有实时需求塞进一种框架抽象里。

你可以按业务场景,选择最轻但足够可靠的通信方式。