using Ayay.SerilogLogs; using Core.WcfProtocol; using Serilog; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Text; namespace SHH.MjpegPlayer { /// /// Mjpeg 会话工作单元 /// public class MjpegSession : IDisposable { private static readonly ILogger _sysLog = Log.ForContext("SourceContext", LogModules.Core); #region Counter private SumByTime _sumBySecond = new SumByTime(); /// /// 计数器 /// public SumByTime Counter => _sumBySecond; #endregion #region Info /// /// 基础信息 /// public SessionInfo Info { get; private set; } #endregion #region Cmd /// /// 命令 /// public string? Cmd { get; set; } #endregion // [修复] 引入 Disposed 标志位 private volatile bool _isDisposed = false; #region Constructor /// /// 构造函数 /// public MjpegSession() { Info = new SessionInfo { Counter = _sumBySecond }; } #endregion #region DoHttpHeader private bool DoHttpHeader(NetworkStream stream) { try { byte[] buffer = new byte[4096]; int totalBytesRead = 0; string httpRequest = string.Empty; while (totalBytesRead < buffer.Length) { int bytesRead = stream.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead); if (bytesRead == 0) return false; totalBytesRead += bytesRead; httpRequest = Encoding.ASCII.GetString(buffer, 0, totalBytesRead); if (httpRequest.Contains("\r\n\r\n")) break; } if (!httpRequest.Contains("\r\n\r\n")) return false; if (!string.IsNullOrEmpty(httpRequest)) { int queryStartIndex = httpRequest.IndexOf('?'); if (queryStartIndex > -1) { int spaceIndex = httpRequest.IndexOf(' ', queryStartIndex); if (spaceIndex == -1) spaceIndex = httpRequest.Length; string queryString = httpRequest.Substring(queryStartIndex + 1, spaceIndex - queryStartIndex - 1); var queryParams = System.Web.HttpUtility.ParseQueryString(queryString); if (queryParams != null) { Info.DeviceId = queryParams["id"]; Info.TypeCode = queryParams["typeCode"]; Cmd = queryParams["cmd"]; } } } if (string.IsNullOrEmpty(Cmd)) { if (string.IsNullOrEmpty(Info.DeviceId) || string.IsNullOrEmpty(Info.TypeCode)) { SendErrorResponse(stream, "错误:缺少必要参数(id 或 typeCode)"); return false; } } else { if (MjpegHttpCmd.DoHttpCmd(stream, Info, Cmd)) return false; } return true; } catch (Exception ex) { SendErrorResponse(stream, $"解析异常: {ex.Message}"); return false; } } private void SendErrorResponse(NetworkStream stream, string msg) { try { byte[] response = Encoding.UTF8.GetBytes( $"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{msg}"); stream.Write(response, 0, response.Length); stream.Flush(); } catch { } } #endregion #region Create /// /// 创建会话 /// public void Create(TcpClient client) { try { if (Info == null) return; Info.AcceptTime = DateTime.Now; // 初始化最近接收时间,避免刚连接就被判定为超时 LastRecImgTime = DateTime.Now; Task.Run(() => { DoWorkTask(client); }); } catch (Exception ex) { _sysLog.Error($"MjpegSession Create Exception, 异常信息:{ex.Message}, {ex.StackTrace}"); } } #endregion #region DoWorkTask private void DoWorkTask(TcpClient client) { try { using (var stream = client.GetStream()) { // 设置写入超时 3秒 stream.WriteTimeout = 3000; #region 等待连接, 处理地址栏参数 int iLoc = 0; while (!client.Connected) { Thread.Sleep(50); if (++iLoc > 60) return; } try { if (client.Client?.RemoteEndPoint is IPEndPoint endpoint) { Info.ClientIp = endpoint.Address.ToString(); Info.ClientPort = endpoint.Port; } } catch { } if (!DoHttpHeader(stream)) return; #endregion MjpegStatics.Sessions.AddSession(this); byte[] header = Encoding.ASCII.GetBytes( "HTTP/1.1 200 OK\r\n" + "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n"); stream.Write(header, 0, header.Length); var frameInterval = MjpegStatics.Cfg.FrameInterval; if (frameInterval < 1 || frameInterval > 500) frameInterval = 125; Stopwatch stopwatch = Stopwatch.StartNew(); UploadImageRequest? lastProcItem = null; byte[] boundaryBytes = Encoding.ASCII.GetBytes("\r\n--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "); byte[] doubleNewLine = Encoding.ASCII.GetBytes("\r\n\r\n"); while (client.Connected && !_isDisposed) { try { // [新增] 僵尸连接熔断机制:如果源头超过 60 秒没有新图片,主动断开连接 if ((DateTime.Now - LastRecImgTime).TotalSeconds > 60) { _sysLog.Warning($"会话超时断开 (源头无数据 > 60s): {Info.Key} - Client: {Info.ClientIp}"); break; } stopwatch.Restart(); if (_lastRecObj == null || _lastRecObj.ImageBytes == null) { Info.Message = "等待图片数据抵达"; Thread.Sleep(40); continue; } Info.Message = "视频流播放中"; if (lastProcItem != null && lastProcItem != _lastRecObj) { _sumBySecond.Refresh("有效帧数"); } lastProcItem = _lastRecObj; // 如果图片太旧(超过3秒),认为是滞后数据,暂时不发 if ((DateTime.Now - _lastRecObj.Time).TotalSeconds > 3) { Thread.Sleep(40); continue; } byte[] imageData = _lastRecObj.ImageBytes; stream.Write(boundaryBytes, 0, boundaryBytes.Length); byte[] lengthBytes = Encoding.ASCII.GetBytes(imageData.Length.ToString()); stream.Write(lengthBytes, 0, lengthBytes.Length); stream.Write(doubleNewLine, 0, doubleNewLine.Length); stream.Write(imageData, 0, imageData.Length); _sumBySecond.Refresh("播放帧数"); stopwatch.Stop(); var needSleep = frameInterval - (int)stopwatch.ElapsedMilliseconds; if (needSleep > 0) Thread.Sleep(needSleep); } catch (Exception ex) { _sysLog.Warning($"播放连接断开, : {ex.Message}"); break; } } } } catch (Exception ex) { _sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}"); } finally { Dispose(); if (client != null) { try { client.Close(); client.Dispose(); } catch { } } } } #endregion #region DoImageProc private UInt64 _lastPlayImageOrder = 0; private UploadImageRequest? _lastRecObj = null; /// /// 处置图片 /// public void DoImageProc(UploadImageRequest req) { try { LastRecImgTime = DateTime.Now; _sumBySecond.Refresh("接收帧数"); if (req == null || req.ImageBytes == null || req.ImageBytes.Length < 100) return; // 防止死锁:序号重置检测 if (req.Order < _lastPlayImageOrder) { if (_lastPlayImageOrder > 100 && req.Order < 100) { _lastPlayImageOrder = req.Order; } else { return; } } _lastPlayImageOrder = req.Order; _lastRecObj = req; } catch (Exception ex) { _sysLog.Error($"异常信息:{ex.Message}, {ex.StackTrace}"); } } /// /// 最近收到图片时间 /// public DateTime LastRecImgTime { get; set; } = DateTime.MinValue; #endregion #region Dispose public void Dispose() { if (_isDisposed) return; _isDisposed = true; MjpegStatics.Sessions.RemoveSession(this); } #endregion } }