兼容win7版应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

622 lines
20 KiB

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
{
/// <summary>WebSocket 连接状态(与 websocket-sharp 兼容的取值)。</summary>
public enum WebSocketState
{
New,
Connecting,
Open,
Closing,
Closed
}
/// <summary>WebSocket 关闭状态码(RFC 6455 常用值,与 websocket-sharp 兼容)。</summary>
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
}
/// <summary>收到 WebSocket 消息的事件参数(与 websocket-sharp 兼容)。</summary>
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;
}
}
/// <summary>WebSocket 错误事件参数。</summary>
public class ErrorEventArgs : EventArgs
{
public string Message { get; }
public Exception Exception { get; }
internal ErrorEventArgs(string message, Exception exception = null)
{
Message = message;
Exception = exception;
}
}
/// <summary>WebSocket 关闭事件参数。</summary>
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;
}
}
/// <summary>
/// 基于 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)。
/// </summary>
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<MessageEventArgs> OnMessage;
public event EventHandler<ErrorEventArgs> OnError;
public event EventHandler<CloseEventArgs> OnClose;
/// <summary>连接/关闭超时(默认 15 秒,与原先 websocket-sharp WaitTime 一致)。</summary>
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));
}
}
/// <summary>
/// 建立连接:TCP → BouncyCastle TLS 1.2 握手 → WebSocket 升级握手,成功后启动接收线程。
/// 同步阻塞(与 websocket-sharp Connect() 一致,调用方可 Task.Run 包装)。
/// </summary>
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;
}
}
/// <summary>发送文本消息(客户端帧必须加掩码,RFC 6455)。</summary>
public void Send(string data)
{
EnsureOpen();
var bytes = Encoding.UTF8.GetBytes(data ?? string.Empty);
SendFrame(0x1, bytes);
}
/// <summary>发送二进制消息(STOMP 心跳 0x0A 等)。</summary>
public void Send(byte[] data)
{
EnsureOpen();
SendFrame(0x2, data ?? new byte[0]);
}
/// <summary>主动关闭连接(发送 Close 帧并等待接收线程退出)。</summary>
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<string, string>(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();
}
/// <summary>接收循环:解析帧,分派事件;处理 ping/pong/close/分片。</summary>
private void ReceiveLoop()
{
var fragmentBuffer = new List<byte>();
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));
}
/// <summary>发送一个 WebSocket 帧(客户端帧强制加掩码)。</summary>
private void SendFrame(byte opcode, byte[] payload)
{
lock (_sendLock)
{
EnsureNotDisposed();
var len = payload.Length;
var header = new List<byte>(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
}
}