从Serein.Library分离了WebSocket/Modbus;新增了Json门户类,用于未来的Http、WebSocket、Mqtt、gRPC、QUIC扩展。

This commit is contained in:
fengjiayi
2025-07-27 23:34:01 +08:00
parent ab2adfde80
commit d3c3210ccc
70 changed files with 2306 additions and 554 deletions

View File

@@ -929,7 +929,7 @@ namespace Serein.Library.Api
#region
/// <summary>
/*/// <summary>
/// 启动远程服务
/// </summary>
Task StartRemoteServerAsync(int port = 7525);
@@ -957,7 +957,7 @@ namespace Serein.Library.Api
/// 退出远程环境
/// </summary>
void ExitRemoteEnv();
*/
/// <summary>
/// (用于远程)通知节点属性变更
/// </summary>

View File

@@ -14,7 +14,7 @@ namespace Serein.Library.Api
/// <summary>
/// 流程节点
/// </summary>
public interface IFlowNode : INotifyPropertyChanged, IDynamicFlowNode
public interface IFlowNode : INotifyPropertyChanged, ISereinFlow
{
/// <summary>
/// 节点持有的运行环境

View File

@@ -0,0 +1,285 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Api
{
/// <summary>
/// JSON数据交互的Token接口允许使用不同的JSON库进行数据处理。
/// </summary>
public interface IJsonToken
{
/// <summary>
/// 获取指定名称的属性如果存在则返回true并通过out参数返回对应的IJsonToken对象。
/// </summary>
/// <param name="name"></param>
/// <param name="token"></param>
/// <returns></returns>
bool TryGetValue(string name, out IJsonToken token);
/// <summary>
/// 获取指定名称的属性值如果不存在则返回null。
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
#nullable enable
IJsonToken? GetValue(string name);
/// <summary>
/// 获取当前Token的字符串值如果是null则返回null。
/// </summary>
/// <returns></returns>
string GetString();
/// <summary>
/// 获取当前Token的整数值如果是null则抛出异常。
/// </summary>
/// <returns></returns>
int GetInt32();
/// <summary>
/// 获取当前Token的布尔值如果是null则抛出异常。
/// </summary>
/// <returns></returns>
bool GetBoolean();
/// <summary>
/// 判断当前Token是否为null。
/// </summary>
bool IsNull { get; }
/// <summary>
/// 枚举当前Token作为数组时的所有元素返回一个IEnumerable&lt;IJsonTokens&gt;。
/// </summary>
/// <returns></returns>
IEnumerable<IJsonToken> EnumerateArray();
/// <summary>
/// 将当前Token转换为指定类型的对象。
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T ToObject<T>();
/// <summary>
/// 将当前Token转换为指定类型的对象。
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
object ToObject(Type type);
/// <summary>
/// 将当前Token转换为字符串表示形式。
/// </summary>
/// <returns></returns>
string ToString();
}
/// <summary>
/// 使用Json进行数据交互的门户,允许外部使用第三方JSON库进行数据处理。
/// </summary>
public interface IJsonProvider
{
/// <summary>
/// JSON文本转为指定类型
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="jsonText"></param>
/// <returns></returns>
T Deserialize<T>(string jsonText);
/// <summary>
/// JSON文本转为指定类型
/// </summary>
/// <param name="jsonText"></param>
/// <param name="type"></param>
/// <returns></returns>
object Deserialize(string jsonText, Type type);
/// <summary>
/// 从对象转换为JSON文本
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
string Serialize(object obj);
/// <summary>
/// 解析为Token
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
IJsonToken Parse(string json);
/// <summary>
/// 创建对象
/// </summary>
/// <param name="values"></param>
/// <returns></returns>
IJsonToken CreateObject(IDictionary<string, object> values = null);
/// <summary>
/// 创建数组
/// </summary>
/// <param name="values"></param>
/// <returns></returns>
IJsonToken CreateArray(IEnumerable<object> values = null);
/// <summary>
/// 将对象转换为JSON Token自动转换为 JObject/JArray。
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
IJsonToken FromObject(object obj);
}
}
/*
using System.Text.Json;
public class SystemTextJsonToken : IJsonToken
{
private readonly JsonElement _element;
public SystemTextJsonToken(JsonElement element) => _element = element;
public bool TryGetProperty(string name, out IJsonToken token)
{
if (_element.TryGetProperty(name, out var property))
{
token = new SystemTextJsonToken(property);
return true;
}
token = null;
return false;
}
public string GetString() => _element.ValueKind == JsonValueKind.Null ? null : _element.GetString();
public int GetInt32() => _element.GetInt32();
public bool GetBoolean() => _element.GetBoolean();
public bool IsNull => _element.ValueKind == JsonValueKind.Null;
public IEnumerable<IJsonToken> EnumerateArray()
{
if (_element.ValueKind == JsonValueKind.Array)
return _element.EnumerateArray().Select(e => new SystemTextJsonToken(e));
return Enumerable.Empty<IJsonToken>();
}
}
public class SystemTextJsonProvider : IJsonProvider
{
public string Serialize(object obj) => JsonSerializer.Serialize(obj);
public T Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json);
public IJsonToken Parse(string json) => new SystemTextJsonToken(JsonDocument.Parse(json).RootElement);
}
public T ToObject<T>()
{
// JsonElement -> string -> T
return JsonSerializer.Deserialize<T>(_element.GetRawText());
}
public IJsonToken CreateObject(IDictionary<string, object> values = null)
{
string json = values == null
? "{}"
: JsonSerializer.Serialize(values);
return Parse(json);
}
public IJsonToken CreateArray(IEnumerable<object> values = null)
{
string json = values == null
? "[]"
: JsonSerializer.Serialize(values);
return Parse(json);
}
public IJsonToken FromObject(object obj)
{
string json = JsonSerializer.Serialize(obj);
return Parse(json);
}
*/
/*
using Newtonsoft.Json.Linq;
public class NewtonsoftJsonToken : IJsonToken
{
private readonly JToken _token;
public NewtonsoftJsonToken(JToken token) => _token = token;
public bool TryGetProperty(string name, out IJsonToken token)
{
if (_token is JObject obj && obj.TryGetValue(name, out var value))
{
token = new NewtonsoftJsonToken(value);
return true;
}
token = null;
return false;
}
public string GetString() => _token.Type == JTokenType.Null ? null : _token.ToString();
public int GetInt32() => _token.Value<int>();
public bool GetBoolean() => _token.Value<bool>();
public bool IsNull => _token.Type == JTokenType.Null;
public IEnumerable<IJsonToken> EnumerateArray()
{
if (_token is JArray array)
return array.Select(x => new NewtonsoftJsonToken(x));
return Enumerable.Empty<IJsonToken>();
}
}
public class NewtonsoftJsonProvider : IJsonProvider
{
public string Serialize(object obj) => JsonConvert.SerializeObject(obj);
public T Deserialize<T>(string json) => JsonConvert.DeserializeObject<T>(json);
public IJsonToken Parse(string json) => new NewtonsoftJsonToken(JToken.Parse(json));
}
public T ToObject<T>()
{
return _token.ToObject<T>();
}
public IJsonToken CreateObject(IDictionary<string, object> values = null)
{
var obj = new JObject();
if (values != null)
{
foreach (var kvp in values)
{
obj[kvp.Key] = kvp.Value == null ? JValue.CreateNull() : JToken.FromObject(kvp.Value);
}
}
return new NewtonsoftJsonToken(obj);
}
public IJsonToken CreateArray(IEnumerable<object> values = null)
{
var array = values != null ? new JArray(values.Select(JToken.FromObject)) : new JArray();
return new NewtonsoftJsonToken(array);
}
public IJsonToken FromObject(object obj)
{
if (obj == null) return new NewtonsoftJsonToken(JValue.CreateNull());
if (obj is System.Collections.IEnumerable && !(obj is string))
return new NewtonsoftJsonToken(JArray.FromObject(obj));
return new NewtonsoftJsonToken(JObject.FromObject(obj));
}
*/

View File

@@ -10,9 +10,9 @@ namespace Serein.Library.Api
/// <summary>
/// 空接口
/// </summary>
public interface IDynamicFlowNode
public interface ISereinFlow
{
Task<FlowResult> ExecutingAsync(IFlowContext context, CancellationToken token);
}
}

View File

@@ -1,14 +1,8 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.ObjectPool;
using Microsoft.VisualBasic;
using Microsoft.Extensions.ObjectPool;
using Serein.Library.Api;
using Serein.Library.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -648,10 +642,10 @@ namespace Serein.Library
public UIContextOperation UIContextOperation => throw new NotImplementedException();
public Task<(bool, RemoteMsgUtil)> ConnectRemoteEnv(string addres, int port, string token)
/* public Task<(bool, RemoteMsgUtil)> ConnectRemoteEnv(string addres, int port, string token)
{
throw new NotImplementedException();
}
}*/
public void ExitRemoteEnv()
{

View File

@@ -1,10 +1,6 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Library.Api;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library

View File

@@ -222,7 +222,7 @@ namespace Serein.Library
// 3. 显式常量参数
if (IsExplicitData && !DataValue.StartsWith("@", StringComparison.OrdinalIgnoreCase))
return DataValue.ToConvert(DataType);
return DataValue.ToConvertValueType(DataType);
// 4. 来自其他节点
object inputParameter = null;
@@ -319,7 +319,7 @@ namespace Serein.Library
// 显式设置的参数
if (IsExplicitData && !DataValue.StartsWith("@", StringComparison.OrdinalIgnoreCase))
{
return DataValue.ToConvert(DataType); // 并非表达式,同时是显式设置的参数
return DataValue.ToConvertValueType(DataType); // 并非表达式,同时是显式设置的参数
}
#endregion

View File

@@ -1,12 +1,5 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library
{

View File

@@ -1,12 +1,11 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Library.Web;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Serein.Library.Web;
namespace Serein.Library.Network
{
@@ -15,6 +14,7 @@ namespace Serein.Library.Network
/// </summary>
public class ApiHandleConfig
{
private readonly IJsonPortal jsonPortal;
private readonly DelegateDetails delegateDetails;
/// <summary>
@@ -39,8 +39,9 @@ namespace Serein.Library.Network
/// <summary>
/// 添加处理配置
/// </summary>
/// <param name="jsonPortal"></param>
/// <param name="methodInfo"></param>
public ApiHandleConfig(MethodInfo methodInfo)
public ApiHandleConfig(IJsonPortal jsonPortal, MethodInfo methodInfo)
{
delegateDetails = new DelegateDetails(methodInfo);
var parameterInfos = methodInfo.GetParameters();
@@ -92,7 +93,7 @@ namespace Serein.Library.Network
}
else // if (type.IsValueType)
{
args[i] = JsonConvert.DeserializeObject(argValue, type);
args[i] = jsonPortal.Deserialize(argValue, type); // JsonConvert.DeserializeObject(argValue, type);
}
}
else
@@ -103,7 +104,7 @@ namespace Serein.Library.Network
return args;
}
public object[] GetArgsOfPost(Dictionary<string, string> routeData, JObject jsonObject)
public object[] GetArgsOfPost(Dictionary<string, string> routeData, IJsonToken jsonObject)
{
object[] args = new object[ParameterType.Length];
for (int i = 0; i < ParameterType.Length; i++)
@@ -120,7 +121,7 @@ namespace Serein.Library.Network
}
else // if (type.IsValueType)
{
args[i] = JsonConvert.DeserializeObject(argValue, type);
args[i] = jsonPortal.Deserialize(argValue, type);
}
}
else
@@ -134,16 +135,15 @@ namespace Serein.Library.Network
}
else if (jsonObject != null)
{
var jsonValue = jsonObject.GetValue(argName);
if (jsonValue is null)
if(jsonObject.TryGetProperty(argName, out var jsonToken))
{
// 值类型返回默认值引用类型返回null
args[i] = type.IsValueType ? Activator.CreateInstance(type) : null;
args[i] = jsonToken.ToObject(type);
}
else
{
args[i] = jsonValue.ToObject(type);
args[i] = type.IsValueType ? Activator.CreateInstance(type) : null;
}
}
}
return args;

View File

@@ -1,8 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
using Serein.Library.Api;
using Serein.Library.Network;
using Serein.Library.Utils;
using System;
using System.Collections;
using System.Collections.Concurrent;
@@ -47,7 +44,7 @@ namespace Serein.Library.Web
public class Router : IRouter
{
private readonly ISereinIOC SereinIOC; // 用于存储路由信息
private readonly IJsonPortal jsonPortal; // JSON门户
/// <summary>
/// 控制器实例对象的类型,每次调用都会重新实例化,[Url - ControllerType]
@@ -109,7 +106,7 @@ namespace Serein.Library.Web
SereinEnv.WriteLine(InfoType.INFO, url);
var apiType = routeAttribute.ApiType.ToString();
var config = new ApiHandleConfig(method);
var config = new ApiHandleConfig(jsonPortal, method);
if(!HandleModels.TryGetValue(apiType, out var configs))
{
configs = new ConcurrentDictionary<string, ApiHandleConfig>();

View File

@@ -1,13 +1,6 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Network.Http
namespace Serein.Library.Network.Http
{
internal static partial class SereinExtension
/*internal static partial class SereinExtension
{
#region JSON相关
@@ -60,5 +53,5 @@ namespace Serein.Library.Network.Http
}
}
#endregion
}
}*/
}

View File

@@ -1,168 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net.WebSockets;
namespace Serein.Library.Network.WebSocketCommunication
{
/// <summary>
/// <para>标记该类是处理模板需要获取WebSocketServer/WebSocketClient了实例后使用(Server/Client).MsgHandleHelper.AddModule()进行添加。</para>
/// <para>处理模板需要继承 ISocketHandleModule 接口否则WebSocket接受到数据时将无法进行调用相应的处理模板。</para>
/// <para>使用方式:</para>
/// <para>[AutoSocketModule(ThemeKey = "theme", DataKey = "data")]</para>
/// <para>public class PlcSocketService : ISocketHandleModule</para>
/// <para>类中方法示例void AddUser(string name,int age)</para>
/// <para>Json示例{ "theme":"AddUser", //【ThemeKey】 </para>
/// <para> "data": { // 【DataKey】 </para>
/// <para> "name":"张三", </para>
/// <para> "age":35, } } </para>
/// <para>WebSocket中收到以上该Json时通过ThemeKey获取到"AddUser"然后找到AddUser()方法</para>
/// <para>然后根据方法入参名称从data对应的json数据中取出"name""age"对应的数据作为入参进行调用。AddUser("张三",35)</para>
/// <para></para>
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class AutoSocketModuleAttribute : Attribute
{
/// <summary>
/// 业务标识
/// </summary>
public string ThemeKey;
/// <summary>
/// 数据标识
/// </summary>
public string DataKey;
/// <summary>
/// ID标识
/// </summary>
public string MsgIdKey;
}
/// <summary>
/// <para>作用WebSocket中处理Json时将通过Json中ThemeKey 对应的内容ThemeValue自动路由到相应方法进行处理同时要求Data中必须存在对应入参。</para>
/// <para>如果没有显式设置 ThemeValue将默认使用方法名称作为ThemeValue。</para>
/// <para>如果没有显式设置 IsReturnValue 标记为 false 当方法顺利完成没有抛出异常且返回对象非null会自动转为json文本发送回去</para>
/// <para>如果没有显式设置 ArgNotNull 标记为 false ,当外部尝试调用时,若 Json Data 不包含响应的数据,将会被忽略此次调用</para>
/// <para>如果返回类型为Task或Task&lt;TResult&gt;将会自动等待异步完成并获取结果无法处理Task&lt;Task&lt;TResult&gt;&gt;的情况)。</para>
/// <para>如果返回了值类型,会自动装箱为引用对象。</para>
/// <para>如果有方法执行过程中发送消息的需求,请在入参中声明以下类型的成员,调用时将传入发送消息的委托。</para>
/// <para>Action&lt;string&gt; : 发送文本内容。</para>
/// <para>Action&lt;object&gt; : 会自动将对象解析为Json字符串发送文本内容。</para>
/// <para>Action&lt;dynamic&gt; : 会自动将对象解析为Json字符串发送文本内容。</para>
/// <para>Func&lt;string,Task&gt; : 异步发送文本内容。</para>
/// <para>Func&lt;object,Task&gt; : 会自动将对象解析为Json字符串异步发送文本内容。</para>
/// <para>Func&lt;dynamic,Task&gt; : 会自动将对象解析为Json字符串异步发送文本内容。</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class AutoSocketHandleAttribute : Attribute
{
/// <summary>
/// 描述Json业务字段如果不设置将默认使用方法名称。
/// </summary>
public string ThemeValue = string.Empty;
/// <summary>
/// <para>标记方法执行完成后是否需要将结果发送。</para>
/// <para>注意以下返回值,返回的 json 中将不会新建 DataKey 字段:</para>
/// <para>1.返回类型为 void </para>
/// <para>2.返回类型为 Task </para>
/// <para>2.返回类型为 Unit </para>
/// <para>补充如果返回类型是Task&lt;TResult&gt;</para>
/// <para>会进行异步等待当Task结束后自动获取TResult进行发送请避免Task&lt;Task&lt;TResult&gt;&gt;诸如此类的Task泛型嵌套</para>
/// </summary>
public bool IsReturnValue = true;
/// <summary>
/// <para>表示该方法所有入参不能为空所需的参数在请求Json的Data不存在</para>
/// <para>若有一个参数无法从data获取则不会进行调用该方法</para>
/// <para>如果设置该属性为 false ,但某些入参不能为空,而不希望在代码中进行检查,请为入参添加[NotNull]/[Needful]特性</para>
/// </summary>
public bool ArgNotNull = true;
}
/// <summary>
/// 使用 DataKey 整体数据
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class UseDataAttribute : Attribute
{
}
/// <summary>
/// 使用 MsgIdKey 整体数据
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class UseMsgIdAttribute : Attribute
{
}
internal class WebSocketHandleConfiguration : HandleConfiguration
{
/// <summary>
/// 主题
/// </summary>
public string ThemeValue { get; set; } = string.Empty;
}
/// <summary>
/// socket模块处理数据配置
/// </summary>
public class HandleConfiguration
{
/// <summary>
/// Emit委托
/// </summary>
public DelegateDetails DelegateDetails { get; set; }
/// <summary>
/// 未捕获的异常跟踪
/// </summary>
public Action<Exception, Action<object>> OnExceptionTracking { get; set; }
/// <summary>
/// 所使用的实例
/// </summary>
public ISocketHandleModule Instance { get; set; }
/// <summary>
/// 是否需要返回
/// </summary>
public bool IsReturnValue { get; set; } = true;
/// <summary>
/// 是否要求必须不为null
/// </summary>
public bool ArgNotNull { get; set; } = true;
/// <summary>
/// 是否使Data整体内容作为入参参数
/// </summary>
public bool[] UseData { get; set; }
/// <summary>
/// 是否使用消息ID作为入参参数
/// </summary>
public bool[] UseMsgId { get; set; }
/// <summary>
/// 参数名称
/// </summary>
public string[] ParameterName { get; set; }
/// <summary>
/// 参数类型
/// </summary>
public Type[] ParameterType { get; set; }
/// <summary>
/// 是否检查变量为空
/// </summary>
public bool[] IsCheckArgNotNull { get; set; }
}
}

View File

@@ -1,20 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Serein.Library.Network.WebSocketCommunication.Handle
{
/// <summary>
/// 表示参数不能为空(Net462不能使用NutNull的情况
/// </summary>
public sealed class NeedfulAttribute : Attribute
{
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace Serein.Library.Network.WebSocketCommunication
{
public interface ISocketHandleModule
{
Guid HandleGuid { get; }
}
}

View File

@@ -24,13 +24,13 @@
<ItemGroup>
<Compile Remove="Http\**" />
<Compile Remove="Network\Socket\**" />
<Compile Remove="Network\**" />
<Compile Remove="Utils\SerinExpression\**" />
<EmbeddedResource Remove="Http\**" />
<EmbeddedResource Remove="Network\Socket\**" />
<EmbeddedResource Remove="Network\**" />
<EmbeddedResource Remove="Utils\SerinExpression\**" />
<None Remove="Http\**" />
<None Remove="Network\Socket\**" />
<None Remove="Network\**" />
<None Remove="Utils\SerinExpression\**" />
</ItemGroup>
@@ -40,6 +40,7 @@
<Compile Remove="FlowNode\NodeModelBaseFunc.cs" />
<Compile Remove="FlowNode\ScriptFlowApi.cs" />
<Compile Remove="Utils\NativeDllHelper.cs" />
<Compile Remove="Utils\RemoteMsgUtil.cs" />
</ItemGroup>
<ItemGroup>
@@ -47,10 +48,10 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0" />
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
<!--<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />-->
</ItemGroup>
<ItemGroup>

View File

@@ -1,11 +1,5 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Utils
{
@@ -118,37 +112,7 @@ namespace Serein.Library.Utils
/// <summary>
/// 对象转JSON文本
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static string ToJsonText(this object obj)
{
var jsonText = JsonConvert.SerializeObject(obj, Formatting.Indented);
return jsonText;
}
/// <summary>
/// JSON文本转对象
/// </summary>
/// <typeparam name="T">转换类型</typeparam>
/// <param name="json">JSON文本</param>
/// <returns></returns>
public static T ToJsonObject<T>(this string json)
{
try
{
return JsonConvert.DeserializeObject<T>(json);
}
catch (Exception)
{
return default(T);
}
}
/// <summary>
/// 对象转换为对应类型
/// </summary>
@@ -162,18 +126,18 @@ namespace Serein.Library.Utils
{
return default;
}
return (TResult)data.ToConvert(type);
return (TResult)data.ToConvertValueType(type);
}
/// <summary>
/// 对象转换(好像没啥用)
/// 对象转换
/// </summary>
/// <param name="data"></param>
/// <param name="type"></param>
/// <returns></returns>
public static object ToConvert(this object data, Type type)
public static object ToConvertValueType(this object data, Type type)
{
if (type.IsValueType)
{

View File

@@ -1,16 +1,9 @@
using Microsoft.Extensions.ObjectPool;
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Library.Api;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Transactions;
namespace Serein.Library.Utils
{

View File

@@ -0,0 +1,70 @@
using Serein.Library.Api;
using System;
using System.Collections.Generic;
namespace Serein.Library.Utils
{
/// <summary>
/// Json门户类需要你提供实现
/// </summary>
public static class JsonHelper
{
/// <summary>
/// Json门户类需要你提供实现
/// </summary>
private static IJsonProvider provider;
/// <summary>
/// 使用第三方包进行解析
/// </summary>
/// <param name="jsonPortal"></param>
public static void UseJsonLibrary(IJsonProvider jsonPortal)
{
JsonHelper.provider = jsonPortal;
}
public static T Deserialize<T>(string jsonText)
{
return provider.Deserialize<T>(jsonText);
}
public static object Deserialize(string jsonText, Type type)
{
return provider.Deserialize(jsonText, type);
}
public static IJsonToken Parse(string json)
{
return provider.Parse(json);
}
public static string Serialize(object obj)
{
return provider.Serialize(obj);
}
public static IJsonToken Object(Action<Dictionary<string, object>> init)
{
var dict = new Dictionary<string, object>();
init(dict);
return provider.CreateObject(dict);
}
public static IJsonToken Array(IEnumerable<object> values)
{
return provider.CreateArray(values);
}
public static IJsonToken FromObject(object obj)
{
if (obj is System.Collections.IEnumerable && !(obj is string))
return provider.CreateObject(obj as IDictionary<string, object>);
return provider.CreateArray(obj as IEnumerable<object>);
}
}
}

View File

@@ -1,12 +1,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Utils
{

View File

@@ -1,11 +1,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serein.Library.Network.WebSocketCommunication;
using Serein.Library.Network.WebSocketCommunication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Utils

View File

@@ -12,6 +12,9 @@ using System.Xml.Linq;
namespace Serein.Library
{
/// <summary>
/// 全局运行环境
/// </summary>
public static class SereinEnv
{
private static IFlowEnvironment environment;

View File

@@ -1,9 +1,6 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Utils;
using Serein.Library.Utils.SereinExpression.Resolver;
using Serein.Library.Utils.SereinExpression.Resolver;
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Globalization;
using System.Linq;
using System.Reflection;

View File

@@ -276,7 +276,7 @@ namespace Serein.Library.Utils.SereinExpression
if (hasType)
{
target = target.ToConvert(type);
target = target.ToConvertValueType(type);
}
@@ -437,7 +437,7 @@ namespace Serein.Library.Utils.SereinExpression
int endIndex = expression.IndexOf('>');
if(endIndex == expression.Length -1)
{
return value.ToConvert(type);
return value.ToConvertValueType(type);
}
else
{

View File

@@ -152,7 +152,7 @@ namespace Serein.NodeFlow.Env
currentFlowEnvironment.FlowControl.ActivateFlipflopNode(nodeGuid);
}
/// <inheritdoc/>
/* /// <inheritdoc/>
public async Task<(bool, RemoteMsgUtil)> ConnectRemoteEnv(string addres, int port, string token)
{
// 连接成功,切换远程环境
@@ -160,11 +160,11 @@ namespace Serein.NodeFlow.Env
if (isConnect)
{
/* remoteFlowEnvironment ??= new RemoteFlowEnvironment(remoteMsgUtil, this.Event, this.UIContextOperation);
currentFlowEnvironment = remoteFlowEnvironment;*/
*//* remoteFlowEnvironment ??= new RemoteFlowEnvironment(remoteMsgUtil, this.Event, this.UIContextOperation);
currentFlowEnvironment = remoteFlowEnvironment;*//*
}
return (isConnect, remoteMsgUtil);
}
}*/
/// <inheritdoc/>
public async Task<bool> ExitFlowAsync()
@@ -172,7 +172,7 @@ namespace Serein.NodeFlow.Env
return await currentFlowEnvironment.FlowControl.ExitFlowAsync();
}
/// <inheritdoc/>
/* /// <inheritdoc/>
public void ExitRemoteEnv()
{
currentFlowEnvironment.ExitRemoteEnv();
@@ -183,7 +183,7 @@ namespace Serein.NodeFlow.Env
public async Task<FlowEnvInfo> GetEnvInfoAsync()
{
return await currentFlowEnvironment.GetEnvInfoAsync();
}
}*/
/// <inheritdoc/>
public async Task<SereinProjectData> GetProjectInfoAsync()
@@ -288,7 +288,7 @@ namespace Serein.NodeFlow.Env
return await currentFlowEnvironment.FlowControl.StartFlowAsync<TResult>(startNodeGuid);
}
/// <inheritdoc/>
/* /// <inheritdoc/>
public async Task StartRemoteServerAsync(int port = 7525)
{
await currentFlowEnvironment.StartRemoteServerAsync(port);
@@ -298,7 +298,7 @@ namespace Serein.NodeFlow.Env
public void StopRemoteServer()
{
currentFlowEnvironment.StopRemoteServer();
}
}*/
/// <inheritdoc/>
public void TerminateFlipflopNode(string nodeGuid)

View File

@@ -1,25 +1,10 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.FlowNode;
using Serein.Library.Utils;
using Serein.Library.Utils.SereinExpression;
using Serein.NodeFlow.Services;
using Serein.NodeFlow.Tool;
using Serein.Script.Node;
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Reactive;
using System.Reflection;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using static Serein.Library.Api.IFlowEnvironment;
namespace Serein.NodeFlow.Env
{
@@ -296,7 +281,7 @@ namespace Serein.NodeFlow.Env
public void LoadProject(string filePath)
{
string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容
var FlowProjectData = JsonConvert.DeserializeObject<SereinProjectData>(content);
var FlowProjectData = JsonHelper.Deserialize<SereinProjectData>(content);
var FileDataPath = System.IO.Path.GetDirectoryName(filePath)!; // filePath;//
@@ -351,7 +336,7 @@ namespace Serein.NodeFlow.Env
public async Task LoadProjetAsync(string filePath)
{
string content = await System.IO.File.ReadAllTextAsync(filePath); // 读取整个文件内容
var FlowProjectData = JsonConvert.DeserializeObject<SereinProjectData>(content);
var FlowProjectData = JsonHelper.Deserialize<SereinProjectData>(content);
var FileDataPath = System.IO.Path.GetDirectoryName(filePath)!; // filePath;//
if(FlowProjectData is null)
{
@@ -396,9 +381,10 @@ namespace Serein.NodeFlow.Env
/// <param name="addres">远程环境地址</param>
/// <param name="port">远程环境端口</param>
/// <param name="token">密码</param>
public async Task<(bool, RemoteMsgUtil)> ConnectRemoteEnv(string addres, int port, string token)
/*public async Task<(bool, RemoteMsgUtil)> ConnectRemoteEnv(string addres, int port, string token)
{
if (IsControlRemoteEnv)
throw new NotImplementedException("远程环境未实现的方法 ConnectRemoteEnv");
*//*if (IsControlRemoteEnv)
{
await Console.Out.WriteLineAsync($"当前已经连接远程环境");
return (false, null);
@@ -410,9 +396,9 @@ namespace Serein.NodeFlow.Env
Addres = addres,
Port = port,
Token = token,
/*ThemeJsonKey = LocalFlowEnvironment.ThemeKey,
*//*ThemeJsonKey = LocalFlowEnvironment.ThemeKey,
MsgIdJsonKey = LocalFlowEnvironment.MsgIdKey,
DataJsonKey = LocalFlowEnvironment.DataKey,*/
DataJsonKey = LocalFlowEnvironment.DataKey,*//*
};
var remoteMsgUtil = new RemoteMsgUtil(controlConfiguration);
var result = await remoteMsgUtil.ConnectAsync();
@@ -423,8 +409,8 @@ namespace Serein.NodeFlow.Env
}
await Console.Out.WriteLineAsync("连接成功开始验证Token");
IsControlRemoteEnv = true;
return (true, remoteMsgUtil);
}
return (true, remoteMsgUtil);*//*
}*/
/// <summary>
/// 退出远程环境

View File

@@ -80,7 +80,7 @@ namespace Serein.NodeFlow.Model
}
public abstract partial class NodeModelBase : IDynamicFlowNode
public abstract partial class NodeModelBase : ISereinFlow
{
/// <summary>
/// 是否为基础节点

View File

@@ -1,22 +1,5 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Library.Utils.SereinExpression;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http.Headers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Serein.NodeFlow.Model
{
@@ -29,7 +12,7 @@ namespace Serein.NodeFlow.Model
/// <summary>
/// 节点基类
/// </summary>
public abstract partial class NodeModelBase : IDynamicFlowNode
public abstract partial class NodeModelBase : ISereinFlow
{
/// <summary>
/// 实体节点创建完成后调用的方法,调用时间早于 LoadInfo() 方法

View File

@@ -1,11 +1,7 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Library.Utils.SereinExpression;
using System.Dynamic;
using System.Reactive;
using System.Reflection.Metadata;
namespace Serein.NodeFlow.Model
{

View File

@@ -1,17 +1,8 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.NodeFlow.Services;
using Serein.Script;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace Serein.NodeFlow.Model
{

View File

@@ -1,14 +1,6 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace Serein.NodeFlow.Model
{

View File

@@ -1,18 +1,11 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.NodeFlow.Env;
using Serein.NodeFlow.Model;
using Serein.Script;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Serein.NodeFlow.Services
{
@@ -233,7 +226,7 @@ namespace Serein.NodeFlow.Services
}
else
{
var value = pd.DataValue.ToConvert(parameterInfo.ParameterType);
var value = pd.DataValue.ToConvertValueType(parameterInfo.ParameterType);
sb_invoke_login.AppendCode(3, $"global::{paramtTypeFullName} value{index} = (global::{paramtTypeFullName}){value}; // 获取当前节点的上一节点数据");
}
@@ -603,7 +596,7 @@ namespace Serein.NodeFlow.Services
}
else
{
var value = pd.DataValue.ToConvert(parameterInfo.ParameterType);
var value = pd.DataValue.ToConvertValueType(parameterInfo.ParameterType);
sb_invoke_login.AppendCode(3, $"global::{paramtTypeFullName} value{index} = (global::{paramtTypeFullName}){value}; // 获取当前节点的上一节点数据");
}

View File

@@ -1,21 +1,6 @@
using Microsoft.Extensions.ObjectPool;
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.NodeFlow;
using Serein.NodeFlow.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
namespace Serein.NodeFlow.Services
{

View File

@@ -0,0 +1,90 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Serein.Library.Api;
namespace Serein.Extend.NewtonsoftJson
{
public enum JsonType
{
Default = 0,
Web = 1,
}
/// <summary>
/// 基于Newtonsoft.Json的IJsonProvider实现
/// </summary>
public sealed class NewtonsoftJsonProvider : IJsonProvider
{
private JsonSerializerSettings settings;
public NewtonsoftJsonProvider()
{
}
public NewtonsoftJsonProvider(JsonType jsonType)
{
settings = jsonType switch
{
JsonType.Web => new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(), // 控制首字母小写
NullValueHandling = NullValueHandling.Ignore // 可选:忽略 null
},
_ => new JsonSerializerSettings
{
},
};
}
public NewtonsoftJsonProvider(JsonSerializerSettings settings)
{
settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(), // 控制首字母小写
NullValueHandling = NullValueHandling.Ignore // 可选:忽略 null
};
this.settings = settings;
}
public T? Deserialize<T>(string jsonText)
{
return JsonConvert.DeserializeObject<T>(jsonText, settings);
}
public object? Deserialize(string jsonText, Type type)
{
return JsonConvert.DeserializeObject(jsonText, type, settings);
}
public string Serialize(object obj)
{
return JsonConvert.SerializeObject(obj, settings);
}
public IJsonToken Parse(string json)
{
var token = JToken.Parse(json);
return new NewtonsoftJsonToken(token);
}
public IJsonToken CreateObject(IDictionary<string, object> values = null)
{
var jobj = values != null ? JObject.FromObject(values) : new JObject();
return new NewtonsoftJsonToken(jobj);
}
public IJsonToken CreateArray(IEnumerable<object> values = null)
{
var jarr = values != null ? JArray.FromObject(values) : new JArray();
return new NewtonsoftJsonToken(jarr);
}
public IJsonToken FromObject(object obj)
{
var token = JToken.FromObject(obj);
return new NewtonsoftJsonToken(token);
}
}
}

View File

@@ -0,0 +1,59 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
namespace Serein.Extend.NewtonsoftJson
{
/// <summary>
/// 基于Newtonsoft.Json的IJsonToken实现
/// </summary>
public sealed class NewtonsoftJsonToken : IJsonToken
{
private readonly JToken _token;
public NewtonsoftJsonToken(JToken token)
{
_token = token ?? throw new ArgumentNullException(nameof(token));
}
public bool TryGetValue(string name, out IJsonToken token)
{
if (_token is JObject obj && obj.TryGetValue(name, out JToken value))
{
token = new NewtonsoftJsonToken(value);
return true;
}
token = null;
return false;
}
public IJsonToken? GetValue(string name)
{
if (_token is JObject obj && obj.TryGetValue(name, out JToken value))
{
return new NewtonsoftJsonToken(value);
}
return null;
}
public string GetString() => _token.Type == JTokenType.Null ? null : _token.ToString();
public int GetInt32() => _token.Value<int>();
public bool GetBoolean() => _token.Value<bool>();
public bool IsNull => _token.Type == JTokenType.Null || _token.Type == JTokenType.Undefined;
public IEnumerable<IJsonToken> EnumerateArray()
{
if (_token is JArray arr)
return arr.Select(x => new NewtonsoftJsonToken(x));
throw new InvalidOperationException("当前Token不是数组类型。");
}
public T ToObject<T>() => _token.ToObject<T>();
public object ToObject(Type type) => _token.ToObject(type);
public override string ToString() => _token.ToString();
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net462</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Library\Serein.Library.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
namespace Serein.Proto.Modbus
{
public static class HexExtensions
{
public static string ToHexString(this byte[] data, string separator = " ")
{
if (data == null) return string.Empty;
return BitConverter.ToString(data).Replace("-", separator);
}
}
}

View File

@@ -0,0 +1,58 @@
namespace Serein.Proto.Modbus
{
/// <summary>
/// Modbus 客户端通用接口 (TCP/RTU 通用)
/// </summary>
public interface IModbusClient : IDisposable
{
/// <summary>
/// 报文发送时
/// </summary>
Action<byte[]> OnTx { get; set; }
/// <summary>
/// 接收到报文时
/// </summary>
Action<byte[]> OnRx { get; set; }
/// <summary>
/// 读取线圈状态 (0x01)
/// </summary>
Task<bool[]> ReadCoils(ushort startAddress, ushort quantity);
/// <summary>
/// 读取离散输入状态 (0x02)
/// </summary>
Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity);
/// <summary>
/// 读取保持寄存器 (0x03)
/// </summary>
Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity);
/// <summary>
/// 读取输入寄存器 (0x04)
/// </summary>
Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity);
/// <summary>
/// 写单个线圈 (0x05)
/// </summary>
Task WriteSingleCoil(ushort address, bool value);
/// <summary>
/// 写单个寄存器 (0x06)
/// </summary>
Task WriteSingleRegister(ushort address, ushort value);
/// <summary>
/// 写多个线圈 (0x0F)
/// </summary>
Task WriteMultipleCoils(ushort startAddress, bool[] values);
/// <summary>
/// 写多个寄存器 (0x10)
/// </summary>
Task WriteMultipleRegisters(ushort startAddress, ushort[] values);
}
}

View File

@@ -0,0 +1,114 @@
using System.IO.Ports;
namespace Serein.Proto.Modbus
{
public static class ModbusClientFactory
{
private static readonly char[] separator = new[] { ':' };
/// <summary>
/// 创建 Modbus 客户端实例
/// </summary>
/// <param name="connectionString">
/// 连接字符串格式:
/// TCP示例"tcp:192.168.1.100:502"
/// UCP示例"ucp:192.168.1.100:502"
/// RTU示例"rtu:COM3:9600:1" 格式rtu:串口名:波特率:从站地址)
/// </param>
public static IModbusClient Create(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("connectionString 不能为空");
var parts = connectionString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
//var parts = connectionString.Split(':',options: StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
throw new ArgumentException("connectionString 格式错误");
var protocol = parts[0].ToLower();
if (protocol == "tcp")
{
// tcp:host:port
if (parts.Length < 3)
throw new ArgumentException("TCP格式应为 tcp:host:port");
string host = parts[1];
if (!int.TryParse(parts[2], out int port))
port = 502; // 默认端口
return new ModbusTcpClient(host, port);
}
else if (protocol == "ucp")
{
// ucp:host:port
if (parts.Length < 3)
throw new ArgumentException("TCP格式应为 tcp:host:port");
string host = parts[1];
if (!int.TryParse(parts[2], out int port))
port = 502; // 默认端口
return new ModbusUdpClient(host, port);
}
else if (protocol == "rtu")
{
// rtu:portName:baudRate:slaveId
if (parts.Length < 4)
throw new ArgumentException("RTU格式应为 rtu:portName:baudRate:slaveId");
string portName = parts[1];
if (!int.TryParse(parts[2], out int baudRate))
baudRate = 9600;
if (!byte.TryParse(parts[3], out byte slaveId))
slaveId = 1;
return new ModbusRtuClient(portName, baudRate, slaveId: slaveId);
}
else
{
throw new NotSupportedException($"不支持的协议类型: {protocol}");
}
}
/// <summary>
/// 创建 Modbus TCP 客户端
/// </summary>
/// <param name="host">服务器地址</param>
/// <param name="port">端口默认502</param>
public static ModbusTcpClient CreateTcpClient(string host, int port = 502)
{
return new ModbusTcpClient(host, port);
}
/// <summary>
/// 创建 Modbus TCP 客户端
/// </summary>
/// <param name="host">服务器地址</param>
/// <param name="port">端口默认502</param>
public static ModbusUdpClient CreateUdpClient(string host, int port = 502)
{
return new ModbusUdpClient(host, port);
}
/// <summary>
/// 创建 Modbus RTU 客户端
/// </summary>
/// <param name="portName">串口名,比如 "COM3"</param>
/// <param name="baudRate">波特率默认9600</param>
/// <param name="parity">校验默认None</param>
/// <param name="dataBits">数据位默认8</param>
/// <param name="stopBits">停止位默认1</param>
/// <param name="slaveId">从站地址默认1</param>
public static ModbusRtuClient CreateRtuClient(string portName,
int baudRate = 9600,
Parity parity = Parity.None,
int dataBits = 8,
StopBits stopBits = StopBits.One,
byte slaveId = 1)
{
return new ModbusRtuClient(portName, baudRate, parity, dataBits, stopBits, slaveId);
}
}
}

View File

@@ -0,0 +1,29 @@
namespace Serein.Proto.Modbus
{
public class ModbusException : Exception
{
public byte FunctionCode { get; }
public byte ExceptionCode { get; }
public ModbusException(byte functionCode, byte exceptionCode)
: base($"Modbus异常码=0x{functionCode:X2}0x{exceptionCode:X2}{GetExceptionMessage(exceptionCode)})")
{
FunctionCode = functionCode;
ExceptionCode = exceptionCode;
}
private static string GetExceptionMessage(byte code) => code switch
{
0x01 => "非法功能。确认功能码是否被目标设备支持;检查设备固件版本是否过低;修改主站请求为设备支持的功能码", // 功能码错误
0x02 => "非法数据地址。检查主站请求的寄存器地址和长度是否越界;确保设备配置的寄存器数量正确", // 数据地址错误
0x03 => "非法数据值。检查写入的数值是否在设备支持的范围内;核对协议文档中对应寄存器的取值要求", // 数据值错误
0x04 => "从站设备故障。检查设备运行状态和日志;尝试重启设备;排查硬件或内部程序错误", // 从设备故障
0x05 => "确认。主站需通过轮询或延时机制等待处理完成,再次查询结果", // 确认
0x06 => "从站设备忙。增加请求重试延时;避免高频率发送编程指令", // 从设备忙
0x08 => "存储奇偶性差错。尝试重新发送请求;如错误持续出现,检查存储器硬件或文件一致性", // 内存奇偶校验错误
0x0A => "不可用网关路径。检查网关配置和负载;确认目标设备的网络连接可用性", // 网关路径不可用
0x0B => "网关目标设备响应失败。检查目标设备是否在线;检查网关的路由配置与网络连接", // 网关目标设备未响应
_ => $"未知错误" // 未知错误
};
}
}

View File

@@ -0,0 +1,41 @@
namespace Serein.Proto.Modbus
{
/// <summary>
/// Modbus 功能码枚举
/// </summary>
public enum ModbusFunctionCode
{
/// <summary>
/// 读线圈
/// </summary>
ReadCoils = 0x01,
/// <summary>
/// 读离散输入
/// </summary>
ReadDiscreteInputs = 0x02,
/// <summary>
/// 读保持寄存器
/// </summary>
ReadHoldingRegisters = 0x03,
/// <summary>
/// 读输入寄存器
/// </summary>
ReadInputRegisters = 0x04,
/// <summary>
/// 写单个线圈
/// </summary>
WriteSingleCoil = 0x05,
/// <summary>
/// 写单个寄存器
/// </summary>
WriteSingleRegister = 0x06,
/// <summary>
/// 写多个线圈
/// </summary>
WriteMultipleCoils = 0x0F,
/// <summary>
/// 写多个寄存器
/// </summary>
WriteMultipleRegister = 0x10,
}
}

View File

@@ -0,0 +1,21 @@
namespace Serein.Proto.Modbus
{
public class ModbusRequest
{
/// <summary>
/// 功能码
/// </summary>
public ModbusFunctionCode FunctionCode { get; set; }
/// <summary>
/// PDU (Protocol Data Unit) 数据不包括从站地址和CRC
/// </summary>
public byte[] PDU { get; set; }
/// <summary>
/// 异步任务完成源,用于等待响应
/// </summary>
public TaskCompletionSource<byte[]> Completion { get; set; }
}
}

View File

@@ -0,0 +1,322 @@
using System.Buffers.Binary;
using System.IO.Ports;
namespace Serein.Proto.Modbus
{
public class ModbusRtuClient : IModbusClient
{
/// <summary>
/// 消息发送时触发的事件
/// </summary>
public Action<byte[]> OnTx { get; set; }
/// <summary>
/// 接收到消息时触发的事件
/// </summary>
public Action<byte[]> OnRx { get; set; }
private readonly SerialPort _serialPort;
private readonly SemaphoreSlim _requestLock = new SemaphoreSlim(1, 1);
private readonly byte _slaveId;
private readonly CancellationTokenSource _cts = new();
public ModbusRtuClient(string portName, int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One, byte slaveId = 1)
{
_slaveId = slaveId;
_serialPort = new SerialPort(portName, baudRate, parity, dataBits, stopBits)
{
ReadTimeout = 1000,
WriteTimeout = 1000
};
_serialPort.Open();
}
#region
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var response = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
return ParseDiscreteBits(response, quantity);
}
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var response = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
return ParseDiscreteBits(response, quantity);
}
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var response = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
return ParseRegisters(response, quantity);
}
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var response = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
return ParseRegisters(response, quantity);
}
public async Task WriteSingleCoil(ushort address, bool value)
{
var pdu = new byte[]
{
(byte)(address >> 8),
(byte)(address & 0xFF),
value ? (byte)0xFF : (byte)0x00,
0x00
};
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
}
public async Task WriteSingleRegister(ushort address, ushort value)
{
var pdu = new byte[]
{
(byte)(address >> 8),
(byte)(address & 0xFF),
(byte)(value >> 8),
(byte)(value & 0xFF)
};
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
}
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
{
if (values == null || values.Length == 0)
throw new ArgumentException("values 不能为空");
int byteCount = (values.Length + 7) / 8; // 需要多少字节
byte[] coilData = new byte[byteCount];
for (int i = 0; i < values.Length; i++)
{
if (values[i])
coilData[i / 8] |= (byte)(1 << (i % 8)); // 设置对应位
}
var pdu = new List<byte>
{
(byte)(startAddress >> 8), // 起始地址高字节
(byte)(startAddress & 0xFF), // 起始地址低字节
(byte)(values.Length >> 8), // 数量高字节
(byte)(values.Length & 0xFF), // 数量低字节
(byte)coilData.Length // 数据字节数
};
pdu.AddRange(coilData);
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
}
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
{
if (values == null || values.Length == 0)
throw new ArgumentException("values 不能为空");
var arrlen = 5 + values.Length * 2;
var pdu = new byte[arrlen];
pdu[0] = (byte)(startAddress >> 8); // 起始地址高字节
pdu[1] = (byte)(startAddress & 0xFF); // 起始地址低字节
pdu[2] = (byte)(values.Length >> 8); // 寄存器数量高字节
pdu[3] = (byte)(values.Length & 0xFF); // 寄存器数量低字节
pdu[4] = (byte)(values.Length * 2); // 数据字节数
// 添加寄存器数据(每个寄存器 2 字节:高字节在前)
var index = 5;
foreach(var val in values)
{
pdu[index++] = (byte)(val >> 8);
pdu[index++] = (byte)(val & 0xFF);
}
/* var pdu = new List<byte>
{
(byte)(startAddress >> 8), // 起始地址高字节
(byte)(startAddress & 0xFF), // 起始地址低字节
(byte)(values.Length >> 8), // 寄存器数量高字节
(byte)(values.Length & 0xFF), // 寄存器数量低字节
(byte)(values.Length * 2) // 数据字节数
};
foreach (var val in values)
{
pdu.Add((byte)(val >> 8));
pdu.Add((byte)(val & 0xFF));
}*/
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu);
}
#endregion
#region
public async Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
{
await _requestLock.WaitAsync();
try
{
// 构造 RTU 帧
byte[] frame = BuildFrame(_slaveId, (byte)functionCode, pdu);
OnTx?.Invoke(frame); // 触发发送日志
await _serialPort.BaseStream.WriteAsync(frame, 0, frame.Length, _cts.Token);
await _serialPort.BaseStream.FlushAsync(_cts.Token);
// 接收响应
var response = await ReceiveResponseAsync();
OnRx?.Invoke(response); // 触发接收日志
// 检查功能码是否异常响应
if ((response[1] & 0x80) != 0)
{
byte exceptionCode = response[2];
throw new ModbusException(response[1], exceptionCode);
}
return response;
}
finally
{
_requestLock.Release();
}
}
/// <summary>
/// 接收响应
/// </summary>
private async Task<byte[]> ReceiveResponseAsync()
{
var buffer = new byte[256];
int offset = 0;
while (true)
{
int read = await _serialPort.BaseStream.ReadAsync(buffer, offset, buffer.Length - offset, _cts.Token);
offset += read;
// 最小RTU帧地址(1) + 功能码(1) + 数据(N) + CRC(2)
if (offset >= 5)
{
int frameLength = offset;
if (!ValidateCrc(buffer, 0, frameLength))
throw new IOException("CRC 校验失败");
byte[] response = new byte[frameLength - 2];
Array.Copy(buffer, 0, response, 0, frameLength - 2);
return response;
}
}
}
private byte[] BuildFrame(byte slaveAddr, byte functionCode, byte[] pdu)
{
var frame = new byte[2 + pdu.Length + 2]; // 地址 + 功能码 + PDU + CRC
frame[0] = slaveAddr;
frame[1] = functionCode;
Array.Copy(pdu, 0, frame, 2, pdu.Length);
ushort crc = Crc16(frame, 0, frame.Length - 2);
frame[frame.Length - 2] = (byte)(crc & 0xFF);
frame[frame.Length - 1] = (byte)(crc >> 8);
return frame;
}
private static bool ValidateCrc(byte[] buffer, int offset, int length)
{
ushort crcCalc = Crc16(buffer, offset, length - 2);
ushort crcRecv = (ushort)(buffer[length - 2] | (buffer[length - 1] << 8));
return crcCalc == crcRecv;
}
#endregion
#region PDU与解析
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
{
byte[] buffer = new byte[4];
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress); // 起始地址高低字节
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity); // 读取数量高低字节
return buffer;
}
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
{
int byteCount = pdu[2]; // 第2字节是后续的字节数量
int dataIndex = 3; // 数据从第3字节开始0-based
var result = new bool[count];
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
{
if (bitPos == 8)
{
bitPos = 0;
bytePos++;
}
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
}
return result;
}
private ushort[] ParseRegisters(byte[] pdu, ushort count)
{
var result = new ushort[count];
int dataStart = 3; // 数据从第3字节开始
for (int i = 0; i < count; i++)
{
int offset = dataStart + i * 2;
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
}
return result;
}
#endregion
#region CRC16
private static ushort Crc16(byte[] data, int offset, int length)
{
const ushort polynomial = 0xA001;
ushort crc = 0xFFFF;
for (int i = offset; i < offset + length; i++)
{
crc ^= data[i];
for (int j = 0; j < 8; j++)
{
if ((crc & 0x0001) != 0)
crc = (ushort)((crc >> 1) ^ polynomial);
else
crc >>= 1;
}
}
return crc;
}
#endregion
public void Dispose()
{
_cts.Cancel();
_serialPort?.Close();
}
}
}

