From 51c268baaded27a3b1fe5bb2031714bf9dd307f7 Mon Sep 17 00:00:00 2001 From: fengjiayi <12821976+ning_xi@user.noreply.gitee.com> Date: Thu, 4 Sep 2025 10:39:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=B7=B2=E7=9F=A5?= =?UTF-8?q?=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Library/Serein.Library.csproj | 2 +- Library/Utils/ObjectConvertHelper.cs | 11 +- Library/Utils/SereinEnv.cs | 1 + Library/Utils/SereinIoc.cs | 15 +- NodeFlow/Serein.NodeFlow.csproj | 2 +- .../NewtonsoftJsonExtend.cs | 16 + .../NewtonsoftJsonProvider.cs | 39 +- .../Serein.Extend.NewtonsoftJson.csproj | 2 +- .../Serein.Library.NodeGenerator.csproj | 2 +- Serein.Proto.HttpApi/ApiHandleConfig.cs | 200 ++++++++ Serein.Proto.HttpApi/Attribute.cs | 80 ++++ Serein.Proto.HttpApi/ControllerBase.cs | 32 ++ Serein.Proto.HttpApi/PathRouter.cs | 437 ++++++++++++++++++ Serein.Proto.HttpApi/QueryStringParser.cs | 39 ++ Serein.Proto.HttpApi/RequestLimiter.cs | 59 +++ .../Serein.Proto.HttpApi.csproj | 23 + Serein.Proto.HttpApi/SereinWebApiService.cs | 121 +++++ Serein.Proto.HttpApi/WebApiServer.cs | 17 + .../Serein.Proto.Modbus.csproj | 9 +- .../Handle/WebSocketHandleModule.cs | 7 +- .../ISereinWebSocketService.cs | 7 + .../Serein.Proto.WebSocket.csproj | 6 +- .../SereinWebSocketService.cs | 32 +- Serein.Proto.WebSocket/TestClass.cs | 33 -- .../WebSocketHandleContext.cs | 7 +- Serein.Script/Serein.Script.csproj | 1 + SereinFlow.sln | 6 + Workbench/Serein.WorkBench.csproj | 1 - 28 files changed, 1099 insertions(+), 108 deletions(-) create mode 100644 Serein.Proto.HttpApi/ApiHandleConfig.cs create mode 100644 Serein.Proto.HttpApi/Attribute.cs create mode 100644 Serein.Proto.HttpApi/ControllerBase.cs create mode 100644 Serein.Proto.HttpApi/PathRouter.cs create mode 100644 Serein.Proto.HttpApi/QueryStringParser.cs create mode 100644 Serein.Proto.HttpApi/RequestLimiter.cs create mode 100644 Serein.Proto.HttpApi/Serein.Proto.HttpApi.csproj create mode 100644 Serein.Proto.HttpApi/SereinWebApiService.cs create mode 100644 Serein.Proto.HttpApi/WebApiServer.cs delete mode 100644 Serein.Proto.WebSocket/TestClass.cs diff --git a/Library/Serein.Library.csproj b/Library/Serein.Library.csproj index 9a0c6ae..5b49cd5 100644 --- a/Library/Serein.Library.csproj +++ b/Library/Serein.Library.csproj @@ -2,7 +2,7 @@ SereinFow - 1.2.4 + 1.2.4.1 动态节点流、可视化编辑的基本依赖,支持导入C# DLL生成自定义节点,提供二次开发支持,适合用于可视化编程和流程设计 README.md https://github.com/fhhyyp/serein-flow diff --git a/Library/Utils/ObjectConvertHelper.cs b/Library/Utils/ObjectConvertHelper.cs index f7e0643..378ced0 100644 --- a/Library/Utils/ObjectConvertHelper.cs +++ b/Library/Utils/ObjectConvertHelper.cs @@ -257,7 +257,16 @@ namespace Serein.Library.Utils object result; if (type.IsEnum) { - result = Enum.Parse(type, valueStr); + if (int.TryParse(valueStr, out int numericValue)) + { + // 输入是数字,直接转换 + result = Enum.ToObject(type, numericValue); + } + else + { + // 输入是枚举名称 + result = Enum.Parse(type, valueStr, ignoreCase: true); + } } else if (type == typeof(bool)) { diff --git a/Library/Utils/SereinEnv.cs b/Library/Utils/SereinEnv.cs index a070b13..00da5c5 100644 --- a/Library/Utils/SereinEnv.cs +++ b/Library/Utils/SereinEnv.cs @@ -125,6 +125,7 @@ namespace Serein.Library public static void WriteLine(InfoType type, string message, InfoClass @class = InfoClass.General) { Debug.WriteLine($"{type} : {message}"); + Console.WriteLine($"{type} : {message}"); SereinEnv.environment?.WriteLine(type,message,@class); } diff --git a/Library/Utils/SereinIoc.cs b/Library/Utils/SereinIoc.cs index dc06597..92629ca 100644 --- a/Library/Utils/SereinIoc.cs +++ b/Library/Utils/SereinIoc.cs @@ -194,16 +194,15 @@ namespace Serein.Library.Utils /// /// public T InjectDependenciesProperty(T instance) - { + { var type = instance.GetType(); - var properties = type.GetType() - .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).ToArray() - .Where(p => p.CanWrite // 可写属性 - && p.GetCustomAttribute() != null // 有特性标注需要注入 - && p.GetValue(instance) == null); // 属性为空 + var propertys = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(p => p.CanWrite // 可写属性 + && p.GetCustomAttribute() is not null // 有特性标注需要注入 + && p.GetValue(instance) is null); // 属性为空 // 属性注入 - foreach (var property in properties) + foreach (var property in propertys) { var propertyType = property.PropertyType; if (_dependencies.TryGetValue(propertyType.FullName, out var dependencyInstance)) @@ -215,7 +214,7 @@ namespace Serein.Library.Utils // 字段注入 var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic ) - .Where(f => f.GetCustomAttribute() != null + .Where(f => f.GetCustomAttribute() != null && f.GetValue(instance) == null); foreach (var field in fields) diff --git a/NodeFlow/Serein.NodeFlow.csproj b/NodeFlow/Serein.NodeFlow.csproj index 7011358..c4bffb3 100644 --- a/NodeFlow/Serein.NodeFlow.csproj +++ b/NodeFlow/Serein.NodeFlow.csproj @@ -1,7 +1,7 @@  - 1.2.4 + 1.2.4.1 net8.0 enable enable diff --git a/Serein.Extend.NewtonsoftJson/NewtonsoftJsonExtend.cs b/Serein.Extend.NewtonsoftJson/NewtonsoftJsonExtend.cs index c3957c5..fcd8924 100644 --- a/Serein.Extend.NewtonsoftJson/NewtonsoftJsonExtend.cs +++ b/Serein.Extend.NewtonsoftJson/NewtonsoftJsonExtend.cs @@ -4,9 +4,25 @@ namespace Serein.Extend.NewtonsoftJson { public static class NewtonsoftJsonExtend { + /// + /// 对象转 Json 文本 + /// + /// + /// public static string ToJsonText(this object data) { return JsonHelper.Serialize(data); } + + /// + /// Json 文本转对象 + /// + /// + /// + /// + public static T ToJsonObject(this string jsonText) where T : class + { + return JsonHelper.Deserialize(jsonText); + } } } diff --git a/Serein.Extend.NewtonsoftJson/NewtonsoftJsonProvider.cs b/Serein.Extend.NewtonsoftJson/NewtonsoftJsonProvider.cs index 6273c71..c795e0f 100644 --- a/Serein.Extend.NewtonsoftJson/NewtonsoftJsonProvider.cs +++ b/Serein.Extend.NewtonsoftJson/NewtonsoftJsonProvider.cs @@ -7,11 +7,6 @@ using System.Diagnostics.CodeAnalysis; namespace Serein.Extend.NewtonsoftJson { - public enum ProviderType - { - Default = 0, - Web = 1, - } /// /// 基于Newtonsoft.Json的IJsonProvider实现 /// @@ -20,39 +15,23 @@ namespace Serein.Extend.NewtonsoftJson private JsonSerializerSettings? settings; /// - /// 基于Newtonsoft.Json的JSON门户实现 + /// 基于Newtonsoft.Json的JSON门户实现,默认首字母小写、忽略null /// public NewtonsoftJsonProvider() - { - - } - - /// - /// 基于Newtonsoft.Json的JSON门户实现 - /// - /// - public NewtonsoftJsonProvider(ProviderType jsonType) - { - settings = jsonType switch - { - ProviderType.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 }; + } + + /// + /// 使用自定义的序列化设置 + /// + /// + public NewtonsoftJsonProvider(JsonSerializerSettings settings) + { this.settings = settings; } diff --git a/Serein.Extend.NewtonsoftJson/Serein.Extend.NewtonsoftJson.csproj b/Serein.Extend.NewtonsoftJson/Serein.Extend.NewtonsoftJson.csproj index 84a8f89..a0872b7 100644 --- a/Serein.Extend.NewtonsoftJson/Serein.Extend.NewtonsoftJson.csproj +++ b/Serein.Extend.NewtonsoftJson/Serein.Extend.NewtonsoftJson.csproj @@ -8,7 +8,7 @@ ..\.\.Output 为 SereinFlow 提供的 JSON 扩展 - 1.0.1 + 1.0.2 通过 NewtonsoftJson 实现JSON门户扩展,用于解决 Serein.Proto.* 项目下需要 JSON 序列化与反序列化的场景 MIT True diff --git a/Serein.Library.MyGenerator/Serein.Library.NodeGenerator.csproj b/Serein.Library.MyGenerator/Serein.Library.NodeGenerator.csproj index e38fc83..eec9a5c 100644 --- a/Serein.Library.MyGenerator/Serein.Library.NodeGenerator.csproj +++ b/Serein.Library.MyGenerator/Serein.Library.NodeGenerator.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 1.2.4 + 1.2.4.1 false ..\.\.Output diff --git a/Serein.Proto.HttpApi/ApiHandleConfig.cs b/Serein.Proto.HttpApi/ApiHandleConfig.cs new file mode 100644 index 0000000..bfe3adb --- /dev/null +++ b/Serein.Proto.HttpApi/ApiHandleConfig.cs @@ -0,0 +1,200 @@ +using Serein.Library; +using Serein.Library.Api; +using Serein.Library.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Serein.Proto.HttpApi +{ + /// + /// api请求处理模块 + /// + public class ApiHandleConfig + { + private readonly DelegateDetails delegateDetails; + + /// + /// Post请求处理方法中,入参参数类型 + /// + public enum PostArgType + { + /// + /// 不做处理 + /// + None, + /// + /// 使用Url参数 + /// + IsUrlData, + /// + /// 使用整体的Boby参数 + /// + IsBobyData, + } + + /// + /// 添加处理配置 + /// + /// + public ApiHandleConfig(RouterInfo routerInfo) + { + + var methodInfo = routerInfo.MethodInfo; + delegateDetails = new DelegateDetails(methodInfo); + var parameterInfos = methodInfo.GetParameters(); + ParameterType = parameterInfos.Select(t => t.ParameterType).ToArray(); + ParameterName = parameterInfos.Select(t => t.Name!).ToArray(); + + PostArgTypes = parameterInfos.Select(p => + { + bool isUrlData = p.GetCustomAttribute(typeof(UrlAttribute)) != null; + bool isBobyData = p.GetCustomAttribute(typeof(BodyAttribute)) != null; + if (isBobyData) + { + return PostArgType.IsBobyData; + } + else if (isUrlData) + { + return PostArgType.IsUrlData; + } + else + { + return PostArgType.None; + } + }).ToArray(); + RouterInfo = routerInfo; + } + + /// + /// 路由信息 + /// + public RouterInfo RouterInfo { get; } + + /// + /// Post请求处理方法中,入参参数类型 + /// + public PostArgType[] PostArgTypes { get;private set; } + /// + /// 参数名称 + /// + public string[] ParameterName { get; private set; } + /// + /// 参数类型 + /// + public Type[] ParameterType { get; private set; } + + /// + /// 处理Get请求 + /// + /// + public object?[] GetArgsOfGet(Dictionary routeData) + { + object?[] args = new object[ParameterType.Length]; + for (int i = 0; i < ParameterType.Length; i++) + { + var type = ParameterType[i]; + var argName = ParameterName[i]; + if (routeData.TryGetValue(argName, out var argValue)) + { + if (type == typeof(string)) + { + args[i] = argValue; + } + else // if (type.IsValueType) + { + args[i] = JsonHelper.Deserialize(argValue, type); // JsonConvert.DeserializeObject(argValue, type); + } + } + else + { + args[i] = type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + return args; + } + + /// + /// 从 POST 获取参数 + /// + /// + /// + /// + public (bool, int, Type, Exception?) TryGetArgsOfPost(Dictionary routeData, IJsonToken jsonObject, out object?[] argData) + { + argData = null; + object?[] args = new object[ParameterType.Length]; + int i = 0; + + try + { + for (; i < ParameterType.Length; i++) + { + var type = ParameterType[i]; + var argName = ParameterName[i]; + if (PostArgTypes[i] == PostArgType.IsUrlData) + { + if (routeData.TryGetValue(argName, out var argValue)) + { + if (type == typeof(string)) + { + args[i] = argValue; + } + else // if (type.IsValueType) + { + args[i] = JsonHelper.Deserialize(argValue, type); + } + } + else + { + args[i] = type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + else if (jsonObject != null && PostArgTypes[i] == PostArgType.IsBobyData) + { + args[i] = jsonObject.ToObject(type); + } + else if (jsonObject != null) + { + if (jsonObject.TryGetValue(argName, out var jsonToken)) + { + args[i] = jsonToken.ToObject(type); + } + else + { + args[i] = type.IsValueType ? Activator.CreateInstance(type) : null; + } + + } + } + argData = args; + return (true ,-1, null, null); + } + catch (Exception ex) + { + argData = []; + return (false,i, ParameterType[i], ex); + } + } + + + /// + /// 处理请求 + /// + /// + /// + /// + public async Task HandleAsync(object instance, object?[] args) + { + var result = await delegateDetails.InvokeAsync(instance, args); + return result; + + } + + } + + + +} diff --git a/Serein.Proto.HttpApi/Attribute.cs b/Serein.Proto.HttpApi/Attribute.cs new file mode 100644 index 0000000..8fe5aec --- /dev/null +++ b/Serein.Proto.HttpApi/Attribute.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Serein.Proto.HttpApi +{ + /// + /// 表示参数为url中的数据 + /// + public class UrlAttribute : Attribute + { + + } + /// + /// 表示入参参数为整个boby的数据 + /// + /// 例如:User类型含有int id、string name字段 + /// + /// ① Add(User user) + /// 请求需要传入的json为 + /// {"user":{ + /// "id":2, + /// "name":"李志忠"}} + /// + /// ② Add([Boby]User user) + /// 请求需要传入的json为 + /// {"id":2,"name":"李志忠"} + /// + /// + public class BodyAttribute : Attribute + { + + } + /// + /// 标记该类为 Web Api 处理类 + /// + public class WebApiControllerAttribute : Attribute + { + /// + /// URL 路径 + /// + public string Url { get; } + public WebApiControllerAttribute(string url = "") + { + Url = url; + } + } + + /// + /// 方法的接口类型与附加URL + /// + [AttributeUsage(AttributeTargets.Method)] + public class WebApiAttribute : Attribute + { + /// + /// HTTP 请求类型 + /// + public ApiType ApiType; + /// + /// URL 路径 + /// + public string Url = string.Empty; + + public WebApiAttribute(ApiType http = ApiType.POST, string url = "") + { + ApiType = http; + Url = url; + } + } + + public enum ApiType + { + POST, + GET, + //PUT, + //DELETE + } +} diff --git a/Serein.Proto.HttpApi/ControllerBase.cs b/Serein.Proto.HttpApi/ControllerBase.cs new file mode 100644 index 0000000..83302e7 --- /dev/null +++ b/Serein.Proto.HttpApi/ControllerBase.cs @@ -0,0 +1,32 @@ +using System; + +namespace Serein.Proto.HttpApi +{ + /// + /// Web Api 控制器基类 + /// + public class ControllerBase + { + /// + /// 请求的url + /// + public string? Url { get; set; } + + /// + /// 请求的body数据 + /// + public string? Body { get; set; } + + /// + /// 获取日志信息 + /// + /// + /// + public string GetLog(Exception ex) + { + return "Url : " + Url + Environment.NewLine + + "Ex : " + ex + Environment.NewLine + + "Data : " + Body + Environment.NewLine; + } + } +} diff --git a/Serein.Proto.HttpApi/PathRouter.cs b/Serein.Proto.HttpApi/PathRouter.cs new file mode 100644 index 0000000..5838557 --- /dev/null +++ b/Serein.Proto.HttpApi/PathRouter.cs @@ -0,0 +1,437 @@ +using Serein.Library; +using Serein.Library.Api; +using Serein.Library.Utils; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using static Serein.Proto.HttpApi.PathRouter; +using Enum = System.Enum; +using Type = System.Type; + +namespace Serein.Proto.HttpApi +{ + /// + /// 路由接口 + /// + public interface IPathRouter + { + /// + /// 添加处理模块 + /// + /// + void AddHandle(Type controllerType); + /// + /// 获取路由信息 + /// + /// + List GetRouterInfos(); + + /// + /// 路由解析开始处理 + /// + /// + /// + Task HandleAsync(HttpListenerContext context); + } + + + public enum HandleState + { + /// + /// 默认值 + /// + None , + /// + /// 没有异常 + /// + Ok, + /// + /// 没有对应的控制器 + /// + NotHanleController, + /// + /// 没有对应的Http请求类型 + /// + NotHttpApiRequestType, + /// + /// 没有处理配置 + /// + NotHandleConfig, + /// + /// 无法实例化控制器 + /// + HanleControllerIsNull, + + /// + /// 调用参数获取错误 + /// + InvokeArgError, + + /// + /// 调用发生异常 + /// + InvokeErrored, + } + + /// + /// 调用结果 + /// + public class InvokeResult + { + /// + /// 调用状态 + /// + public HandleState State { get; set; } + /// + /// 调用正常时这里会有数据 + /// + public object? Data{ get; set; } + /// + /// 调用失败时这里可能会有异常数据 + /// + public Exception? Exception { get; set; } + + public static InvokeResult Ok(object? data) => new InvokeResult + { + Data = data, + State = HandleState.Ok, + }; + public static InvokeResult Fail(HandleState state, Exception? ex = null) => new InvokeResult + { + State = state, + Exception = ex, + }; + } + + /// + /// 路由信息 + /// + public class RouterInfo + { +#if NET6_0_OR_GREATER + /// + /// 接口类型 + /// + public ApiType ApiType { get; set; } + /// + /// 接口URL + /// + public required string Url { get; set; } + /// + /// 对应的处理方法 + /// + public required MethodInfo MethodInfo { get; set; } +#else + /// + /// 接口类型 + /// + public ApiType ApiType { get; set; } + /// + /// 接口URL + /// + public string Url { get; set; } + /// + /// 对应的处理方法 + /// + public MethodInfo MethodInfo { get; set; } +#endif + } + + + /// + /// 路由注册与解析 + /// + public class PathRouter : IPathRouter + { + private readonly ISereinIOC SereinIOC; // 用于存储路由信息 + + /// + /// 控制器实例对象的类型,每次调用都会重新实例化,[Url - ControllerType] + /// + private readonly ConcurrentDictionary _controllerTypes = new ConcurrentDictionary(); + + /// + /// 用于存储路由信息,[GET|POST - [Url - ApiHandleConfig]] + /// + private readonly ConcurrentDictionary> HandleModels = new ConcurrentDictionary>(); + + public PathRouter(ISereinIOC SereinIOC) + { + this.SereinIOC = SereinIOC; + foreach (ApiType method in Enum.GetValues(typeof(ApiType))) // 遍历 HTTP 枚举类型的所有值 + { + HandleModels.TryAdd(method.ToString(), new ConcurrentDictionary()); // 初始化每种 HTTP 方法对应的路由字典 + } + } + + /// + /// 获取路由信息 + /// + /// + public List GetRouterInfos() + { + var t = HandleModels.Select(kvp => kvp.Value.Select(kvp2 => kvp2.Value.RouterInfo)); + return t.SelectMany(x => x).ToList(); + } + + /// + /// 添加处理模块 + /// + /// + public void AddHandle(Type controllerType) + { + if (!controllerType.IsClass || controllerType.IsAbstract) return; // 如果不是类或者是抽象类,则直接返回 + + var autoHostingAttribute = controllerType.GetCustomAttribute(); + if(autoHostingAttribute is null) + { + SereinEnv.WriteLine(InfoType.WARN, $"类型 \"{controllerType}\" 需要实现 \"WebApiControllerAttribute\" 才可以作为路由控制器"); + return; + } + var methods = controllerType.GetMethods().Where(m => m.GetCustomAttribute() != null).ToArray(); + + + foreach (var method in methods) // 遍历控制器类型的所有方法 + { + var routeAttribute = method.GetCustomAttribute(); // 获取方法上的 WebAPIAttribute 自定义属性 + if (routeAttribute is null) // 如果存在 WebAPIAttribute 属性 + { + continue; + } + var url = GetRoutesUrl(autoHostingAttribute, routeAttribute, controllerType, method); + if (url is null) continue; + + SereinEnv.WriteLine(InfoType.INFO, url); + var apiType = routeAttribute.ApiType.ToString(); + + var routerInfo = new RouterInfo + { + ApiType = routeAttribute.ApiType, + Url = url, + MethodInfo = method, + }; + + var config = new ApiHandleConfig(routerInfo); + if(!HandleModels.TryGetValue(apiType, out var configs)) + { + configs = new ConcurrentDictionary(); + HandleModels[apiType] = configs; + } + configs.TryAdd(url, config); + _controllerTypes.TryAdd(url,controllerType); + } + return; + } + + /// + /// 在外部调用API后,解析路由,调用对应的方法 + /// + /// + /// + public async Task HandleAsync(HttpListenerContext context) + { + var request = context.Request; // 获取请求对象 + var httpMethod = request.HttpMethod.ToUpper(); // 获取请求的 HTTP 方法 + if (!HandleModels.TryGetValue(httpMethod, out var modules) || request.Url is null) + { + return InvokeResult.Fail(HandleState.NotHttpApiRequestType); // 没有对应HTTP请求方法的处理 + } + var template = request.Url.AbsolutePath.ToLower() ; + if (!_controllerTypes.TryGetValue(template, out var controllerType)) + { + return InvokeResult.Fail(HandleState.NotHanleController); // 没有对应的处理器 + } + if (!modules.TryGetValue(template, out var config)) + { + return InvokeResult.Fail(HandleState.NotHandleConfig); // 没有对应的处理配置 + } + + ControllerBase controllerInstance; + try + { + controllerInstance = (ControllerBase)SereinIOC.CreateObject(controllerType); + } + catch + { + return InvokeResult.Fail(HandleState.HanleControllerIsNull); // 未找到控制器实例 + } + + + var url = request.Url; // 获取请求的完整URL + var routeValues = GetUrlData(url); // 解析 URL 获取路由参数 + controllerInstance.Url = url.AbsolutePath; + + object?[] args; + switch (httpMethod) + { + case "GET": + args = config.GetArgsOfGet(routeValues); // Get请求 + break; + case "POST": + var requestBody = await ReadRequestBodyAsync(request); // 读取请求体内容 + controllerInstance.Body = requestBody; + if (!JsonHelper.TryParse(requestBody, out var requestJObject)) + { + var exTips = $"body 无法转换为 json 数据, body: {requestBody}"; + return InvokeResult.Fail(HandleState.InvokeArgError, new Exception(exTips)); + } + (var isPass, var index, var type, var ex) = config.TryGetArgsOfPost(routeValues, requestJObject, out args); + if (!isPass) + { + var exTips = $"尝试转换第{index}个入参参数时,类型 {type.FullName} 参数获取失败:{ex?.Message}"; + return InvokeResult.Fail(HandleState.InvokeArgError, new Exception(exTips)); + } + break; + default: + return InvokeResult.Fail(HandleState.NotHttpApiRequestType); + } + try + { + var invokeResult = await config.HandleAsync(controllerInstance, args); + + return InvokeResult.Ok(invokeResult); + } + catch (Exception ex) + { + return InvokeResult.Fail(HandleState.InvokeErrored, ex); + } + + } + + + /// + /// 读取Body中的消息 + /// + /// + /// + private async Task ReadRequestBodyAsync(HttpListenerRequest request) + { + using (Stream stream = request.InputStream) + { + using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) + { + return await reader.ReadToEndAsync(); + } + } + } + + /// + /// 从方法中收集路由信息,返回方法对应的url + /// + /// 类的特性 + /// 方法的特性 + /// 控制器类型 + /// 方法信息 + /// 方法对应的urk + private static string GetRoutesUrl(WebApiControllerAttribute autoHostingAttribute, WebApiAttribute webAttribute, Type controllerType, MethodInfo method) + { + string controllerName; + if (string.IsNullOrWhiteSpace(autoHostingAttribute.Url)) + { + controllerName = controllerType.Name.Replace("Controller", "").ToLower(); // 获取控制器名称并转换为小写 + } + else + { + controllerName = autoHostingAttribute.Url; + } + + var httpMethod = webAttribute.ApiType; // 获取 HTTP 方法 + var customUrl = webAttribute.Url; // 获取自定义 URL + + string url; + controllerName = CleanUrl(controllerName); + if (string.IsNullOrWhiteSpace(customUrl)) + { + url = $"/{controllerName}/{method.Name}".ToLower();// 清理自定义 URL,并构建新的 URL + } + else + { + if(customUrl == "/") + { + url = $"/{controllerName}".ToLower(); // 使用控制器 + } + else + { + + customUrl = CleanUrl(customUrl); + url = $"/{controllerName}/{customUrl}".ToLower();// 清理自定义 URL,并构建新的 URL + } + } + return url; + + } + + /// + /// 修正方法特性中的URL格式 + /// + /// + /// + private static string CleanUrl(string url) + { + + while (url.Length > 0 && url[0] == '/') // 去除开头的斜杠 + { + url = url.Substring(1); + } + + while (url.Length > 0 && url[url.Length - 1] == '/') // 去除末尾的斜杠 + { + url = url.Substring(0, url.Length - 1); + } + + for (int i = 0; i < url.Length - 1; i++) // 去除连续的斜杠 + { + if (url[i] == '/' && url[i + 1] == '/') + { + url = url.Remove(i, 1); + i--; + } + } + + return url; // 返回清理后的 URL + } + + /// + /// 方法声明,用于解析 URL 获取路由参数 + /// + /// + /// + private Dictionary GetUrlData(Uri uri) + { + Dictionary routeValues = new Dictionary(); + + var pathParts = uri.ToString().Split('?'); // 拆分 URL,获取路径部分 + + if (pathParts.Length > 1) // 如果包含查询字符串 + { + //var queryParams = HttpUtility.ParseQueryString(pathParts[1]); // 解析查询字符串 + //foreach (string key in queryParams) // 遍历查询字符串的键值对 + //{ + // if (key == null) continue; + // routeValues[key] = queryParams[key]; // 将键值对添加到路由参数字典中 + //} + var parsedQuery = QueryStringParser.ParseQueryString(pathParts[1]); + foreach (var kvp in parsedQuery) + { + //Console.WriteLine($"{kvp.Key}: {kvp.Value}"); + routeValues[kvp.Key.ToLower()] = kvp.Value; // 将键值对添加到路由参数字典中 + } + } + + return routeValues; // 返回路由参数字典 + } + + + } + + + +} + diff --git a/Serein.Proto.HttpApi/QueryStringParser.cs b/Serein.Proto.HttpApi/QueryStringParser.cs new file mode 100644 index 0000000..d80e055 --- /dev/null +++ b/Serein.Proto.HttpApi/QueryStringParser.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Serein.Proto.HttpApi +{ + internal class QueryStringParser + { + public static Dictionary ParseQueryString(string query) + { + var result = new Dictionary(); + + if (string.IsNullOrEmpty(query)) + return result; + + // 如果字符串以'?'开头,移除它 + if (query.StartsWith("?")) + query = query.Substring(1); + + // 拆分键值对 + var pairs = query.Split('&'); + foreach (var pair in pairs) + { + // 忽略空的键值对 + if (string.IsNullOrEmpty(pair)) continue; + + // 用等号分隔键和值 + var keyValue = pair.Split(new[] { '=' }, 2); + + var key = Uri.UnescapeDataString(keyValue[0]); // 解码键 + var value = keyValue.Length > 1 ? Uri.UnescapeDataString(keyValue[1]) : string.Empty; // 解码值 + + result[key] = value; // 添加到字典中 + } + + return result; + } + } +} diff --git a/Serein.Proto.HttpApi/RequestLimiter.cs b/Serein.Proto.HttpApi/RequestLimiter.cs new file mode 100644 index 0000000..92b6499 --- /dev/null +++ b/Serein.Proto.HttpApi/RequestLimiter.cs @@ -0,0 +1,59 @@ +using System.Collections.Concurrent; +using System.Net; + +namespace Serein.Proto.HttpApi +{ + /// + /// 判断访问接口的频次是否正常 + /// + public class RequestLimiter + { + private readonly ConcurrentDictionary> requestHistory = new ConcurrentDictionary>(); + private readonly TimeSpan interval; + private readonly int maxRequests; + + public RequestLimiter(int seconds, int maxRequests) + { + interval = TimeSpan.FromSeconds(seconds); + this.maxRequests = maxRequests; + } + + /// + /// 判断访问接口的频次是否正常 + /// + /// + public bool AllowRequest(HttpListenerRequest request) + { + var clientIp = request.RemoteEndPoint.Address.ToString(); + var clientPort = request.RemoteEndPoint.Port; + var clientKey = clientIp + ":" + clientPort; + + var now = DateTime.Now; + + // 尝试从字典中获取请求队列,不存在则创建新的队列 + var requests = requestHistory.GetOrAdd(clientKey, new Queue()); + + lock (requests) + { + // 移除超出时间间隔的请求记录 + while (requests.Count > 0 && now - requests.Peek() > interval) + { + requests.Dequeue(); + } + + // 如果请求数超过限制,拒绝请求 + if (requests.Count >= maxRequests) + { + return false; + } + + // 添加当前请求时间,并允许请求 + requests.Enqueue(now); + } + + return true; + + } + + } +} diff --git a/Serein.Proto.HttpApi/Serein.Proto.HttpApi.csproj b/Serein.Proto.HttpApi/Serein.Proto.HttpApi.csproj new file mode 100644 index 0000000..3959e18 --- /dev/null +++ b/Serein.Proto.HttpApi/Serein.Proto.HttpApi.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net462 + enable + latest + enable + ..\.\.Output + + 基于Json数据载体的HttpApi交互工具包 + 0.0.9 + 基于Json数据载体的HttpApi交互工具包 + MIT + True + True + true + + + + + + + diff --git a/Serein.Proto.HttpApi/SereinWebApiService.cs b/Serein.Proto.HttpApi/SereinWebApiService.cs new file mode 100644 index 0000000..aeddd33 --- /dev/null +++ b/Serein.Proto.HttpApi/SereinWebApiService.cs @@ -0,0 +1,121 @@ +using Serein.Library; +using Serein.Library.Utils; +using System.Net; +using System.Text; + +namespace Serein.Proto.HttpApi +{ + + /// + /// 对于 HttpListenerContext 的拓展服务 + /// + public class SereinWebApiService + { + private readonly IPathRouter _pathRouter; + //private RequestLimiter? requestLimiter; + + /// + /// 初始化处理器 + /// + /// + public SereinWebApiService(IPathRouter pathRouter) + { + _pathRouter = pathRouter; + } + + /// + /// 添加处理模块 + /// + /// + public void AddHandleModule() where T : ControllerBase + { + _pathRouter.AddHandle(typeof(T)); + } + + /// + /// 添加处理模块 + /// + /// + public void AddHandleModule(Type type) + { + _pathRouter.AddHandle(type); + } + + /// + /// 获取路由信息 + /// + /// + public List GetRouterInfos() + { + return _pathRouter.GetRouterInfos(); + } + + private Func OnBeforeReplying; + + /// + /// 设置回复前的处理函数 + /// + /// + public void SetOnBeforeReplying(Func func) + { + OnBeforeReplying = func; + } + + /// + /// 处理请求 + /// + /// + /// + public async Task HandleAsync(HttpListenerContext context) + { + // 获取远程终结点信息 + var remoteEndPoint = context.Request.RemoteEndPoint; + // 获取用户的IP地址和端口 + IPAddress ipAddress = remoteEndPoint.Address; + int port = remoteEndPoint.Port; + SereinEnv.WriteLine(InfoType.INFO, "外部连接:" + ipAddress.ToString() + ":" + port); + + // 添加CORS头部 + context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); + context.Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + context.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type"); + + // 处理OPTIONS预检请求 + if (context.Request.HttpMethod == "OPTIONS") + { + context.Response.StatusCode = (int)HttpStatusCode.OK; + context.Response.Close(); + return; + } + /*if (requestLimiter is not null) + { + if (!requestLimiter.AllowRequest(context.Request)) + { + SereinEnv.WriteLine(InfoType.INFO, "接口超时"); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; // 返回 404 错误 + } + }*/ + + var invokeResult = await _pathRouter.HandleAsync(context); // 路由解析 + (var result, var code) = (OnBeforeReplying is not null) switch + { + true => OnBeforeReplying.Invoke(invokeResult), + false => (invokeResult.Data, HttpStatusCode.OK), + }; + var json = (result is not null) switch + { + true => JsonHelper.Serialize(result), + false => string.Empty + }; + byte[] buffer = Encoding.UTF8.GetBytes(json); + var response = context.Response; // 获取响应对象 + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + context.Response.StatusCode = (int)code; // 返回 200 成功 + context.Response.Close(); // 关闭响应 + + + + } + } +} diff --git a/Serein.Proto.HttpApi/WebApiServer.cs b/Serein.Proto.HttpApi/WebApiServer.cs new file mode 100644 index 0000000..23a4ddb --- /dev/null +++ b/Serein.Proto.HttpApi/WebApiServer.cs @@ -0,0 +1,17 @@ + +using Serein.Library; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Serein.Proto.HttpApi +{ + + /// + /// HTTP接口监听类 + /// + public partial class WebApiServer + { + } +} diff --git a/Serein.Proto.Modbus/Serein.Proto.Modbus.csproj b/Serein.Proto.Modbus/Serein.Proto.Modbus.csproj index be2fdbc..1e1e2f4 100644 --- a/Serein.Proto.Modbus/Serein.Proto.Modbus.csproj +++ b/Serein.Proto.Modbus/Serein.Proto.Modbus.csproj @@ -1,23 +1,26 @@  - net8.0; + net8.0 + enable latest enable ..\.\.Output 全异步Modbus协议客户端工具包 - 1.0.0 + 1.0.1 提供TCP、UDP、RTU三种方式客户端;创建方式:IModbusClient client = ModbusClientFactory.Create...; MIT True True - + true + + diff --git a/Serein.Proto.WebSocket/Handle/WebSocketHandleModule.cs b/Serein.Proto.WebSocket/Handle/WebSocketHandleModule.cs index c579b2d..47f330c 100644 --- a/Serein.Proto.WebSocket/Handle/WebSocketHandleModule.cs +++ b/Serein.Proto.WebSocket/Handle/WebSocketHandleModule.cs @@ -1,11 +1,6 @@ -using Serein.Library; -using Serein.Library.Api; +using Serein.Library.Api; using Serein.Library.Utils; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Text.Json.Nodes; -using System.Threading.Tasks; namespace Serein.Proto.WebSocket.Handle { diff --git a/Serein.Proto.WebSocket/ISereinWebSocketService.cs b/Serein.Proto.WebSocket/ISereinWebSocketService.cs index cef7bca..7b0894d 100644 --- a/Serein.Proto.WebSocket/ISereinWebSocketService.cs +++ b/Serein.Proto.WebSocket/ISereinWebSocketService.cs @@ -24,6 +24,13 @@ namespace Serein.Proto.WebSocket /// ISereinWebSocketService AddHandleModule() where T : ISocketHandleModule, new(); + /// + /// 添加处理模块 + /// + /// 接口实例 + /// + ISereinWebSocketService AddHandleModule(ISocketHandleModule socketHandleModule); + /// /// 添加处理模块 /// diff --git a/Serein.Proto.WebSocket/Serein.Proto.WebSocket.csproj b/Serein.Proto.WebSocket/Serein.Proto.WebSocket.csproj index 7c67d79..28fbfb5 100644 --- a/Serein.Proto.WebSocket/Serein.Proto.WebSocket.csproj +++ b/Serein.Proto.WebSocket/Serein.Proto.WebSocket.csproj @@ -1,19 +1,19 @@  - net8.0; + net8.0;net462 enable latest enable ..\.\.Output 基于Json数据载体的WebSocket交互工具包 - 1.0.1 + 1.0.2 基于Json数据载体的WebSocket交互工具包 MIT True True - + true diff --git a/Serein.Proto.WebSocket/SereinWebSocketService.cs b/Serein.Proto.WebSocket/SereinWebSocketService.cs index 4999ed0..7e6ff52 100644 --- a/Serein.Proto.WebSocket/SereinWebSocketService.cs +++ b/Serein.Proto.WebSocket/SereinWebSocketService.cs @@ -2,19 +2,10 @@ using Serein.Library.Utils; using Serein.Proto.WebSocket.Attributes; using Serein.Proto.WebSocket.Handle; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Data; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net.Sockets; using System.Net.WebSockets; -using System.Reactive; using System.Reflection; using System.Text; -using System.Text.Json; -using System.Threading.Tasks; using NetWebSocket = System.Net.WebSockets.WebSocket; namespace Serein.Proto.WebSocket @@ -73,6 +64,19 @@ namespace Serein.Proto.WebSocket var type = typeof(T); Func instanceFactory = () => (T)Activator.CreateInstance(type); return AddHandleModule(type, instanceFactory); + } + + /// + /// 添加处理模块,使用指定的实例工厂和异常追踪回调 + /// + /// + /// + /// + public ISereinWebSocketService AddHandleModule(ISocketHandleModule socketHandleModule) + { + var type = socketHandleModule.GetType(); + Func instanceFactory = () => socketHandleModule; + return AddHandleModule(type, instanceFactory); } /// @@ -380,11 +384,11 @@ namespace Serein.Proto.WebSocket /// public async Task PushDataAsync(object latestData) { - var options = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - var json = JsonSerializer.Serialize(latestData, options); + //var options = new JsonSerializerOptions + //{ + // PropertyNamingPolicy = JsonNamingPolicy.CamelCase + //}; + var json = JsonHelper.Serialize(latestData); var buffer = Encoding.UTF8.GetBytes(json); var segment = new ArraySegment(buffer); diff --git a/Serein.Proto.WebSocket/TestClass.cs b/Serein.Proto.WebSocket/TestClass.cs deleted file mode 100644 index 34a99a6..0000000 --- a/Serein.Proto.WebSocket/TestClass.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Serein.Proto.WebSocket -{ - - public class ClassA : ISocketHandleModule - { - - } - public class ClassB : ISocketHandleModule - { - - } - public class ClassC : ISocketHandleModule - { - - } - internal class TestClass - { - public void Run() - { - SereinWebSocketService sereinWebSocketService = new SereinWebSocketService(); - sereinWebSocketService.AddHandleModule(); - sereinWebSocketService.AddHandleModule(() => new ClassB()); - } - - - } -} diff --git a/Serein.Proto.WebSocket/WebSocketHandleContext.cs b/Serein.Proto.WebSocket/WebSocketHandleContext.cs index 5ab2d35..1c0ea60 100644 --- a/Serein.Proto.WebSocket/WebSocketHandleContext.cs +++ b/Serein.Proto.WebSocket/WebSocketHandleContext.cs @@ -1,10 +1,6 @@ using Serein.Library.Api; using Serein.Library.Utils; using Serein.Proto.WebSocket.Handle; -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Serein.Proto.WebSocket { @@ -110,7 +106,8 @@ namespace Serein.Proto.WebSocket /// /// /// - public bool TryGetTag([NotNullWhen(true)] out T? tag) + //public bool TryGetTag([NotNullWhen(true)] out T? tag) + public bool TryGetTag(out T? tag) { lock (_wsTagLockObj) { diff --git a/Serein.Script/Serein.Script.csproj b/Serein.Script/Serein.Script.csproj index c4c2ac7..f0d2387 100644 --- a/Serein.Script/Serein.Script.csproj +++ b/Serein.Script/Serein.Script.csproj @@ -14,6 +14,7 @@ https://github.com/fhhyyp/serein-flow True True + true diff --git a/SereinFlow.sln b/SereinFlow.sln index cd1e0fa..8e92a96 100644 --- a/SereinFlow.sln +++ b/SereinFlow.sln @@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Proto.WebSocket", "S EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Proto.Modbus", "Serein.Proto.Modbus\Serein.Proto.Modbus.csproj", "{EF9E51C0-CDDB-4B02-A304-87FFC31E61E0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serein.Proto.HttpApi", "Serein.Proto.HttpApi\Serein.Proto.HttpApi.csproj", "{281B8E55-B9CD-4FE5-A72F-59CBB968D844}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,6 +76,10 @@ Global {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 + {281B8E55-B9CD-4FE5-A72F-59CBB968D844}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {281B8E55-B9CD-4FE5-A72F-59CBB968D844}.Debug|Any CPU.Build.0 = Debug|Any CPU + {281B8E55-B9CD-4FE5-A72F-59CBB968D844}.Release|Any CPU.ActiveCfg = Release|Any CPU + {281B8E55-B9CD-4FE5-A72F-59CBB968D844}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Workbench/Serein.WorkBench.csproj b/Workbench/Serein.WorkBench.csproj index 0887490..21e22fc 100644 --- a/Workbench/Serein.WorkBench.csproj +++ b/Workbench/Serein.WorkBench.csproj @@ -69,7 +69,6 @@ -