SSE Practice
Record the use of SSE to achieve the GPT typewriter effect
ChatGPT 的出现给我们的工作生活带来了很大影响。出于对ChatGPT 是如何实现让文字一个字符一个字符地出现在屏幕上的疑问?在网络上进行了一番折腾。最后发现这种实时流式显示的实现依靠着 SSE(Server-Sent Events) 技术。
让我们一起来实现一个类似的打字机效果吧。
SSE
什么是SSE ?
SSE(Server-Sent Events) 是一种 Web 标准,允许服务器向客户端推送实时数据。与 WebSocket 不同,SSE 是单向通信(服务器到客户端),专门用于实时数据流传输。
SSE 的核心概念
1. 连接建立
SSE 连接通过 HTTP 协议建立,使用特殊的 MIME 类型:text/event-stream
2. 数据格式
SSE 使用特定的文本格式:
event: message
id: 123
data: {"content": "Hello World"}
data: It is a message
SSE vs WebSocket
特性 | SSE | WebSocket |
---|---|---|
通信方向 | 单向(服务器 → 客户端) | 双向 |
实时性 | 高 | 高 |
实现复杂度 | 简单 | 中等 |
适用场景 | 通知、股票、仪表盘等 | 聊天、游戏帧同步等 |
实现一个 SSE 应用
后端实现(Koa)
首先安装依赖
json
"dependencies": {
"koa": "^2.14.2",
"koa-router": "^12.0.0",
"koa-cors": "^0.0.16",
"koa-bodyparser": "^4.4.1"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
我们安装了结果模块,nodemon
实现热重载, koa-cors
解决跨域问题, 一路由和参数解析模块。
完整代码实现
js
const Koa = require('koa');
const Router = require('koa-router');
const cors = require('koa-cors');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
const router = new Router();
app.use(cors());
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
router.get('/chat', async (ctx) => {
const { message } = ctx.query;
// set header
ctx.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
ctx.respond = false;
ctx.status = 200;
const res = ctx.res;
const response = `我收到了你的消息:"${message}"。这是一个很有趣的问题,让我想想...
根据我的理解,这是一个需要深入思考的话题。我建议你可以从多个角度来分析这个问题,这样会得到更全面的答案。
如果你有更具体的问题,我很乐意为你提供更详细的解答!`;
let charIndex = 0;
const streamInterval = setInterval(() => {
if (charIndex < response.length) {
res.write(`data: {"type": "char", "char": "${response[charIndex]}", "progress": ${charIndex + 1}, "total": ${response.length}}
`);
charIndex++;
} else {
res.write('data: {"type": "finished", "message": "finished"}
');
clearInterval(streamInterval);
res.end();
}
}, 100);
ctx.req.on('close', () => {
clearInterval(streamInterval);
console.log('Client disconnected');
});
});
app.listen(4200, () => {
console.log(`Server is running`);
});
1.其中我们首先将响应头这设置为允许跨域和保持长连接。
2.然后接口的实现代码中用定时器不断地发送数据模拟流式数据,当数据传输完后关闭连接。
3.需要注意的是 ctx.respond = false 很重要,如果没有这个配置,Koa 会在函数结束时自动发送响应,导致SSE连接关闭。
javascript
ctx.respond = false;
4.启动服务 执行命令 npm run dev
json
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
5.我们的SSE服务就准备启动完成了。
前端实现
完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSE Practice</title>
<style>
body {
background: #373232;
font-size: 14px;
}
.chat-container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chat-content {
padding: 20px;
min-height: 200px;
max-height: 300px;
overflow-y: auto;
border-bottom: 1px solid #eee;
}
.message {
background: #ffffff;
padding: 10px 15px;
border-radius: 8px;
color: #333;
border: 1px solid #e9ecef;
}
.chat-input-content {
display: flex;
gap: 10px;
padding: 20px;
}
#chatInput {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
#chatButton {
cursor: pointer;
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-content" id="chatContent">
<div class="message">Hello, It's GPT</div>
</div>
<div class="chat-input-content">
<input type="text" id="chatInput" placeholder="please input..." />
<button onclick="sendMessage()" id="chatButton">Send</button>
</div>
</div>
<script>
let chatEventSource = null;
function sendMessage() {
const input = document.getElementById("chatInput");
const button = document.getElementById("chatButton");
const message = input.value.trim();
button.textContent = "Stop";
button.onclick = stopMessage;
if (chatEventSource) {
chatEventSource.close();
}
chatEventSource = new EventSource(
`http://localhost:4200/chat?message=${encodeURIComponent(message)}`
);
let aiResponse = "";
chatEventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "char") {
aiResponse += data.char;
//update message
const aiMessage = chatContent.querySelector(".message");
aiMessage.textContent = aiResponse;
} else if (data.type === "finished") {
button.textContent = "Send";
button.onclick = sendMessage; // 恢复发送功能
chatEventSource.close();
}
};
chatEventSource.onerror = (error) => {};
}
function stopMessage() {
const button = document.getElementById("chatButton");
if (chatEventSource) {
chatEventSource.close();
}
button.textContent = "Send";
button.onclick = sendMessage;
}
</script>
</body>
</html>
核心代码:
javascript
chatEventSource = new EventSource(
`http://localhost:4200/chat?message=${encodeURIComponent(message)}`
);
chatEventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "char") {
aiResponse += data.char;
//update message
const aiMessage = chatContent.querySelector(".message");
aiMessage.textContent = aiResponse;
} else if (data.type === "finished") {
button.textContent = "Send";
button.onclick = sendMessage; // 恢复发送功能
chatEventSource.close();
}
};
chatEventSource.onerror = (error) => {};
1.首先调用 New EventSource 创建一个SSE连接。
2.chatEventSource.onmessage 用于监听响应,这样当服务端有数据流返回时就可以实时的就受到数据,同时更改dom的数据。
3.chatEventSource.onerror 用于注册发生异常时的回调。
可以看到SSE 的实现非常的简单,相比于WebSocket 简单的多。因为它本身就基于http, 还内置了连接丢失后的重连机制。
最终实现效果:
