using System.Net.Sockets;
|
using System.Text;
|
using System.Text.Json;
|
using System.IO;
|
|
namespace WIDESEAWCS_Tasks.SocketServer
|
{
|
public partial class TcpSocketServer
|
{
|
/// <summary>
|
/// 处理客户端连接的消息循环
|
/// </summary>
|
/// <remarks>
|
/// 持续接收客户端消息,直到连接断开或取消。
|
/// 处理流程:
|
/// 1. 接收消息(帧解析)
|
/// 2. 更新客户端状态(活跃时间、编码)
|
/// 3. 处理设备注册
|
/// 4. 触发 MessageReceived 事件
|
/// 连接断开时清理资源并触发 RobotReceived 事件。
|
/// </remarks>
|
/// <param name="client">TCP 客户端连接</param>
|
/// <param name="clientId">客户端唯一标识</param>
|
/// <param name="cancellationToken">取消令牌</param>
|
/// <param name="robotCrane">机器人状态</param>
|
public async Task HandleClientAsync(TcpClient client, string clientId, CancellationToken cancellationToken, RobotSocketState robotCrane)
|
{
|
using (client)
|
using (NetworkStream networkStream = client.GetStream())
|
using (StreamReader reader = new(networkStream, _textEncoding, false, 1024, true))
|
using (StreamWriter writer = new(networkStream, _textEncoding, 1024, true) { AutoFlush = true })
|
{
|
CancellationTokenSource? localCts = null;
|
if (_options.EnableHeartbeat || _options.IdleTimeoutSeconds > 0)
|
{
|
// 创建链接的取消令牌源
|
localCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
}
|
|
try
|
{
|
// 消息接收循环
|
while (!cancellationToken.IsCancellationRequested && client.Connected)
|
{
|
string? message;
|
try
|
{
|
var ct = localCts?.Token ?? cancellationToken;
|
// 接收完整消息(帧解析)
|
message = await ReceiveFullMessageAsync(networkStream, _textEncoding, ct);
|
}
|
catch (OperationCanceledException)
|
{
|
break;
|
}
|
|
if (message == null)
|
{
|
break;
|
}
|
|
// 更新客户端状态
|
UpdateClientStatus(clientId, message);
|
|
string messageLower = message.ToLowerInvariant();
|
|
// 处理注册消息
|
if (TryHandleRegister(messageLower, message, clientId, networkStream, cancellationToken))
|
{
|
continue;
|
}
|
|
// 触发消息接收事件
|
if (MessageReceived != null)
|
{
|
try
|
{
|
// 判断是否为 JSON 格式
|
bool isJsonFormat = TryParseJsonSilent(message);
|
_ = MessageReceived.Invoke(message, isJsonFormat, client, robotCrane);
|
}
|
catch { }
|
}
|
}
|
}
|
finally
|
{
|
// 清理资源
|
try { localCts?.Cancel(); localCts?.Dispose(); } catch { }
|
RemoveClient(clientId);
|
try { _ = RobotReceived.Invoke(clientId); } catch { }
|
}
|
}
|
}
|
|
/// <summary>
|
/// 处理设备注册消息
|
/// </summary>
|
/// <remarks>
|
/// 注册消息格式:register,{deviceId}
|
/// 将设备 ID 绑定到当前客户端 ID。
|
/// </remarks>
|
/// <param name="messageLower">消息小写版本</param>
|
/// <param name="message">原始消息</param>
|
/// <param name="clientId">客户端 ID</param>
|
/// <param name="networkStream">网络流</param>
|
/// <param name="cancellationToken">取消令牌</param>
|
/// <returns>是否处理了注册消息</returns>
|
private bool TryHandleRegister(string messageLower, string message, string clientId, NetworkStream networkStream, CancellationToken cancellationToken)
|
{
|
if (!messageLower.StartsWith("register,"))
|
{
|
return false;
|
}
|
|
// 提取设备 ID
|
string deviceId = message.Substring("register,".Length).Trim();
|
if (!string.IsNullOrEmpty(deviceId))
|
{
|
lock (_syncRoot)
|
{
|
// 绑定设备到客户端
|
_deviceBindings[deviceId] = clientId;
|
}
|
|
// 回复注册成功
|
_ = WriteToClientAsync(clientId, networkStream, $"Registered,{deviceId}", cancellationToken);
|
}
|
|
return true;
|
}
|
|
/// <summary>
|
/// 更新客户端状态
|
/// </summary>
|
/// <remarks>
|
/// 更新最后活跃时间和字符编码。
|
/// 如果开启了自动编码检测,根据消息内容判断是 UTF-8 还是 GBK。
|
/// </remarks>
|
/// <param name="clientId">客户端 ID</param>
|
/// <param name="message">最新接收的消息</param>
|
private void UpdateClientStatus(string clientId, string message)
|
{
|
lock (_syncRoot)
|
{
|
// 更新最后活跃时间
|
_clientLastActive[clientId] = DateTime.Now;
|
|
// 如果还没有记录编码
|
if (!_clientEncodings.ContainsKey(clientId))
|
{
|
if (_options.AutoDetectEncoding && _autoDetectedGb2312 != null)
|
{
|
// 自动检测编码:JSON 或 UTF-8 字节特征则用 UTF-8,否则用 GBK
|
bool isUtf8 = TryParseJsonSilent(message) || IsLikelyUtf8(_textEncoding.GetBytes(message));
|
_clientEncodings[clientId] = isUtf8 ? _textEncoding : _autoDetectedGb2312;
|
}
|
else
|
{
|
_clientEncodings[clientId] = _textEncoding;
|
}
|
}
|
}
|
}
|
|
/// <summary>
|
/// 异步发送消息到客户端
|
/// </summary>
|
/// <remarks>
|
/// 内部方法,不使用帧格式,直接发送原始消息。
|
/// </remarks>
|
private async Task WriteToClientAsync(string clientId, NetworkStream networkStream, string message, CancellationToken cancellationToken)
|
{
|
SemaphoreSlim? sem = null;
|
Encoding? enc = null;
|
lock (_syncRoot)
|
{
|
_clientLocks.TryGetValue(clientId, out sem);
|
_clientEncodings.TryGetValue(clientId, out enc);
|
}
|
|
enc ??= _textEncoding;
|
|
if (sem != null) await sem.WaitAsync(cancellationToken);
|
try
|
{
|
var framedMessage = BuildFramedMessage(message);
|
var data = enc.GetBytes(framedMessage);
|
await networkStream.WriteAsync(data, 0, data.Length, cancellationToken);
|
}
|
finally
|
{
|
if (sem != null) sem.Release();
|
}
|
}
|
|
/// <summary>
|
/// 构建帧消息
|
/// </summary>
|
/// <remarks>
|
/// 在消息前后添加头尾标识。
|
/// </remarks>
|
/// <param name="message">原始消息</param>
|
/// <returns>带帧标识的消息</returns>
|
private string BuildFramedMessage(string message)
|
{
|
var header = _options.MessageHeader ?? string.Empty;
|
var footer = _options.MessageFooter ?? string.Empty;
|
return header + (message ?? string.Empty) + footer;
|
}
|
|
/// <summary>
|
/// 静默尝试解析 JSON
|
/// </summary>
|
/// <remarks>
|
/// 判断消息是否以 { 或 [ 开头,如果是则尝试解析。
|
/// 解析失败不抛异常。
|
/// </remarks>
|
/// <param name="message">消息内容</param>
|
/// <returns>是否是有效的 JSON 格式</returns>
|
private static bool TryParseJsonSilent(string message)
|
{
|
if (string.IsNullOrWhiteSpace(message)) return false;
|
char c = message.TrimStart()[0];
|
if (c != '{' && c != '[') return false;
|
try { JsonDocument.Parse(message); return true; } catch { return false; }
|
}
|
|
/// <summary>
|
/// 判断字节数组是否为 UTF-8 编码
|
/// </summary>
|
/// <remarks>
|
/// 通过检查字节序列是否符合 UTF-8 多字节字符的编码规则。
|
/// </remarks>
|
/// <param name="data">字节数组</param>
|
/// <returns>是否可能是 UTF-8 编码</returns>
|
private static bool IsLikelyUtf8(byte[] data)
|
{
|
int i = 0;
|
while (i < data.Length)
|
{
|
byte b = data[i];
|
if (b <= 0x7F) { i++; continue; } // ASCII 字符
|
|
// 检查多字节字符
|
if (b >= 0xC2 && b <= 0xDF) // 2字节字符
|
{
|
if (i + 1 >= data.Length) return false;
|
if ((data[i + 1] & 0xC0) != 0x80) return false;
|
i += 2; continue;
|
}
|
if (b >= 0xE0 && b <= 0xEF) // 3字节字符
|
{
|
if (i + 2 >= data.Length) return false;
|
if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80) return false;
|
i += 3; continue;
|
}
|
if (b >= 0xF0 && b <= 0xF4) // 4字节字符
|
{
|
if (i + 3 >= data.Length) return false;
|
if ((data[i + 1] & 0xC0) != 0x80 || (data[i + 2] & 0xC0) != 0x80 || (data[i + 3] & 0xC0) != 0x80) return false;
|
i += 4; continue;
|
}
|
return false;
|
}
|
return true;
|
}
|
|
/// <summary>
|
/// 接收完整消息(帧解析)
|
/// </summary>
|
/// <remarks>
|
/// 根据配置的头尾标识解析消息。
|
/// 如果未配置头尾,则一直读到数据不可用。
|
/// </remarks>
|
/// <param name="networkStream">网络流</param>
|
/// <param name="encoding">字符编码</param>
|
/// <param name="cancellationToken">取消令牌</param>
|
/// <returns>接收到的消息</returns>
|
private async Task<string?> ReceiveFullMessageAsync(NetworkStream networkStream, Encoding encoding, CancellationToken cancellationToken)
|
{
|
var header = _options.MessageHeader ?? string.Empty;
|
var footer = _options.MessageFooter ?? string.Empty;
|
|
var buffer = new byte[1024];
|
var builder = new StringBuilder();
|
|
while (true)
|
{
|
// 读取数据
|
int bytesRead = await networkStream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken);
|
if (bytesRead <= 0)
|
{
|
if (builder.Length == 0) return null;
|
// 无头尾配置时,返回已有数据
|
return string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer) ? builder.ToString() : null;
|
}
|
|
builder.Append(encoding.GetString(buffer, 0, bytesRead));
|
|
// 如果没有配置头尾,且数据不可用,返回已有数据
|
if (string.IsNullOrEmpty(header) && string.IsNullOrEmpty(footer))
|
{
|
if (!networkStream.DataAvailable)
|
{
|
break;
|
}
|
continue;
|
}
|
|
// 查找帧头
|
var data = builder.ToString();
|
var headerIndex = string.IsNullOrEmpty(header) ? 0 : data.IndexOf(header, StringComparison.Ordinal);
|
if (headerIndex < 0)
|
{
|
continue;
|
}
|
|
// 提取帧内容
|
var startIndex = headerIndex + header.Length;
|
var footerIndex = string.IsNullOrEmpty(footer) ? data.Length : data.IndexOf(footer, startIndex, StringComparison.Ordinal);
|
if (footerIndex >= 0)
|
{
|
return data.Substring(startIndex, footerIndex - startIndex);
|
}
|
}
|
|
return builder.ToString();
|
}
|
}
|
}
|