使用SSE推送信息给前端
SSE概念
什么是SSE
SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。
SSE 和 WebSocket 都有各自的优缺点,适用于不同的场景和需求。如果只需要服务器向客户端单向推送数据,并且应用在前端的浏览器环境中,则 SSE 是一个更加轻量级、易于实现和维护的选择。而如果需要双向传输数据、支持自定义协议、或者在更加复杂的网络环境中应用,则 WebSocket 可能更加适合。
SSE适用于场景
ChatGPT聊天机器人,股票价格更新,新闻实时推送,实时监控等需要服务器推送消息给客户端的场景,用在数据更新频繁,低延迟,单向通信的应用非常合适。
SSE技术实现
服务端
完成sse连接,向指定客户端发消息
asp.net core项目中,引用Lib.AspNetCore.ServerSentEvents
简单配置一下即可使用
public class Startup { public void ConfigureServices(IServiceCollection services) { ... services.AddServerSentEvents(); ... } public void Configure(IApplicationBuilder app) { ... app.MapServerSentEvents("/sse"); ... } }
如果向指定客户端发消息
internal interface INotificationsServerSentEventsService : IServerSentEventsService { } internal class NotificationsServerSentEventsService : ServerSentEventsService, INotificationsServerSentEventsService { public NotificationsServerSentEventsService(IOptions options) : base(options.ToBaseServerSentEventsServiceOptions()) { } } public class Startup { public void ConfigureServices(IServiceCollection services) { ... services.AddServerSentEvents(); services.AddServerSentEvents(); ... } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider) { ... app.MapServerSentEvents("/default-sse-endpoint"); app.MapServerSentEvents("/notifications-sse-endpoint"); ... } }
发消息
public class NotificationsController : Controller { private readonly INotificationsServerSentEventsService _serverSentEventsService; IHttpContextAccessor httpContextAccessor; public NotificationsController(INotificationsServerSentEventsService serverSentEventsService,IHttpContextAccessor httpContextAccessor) { _serverSentEventsService = serverSentEventsService; this.httpContextAccessor = httpContextAccessor; } public async Task SendTest() { //向所有客户端发消息 _serverSentEventsService.SendEventAsync(""); //向指定客户端发消息 var clientId = serverSentEventsClientIdProvider.AcquireClientId(httpContextAccessor.HttpContext); await SendEventAsync(msg, x => x.Id == clientId); } }
nginx配置
location / { proxy_pass http://127.0.0.1:55005; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_connect_timeout 4s; proxy_read_timeout 600s; #心跳值 单位秒 proxy_send_timeout 12s; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffer_size 1024k; proxy_buffers 16 1024k; proxy_busy_buffers_size 2048k; proxy_temp_file_write_size 2048k; proxy_buffering off; proxy_cache off; gzip off; chunked_transfer_encoding off; }
客户端
连接sse服务器,接收服务端消息
ts版本
sseConnect(){ const url=window.location.origin+'/sse' //'https://localhost:6601/sse' const sse=new EventSource(url); sse.addEventListener("open",(e)=>{ //sse open $eventBus.emit('SseOpenEvent') }) sse.addEventListener("message",({data})=>{ //收到消息 console.log('message',data) const d=JSON.parse(data); $eventBus.emit("receiveMsgEvent",data) }) sse.addEventListener('error',(err:any)=>{ //报错 console.log('err '+JSON.stringify(err)); }) },
在收到消息后,发送receiveMsgEvent 处理收到消息后的事件逻辑
通信消息格式
{ "type":"xx", "data":object }
SSE实例项目
网站上实现微信二维码登录
思路:准备一个网站A,负责与微信通信,处理微信业务;其它网站,连接sse,得到sseId后,调用并显示A站点微信登录二维码,用户扫码后,通过sse推送消息给前端页面
1.站点A开发配置
引用RsCode.Wechat ,并添加微信API服务,具体用法可以查看文档
builder.Services.AddWeChat(options => { Configuration.GetSection("Tencent:WeChat").Bind(options); }); ... app.UseWeChat();
添加微信带场景二维码逻辑
[EnableCors] [HttpPost("/oauth/login/qrcode")] public async Task CreateLoginQrcodeAsync([FromBody] LoginQrcodeRequestDto dto) { string appId = "wx7c829604a62b02e8"; var ret = await oauthService.CreateLoginQrcodeAsync(appId, dto); return Json(ret); } public class LoginQrcodeRequestDto { [JsonPropertyName("sseId")] public string SseId { get; set; } = ""; [JsonPropertyName("domain")] public string Domain { get; set; } = ""; }
扫码登录成功后,推送用户token信息,自定义接收微信服务器消息CustomWxMsgHandler.cs
public class CustomWxMsgHandler : WeChatEventHandler { //扫描二维码 public override async Task OnCanEvent(ScanEventMessage scanEventMessage) { log.LogInformation($"scanEventMessage.EventKey={scanEventMessage.EventKey}"); var sceneInfo = JsonSerializer.Deserialize(scanEventMessage.EventKey); string scene = sceneInfo.SceneStr; //扫码后,如果有场景值,获取新用户完成登录 if(string.IsNullOrWhiteSpace(scene)) { return "success"; } string ghId = scanEventMessage.ToUserName; string appId = WeChatOptions.FirstOrDefault(c => c.Id == ghId).AppId; string openId = scanEventMessage.FromUserName; //查询并创建用户 var user=await userDomainService.GetOrCreateUserAysnc(new OAuthUserValueObject { AppId = appId, OpenId = openId, }); if (user != null) { if (scene == "wxlogin") { List claims = user.GetClaims(); var tokenInfo = jwt.CreateAccessToken(claims); var clientId = sceneInfo.SignalrConnectId; if(!string.IsNullOrWhiteSpace(sceneInfo.Domain)) { var infoMsg = new { domain = sceneInfo.Domain, clientId=sceneInfo.SseId, type = "login.success", data = tokenInfo }; log.LogInformation("发送wxLoginSuccess"); await capPublisher.PublishAsync("wxLoginSuccess", infoMsg); } } //向用户发消息 TextMessage msg = new TextMessage(openId,ghId,"您己成功登录"); wechat.UseAppId(appId); return await wechat.SendMessageAsync(msg); } return "success"; } }
2.业务网站开发配置
引用SSE,Lib.AspNetCore.ServerSentEvents,添加sse服务
services.AddSse(); app.UseSse();
订阅CAP消息,在收到登录成功后,将登录结果推送给前端
//微信登录成功 [CapSubscribe("wxLoginSuccess")] public async Task WxLoginSuccessAsync(WxLoginSuccessDto dto) { if(dto.Domain.Contains("pan.rs888.net")) { var s = JsonSerializer.Serialize(dto); await sse.SendEventAsync(s, x => x.Id == Guid.Parse(dto.ClientId)); } }
3.前端页面配置
以vue项目为例,/src/store/model/websocket.s中,添加
actions: { sseConnect(){ const url=window.location.origin+'/sse' const sse=new EventSource(url); sse.addEventListener("open",(e)=>{ console.log('sse open') $eventBus.emit('SseOpenEvent') }) sse.addEventListener("message",({data})=>{ console.log('message',data) const d=JSON.parse(data); $eventBus.emit("receiveMsgEvent",data) }) sse.addEventListener('error',(err:any)=>{ console.log('err '+JSON.stringify(err)); }) }, }
编写登录二维码组件 /src/components/login/wecchatLogin.vue
import { defineProps } from 'vue'; import QrcodeVue from 'qrcode.vue'; const props = defineProps({ qrcodeUrl: { type: String, default: '', }, }); .main { text-align: center; } .description { font-size: 13px; color: #bdbdbd; }
/src/layouts/index.vue中调用登录二维码
import { ref } from 'vue'; import { useWebsocketStore, useUserStore } from '@/store'; import $eventBus from '@/utils/eventBus'; import wechatLogin from '@/components/Login/wechatLogin.vue'; const websocketStore = useWebsocketStore(); //收到推送消息后的逻辑, 收到token后保存 $eventBus.on('receiveMsgEvent', (data: string) => { const d = JSON.parse(data); if (d.type === 'login.success') { userStore.login(d.data.access_token); visibleQrLogin.value = false; } if (d.type === 'login.fail') { MessagePlugin.error('登录失败'); visibleQrLogin.value = false; } }); $eventBus.on('OpenLoginEvent', () => { visibleQrLogin.value = true; }); const loginQrcode = ref(''); $eventBus.on('SseOpenEvent', (d: any) => { if (userStore.token != null && userStore.token.length > 12) { return; } fetchQrcode(); // 拉取二维码 setTimeout(() => { fetchQrcode(); }, 60000 * 5); }); // 二维码登录 const visibleQrLogin = ref(false); const fetchQrcode = () => { userStore.fetchLoginQrcode().then((ret: any) => { loginQrcode.value = ret.url; }); }; //sse连接 websocket.sseConnect();
整个流程
1.前端先创建sse连接,然后通过userStore.fetchLoginQrcode()拉取网站A的二维码,取到二维码后,显示给用户;
2.用户扫码成功,触发CustomWxMsgHandler中的OnCanEvent事件,在该事件中注册并登录用户,返回用户token给消息中间件;
3.消息中间件再把token消息发给应用站点,应用站点再通过sse,推送token给前端页面
4.最后,前端页面保存token,并进行逻辑处理