
1. 为什么选择SSE技术实现股票行情推送在金融交易场景中实时行情推送是核心需求。传统轮询方式每3秒请求一次服务器假设有1万用户在线服务器每分钟就要处理20万次请求。而使用SSEServer-Sent Events技术服务端与客户端建立长连接后数据变更时可以主动推送同样场景下连接数降低到1万且避免了大量无效请求。我曾在某证券APP项目中做过对比测试当大盘剧烈波动时轮询方案服务器CPU飙升至78%而SSE方案稳定在32%左右。更重要的是SSE基于标准HTTP协议不需要像WebSocket那样额外处理协议升级对现有系统改造成本更低。2. 搭建ASP.NET Core SSE服务端2.1 初始化项目环境首先创建ASP.NET Core Web API项目dotnet new webapi -n StockPushService cd StockPushService dotnet add package Microsoft.AspNetCore.Cors修改Program.cs配置SSE支持var builder WebApplication.CreateBuilder(args); builder.Services.AddCors(); // 允许跨域 var app builder.Build(); app.UseCors(policy policy.AllowAnyOrigin().AllowAnyMethod()); app.MapGet(/stocks/stream, async context { // SSE配置将在下一步实现 });2.2 实现行情推送核心逻辑在Controllers目录新建StockController.cs[ApiController] [Route([controller])] public class StockController : ControllerBase { private static readonly Dictionarystring, decimal _stocks new() { {AAPL, 182.72m}, {MSFT, 407.54m}, {GOOGL, 167.23m} }; [HttpGet(stream)] public async Task StreamPrices() { Response.Headers.Add(Content-Type, text/event-stream); Response.Headers.Add(Cache-Control, no-cache); Response.Headers.Add(Connection, keep-alive); var random new Random(); while (!HttpContext.RequestAborted.IsCancellationRequested) { // 模拟价格波动 var updatedStocks _stocks.ToDictionary( kv kv.Key, kv Math.Max(1, kv.Value (decimal)(random.NextDouble() - 0.5)) ); // SSE格式要求每个消息以data:开头以\n\n结尾 await Response.WriteAsync($data: {JsonSerializer.Serialize(updatedStocks)}\n\n); await Response.Body.FlushAsync(); await Task.Delay(2000); // 2秒更新一次 } } }实测发现几个关键点必须设置no-cache头部否则浏览器可能缓存事件流每次价格更新后要立即Flush避免缓冲区延迟需要处理客户端断开连接的情况通过RequestAborted3. 构建专业级Web客户端3.1 基础事件监听实现创建wwwroot/index.html!DOCTYPE html html head title股票行情看板/title style .stock { margin: 10px; padding: 15px; border-radius: 5px; } .up { background-color: #d4edda; border-left: 5px solid #28a745; } .down { background-color: #f8d7da; border-left: 5px solid #dc3545; } /style /head body div idstockContainer/div script const eventSource new EventSource(http://localhost:5000/stocks/stream); const container document.getElementById(stockContainer); eventSource.onmessage e { const stocks JSON.parse(e.data); container.innerHTML ; for (const [symbol, price] of Object.entries(stocks)) { const div document.createElement(div); div.className stock; div.textContent ${symbol}: $${price.toFixed(2)}; container.appendChild(div); } }; /script /body /html3.2 增强用户体验方案实际项目中我们还需要价格变化动画效果断线自动重连历史数据缓存改进后的客户端代码let lastPrices {}; const eventSource new EventSource(/stocks/stream); eventSource.addEventListener(error, () { setTimeout(() location.reload(), 5000); // 5秒后重连 }); eventSource.onmessage e { const stocks JSON.parse(e.data); Object.entries(stocks).forEach(([symbol, price]) { const element document.getElementById(symbol) || createStockElement(symbol); const change lastPrices[symbol] ? price - lastPrices[symbol] : 0; element.className stock ${change 0 ? up : down}; element.innerHTML span classsymbol${symbol}/span span classprice$${price.toFixed(2)}/span span classchange${change 0 ? : }${change.toFixed(2)}/span ; // 价格变动动画 if (change ! 0) { element.animate([ { backgroundColor: change 0 ? #d4edda : #f8d7da }, { backgroundColor: transparent } ], 1000); } }); lastPrices stocks; }; function createStockElement(symbol) { const div document.createElement(div); div.id symbol; document.getElementById(stockContainer).appendChild(div); return div; }4. 生产环境优化策略4.1 服务端性能调优在负载测试时发现当并发连接超过5000时内存占用会显著上升。通过以下方案解决连接管理优化// 在Program.cs中配置 builder.WebHost.ConfigureKestrel(server { server.Limits.MaxConcurrentConnections 10000; server.Limits.KeepAliveTimeout TimeSpan.FromMinutes(5); });使用连接池管理// 注册为单例服务 builder.Services.AddSingletonStockPriceService(); // 在中间件中获取实例 var stockService context.RequestServices.GetServiceStockPriceService(); await stockService.PushUpdatesAsync(context);4.2 客户端重连机制SSE规范本身支持自动重连但实际项目中需要更精细控制let reconnectAttempts 0; function setupEventSource() { const es new EventSource(/stocks/stream); es.onopen () reconnectAttempts 0; es.onerror () { es.close(); const delay Math.min(reconnectAttempts * 1000, 10000); setTimeout(setupEventSource, delay); }; return es; }4.3 安全防护措施金融数据必须考虑安全性// 添加JWT认证 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new() { ValidateIssuer true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidateAudience true, ValidAudience builder.Configuration[Jwt:Audience], ValidateLifetime true, IssuerSigningKey new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key]) ) }; }); // 在控制器添加授权 [Authorize] [HttpGet(stream)] public async Task StreamPrices() { // ... }5. 与传统方案的对比测试在4核8G的Linux服务器上使用JMeter进行压测方案1000并发5000并发延迟(ms)CPU使用率传统轮询12.3MB/s崩溃32089%WebSocket8.1MB/s38.7MB/s4562%SSE(本方案)5.4MB/s27.2MB/s6851%测试数据表明SSE带宽消耗比WebSocket低33%内存占用约为轮询方案的1/5在突发流量下表现更稳定6. 常见问题解决方案问题1Nginx代理中断连接在nginx.conf中添加proxy_set_header Connection ; proxy_http_version 1.1; proxy_buffering off; proxy_cache off;问题2客户端收不到更新检查服务端是否忘记Flushawait Response.WriteAsync($data: {data}\n\n); await Response.Body.FlushAsync(); // 这行不能少问题3内存泄漏定期清理断开连接的客户端// 每5分钟清理一次 _ Timer.RunPeriodic(TimeSpan.FromMinutes(5), () { foreach (var client in _clients.Where(c c.IsDisconnected)) { _clients.TryRemove(client); } });7. 扩展应用场景除了股票行情该架构还适用于新闻实时推送突发财经新闻交易订单状态更新投资组合盈亏变动市场风险预警通知在最近的一个加密货币交易平台项目中我们基于SSE实现了价格预警通知杠杆爆仓提醒大宗交易实时播报// 多事件类型示例 await Response.WriteAsync(event: priceAlert\ndata: {...}\n\n); await Response.WriteAsync(event: liquidation\ndata: {...}\n\n);8. 监控与运维建议生产环境需要添加Prometheus监控指标app.UseHttpMetrics(); builder.Services.AddMetrics();健康检查端点app.MapHealthChecks(/health);日志记录关键事件_logger.LogInformation(Client connected: {ConnectionId}, context.Connection.Id); _logger.LogWarning(Client disconnected: {ConnectionId}, context.Connection.Id);9. 客户端兼容性处理虽然现代浏览器都支持EventSource API但需要处理特殊情况if (typeof EventSource undefined) { // 降级方案 showFallbackUI(); startPolling(); } else { // 正常使用SSE setupEventSource(); }对于React/Vue等框架推荐使用专用库npm install microsoft/fetch-event-source10. 性能优化实战技巧二进制数据传输// 使用MessagePack压缩 await Response.WriteAsync($data: {MessagePackSerializer.Serialize(data)}\n\n);差分更新// 只发送变化的数据 var changes _stocks.Where(s s.Value ! lastPrices[s.Key]); if (changes.Any()) { await SendUpdate(changes); }心跳机制// 每30秒发送心跳包 _ Timer.RunPeriodic(TimeSpan.FromSeconds(30), () { Response.WriteAsync(: heartbeat\n\n); });在具体实施时建议先用模拟数据测试逐步接入真实交易系统。我在迁移某港股交易系统时采取灰度发布策略先让10%用户使用新推送系统稳定运行一周后再全量切换。