View File

@@ -0,0 +1,15 @@
namespace Serein.Proto.Modbus
{
/// <summary>
/// Modbus RTU 请求实体(串口模式下无效)
/// </summary>
public sealed class ModbusRtuRequest : ModbusRequest
{
/// <summary>
/// 从站地址1~247
/// </summary>
public byte SlaveAddress { get; set; }
}
}

View File

@@ -0,0 +1,428 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net.Sockets;
using System.Threading.Channels;
namespace Serein.Proto.Modbus
{
/// <summary>
/// Modbus TCP 客户端
/// </summary>
public class ModbusTcpClient : IModbusClient
{
/// <summary>
/// 消息发送时触发的事件
/// </summary>
public Action<byte[]> OnTx { get; set; }
/// <summary>
/// 接收到消息时触发的事件
/// </summary>
public Action<byte[]> OnRx { get; set; }
/// <summary>
/// 消息通道
/// </summary>
private readonly Channel<ModbusTcpRequest> _channel = Channel.CreateUnbounded<ModbusTcpRequest>();
/// <summary>
/// TCP客户端
/// </summary>
private readonly TcpClient _tcpClient;
/// <summary>
/// TCP客户端的网络流用于发送和接收数据
/// </summary>
private readonly NetworkStream _stream;
/// <summary>
/// 存储未完成请求的字典键为事务ID值为任务完成源
/// </summary>
private readonly ConcurrentDictionary<ushort, TaskCompletionSource<byte[]>> _pendingRequests = new();
/// <summary>
/// 事务ID计数器用于生成唯一的事务ID
/// </summary>
private int _transactionId = 0;
public ModbusTcpClient(string host, int port = 502)
{
_tcpClient = new TcpClient();
_tcpClient.Connect(host, port);
_stream = _tcpClient.GetStream();
_ = ProcessQueueAsync(); // 启动后台消费者
_ = ReceiveLoopAsync(); // 启动接收响应线程
}
#region
/// <summary>
/// 读取线圈状态
/// </summary>
/// <param name="startAddress"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
return ParseDiscreteBits(responsePdu, quantity);
}
/// <summary>
/// 读取离散输入状态
/// </summary>
/// <param name="startAddress"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
return ParseDiscreteBits(responsePdu, quantity);
}
/// <summary>
/// 读取保持寄存器
/// </summary>
/// <param name="startAddress"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
return ParseRegisters(responsePdu, quantity);
}
/// <summary>
/// 读取输入寄存器
/// </summary>
/// <param name="startAddress"></param>
/// <param name="quantity"></param>
/// <returns></returns>
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
return ParseRegisters(responsePdu, quantity);
}
/// <summary>
/// 写单个线圈
/// </summary>
/// <param name="address"></param>
/// <param name="value"></param>
/// <returns></returns>
public async Task WriteSingleCoil(ushort address, bool value)
{
var pdu = new byte[]
{
(byte)(address >> 8), // 地址高字节
(byte)(address & 0xFF), // 地址低字节
value ? (byte)0xFF : (byte)0x00, // 线圈值高字节高电平为0xFF低电平为00
0x00 // 线圈值低字节
};
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
}
/// <summary>
/// 写单个寄存器
/// </summary>
/// <param name="address"></param>
/// <param name="value"></param>
/// <returns></returns>
public async Task WriteSingleRegister(ushort address, ushort value)
{
var pdu = new byte[]
{
(byte)(address >> 8), // 地址高字节
(byte)(address & 0xFF), // 地址低字节
(byte)(value >> 8), // 寄存器值高字节
(byte)(value & 0xFF) // 寄存器值低字节
};
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
}
/// <summary>
/// 写多个线圈
/// </summary>
/// <param name="startAddress"></param>
/// <param name="values"></param>
/// <returns></returns>
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
{
int byteCount = (values.Length + 7) / 8; // 计算需要的字节数
byte[] data = new byte[byteCount];
for (int i = 0; i < values.Length; i++)
{
if (values[i])
data[i / 8] |= (byte)(1 << (i % 8)); // 设置对应位
}
var pdu = new List<byte>
{
(byte)(startAddress >> 8), // 地址高字节
(byte)(startAddress & 0xFF), // 地址低字节
(byte)(values.Length >> 8), // 数量高字节
(byte)(values.Length & 0xFF), // 数量低字节
(byte)data.Length // 字节数
};
pdu.AddRange(data);
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
}
/// <summary>
/// 写多个寄存器
/// </summary>
/// <param name="startAddress"></param>
/// <param name="values"></param>
/// <returns></returns>
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
{
var pdu = new List<byte>
{
(byte)(startAddress >> 8), // 地址高字节
(byte)(startAddress & 0xFF), // 地址低字节
(byte)(values.Length >> 8), // 数量高字节
(byte)(values.Length & 0xFF), // 数量低字节
(byte)(values.Length * 2) // 字节数
};
foreach (var val in values)
{
pdu.Add((byte)(val >> 8)); // 寄存器值高字节
pdu.Add((byte)(val & 0xFF)); // 寄存器值低字节
}
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu.ToArray());
}
/// <summary>
/// 构建读取PDU数据
/// </summary>
/// <param name="startAddress"></param>
/// <param name="quantity"></param>
/// <returns></returns>
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
{
byte[] buffer = new byte[4];
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress); // 起始地址高低字节
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity); // 读取数量高低字节
return buffer;
}
/// <summary>
/// 解析离散位数据
/// </summary>
/// <param name="pdu"></param>
/// <param name="count"></param>
/// <returns></returns>
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
{
int byteCount = pdu[2]; // 第2字节是后续的字节数量
int dataIndex = 3; // 数据从第3字节开始0-based
var result = new bool[count];
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
{
if (bitPos == 8)
{
bitPos = 0;
bytePos++;
}
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
}
return result;
}
/// <summary>
/// 解析寄存器数据
/// </summary>
/// <param name="pdu"></param>
/// <param name="count"></param>
/// <returns></returns>
private ushort[] ParseRegisters(byte[] pdu, ushort count)
{
var result = new ushort[count];
int dataStart = 3; // 数据从第3字节开始
for (int i = 0; i < count; i++)
{
int offset = dataStart + i * 2;
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
}
return result;
}
#endregion
/// <summary>
/// 处理消息队列,发送请求到服务器
/// </summary>
/// <returns></returns>
private async Task ProcessQueueAsync()
{
while (_tcpClient.Connected)
{
var request = await _channel.Reader.ReadAsync();
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
OnTx?.Invoke(packet); // 触发发送日志
await _stream.WriteAsync(packet, 0, packet.Length);
await _stream.FlushAsync();
}
}
/// <summary>
/// 发送请求并等待响应
/// </summary>
/// <param name="functionCode">功能码</param>
/// <param name="pdu">内容</param>
/// <param name="timeout">超时时间</param>
/// <param name="maxRetries">最大重发次数</param>
/// <returns></returns>
/// <exception cref="TimeoutException"></exception>
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
{
int id = Interlocked.Increment(ref _transactionId);
var transactionId = (ushort)(id % ushort.MaxValue); // 0~65535 循环
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
var request = new ModbusTcpRequest
{
TransactionId = transactionId,
FunctionCode = functionCode,
PDU = pdu,
Completion = tcs
};
_pendingRequests[transactionId] = tcs;
_channel.Writer.TryWrite(request);
return tcs.Task;
}
/// <summary>
/// 接收数据循环
/// </summary>
/// <returns></returns>
private async Task ReceiveLoopAsync()
{
var buffer = new byte[1024];
while (true)
{
int len = await _stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
//int len = await _stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
if (len == 0) return; // 连接关闭
if (len < 6)
{
Console.WriteLine("接收到的数据长度不足");
return;
}
ushort protocolId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(2, 2));
if (protocolId != 0x0000)
{
Console.WriteLine($"协议不匹配: {protocolId:X4}");
return;
}
ushort dataLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(4, 2));
// 检查数据长度是否合法
if (dataLength > 253 || len < 6 + dataLength)
{
Console.WriteLine($"数据长度异常: dataLength={dataLength}, 实际接收={len}");
return;
}
ushort transactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2));
if (_pendingRequests.TryRemove(transactionId, out var tcs))
{
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, dataLength).ToArray();
if (OnRx is not null)
{
var packet = new ReadOnlySpan<byte>(buffer, 0, 6 + dataLength).ToArray();
OnRx?.Invoke(packet); // 触发接收日志
}
// 检查是否异常响应
if ((responsePdu[1] & 0x80) != 0)
{
byte exceptionCode = responsePdu[2];
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
}
else
{
tcs.SetResult(responsePdu);
}
}
else
{
Console.WriteLine($"未匹配到 TransactionId={transactionId} 的请求");
}
}
}
/// <summary>
/// 构造 Modbus Tcp 报文
/// </summary>
/// <param name="transactionId"></param>
/// <param name="unitId"></param>
/// <param name="functionCode"></param>
/// <param name="pduData"></param>
/// <returns></returns>
private byte[] BuildPacket(ushort transactionId, byte unitId, byte functionCode, byte[] pduData)
{
int pduLength = 1 + pduData.Length; // PDU 长度 = 功能码1字节 + 数据长度
int totalLength = 7 + pduLength; // MBAP头长度 = 7字节 + PDU长度
Span<byte> packet = totalLength <= 256 ? stackalloc byte[totalLength] : new byte[totalLength];
// 写入事务ID大端序
packet[0] = (byte)(transactionId >> 8);
packet[1] = (byte)(transactionId);
// 协议ID固定0x0000
packet[2] = 0;
packet[3] = 0;
// 长度PDU长度 + 1字节UnitID
ushort length = (ushort)(pduLength + 1);
packet[4] = (byte)(length >> 8);
packet[5] = (byte)(length);
// UnitID & 功能码
packet[6] = unitId;
packet[7] = functionCode;
// 复制PDU数据
pduData.AsSpan().CopyTo(packet.Slice(8));
return packet.ToArray();
}
public void Dispose()
{
var tcs = _pendingRequests.Values.ToArray();
foreach (var pending in tcs)
{
pending.TrySetCanceled();
}
_stream?.Close();
_tcpClient?.Close();
}
}
}

