C# Web API 2多游戏实例SSE推送触发事件后客户端异常问题
实现问题的核心定位与修正方案
现存实现的致命问题
- 控制器实例字段的时序错乱
你使用的private Game mGame是Web API控制器的实例字段,而Web API控制器本身是「每请求新建、请求返回即销毁」的短生命周期对象。你的OnStreamAvailable回调是由PushStreamContent在异步阶段触发的,此时控制器实例往往已经被请求管线回收,字段捕获的值不仅会出现跨请求的错乱,甚至可能因为内存复用读到其他请求的Game对象引用。 - 事件订阅的内存与逻辑泄漏
你在每次客户端发起订阅请求时,都会执行mGame.GameEvent += game_Event,但全程没有实现订阅移除逻辑。这会导致同一个静态事件处理器被重复绑定N次,一次游戏事件触发时,处理器会被重复调用数十甚至上百次,反复向同一个流写入重复消息;同时静态引用会持有Game对象的强引用,导致游戏对局对象永远无法被垃圾回收,随着对局数量增加内存持续上涨。 async void的线程安全隐患
你写的静态事件处理器返回值是async void,这是C#异步编程的典型坑:这类方法内部抛出的未捕获异常会直接崩溃当前工作线程,没有任何捕获和回溯的手段。一旦流写入时遇到网络断开、IO错误,整个流程会直接崩溃,客户端连接自然锁死。ConcurrentBag的错误操作ConcurrentBag是无序的线程安全集合,你在捕获到写流异常时,调用TryTake移除集合元素的逻辑是完全错误的——这个方法会随机取出集合里的任意元素,大概率会把其他正常工作的客户端连接移除,连带销毁正常连接的StreamWriter,直接导致大量无意义的断连。而服务端因为异常没有正确处理,返回500错误时不会带上配置的跨域响应头,浏览器自然会报CORS错误,这不是真的跨域配置失效。- SSE协议适配的细节错误
你初始化StreamWriter时用了默认配置,默认的UTF8编码带BOM、不自动flush,会导致SSE消息格式不符合协议要求,客户端解析失败后主动断开连接。
针对性修正步骤
- 移除控制器上的
mGame实例字段,直接在回调中传递当次请求对应的Game对象引用,避免跨请求的字段错乱。 - 调整Game类的事件签名,把原本的
void返回值改成可等待的Task返回值,避免async void的异常崩溃问题;同时针对每个单独的订阅连接创建专属的事件处理逻辑,在客户端断连、流写入失败时,第一时间从Game的事件列表中移除对应订阅,彻底解决重复绑定的问题:// 调整Game类的事件定义,支持异步等待 public event Func<Game, EventId, int, Task> OnGameEvent; // 触发事件的逻辑 protected async Task RaiseGameEvent(EventId eventId, int index) { // 依次等待所有订阅者的逻辑执行完成,避免async void的无追踪异常 if (OnGameEvent != null) { await OnGameEvent.Invoke(this, eventId, index); } } - 替换连接存储的实现,用
ConcurrentDictionary<Guid, Tuple<Game, StreamWriter>>存储连接列表,每个连接分配唯一的GUID标识,出现写流异常时直接定位到对应标识的连接元素移除,不再依赖无顺序的TryTake操作。 - 初始化StreamWriter时,使用无BOM的UTF8编码,开启自动flush,严格匹配SSE的文本流协议要求:
// 初始化符合SSE要求的StreamWriter var writer = new StreamWriter( stream: stream, encoding: new UTF8Encoding(false) // 不带BOM,避免消息解析失败 ) { AutoFlush = true // 自动刷新缓冲区,避免消息卡在缓冲区发不出去 }; - 全局捕获SSE流处理过程中的所有IO异常,异常发生时正确关闭连接、清理订阅,保证500错误时依然带上预先配置的跨域响应头,解决浏览器误报的CORS问题。
注意:你参考的基础SSE示例之所以能正常运行,是因为它用定时器触发、没有多实例绑定、没有重复事件订阅的逻辑,所以把定时器换成游戏事件后,原本埋的几个隐患就会全部暴露出来。
内容的提问来源于stack exchange,提问作者trw
相关产品推荐
相关产品推荐

