1、背景
今年春节大火的DeepSeek
,其中大家比较感兴趣的就是,DeepSeek
返回的是一句一句的蹦出来的。这个就是流式响应。C#也可以实现,本篇就是展示流式响应的一个Demo。
2、实现效果
实现的效果如下:
3、具体实现
3.1 API端代码
创建一个asp.net core api项目,在controller中定义流式方式,代码如下:
csharp
using Microsoft.AspNetCore.Mvc;
namespace SSEWebApplication.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class StreamController : ControllerBase
{
[HttpGet]
public async Task GetStreamSse()
{
Response.ContentType = "text/event-stream";
Response.Headers["Cache-Control"] = "no-cache";
Response.Headers["Connection"] = "keep-alive";
Response.Headers["Access-Control-Allow-Origin"] = "*"; //可以实现跨域访问
//假设流式的数据返回
var messages = new string[] {"你好!","我是","北京清华长庚医院","信息管理部","郑林"};
//模拟DeepSeek的流式返回
for (int i = 0; i < messages.Length; i++)
{
if(i== messages.Length-1)
{
await Response.WriteAsync($"data:{messages[i]}\n\n");
await Response.Body.FlushAsync();
}
else
{
await Response.WriteAsync($"data:{messages[i]}\n\n");
await Response.Body.FlushAsync();
await Task.Delay(1000);
}
}
}
}
}
3.2 前端代码
前端代码如下:
html
<!DOCTYPE html>
<html>
<head>
<title>流式展示</title>
</head>
<body>
<div id="messages"></div>
<script>
// 创建SSE连接
const eventSource =new EventSource('http://localhost:5105/api/Stream/GetStreamSse');
// 监听消息事件
eventSource.onmessage=function(event) {
const messageContainer =document.getElementById('messages');
const newMessage =document.createElement('p');
newMessage.textContent= event.data;
messageContainer.appendChild(newMessage);
// 滚动到最新消息
messageContainer.scrollTop= messageContainer.scrollHeight;
};
// 监听打开连接事件
eventSource.onopen=function() {
console.log("连接已打开");
};
// 监听错误事件
eventSource.onerror=function(error) {
console.error("发生错误", error);
eventSource.close();// 关闭连接
};
</script>
</body>
</html>
4、原理
在代码中我们使用了EventSource,这个称之为:服务器发送事件
。有点类似socket,只不过这个是单向
的,只能服务器发给客户端。
4.1 API的实现
1、API部分很简单,本质上就是一个文本流,类似我们下载文件一样,只不过,下载文件的是Stream流(二进制数据流),而EventSource传递的是string字符串。
2、API端发送有点类似海浪,一波一波的。如何判断发送给前端的这波数据结束了呢,就是\n\n
。
3、EventSource发送的数据的时候,是有格式要求的:
csharp
[发送类型]: 待发送的字符串
发送类型有:event、data、id、retry。
他们怎么用呢?有时候,我们需要把消息发给张三、李四,或同一个界面中的不同部分(股票的最新数据,以及当前企业的财务的消息数据),需要展示/更新界面的不同地方。服务器端就会这么写
csharp
if(sendtype=="company")
{
await Response.WriteAsync($"event:company\n");
await Response.WriteAsync($"data:发布了第三季度财务报表");
await Response.WriteAsync($"\n\n");
await Response.Body.FlushAsync();
}else
{
await Response.WriteAsync($"event:stock\n");
await Response.WriteAsync($"data:123");
await Response.WriteAsync($"\n\n");
await Response.Body.FlushAsync();
}
然后前端就可以这么写
typescript
evtSource.addEventListener("company", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
const time = JSON.parse(event.data).time;
newElement.textContent = `ping at ${time}`;
eventList.appendChild(newElement);
});
evtSource.addEventListener("stock", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
const time = JSON.parse(event.data).time;
newElement.textContent = `ping at ${time}`;
eventList.appendChild(newElement);
});