View File

@@ -0,0 +1,12 @@
namespace Serein.Proto.Modbus
{
/// <summary>
/// Modbus TCP 请求实体
/// </summary>
public class ModbusTcpRequest : ModbusRequest
{
public ushort TransactionId { get; set; }
}
}

View File

@@ -0,0 +1,261 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using System.Threading.Channels;
namespace Serein.Proto.Modbus
{
public class ModbusUdpClient : IModbusClient
{
/// <summary>
/// 消息发送时触发的事件
/// </summary>
public Action<byte[]> OnTx { get; set; }
/// <summary>
/// 接收到消息时触发的事件
/// </summary>
public Action<byte[]> OnRx { get; set; }
private readonly Channel<ModbusTcpRequest> _channel = Channel.CreateUnbounded<ModbusTcpRequest>();
private readonly UdpClient _udpClient;
private readonly IPEndPoint _remoteEndPoint;
private readonly ConcurrentDictionary<ushort, TaskCompletionSource<byte[]>> _pendingRequests = new();
private int _transactionId = 0;
public ModbusUdpClient(string host, int port = 502)
{
_remoteEndPoint = new IPEndPoint(IPAddress.Parse(host), port);
_udpClient = new UdpClient();
_udpClient.Connect(_remoteEndPoint);
_ = ProcessQueueAsync();
_ = ReceiveLoopAsync();
}
#region
public async Task<bool[]> ReadCoils(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadCoils, pdu);
return ParseDiscreteBits(responsePdu, quantity);
}
public async Task<bool[]> ReadDiscreteInputs(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadDiscreteInputs, pdu);
return ParseDiscreteBits(responsePdu, quantity);
}
public async Task<ushort[]> ReadHoldingRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadHoldingRegisters, pdu);
return ParseRegisters(responsePdu, quantity);
}
public async Task<ushort[]> ReadInputRegisters(ushort startAddress, ushort quantity)
{
var pdu = BuildReadPdu(startAddress, quantity);
var responsePdu = await SendAsync(ModbusFunctionCode.ReadInputRegisters, pdu);
return ParseRegisters(responsePdu, quantity);
}
public async Task WriteSingleCoil(ushort address, bool value)
{
var pdu = new byte[]
{
(byte)(address >> 8),
(byte)(address & 0xFF),
value ? (byte)0xFF : (byte)0x00,
0x00
};
await SendAsync(ModbusFunctionCode.WriteSingleCoil, pdu);
}
public async Task WriteSingleRegister(ushort address, ushort value)
{
var pdu = new byte[]
{
(byte)(address >> 8),
(byte)(address & 0xFF),
(byte)(value >> 8),
(byte)(value & 0xFF)
};
await SendAsync(ModbusFunctionCode.WriteSingleRegister, pdu);
}
public async Task WriteMultipleCoils(ushort startAddress, bool[] values)
{
int byteCount = (values.Length + 7) / 8;
byte[] data = new byte[byteCount];
for (int i = 0; i < values.Length; i++)
{
if (values[i])
data[i / 8] |= (byte)(1 << (i % 8));
}
var pdu = new List<byte>
{
(byte)(startAddress >> 8),
(byte)(startAddress & 0xFF),
(byte)(values.Length >> 8),
(byte)(values.Length & 0xFF),
(byte)data.Length
};
pdu.AddRange(data);
await SendAsync(ModbusFunctionCode.WriteMultipleCoils, pdu.ToArray());
}
public async Task WriteMultipleRegisters(ushort startAddress, ushort[] values)
{
var pdu = new List<byte>
{
(byte)(startAddress >> 8),
(byte)(startAddress & 0xFF),
(byte)(values.Length >> 8),
(byte)(values.Length & 0xFF),
(byte)(values.Length * 2)
};
foreach (var val in values)
{
pdu.Add((byte)(val >> 8));
pdu.Add((byte)(val & 0xFF));
}
await SendAsync(ModbusFunctionCode.WriteMultipleRegister, pdu.ToArray());
}
#endregion
#region
public Task<byte[]> SendAsync(ModbusFunctionCode functionCode, byte[] pdu)
{
int id = Interlocked.Increment(ref _transactionId);
var transactionId = (ushort)(id % ushort.MaxValue);
var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
var request = new ModbusTcpRequest
{
TransactionId = transactionId,
FunctionCode = functionCode,
PDU = pdu,
Completion = tcs
};
_pendingRequests[transactionId] = tcs;
_channel.Writer.TryWrite(request);
return tcs.Task;
}
private async Task ProcessQueueAsync()
{
while (true)
{
var request = await _channel.Reader.ReadAsync();
byte[] packet = BuildPacket(request.TransactionId, 0x01, (byte)request.FunctionCode, request.PDU);
OnTx?.Invoke(packet);
await _udpClient.SendAsync(packet, packet.Length);
}
}
private async Task ReceiveLoopAsync()
{
while (true)
{
UdpReceiveResult result = await _udpClient.ReceiveAsync();
var buffer = result.Buffer;
if (buffer.Length < 6) continue;
ushort transactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2));
if (_pendingRequests.TryRemove(transactionId, out var tcs))
{
OnRx?.Invoke(buffer);
var responsePdu = new ReadOnlySpan<byte>(buffer, 6, buffer.Length - 6).ToArray();
if ((responsePdu[1] & 0x80) != 0)
{
byte exceptionCode = responsePdu[2];
tcs.SetException(new ModbusException(responsePdu[1], exceptionCode));
}
else
{
tcs.SetResult(responsePdu);
}
}
}
}
private byte[] BuildPacket(ushort transactionId, byte unitId, byte functionCode, byte[] pduData)
{
int pduLength = 1 + pduData.Length;
int totalLength = 7 + pduLength;
Span<byte> packet = totalLength <= 256 ? stackalloc byte[totalLength] : new byte[totalLength];
packet[0] = (byte)(transactionId >> 8);
packet[1] = (byte)(transactionId);
packet[2] = 0; packet[3] = 0;
ushort length = (ushort)(pduLength + 1);
packet[4] = (byte)(length >> 8);
packet[5] = (byte)(length);
packet[6] = unitId;
packet[7] = functionCode;
pduData.AsSpan().CopyTo(packet.Slice(8));
return packet.ToArray();
}
#endregion
private byte[] BuildReadPdu(ushort startAddress, ushort quantity)
{
byte[] buffer = new byte[4];
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), startAddress);
BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(2, 2), quantity);
return buffer;
}
private bool[] ParseDiscreteBits(byte[] pdu, ushort count)
{
var result = new bool[count];
int byteCount = pdu[2];
int dataIndex = 3;
for (int i = 0, bytePos = 0, bitPos = 0; i < count; i++, bitPos++)
{
if (bitPos == 8)
{
bitPos = 0;
bytePos++;
}
result[i] = ((pdu[dataIndex + bytePos] >> bitPos) & 0x01) != 0;
}
return result;
}
private ushort[] ParseRegisters(byte[] pdu, ushort count)
{
var result = new ushort[count];
int dataStart = 3;
for (int i = 0; i < count; i++)
{
int offset = dataStart + i * 2;
result[i] = (ushort)((pdu[offset] << 8) | pdu[offset + 1]);
}
return result;
}
public void Dispose()
{
foreach (var tcs in _pendingRequests.Values)
tcs.TrySetCanceled();
_udpClient?.Dispose();
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="9.0.7" />
<PackageReference Include="System.Reactive" Version="6.0.1" />
<PackageReference Include="System.Threading.Channels" Version="8.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
<!--<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />-->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,46 @@
namespace Serein.Proto.WebSocket.Attributes
{
/// <summary>
/// <para>作用WebSocket中处理Json时将通过Json中ThemeKey 对应的内容ThemeValue自动路由到相应方法进行处理同时要求Data中必须存在对应入参。</para>
/// <para>如果没有显式设置 ThemeValue将默认使用方法名称作为ThemeValue。</para>
/// <para>如果没有显式设置 IsReturnValue 标记为 false 当方法顺利完成没有抛出异常且返回对象非null会自动转为json文本发送回去</para>
/// <para>如果没有显式设置 ArgNotNull 标记为 false ,当外部尝试调用时,若 Json Data 不包含响应的数据,将会被忽略此次调用</para>
/// <para>如果返回类型为Task或Task&lt;TResult&gt;将会自动等待异步完成并获取结果无法处理Task&lt;Task&lt;TResult&gt;&gt;的情况)。</para>
/// <para>如果返回了值类型,会自动装箱为引用对象。</para>
/// <para>如果有方法执行过程中发送消息的需求,请在入参中声明以下类型的成员,调用时将传入发送消息的委托。</para>
/// <para>Action&lt;string&gt; : 发送文本内容。</para>
/// <para>Action&lt;object&gt; : 会自动将对象解析为Json字符串发送文本内容。</para>
/// <para>Action&lt;dynamic&gt; : 会自动将对象解析为Json字符串发送文本内容。</para>
/// <para>Func&lt;string,Task&gt; : 异步发送文本内容。</para>
/// <para>Func&lt;object,Task&gt; : 会自动将对象解析为Json字符串异步发送文本内容。</para>
/// <para>Func&lt;dynamic,Task&gt; : 会自动将对象解析为Json字符串异步发送文本内容。</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class AutoSocketHandleAttribute : Attribute
{
/// <summary>
/// 描述Json业务字段如果不设置将默认使用方法名称。
/// </summary>
public string ThemeValue = string.Empty;
/// <summary>
/// <para>标记方法执行完成后是否需要将结果发送。</para>
/// <para>注意以下返回值,返回的 json 中将不会新建 DataKey 字段:</para>
/// <para>1.返回类型为 void </para>
/// <para>2.返回类型为 Task </para>
/// <para>2.返回类型为 Unit </para>
/// <para>补充如果返回类型是Task&lt;TResult&gt;</para>
/// <para>会进行异步等待当Task结束后自动获取TResult进行发送请避免Task&lt;Task&lt;TResult&gt;&gt;诸如此类的Task泛型嵌套</para>
/// </summary>
public bool IsReturnValue = true;
/// <summary>
/// <para>表示该方法所有入参不能为空所需的参数在请求Json的Data不存在</para>
/// <para>若有一个参数无法从data获取则不会进行调用该方法</para>
/// <para>如果设置该属性为 false ,但某些入参不能为空,而不希望在代码中进行检查,请为入参添加[NotNull]/[Needful]特性</para>
/// </summary>
public bool ArgNotNull = true;
}
}

View File

@@ -0,0 +1,43 @@
namespace Serein.Proto.WebSocket.Attributes
{
/// <summary>
/// <para>标记该类是处理模板需要获取WebSocketServer/WebSocketClient了实例后使用(Server/Client).MsgHandleHelper.AddModule()进行添加。</para>
/// <para>处理模板需要继承 ISocketHandleModule 接口否则WebSocket接受到数据时将无法进行调用相应的处理模板。</para>
/// <para>使用方式:</para>
/// <para>[AutoSocketModule(ThemeKey = "theme", DataKey = "data")]</para>
/// <para>public class PlcSocketService : ISocketHandleModule</para>
/// <para>类中方法示例void AddUser(string name,int age)</para>
/// <para>Json示例{ "theme":"AddUser", //【ThemeKey】 </para>
/// <para> "data": { // 【DataKey】 </para>
/// <para> "name":"张三", </para>
/// <para> "age":35, } } </para>
/// <para>WebSocket中收到以上该Json时通过ThemeKey获取到"AddUser"然后找到AddUser()方法</para>
/// <para>然后根据方法入参名称从data对应的json数据中取出"name""age"对应的数据作为入参进行调用。AddUser("张三",35)</para>
/// <para></para>
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class AutoSocketModuleAttribute : Attribute
{
/// <summary>
/// 业务标识
/// </summary>
public string ThemeKey;
/// <summary>
/// 数据标识
/// </summary>
public string DataKey;
/// <summary>
/// ID标识
/// </summary>
public string MsgIdKey;
/// <summary>
/// 指示应答数据回复方法返回值
/// </summary>
public bool IsResponseUseReturn = true;
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Net.WebSockets;
using Serein.Library;
namespace Serein.Proto.WebSocket.Attributes
{
/// <summary>
/// 使用 DataKey 整体数据
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class UseDataAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,14 @@
namespace Serein.Proto.WebSocket.Attributes
{
/// <summary>
/// 使用 MsgIdKey 整体数据
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class UseMsgIdAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Proto.WebSocket.Attributes
{
internal sealed class UseRequestAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
/// 表示参数不能为空(Net462不能使用NutNull的情况
/// </summary>
public sealed class NeedfulAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,73 @@
using Serein.Library;
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
/// socket模块处理数据配置
/// </summary>
public class HandleConfiguration
{
/// <summary>
/// Emit委托
/// </summary>
public DelegateDetails DelegateDetails { get; set; }
/// <summary>
/// 未捕获的异常跟踪
/// </summary>
public Action<Exception, Action<object>> OnExceptionTracking { get; set; }
/// <summary>
/// 所使用的实例
/// </summary>
public Func<ISocketHandleModule> InstanceFactory { get; set; }
/// <summary>
/// 是否需要返回
/// </summary>
public bool IsReturnValue { get; set; } = true;
/// <summary>
/// 是否要求必须不为null
/// </summary>
public bool ArgNotNull { get; set; } = true;
/// <summary>
/// 是否使用Data整体内容作为入参参数
/// </summary>
public bool[] UseData { get; set; }
/// <summary>
/// 是否使用Request整体内容作为入参参数
/// </summary>
public bool[] UseRequest { get; set; }
/// <summary>
/// 是否使用消息ID作为入参参数
/// </summary>
public bool[] UseMsgId { get; set; }
/// <summary>
/// 参数名称
/// </summary>
public string[] ParameterName { get; set; }
/// <summary>
/// 参数类型
/// </summary>
public Type[] ParameterType { get; set; }
/// <summary>
/// 是否检查变量为空
/// </summary>
public bool[] IsCheckArgNotNull { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Serein.Proto.WebSocket.Handle
{
internal class WebSocketHandleConfiguration : HandleConfiguration
{
/// <summary>
/// 主题
/// </summary>
public string ThemeValue { get; set; } = string.Empty;
}
}

View File

@@ -1,12 +1,11 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Serein.Library.Network.WebSocketCommunication.Handle
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
@@ -19,7 +18,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
/// </summary>
public WebSocketHandleModule(WebSocketHandleModuleConfig config)
{
this.moduleConfig = config;
moduleConfig = config;
}
/// <summary>
@@ -64,10 +63,8 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
foreach (var kv in MyHandleConfigs.ToArray())
{
var config = kv.Value;
if (config.Instance.HandleGuid.Equals(socketControlBase.HandleGuid))
{
MyHandleConfigs.TryRemove(kv.Key, out _);
}
MyHandleConfigs.TryRemove(kv.Key, out _);
}
return MyHandleConfigs.Count == 0;
}
@@ -88,7 +85,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
/// </summary>
public async Task HandleAsync(WebSocketMsgContext context)
{
var jsonObject = context.JsonObject; // 获取到消息
var jsonObject = context.MsgRequest; // 获取到消息
string theme = jsonObject.GetValue(moduleConfig.ThemeJsonKey)?.ToString();
if (!MyHandleConfigs.TryGetValue(theme, out var handldConfig))
{
@@ -106,11 +103,11 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
try
{
var dataObj = jsonObject.GetValue(moduleConfig.DataJsonKey)?.ToObject<JObject>();
var dataObj = jsonObject.GetValue(moduleConfig.DataJsonKey);
context.MsgData = dataObj; // 添加消息
if (WebSocketHandleModule.TryGetParameters(handldConfig, context, out var args))
if (TryGetParameters(handldConfig, context, out var args))
{
var result = await WebSocketHandleModule.HandleAsync(handldConfig, args);
var result = await HandleAsync(handldConfig, args);
if (handldConfig.IsReturnValue)
{
await context.RepliedAsync(moduleConfig, context, result);
@@ -136,7 +133,8 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
/// <returns></returns>
public static async Task<object> HandleAsync(HandleConfiguration config, object[] args)
{
var result = await config.DelegateDetails.InvokeAsync(config.Instance, args);
var instance = config.InstanceFactory.Invoke();
var result = await config.DelegateDetails.InvokeAsync(instance, args);
return result;
}
@@ -155,6 +153,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
for (int i = 0; i < config.ParameterType.Length; i++)
{
var type = config.ParameterType[i]; // 入参变量类型
var argName = config.ParameterName[i]; // 入参参数名称
#region ID
@@ -164,6 +163,12 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
}
#endregion
#region DATA JSON数据
else if (config.UseRequest[i])
{
args[i] = context.MsgRequest.ToObject(type);
}
#endregion
#region DATA JSON数据
else if (config.UseData[i])
{
args[i] = context.MsgData.ToObject(type);
@@ -222,7 +227,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
{
args[i] = new Func<object, Task>(async data =>
{
var jsonText = JsonConvert.SerializeObject(data);
var jsonText = JsonHelper.Serialize(data);
await context.SendAsync(jsonText);
});
}
@@ -237,7 +242,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
{
args[i] = new Action<object>(async data =>
{
var jsonText = JsonConvert.SerializeObject(data);
var jsonText = JsonHelper.Serialize(data);
await context.SendAsync(jsonText);
});
}
@@ -245,7 +250,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
{
args[i] = new Action<string>(async data =>
{
var jsonText = JsonConvert.SerializeObject(data);
var jsonText = JsonHelper.Serialize(data);
await context.SendAsync(jsonText);
});
}

View File

@@ -1,4 +1,4 @@
namespace Serein.Library.Network.WebSocketCommunication.Handle
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
/// 远程环境配置
@@ -17,6 +17,10 @@
/// 有关数据的 Json Key
/// </summary>
public string DataJsonKey { get; set; }
/// <summary>
/// 使用怎么样的数据
/// </summary>
public bool IsResponseUseReturn { get; set; }
}
}

View File

@@ -1,30 +1,31 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Api;
using Serein.Library.Utils;
using System;
using System.Threading.Tasks;
namespace Serein.Library.Network.WebSocketCommunication.Handle
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
/// 消息处理上下文
/// </summary>
public class WebSocketMsgContext /*: IDisposable*/
public class WebSocketMsgContext : IDisposable
{
public WebSocketMsgContext(Func<string, Task> sendAsync)
{
this._sendAsync = sendAsync;
_sendAsync = sendAsync;
}
public void Dispose()
{
JsonObject = null;
MsgRequest = null;
MsgTheme = null;
MsgId = null;
MsgData = null;
MsgData = null;
_sendAsync = null;
}
/// <summary>
/// 标记是否已经处理,如果是,则提前退出
/// </summary>
@@ -35,12 +36,13 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
_handle = value;
}
} }
public bool _handle = false;
/// <summary>
/// 消息本体(JObject
/// 消息本体(IJsonToken
/// </summary>
public JObject JsonObject { get; set; }
public IJsonToken MsgRequest { get; set; }
/// <summary>
/// 此次消息请求的主题
@@ -55,7 +57,7 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
/// <summary>
/// 此次消息的数据
/// </summary>
public JObject MsgData { get; set; }
public IJsonToken MsgData { get; set; }
private Func<string, Task> _sendAsync;
@@ -82,38 +84,29 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
WebSocketMsgContext context,
object data)
{
JObject jsonData;
if (data is null)
if (moduleConfig.IsResponseUseReturn)
{
jsonData = new JObject()
{
[moduleConfig.MsgIdJsonKey] = context.MsgId,
[moduleConfig.ThemeJsonKey] = context.MsgTheme,
};
var responseContent = JsonHelper.Serialize(data);
await SendAsync(responseContent);
}
else
{
JToken dataToken;
if (data is System.Collections.IEnumerable || data is Array)
{
dataToken = JArray.FromObject(data);
}
else
{
dataToken = JObject.FromObject(data);
}
jsonData = new JObject()
IJsonToken jsonData;
jsonData = JsonHelper.Object(obj =>
{
[moduleConfig.MsgIdJsonKey] = context.MsgId,
[moduleConfig.ThemeJsonKey] = context.MsgTheme,
[moduleConfig.DataJsonKey] = dataToken
};
obj[moduleConfig.MsgIdJsonKey] = context.MsgId;
obj[moduleConfig.ThemeJsonKey] = context.MsgTheme;
obj[moduleConfig.DataJsonKey] = data is null ? null
: JsonHelper.FromObject(data);
});
var msg = jsonData.ToString();
//Console.WriteLine($"[{msgId}] => {theme}");
await SendAsync(msg);
}
var msg = jsonData.ToString();
//Console.WriteLine($"[{msgId}] => {theme}");
await SendAsync(msg);
}

View File

@@ -1,11 +1,10 @@
using Serein.Library.Utils;
using System;
using Serein.Library;
using Serein.Proto.WebSocket.Attributes;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
namespace Serein.Library.Network.WebSocketCommunication.Handle
namespace Serein.Proto.WebSocket.Handle
{
/// <summary>
/// 适用于Json数据格式的WebSocket消息处理类
@@ -69,9 +68,10 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
/// </summary>
/// <param name="socketControlBase"></param>
/// <param name="onExceptionTracking"></param>
public void AddModule(ISocketHandleModule socketControlBase, Action<Exception, Action<object>> onExceptionTracking)
public void AddModule<T>(Func<ISocketHandleModule> instanceFactory, Action<Exception, Action<object>> onExceptionTracking)
where T : ISocketHandleModule
{
var type = socketControlBase.GetType();
var type = typeof(T);
var moduleAttribute = type.GetCustomAttribute<AutoSocketModuleAttribute>();
if (moduleAttribute is null)
{
@@ -81,11 +81,13 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
var themeKey = moduleAttribute.ThemeKey;
var dataKey = moduleAttribute.DataKey;
var msgIdKey = moduleAttribute.MsgIdKey;
var isResponseUseReturn = moduleAttribute.IsResponseUseReturn;
var moduleConfig = new WebSocketHandleModuleConfig()
{
ThemeJsonKey = themeKey,
DataJsonKey = dataKey,
MsgIdJsonKey = msgIdKey,
IsResponseUseReturn = isResponseUseReturn,
};
var handleModule = AddMyHandleModule(moduleConfig);
@@ -130,10 +132,11 @@ namespace Serein.Library.Network.WebSocketCommunication.Handle
var parameterInfos = methodInfo.GetParameters();
config.DelegateDetails = new DelegateDetails(methodInfo); // 对应theme的emit构造委托调用工具类
config.Instance = socketControlBase; // 调用emit委托时的实例
config.InstanceFactory = instanceFactory; // 调用emit委托时的实例
config.OnExceptionTracking = onExceptionTracking; // 异常追踪
config.ParameterType = parameterInfos.Select(t => t.ParameterType).ToArray(); // 入参参数类型
config.ParameterName = parameterInfos.Select(t => t.Name).ToArray(); // 入参参数名称
config.UseRequest = parameterInfos.Select(p => p.GetCustomAttribute<UseRequestAttribute>() != null).ToArray(); // 是否使用整体data数据
config.UseData = parameterInfos.Select(p => p.GetCustomAttribute<UseDataAttribute>() != null).ToArray(); // 是否使用整体data数据
config.UseMsgId = parameterInfos.Select(p => p.GetCustomAttribute<UseMsgIdAttribute>() != null).ToArray(); // 是否使用消息ID
#if NET5_0_OR_GREATER

View File

@@ -0,0 +1,10 @@
using System;
namespace Serein.Proto.WebSocket
{
public interface ISocketHandleModule
{
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net462</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Library\Serein.Library.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,7 +9,7 @@ using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace Serein.Library.Network.WebSocketCommunication
namespace Serein.Proto.WebSocket
{
/// <summary>
/// 消息处理工具
@@ -84,7 +84,7 @@ namespace Serein.Library.Network.WebSocketCommunication
/// <param name="webSocket"></param>
/// <param name="message"></param>
/// <returns></returns>
public static async Task SendAsync(WebSocket webSocket, string message)
public static async Task SendAsync(System.Net.WebSockets.WebSocket webSocket, string message)
{
var buffer = Encoding.UTF8.GetBytes(message);
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);

View File

@@ -1,16 +1,13 @@
using Serein.Library.Network.WebSocketCommunication.Handle;
using Serein.Library.Utils;
using Serein.Proto.WebSocket.Handle;
using System;
using System.Diagnostics;
using System.IO.Compression;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
using Newtonsoft.Json.Linq;
namespace Serein.Library.Network.WebSocketCommunication
namespace Serein.Proto.WebSocket
{
/// <summary>
@@ -60,7 +57,7 @@ namespace Serein.Library.Network.WebSocketCommunication
/// <returns></returns>
public async Task SendAsync(string message)
{
await SocketExtension.SendAsync(this._client, message); // 回复客户端
await SocketExtension.SendAsync(_client, message); // 回复客户端
}
/// <summary>
@@ -120,7 +117,7 @@ namespace Serein.Library.Network.WebSocketCommunication
}
public async Task HandleMsgAsync(WebSocket webSocket, MsgHandleUtil msgQueueUtil)
public async Task HandleMsgAsync(System.Net.WebSockets.WebSocket webSocket, MsgHandleUtil msgQueueUtil)
{
async Task sendasync(string text)
{
@@ -130,7 +127,7 @@ namespace Serein.Library.Network.WebSocketCommunication
{
var message = await msgQueueUtil.WaitMsgAsync(); // 有消息时通知
var context = new WebSocketMsgContext(sendasync);
context.JsonObject = JObject.Parse(message);
context.MsgRequest = JsonHelper.Parse(message);
MsgHandleHelper.Handle(context); // 处理消息
//using (var context = new WebSocketMsgContext(sendasync))

View File

@@ -1,5 +1,7 @@
using Newtonsoft.Json.Linq;
using Serein.Library.Network.WebSocketCommunication.Handle;
using Serein.Library;
using Serein.Library.Api;
using Serein.Library.Utils;
using Serein.Proto.WebSocket.Handle;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
@@ -9,7 +11,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Serein.Library.Network.WebSocketCommunication
namespace Serein.Proto.WebSocket
{
/// <summary>
/// WebSocket JSON 消息授权管理
@@ -21,9 +23,9 @@ namespace Serein.Library.Network.WebSocketCommunication
/// </summary>
public WebSocketAuthorizedHelper(string addresPort,string token, Func<dynamic, Task<bool>> inspectionAuthorizedFunc)
{
this.AddresPort = addresPort;
this.TokenKey = token;
this.InspectionAuthorizedFunc = inspectionAuthorizedFunc;
AddresPort = addresPort;
TokenKey = token;
InspectionAuthorizedFunc = inspectionAuthorizedFunc;
}
/// <summary>
@@ -52,7 +54,7 @@ namespace Serein.Library.Network.WebSocketCommunication
{
await semaphoreSlim.WaitAsync(1);
bool isAuthorized = false;
JObject json = JObject.Parse(message);
IJsonToken json = JsonHelper.Parse(message);
if(json.TryGetValue(TokenKey,out var token))
{
// 交给之前定义的授权方法进行判断
@@ -86,9 +88,9 @@ namespace Serein.Library.Network.WebSocketCommunication
/// </summary>
public WebSocketServer()
{
this.AuthorizedClients = new ConcurrentDictionary<string, WebSocketAuthorizedHelper>();
this.InspectionAuthorizedFunc = (tokenObj) => Task.FromResult(true);
this.IsCheckToken = false;
AuthorizedClients = new ConcurrentDictionary<string, WebSocketAuthorizedHelper>();
InspectionAuthorizedFunc = (tokenObj) => Task.FromResult(true);
IsCheckToken = false;
}
/// <summary>
@@ -98,10 +100,10 @@ namespace Serein.Library.Network.WebSocketCommunication
/// <param name="inspectionAuthorizedFunc">验证token的方法</param>
public WebSocketServer(string tokenKey, Func<dynamic, Task<bool>> inspectionAuthorizedFunc)
{
this.TokenKey = tokenKey;
this.AuthorizedClients = new ConcurrentDictionary<string, WebSocketAuthorizedHelper>();
this.InspectionAuthorizedFunc = inspectionAuthorizedFunc;
this.IsCheckToken = true;
TokenKey = tokenKey;
AuthorizedClients = new ConcurrentDictionary<string, WebSocketAuthorizedHelper>();
InspectionAuthorizedFunc = inspectionAuthorizedFunc;
IsCheckToken = true;
}
/// <summary>
@@ -171,7 +173,7 @@ namespace Serein.Library.Network.WebSocketCommunication
listener?.Stop();
}
private async Task HandleWebSocketAsync(WebSocket webSocket, WebSocketAuthorizedHelper authorizedHelper)
private async Task HandleWebSocketAsync(System.Net.WebSockets.WebSocket webSocket, WebSocketAuthorizedHelper authorizedHelper)
{
// 需要授权,却没有成功创建授权类,关闭连接
if (IsCheckToken && authorizedHelper is null)
@@ -230,7 +232,7 @@ namespace Serein.Library.Network.WebSocketCommunication
}
public async Task HandleMsgAsync(WebSocket webSocket,
public async Task HandleMsgAsync(System.Net.WebSockets.WebSocket webSocket,
MsgHandleUtil msgQueueUtil,
WebSocketAuthorizedHelper authorizedHelper)
{
@@ -255,7 +257,7 @@ namespace Serein.Library.Network.WebSocketCommunication
}
}
var context = new WebSocketMsgContext(sendasync);
context.JsonObject = JObject.Parse(message);
context.MsgRequest = JsonHelper.Parse(message);
MsgHandleHelper.Handle(context); // 处理消息
//using (var context = new WebSocketMsgContext(sendasync))

View File

@@ -1,16 +1,9 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library;
using Serein.Library.Utils;
using Serein.Script.Node;
using Serein.Script.Node.FlowControl;
using System;
using System.ComponentModel.Design;
using System.Reactive;
using System.Reflection;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Serein.Script
{

View File

@@ -1,10 +1,4 @@
using Newtonsoft.Json.Linq;
using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Xml.Linq;
using static System.Net.Mime.MediaTypeNames;
namespace Serein.Script
namespace Serein.Script
{
/// <summary>
/// Serein脚本词法分析器的Token类型

View File

@@ -1,15 +1,6 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Library.Utils;
using Serein.Library.Utils;
using Serein.Script.Node;
using Serein.Script.Node.FlowControl;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
namespace Serein.Script
{

View File

@@ -1,17 +1,6 @@
using Newtonsoft.Json.Linq;
using Serein.Library;
using Serein.Script.Node;
using Serein.Script.Node;
using Serein.Script.Node.FlowControl;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Serein.Script
{

View File

@@ -28,6 +28,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serein.Workbench.Avalonia.D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.CollaborationSync", "Serein.CollaborationSync\Serein.CollaborationSync.csproj", "{913AAB34-7383-4F9D-A7DF-A5E6191DDB3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Extend.NewtonsoftJson", "Serein.Extend.NewtonsoftJson\Serein.Extend.NewtonsoftJson.csproj", "{1961CF3C-29FC-4850-91DE-1DD571D9514D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Proto.WebSocket", "Serein.Proto.WebSocket\Serein.Proto.WebSocket.csproj", "{CF98EBDA-A2E1-08D1-3FED-B8F3F61293FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Proto.Modbus", "Serein.Proto.Modbus\Serein.Proto.Modbus.csproj", "{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -74,6 +80,18 @@ Global
{913AAB34-7383-4F9D-A7DF-A5E6191DDB3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{913AAB34-7383-4F9D-A7DF-A5E6191DDB3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{913AAB34-7383-4F9D-A7DF-A5E6191DDB3D}.Release|Any CPU.Build.0 = Release|Any CPU
{1961CF3C-29FC-4850-91DE-1DD571D9514D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1961CF3C-29FC-4850-91DE-1DD571D9514D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1961CF3C-29FC-4850-91DE-1DD571D9514D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1961CF3C-29FC-4850-91DE-1DD571D9514D}.Release|Any CPU.Build.0 = Release|Any CPU
{CF98EBDA-A2E1-08D1-3FED-B8F3F61293FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CF98EBDA-A2E1-08D1-3FED-B8F3F61293FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CF98EBDA-A2E1-08D1-3FED-B8F3F61293FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CF98EBDA-A2E1-08D1-3FED-B8F3F61293FC}.Release|Any CPU.Build.0 = Release|Any CPU
{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -146,7 +146,7 @@ namespace Serein.Workbench.ViewModels
{
Debug.WriteLine(ex.Message);
}
flowEnvironment.StartRemoteServerAsync();
//flowEnvironment.StartRemoteServerAsync();
}
}