using System; using System.Collections.Generic; using System.IO; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; namespace AIK.Service.Service { /// WebSocket 连接状态(与 websocket-sharp 兼容的取值)。 public enum WebSocketState { New, Connecting, Open, Closing, Closed } /// WebSocket 关闭状态码(RFC 6455 常用值,与 websocket-sharp 兼容)。 public enum CloseStatusCode : ushort { Normal = 1000, GoingAway = 1001, ProtocolError = 1002, UnsupportedData = 1003, NoStatus = 1005, Abnormal = 1006, InvalidData = 1007, PolicyViolation = 1008, TooBig = 1009, MandatoryExtension = 1010, ServerError = 1011, TlsHandshakeFailure = 1015 } /// 收到 WebSocket 消息的事件参数(与 websocket-sharp 兼容)。 public class MessageEventArgs : EventArgs { public bool IsText { get; } public bool IsBinary { get; } public string Data { get; } public byte[] RawData { get; } internal MessageEventArgs(bool isText, byte[] rawData) { IsText = isText; IsBinary = !isText; RawData = rawData ?? new byte[0]; Data = isText ? Encoding.UTF8.GetString(RawData) : string.Empty; } } /// WebSocket 错误事件参数。 public class ErrorEventArgs : EventArgs { public string Message { get; } public Exception Exception { get; } internal ErrorEventArgs(string message, Exception exception = null) { Message = message; Exception = exception; } } /// WebSocket 关闭事件参数。 public class CloseEventArgs : EventArgs { public CloseStatusCode Code { get; } public string Reason { get; } public bool WasClean { get; } internal CloseEventArgs(CloseStatusCode code, string reason, bool wasClean = false) { Code = code; Reason = reason ?? string.Empty; WasClean = wasClean; } } /// /// 基于 BouncyCastle 纯托管 TLS 的最小 RFC 6455 WebSocket 客户端,用于 Win7 扫码登录。 /// 背景:websocket-sharp 内部硬编码 new SslStream(...)(走系统 SChannel),Win7 SChannel 未启用 TLS 1.2 /// 时握手必然失败(关闭码 1015)。本客户端用 BouncyCastleTlsHelper 建立 TLS 1.2 连接后自行实现 /// WebSocket 握手与帧编解码,完全绕过 SChannel。 /// 事件模型与 websocket-sharp 兼容:OnMessage / OnError / OnClose,Send(string|byte[]),Close(code, reason)。 /// public class BouncyCastleWebSocketClient : IDisposable { private const string WsGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; private readonly Uri _uri; private TcpClient _tcp; private Stream _stream; private WebSocketState _state = WebSocketState.New; private Thread _receiveThread; private readonly object _sendLock = new object(); private volatile bool _disposed; public event EventHandler OnMessage; public event EventHandler OnError; public event EventHandler OnClose; /// 连接/关闭超时(默认 15 秒,与原先 websocket-sharp WaitTime 一致)。 public TimeSpan WaitTime { get; set; } = TimeSpan.FromSeconds(15); public WebSocketState ReadyState => _state; public BouncyCastleWebSocketClient(string url) { if (string.IsNullOrWhiteSpace(url)) { throw new ArgumentException("WebSocket URL 不能为空", nameof(url)); } _uri = new Uri(url); if (!string.Equals(_uri.Scheme, "wss", StringComparison.OrdinalIgnoreCase) && !string.Equals(_uri.Scheme, "ws", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException($"不支持的 WebSocket 协议: {_uri.Scheme}", nameof(url)); } } /// /// 建立连接:TCP → BouncyCastle TLS 1.2 握手 → WebSocket 升级握手,成功后启动接收线程。 /// 同步阻塞(与 websocket-sharp Connect() 一致,调用方可 Task.Run 包装)。 /// public void Connect() { EnsureNotDisposed(); if (_state != WebSocketState.New) { throw new InvalidOperationException($"当前状态不允许连接: {_state}"); } _state = WebSocketState.Connecting; var port = _uri.IsDefaultPort ? (_uri.Scheme == "wss" ? 443 : 80) : _uri.Port; var host = _uri.Host; try { // ---- 1. TCP 连接(带超时) ---- _tcp = new TcpClient(); var connectTask = _tcp.ConnectAsync(host, port); if (!connectTask.Wait(WaitTime)) { throw new TimeoutException($"TCP 连接超时: {host}:{port}"); } // ---- 2. BouncyCastle TLS 1.2 握手(含 SNI + 证书链校验,绕过 SChannel) ---- _stream = BouncyCastleTlsHelper.ConnectTlsStream(_tcp.GetStream(), host); // ---- 3. WebSocket 升级握手 ---- PerformHandshake(host, port); // ---- 4. 启动接收线程 ---- _state = WebSocketState.Open; _receiveThread = new Thread(ReceiveLoop) { IsBackground = true, Name = "BouncyCastleWebSocket-Receive" }; _receiveThread.Start(); } catch { _state = WebSocketState.Closed; CloseResources(); throw; } } /// 发送文本消息(客户端帧必须加掩码,RFC 6455)。 public void Send(string data) { EnsureOpen(); var bytes = Encoding.UTF8.GetBytes(data ?? string.Empty); SendFrame(0x1, bytes); } /// 发送二进制消息(STOMP 心跳 0x0A 等)。 public void Send(byte[] data) { EnsureOpen(); SendFrame(0x2, data ?? new byte[0]); } /// 主动关闭连接(发送 Close 帧并等待接收线程退出)。 public void Close(CloseStatusCode code, string reason) { if (_state == WebSocketState.Closed || _disposed) { return; } _state = WebSocketState.Closing; try { var reasonBytes = Encoding.UTF8.GetBytes(reason ?? string.Empty); var payload = new byte[2 + reasonBytes.Length]; payload[0] = (byte)((ushort)code >> 8); payload[1] = (byte)((ushort)code & 0xFF); Buffer.BlockCopy(reasonBytes, 0, payload, 2, reasonBytes.Length); SendFrame(0x8, payload); } catch { // 发送 Close 帧失败也继续关闭资源 } // 等待接收线程处理完对端 Close 回帧后自行退出 var thread = _receiveThread; if (thread != null && thread.IsAlive) { thread.Join(WaitTime); } CloseResources(); _state = WebSocketState.Closed; } public void Dispose() { if (_disposed) { return; } _disposed = true; try { if (_state == WebSocketState.Open || _state == WebSocketState.Closing) { Close(CloseStatusCode.GoingAway, "client disposed"); } } catch { // 忽略清理异常 } finally { CloseResources(); _state = WebSocketState.Closed; } } #region 内部实现 private void PerformHandshake(string host, int port) { var keyBytes = new byte[16]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(keyBytes); } var key = Convert.ToBase64String(keyBytes); var path = _uri.PathAndQuery; if (string.IsNullOrEmpty(path)) { path = "/"; } var sb = new StringBuilder(); sb.Append("GET ").Append(path).Append(" HTTP/1.1\r\n"); sb.Append("Host: ").Append(port == 443 ? host : $"{host}:{port}").Append("\r\n"); sb.Append("Upgrade: websocket\r\n"); sb.Append("Connection: Upgrade\r\n"); sb.Append("Sec-WebSocket-Key: ").Append(key).Append("\r\n"); sb.Append("Sec-WebSocket-Version: 13\r\n"); sb.Append("\r\n"); var head = Encoding.ASCII.GetBytes(sb.ToString()); _stream.Write(head, 0, head.Length); _stream.Flush(); // ---- 读取响应状态行与响应头 ---- var statusLine = ReadHandshakeLine(); if (string.IsNullOrEmpty(statusLine)) { throw new IOException("WebSocket 升级握手无响应(连接被关闭)"); } if (!statusLine.Contains("101")) { throw new IOException($"WebSocket 升级握手失败: {statusLine}"); } var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); string line; while (!string.IsNullOrEmpty(line = ReadHandshakeLine())) { int colon = line.IndexOf(':'); if (colon <= 0) { continue; } var name = line.Substring(0, colon).Trim(); var value = line.Substring(colon + 1).Trim(); headers[name] = value; } // 校验 Sec-WebSocket-Accept var expectedAccept = Convert.ToBase64String( SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + WsGuid))); if (!headers.TryGetValue("Sec-WebSocket-Accept", out var accept) || accept != expectedAccept) { throw new IOException("WebSocket 升级握手校验失败(Sec-WebSocket-Accept 不匹配)"); } } private string ReadHandshakeLine() { var sb = new StringBuilder(); var buf = new byte[1]; int prev = -1; while (true) { int read = _stream.Read(buf, 0, 1); if (read == 0) { if (sb.Length == 0) { return null; } break; } int b = buf[0]; if (prev == '\r' && b == '\n') { sb.Remove(sb.Length - 1, 1); // 去掉 \r break; } prev = b; sb.Append((char)b); } return sb.ToString(); } /// 接收循环:解析帧,分派事件;处理 ping/pong/close/分片。 private void ReceiveLoop() { var fragmentBuffer = new List(); byte fragmentOpcode = 0; try { while (_state == WebSocketState.Open || _state == WebSocketState.Closing) { int b0 = _stream.ReadByte(); if (b0 < 0) { break; // 对端关闭连接 } int b1 = _stream.ReadByte(); if (b1 < 0) { break; } bool fin = (b0 & 0x80) != 0; int opcode = b0 & 0x0F; bool masked = (b1 & 0x80) != 0; long payloadLen = b1 & 0x7F; if (payloadLen == 126) { payloadLen = ReadUInt16BigEndian(); } else if (payloadLen == 127) { payloadLen = (long)ReadUInt64BigEndian(); if (payloadLen > int.MaxValue) { throw new IOException("WebSocket 帧过大"); } } byte[] maskKey = null; if (masked) { maskKey = ReadExactly(4); } var payload = payloadLen > 0 ? ReadExactly((int)payloadLen) : new byte[0]; if (masked && payloadLen > 0) { for (int i = 0; i < payload.Length; i++) { payload[i] ^= maskKey[i % 4]; } } switch (opcode) { case 0x0: // continuation fragmentBuffer.AddRange(payload); if (fin) { RaiseMessage(fragmentOpcode == 0x1, fragmentBuffer.ToArray()); fragmentBuffer.Clear(); } break; case 0x1: // text case 0x2: // binary if (!fin) { fragmentOpcode = (byte)opcode; fragmentBuffer.Clear(); fragmentBuffer.AddRange(payload); } else { RaiseMessage(opcode == 0x1, payload); } break; case 0x8: // close HandleCloseFrame(payload); return; case 0x9: // ping → 回 pong SendFrame(0xA, payload); break; case 0xA: // pong break; } } } catch (Exception ex) { if (_state != WebSocketState.Closed) { try { OnError?.Invoke(this, new ErrorEventArgs(ex.Message, ex)); } catch { // 事件处理器异常不影响连接清理 } } } finally { if (_state != WebSocketState.Closed) { _state = WebSocketState.Closed; CloseResources(); try { OnClose?.Invoke(this, new CloseEventArgs(CloseStatusCode.Abnormal, "connection closed")); } catch { // 忽略 } } } } private void HandleCloseFrame(byte[] payload) { CloseStatusCode code = CloseStatusCode.NoStatus; string reason = string.Empty; if (payload.Length >= 2) { code = (CloseStatusCode)(ushort)((payload[0] << 8) | payload[1]); if (payload.Length > 2) { reason = Encoding.UTF8.GetString(payload, 2, payload.Length - 2); } } // 对端发起关闭,回一个 Close 帧 try { SendFrame(0x8, payload.Length >= 2 ? payload : new byte[] { 0x03, 0xE8 }); } catch { // 忽略 } _state = WebSocketState.Closed; CloseResources(); OnClose?.Invoke(this, new CloseEventArgs(code, reason, wasClean: true)); } private void RaiseMessage(bool isText, byte[] rawData) { if (rawData == null || rawData.Length == 0) { return; } OnMessage?.Invoke(this, new MessageEventArgs(isText, rawData)); } /// 发送一个 WebSocket 帧(客户端帧强制加掩码)。 private void SendFrame(byte opcode, byte[] payload) { lock (_sendLock) { EnsureNotDisposed(); var len = payload.Length; var header = new List(14); header.Add((byte)(0x80 | opcode)); // FIN=1 var mask = new byte[4]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(mask); } if (len < 126) { header.Add((byte)(0x80 | len)); } else if (len <= 0xFFFF) { header.Add((byte)(0x80 | 126)); header.Add((byte)(len >> 8)); header.Add((byte)(len & 0xFF)); } else { header.Add((byte)(0x80 | 127)); ulong l = (ulong)len; for (int i = 7; i >= 0; i--) { header.Add((byte)(l >> (i * 8))); } } header.AddRange(mask); var maskedPayload = new byte[len]; for (int i = 0; i < len; i++) { maskedPayload[i] = (byte)(payload[i] ^ mask[i % 4]); } _stream.Write(header.ToArray(), 0, header.Count); if (len > 0) { _stream.Write(maskedPayload, 0, len); } _stream.Flush(); } } private ushort ReadUInt16BigEndian() { var b = ReadExactly(2); return (ushort)((b[0] << 8) | b[1]); } private ulong ReadUInt64BigEndian() { var b = ReadExactly(8); ulong v = 0; for (int i = 0; i < 8; i++) { v = (v << 8) | b[i]; } return v; } private byte[] ReadExactly(int count) { var buffer = new byte[count]; int offset = 0; while (offset < count) { int read = _stream.Read(buffer, offset, count - offset); if (read == 0) { throw new IOException("连接被对端关闭(读取中断)"); } offset += read; } return buffer; } private void EnsureOpen() { EnsureNotDisposed(); if (_state != WebSocketState.Open) { throw new InvalidOperationException($"WebSocket 未处于 Open 状态(当前 {_state})"); } } private void EnsureNotDisposed() { if (_disposed) { throw new ObjectDisposedException(nameof(BouncyCastleWebSocketClient)); } } private void CloseResources() { try { _stream?.Dispose(); } catch { // 忽略 } try { _tcp?.Close(); } catch { // 忽略 } } #endregion } }