mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-11-01 16:13:59 +08:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74a47a1983 | ||
|
|
a48a42abe4 | ||
|
|
feb1d0a3c5 |
@@ -33,22 +33,22 @@ public static class CacheConst
|
||||
/// <summary>
|
||||
/// 资源表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:";
|
||||
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:List";
|
||||
|
||||
/// <summary>
|
||||
/// 角色表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:";
|
||||
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:List";
|
||||
|
||||
/// <summary>
|
||||
/// 用户表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:";
|
||||
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:Hash";
|
||||
|
||||
/// <summary>
|
||||
/// 用户账号关系缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:";
|
||||
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:Hash";
|
||||
|
||||
/// <summary>
|
||||
/// 职位表缓存Key
|
||||
@@ -58,7 +58,7 @@ public static class CacheConst
|
||||
/// <summary>
|
||||
/// 机构表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:";
|
||||
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:List";
|
||||
|
||||
/// <summary>
|
||||
/// 公司表缓存Key
|
||||
@@ -67,12 +67,12 @@ public static class CacheConst
|
||||
/// <summary>
|
||||
/// 公司表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:";
|
||||
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:Hash";
|
||||
|
||||
/// <summary>
|
||||
/// Token表缓存Key
|
||||
/// </summary>
|
||||
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:";
|
||||
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:Hash";
|
||||
|
||||
#region 登录错误次数
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ public class HardwareJob : IJob, IHardwareJob
|
||||
|
||||
#endregion 属性
|
||||
|
||||
private MemoryCache MemoryCache = new() { };
|
||||
private const string CacheKey = "HistoryHardwareInfo";
|
||||
private ICache MemoryCache => App.CacheService;
|
||||
private const string CacheKey = $"{CacheConst.Cache_HardwareInfo}HistoryHardwareInfo";
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
|
||||
{
|
||||
@@ -81,8 +81,7 @@ public class HardwareJob : IJob, IHardwareJob
|
||||
{
|
||||
if (HardwareInfo.MachineInfo == null)
|
||||
{
|
||||
MachineInfo.Register();
|
||||
HardwareInfo.MachineInfo = MachineInfo.Current;
|
||||
HardwareInfo.MachineInfo = MachineInfo.GetCurrent();
|
||||
|
||||
string currentPath = Directory.GetCurrentDirectory();
|
||||
DriveInfo drive = new(Path.GetPathRoot(currentPath));
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.DictionaryExtensions;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -12,8 +12,6 @@ using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using System.Globalization;
|
||||
|
||||
using ThingsGateway.FriendlyException;
|
||||
using ThingsGateway.SqlSugar;
|
||||
|
||||
@@ -23,7 +21,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
|
||||
{
|
||||
private readonly IRelationService _relationService;
|
||||
|
||||
private string CacheKey = $"{CacheConst.Cache_SysResource}-{CultureInfo.CurrentUICulture.Name}";
|
||||
private string CacheKey = $"{CacheConst.Cache_SysResource}";
|
||||
|
||||
public SysResourceService(IRelationService relationService)
|
||||
{
|
||||
@@ -32,7 +30,6 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
|
||||
|
||||
#region 增删改查
|
||||
|
||||
|
||||
[OperDesc("CopyResource")]
|
||||
public async Task CopyAsync(IEnumerable<long> ids, long moduleId)
|
||||
{
|
||||
@@ -143,12 +140,12 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
|
||||
/// <returns>全部资源列表</returns>
|
||||
public async Task<List<SysResource>> GetAllAsync()
|
||||
{
|
||||
var sysResources = App.CacheService.Get<List<SysResource>>(CacheKey);
|
||||
var sysResources = App.CacheService.Get<List<SysResource>>(CacheConst.Cache_SysResource);
|
||||
if (sysResources == null)
|
||||
{
|
||||
using var db = GetDB();
|
||||
sysResources = await db.Queryable<SysResource>().ToListAsync().ConfigureAwait(false);
|
||||
App.CacheService.Set(CacheKey, sysResources);
|
||||
App.CacheService.Set(CacheConst.Cache_SysResource, sysResources);
|
||||
}
|
||||
return sysResources;
|
||||
}
|
||||
@@ -258,7 +255,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
|
||||
/// </summary>
|
||||
public void RefreshCache()
|
||||
{
|
||||
App.CacheService.Remove(CacheKey);
|
||||
App.CacheService.Remove(CacheConst.Cache_SysResource);
|
||||
//删除超级管理员的缓存
|
||||
App.RootServices.GetRequiredService<ISysUserService>().DeleteUserFromCache(RoleConst.SuperAdminId);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.UnifyResult;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
@@ -65,11 +67,76 @@ public class Startup : AppStartup
|
||||
|
||||
services.AddSingleton(typeof(IEventService<>), typeof(EventService<>));
|
||||
|
||||
|
||||
#region 控制台美化
|
||||
|
||||
services.AddConsoleFormatter(options =>
|
||||
{
|
||||
options.WriteFilter = (logMsg) =>
|
||||
{
|
||||
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
|
||||
if (string.IsNullOrEmpty(logMsg.Message)) return false;
|
||||
else return true;
|
||||
};
|
||||
|
||||
options.MessageFormat = (logMsg) =>
|
||||
{
|
||||
//如果不是LoggingMonitor日志才格式化
|
||||
if (logMsg.LogName != "System.Logging.LoggingMonitor")
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
|
||||
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
|
||||
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
|
||||
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
|
||||
if (logMsg.Exception != null)
|
||||
{
|
||||
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return logMsg.Message;
|
||||
}
|
||||
};
|
||||
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
|
||||
{
|
||||
ConsoleColor consoleColor = ConsoleColor.White;
|
||||
switch (logMsg.LogLevel)
|
||||
{
|
||||
case LogLevel.Information:
|
||||
consoleColor = ConsoleColor.DarkGreen;
|
||||
break;
|
||||
|
||||
case LogLevel.Warning:
|
||||
consoleColor = ConsoleColor.DarkYellow;
|
||||
break;
|
||||
|
||||
case LogLevel.Error:
|
||||
consoleColor = ConsoleColor.DarkRed;
|
||||
break;
|
||||
}
|
||||
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
|
||||
};
|
||||
});
|
||||
|
||||
#endregion 控制台美化
|
||||
//日志写入数据库配置
|
||||
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
|
||||
{
|
||||
options.NameFilter = (name) =>
|
||||
{
|
||||
return (
|
||||
name == "System.Logging.RequestAudit"
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public void Use(IServiceProvider serviceProvider)
|
||||
{
|
||||
XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
|
||||
NewLife.Log.XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
|
||||
|
||||
//检查ConfigId
|
||||
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace ThingsGateway.Admin.Application
|
||||
Settings = new UserAgentSettings();
|
||||
}
|
||||
|
||||
private MemoryCache MemoryCache { get; set; } = new();
|
||||
private ICache MemoryCache => App.CacheService;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the specified user agent string.
|
||||
|
||||
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal file
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Cache": {
|
||||
"CacheType": "Memory", // 可选:Memory 或 Redis
|
||||
|
||||
"MemoryCacheOptions": {
|
||||
"Expire": 3600,
|
||||
"Capacity": 100000,
|
||||
"Period": 60
|
||||
},
|
||||
|
||||
"RedisCacheOptions": {
|
||||
"InstanceName": "ThingsGateway",
|
||||
"Configuration": "server=127.0.0.1:6379;password=123456;db=3;timeout=3000",
|
||||
"Server": "127.0.0.1:6379",
|
||||
"Db": 3,
|
||||
"UserName": "",
|
||||
"Password": "123456",
|
||||
"Timeout": 3000,
|
||||
"Prefix": "ThingsGateway:"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,11 @@ using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Unicode;
|
||||
|
||||
using ThingsGateway.Admin.Application;
|
||||
using ThingsGateway.Admin.Razor;
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.VirtualFileServer;
|
||||
|
||||
namespace ThingsGateway.AdminServer;
|
||||
@@ -61,8 +58,6 @@ public class Startup : AppStartup
|
||||
options.AddPersistence<JobPersistence>();
|
||||
});
|
||||
|
||||
// 缓存
|
||||
services.AddSingleton<ICache, MemoryCache>();
|
||||
|
||||
// 允许跨域
|
||||
services.AddCorsAccessor();
|
||||
@@ -153,101 +148,9 @@ public class Startup : AppStartup
|
||||
|
||||
|
||||
|
||||
#region 控制台美化
|
||||
|
||||
services.AddConsoleFormatter(options =>
|
||||
{
|
||||
options.WriteFilter = (logMsg) =>
|
||||
{
|
||||
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
|
||||
if (string.IsNullOrEmpty(logMsg.Message)) return false;
|
||||
else return true;
|
||||
};
|
||||
|
||||
options.MessageFormat = (logMsg) =>
|
||||
{
|
||||
//如果不是LoggingMonitor日志才格式化
|
||||
if (logMsg.LogName != "System.Logging.LoggingMonitor")
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
|
||||
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
|
||||
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
|
||||
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
|
||||
if (logMsg.Exception != null)
|
||||
{
|
||||
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
|
||||
}
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
return logMsg.Message;
|
||||
}
|
||||
};
|
||||
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
|
||||
{
|
||||
ConsoleColor consoleColor = ConsoleColor.White;
|
||||
switch (logMsg.LogLevel)
|
||||
{
|
||||
case LogLevel.Information:
|
||||
consoleColor = ConsoleColor.DarkGreen;
|
||||
break;
|
||||
|
||||
case LogLevel.Warning:
|
||||
consoleColor = ConsoleColor.DarkYellow;
|
||||
break;
|
||||
|
||||
case LogLevel.Error:
|
||||
consoleColor = ConsoleColor.DarkRed;
|
||||
break;
|
||||
}
|
||||
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
|
||||
};
|
||||
});
|
||||
|
||||
#endregion 控制台美化
|
||||
|
||||
#region api日志
|
||||
|
||||
//Monitor日志配置
|
||||
//services.AddMonitorLogging(options =>
|
||||
//{
|
||||
// options.JsonIndented = true;// 是否美化 JSON
|
||||
// options.GlobalEnabled = false;//全局启用
|
||||
// options.ConfigureLogger((logger, logContext, context) =>
|
||||
// {
|
||||
// var httpContext = context.HttpContext;//获取httpContext
|
||||
|
||||
// //获取客户端信息
|
||||
// var client = App.GetService<IAppService>().UserAgent;
|
||||
// // 获取控制器/操作描述器
|
||||
// var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
|
||||
// //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
|
||||
// var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
|
||||
|
||||
// var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
|
||||
// //获取特性
|
||||
// option = desc.Value;//则将操作名称赋值为控制器上写的title
|
||||
|
||||
// logContext.Set(LoggingConst.CateGory, option);//传操作名称
|
||||
// logContext.Set(LoggingConst.Operation, option);//传操作名称
|
||||
// logContext.Set(LoggingConst.Client, client);//客户端信息
|
||||
// logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
|
||||
// logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
|
||||
// });
|
||||
//});
|
||||
|
||||
//日志写入数据库配置
|
||||
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
|
||||
{
|
||||
options.WriteFilter = (logMsg) =>
|
||||
{
|
||||
return logMsg.LogName == "System.Logging.RequestAudit";
|
||||
};
|
||||
});
|
||||
|
||||
#endregion api日志
|
||||
|
||||
//已添加AddOptions
|
||||
// 增加多语言支持配置信息
|
||||
@@ -302,7 +205,7 @@ public class Startup : AppStartup
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("keys"))
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
|
||||
@@ -15,6 +15,8 @@ using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway;
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Redis.Extensions;
|
||||
using ThingsGateway.UnifyResult;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
@@ -198,11 +200,44 @@ public static class AppServiceCollectionExtensions
|
||||
{
|
||||
// 注册全局配置选项
|
||||
services.AddConfigurableOptions<AppSettingsOptions>();
|
||||
services.AddConfigurableOptions<CacheOptions>();
|
||||
|
||||
// 注册内存和分布式内存
|
||||
services.AddMemoryCache();
|
||||
services.AddDistributedMemoryCache();
|
||||
|
||||
var cacheOptions = App.GetConfig<CacheOptions>("Cache", true);
|
||||
// 缓存
|
||||
if (cacheOptions.CacheType == CacheType.Memory)
|
||||
{
|
||||
services.AddSingleton<ICache, MemoryCache>(a => new()
|
||||
{
|
||||
Capacity = cacheOptions.MemoryCacheOptions.Capacity,
|
||||
Expire = cacheOptions.MemoryCacheOptions.Expire,
|
||||
Period = cacheOptions.MemoryCacheOptions.Period
|
||||
});
|
||||
|
||||
}
|
||||
else if (cacheOptions.CacheType == CacheType.Redis)
|
||||
{
|
||||
services.AddDistributedRedisCache(options =>
|
||||
{
|
||||
options.Db = cacheOptions.RedisCacheOptions.Db;
|
||||
options.Configuration = cacheOptions.RedisCacheOptions.Configuration;
|
||||
options.UserName = cacheOptions.RedisCacheOptions.UserName;
|
||||
options.Password = cacheOptions.RedisCacheOptions.Password;
|
||||
options.Server = cacheOptions.RedisCacheOptions.Server;
|
||||
options.Timeout = cacheOptions.RedisCacheOptions.Timeout;
|
||||
options.Prefix = cacheOptions.RedisCacheOptions.Prefix;
|
||||
options.InstanceName = cacheOptions.RedisCacheOptions.InstanceName;
|
||||
options.Expire = cacheOptions.RedisCacheOptions.Expire;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// 注册全局依赖注入
|
||||
services.AddDependencyInjection();
|
||||
|
||||
|
||||
@@ -151,6 +151,8 @@ internal static class InternalApp
|
||||
// 存储服务提供器
|
||||
InternalServices = hostApplicationBuilder.Services;
|
||||
|
||||
|
||||
|
||||
// 存储根服务
|
||||
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();
|
||||
|
||||
|
||||
@@ -93,6 +93,10 @@ public sealed class DatabaseLogger : ILogger, IDisposable
|
||||
{
|
||||
// 判断日志级别是否有效
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
if (_options.NameFilter?.Invoke(_logName) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查日志格式化器
|
||||
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
|
||||
|
||||
@@ -51,7 +51,10 @@ public sealed class DatabaseLoggerOptions
|
||||
/// 是否使用 UTC 时间戳,默认 false
|
||||
/// </summary>
|
||||
public bool UseUtcTimestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称筛选
|
||||
/// </summary>
|
||||
public Func<string, bool> NameFilter { get; set; }
|
||||
/// <summary>
|
||||
/// 日期格式化
|
||||
/// </summary>
|
||||
|
||||
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal file
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
// ------------------------------------------------------------------------
|
||||
// 版权信息
|
||||
// 版权归百小僧及百签科技(广东)有限公司所有。
|
||||
// 所有权利保留。
|
||||
// 官方网站:https://baiqian.com
|
||||
//
|
||||
// 许可证信息
|
||||
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
|
||||
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
using ThingsGateway.ConfigurableOptions;
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
|
||||
namespace ThingsGateway;
|
||||
|
||||
public enum CacheType
|
||||
{
|
||||
/// <summary>
|
||||
/// 内存缓存
|
||||
/// </summary>
|
||||
Memory,
|
||||
/// <summary>
|
||||
/// Redis 缓存
|
||||
/// </summary>
|
||||
Redis
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用全局配置
|
||||
/// </summary>
|
||||
public sealed class CacheOptions : IConfigurableOptions<CacheOptions>
|
||||
{
|
||||
public CacheType CacheType { get; set; }
|
||||
|
||||
public MemoryCacheOptions MemoryCacheOptions { get; set; } = new MemoryCacheOptions();
|
||||
|
||||
public RedisCacheOptions RedisCacheOptions { get; set; } = new RedisCacheOptions();
|
||||
|
||||
/// <summary>
|
||||
/// 后期配置
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="configuration"></param>
|
||||
public void PostConfigure(CacheOptions options, IConfiguration configuration)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
public class MemoryCacheOptions
|
||||
{
|
||||
/// <summary>默认过期时间。避免Set操作时没有设置过期时间,默认3600秒</summary>
|
||||
public Int32 Expire { get; set; } = 3600;
|
||||
/// <summary>容量。容量超标时,采用LRU机制删除,默认100_000</summary>
|
||||
public Int32 Capacity { get; set; } = 100_000;
|
||||
|
||||
/// <summary>定时清理时间,默认60秒</summary>
|
||||
public Int32 Period { get; set; } = 60;
|
||||
}
|
||||
|
||||
public class RedisCacheOptions : RedisOptions
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Caching.Services;
|
||||
using ThingsGateway.NewLife.Configuration;
|
||||
using ThingsGateway.NewLife.Extension;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// DependencyInjectionExtensions
|
||||
/// </summary>
|
||||
public static class DependencyInjectionExtensions
|
||||
{
|
||||
/// <summary>注入FullRedis,应用内可使用FullRedis/Redis/ICache/ICacheProvider</summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="redis"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddRedis(this IServiceCollection services, FullRedis? redis = null)
|
||||
{
|
||||
//if (redis == null) throw new ArgumentNullException(nameof(redis));
|
||||
|
||||
if (redis == null) return services.AddRedisCacheProvider();
|
||||
|
||||
services.AddBasic();
|
||||
services.TryAddSingleton<ICache>(redis);
|
||||
services.AddSingleton<Redis>(redis);
|
||||
services.AddSingleton(redis);
|
||||
|
||||
// 注册Redis缓存服务
|
||||
services.TryAddSingleton<ICacheProvider>(p =>
|
||||
{
|
||||
var provider = new RedisCacheProvider(p);
|
||||
if (provider.Cache is not Redis) provider.Cache = redis;
|
||||
provider.RedisQueue ??= redis;
|
||||
|
||||
return provider;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="tracer"></param>
|
||||
/// <returns></returns>
|
||||
public static FullRedis AddRedis(this IServiceCollection services, String config, ITracer tracer = null!)
|
||||
{
|
||||
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var redis = new FullRedis();
|
||||
redis.Init(config);
|
||||
redis.Tracer = tracer;
|
||||
|
||||
services.AddRedis(redis);
|
||||
|
||||
return redis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="tracer"></param>
|
||||
/// <returns></returns>
|
||||
public static FullRedis AddRedis(this IServiceCollection services, String name, String config, Int32 timeout = 0, ITracer tracer = null!)
|
||||
{
|
||||
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
var redis = new FullRedis();
|
||||
if (!name.IsNullOrEmpty()) redis.Name = name;
|
||||
redis.Init(config);
|
||||
if (timeout > 0) redis.Timeout = timeout;
|
||||
redis.Tracer = tracer;
|
||||
|
||||
services.AddRedis(redis);
|
||||
|
||||
return redis;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="server"></param>
|
||||
/// <param name="psssword"></param>
|
||||
/// <param name="db"></param>
|
||||
/// <param name="timeout"></param>
|
||||
/// <param name="tracer"></param>
|
||||
/// <returns></returns>
|
||||
public static FullRedis AddRedis(this IServiceCollection services, String server, String psssword, Int32 db, Int32 timeout = 0, ITracer tracer = null!)
|
||||
{
|
||||
if (String.IsNullOrEmpty(server)) throw new ArgumentNullException(nameof(server));
|
||||
|
||||
var redis = new FullRedis(server, psssword, db);
|
||||
if (timeout > 0) redis.Timeout = timeout;
|
||||
redis.Tracer = tracer;
|
||||
|
||||
services.AddRedis(redis);
|
||||
|
||||
return redis;
|
||||
}
|
||||
|
||||
/// <summary>添加Redis缓存</summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="setupAction"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static IServiceCollection AddRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
if (setupAction == null)
|
||||
throw new ArgumentNullException(nameof(setupAction));
|
||||
|
||||
services.AddBasic();
|
||||
services.AddOptions();
|
||||
services.Configure(setupAction);
|
||||
//services.Add(ServiceDescriptor.Singleton<ICache, FullRedis>());
|
||||
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
|
||||
services.TryAddSingleton<ICache>(p => p.GetRequiredService<FullRedis>());
|
||||
services.TryAddSingleton<Redis>(p => p.GetRequiredService<FullRedis>());
|
||||
|
||||
// 注册Redis缓存服务
|
||||
services.TryAddSingleton<ICacheProvider>(p =>
|
||||
{
|
||||
var redis = p.GetRequiredService<FullRedis>();
|
||||
var provider = new RedisCacheProvider(p);
|
||||
if (provider.Cache is not Redis) provider.Cache = redis;
|
||||
provider.RedisQueue ??= redis;
|
||||
|
||||
return provider;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加键前缀的PrefixedRedis缓存
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="setupAction"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
[Obsolete("=>AddRedis")]
|
||||
public static IServiceCollection AddPrefixedRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
if (setupAction == null)
|
||||
throw new ArgumentNullException(nameof(setupAction));
|
||||
|
||||
services.AddBasic();
|
||||
services.AddOptions();
|
||||
services.Configure(setupAction);
|
||||
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>添加Redis缓存提供者ICacheProvider。从配置读取RedisCache和RedisQueue</summary>
|
||||
/// <param name="services"></param>
|
||||
/// <returns></returns>
|
||||
public static IServiceCollection AddRedisCacheProvider(this IServiceCollection services)
|
||||
{
|
||||
services.AddBasic();
|
||||
services.AddSingleton<ICacheProvider, RedisCacheProvider>();
|
||||
services.TryAddSingleton<ICache>(p => p.GetRequiredService<ICacheProvider>().Cache);
|
||||
services.TryAddSingleton<Redis>(p =>
|
||||
{
|
||||
var redis = p.GetRequiredService<ICacheProvider>().Cache as Redis;
|
||||
if (redis == null) throw new InvalidOperationException("未配置Redis,可在配置文件或配置中心指定名为RedisCache的连接字符串");
|
||||
|
||||
return redis;
|
||||
});
|
||||
services.TryAddSingleton<FullRedis>(p =>
|
||||
{
|
||||
var redis = p.GetRequiredService<ICacheProvider>().Cache as FullRedis;
|
||||
if (redis == null) throw new InvalidOperationException("未配置Redis,可在配置文件或配置中心指定名为RedisCache的连接字符串");
|
||||
|
||||
return redis;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
static void AddBasic(this IServiceCollection services)
|
||||
{
|
||||
// 注册依赖项
|
||||
services.TryAddSingleton<ILog>(XTrace.Log);
|
||||
services.TryAddSingleton<ITracer>(DefaultTracer.Instance ??= new DefaultTracer());
|
||||
|
||||
if (!services.Any(e => e.ServiceType == typeof(IConfigProvider)))
|
||||
services.TryAddSingleton<IConfigProvider>(JsonConfigProvider.LoadAppSettings());
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal file
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
|
||||
namespace ThingsGateway.NewLife.Redis.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Redis分布式缓存
|
||||
/// </summary>
|
||||
public class RedisCache : FullRedis, IDistributedCache, IDisposable
|
||||
{
|
||||
#region 属性
|
||||
|
||||
/// <summary>刷新时的过期时间。默认24小时</summary>
|
||||
public new TimeSpan Expire { get; set; } = TimeSpan.FromHours(24);
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>
|
||||
/// 实例化Redis分布式缓存
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
/// <param name="optionsAccessor"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public RedisCache(IServiceProvider serviceProvider, IOptions<RedisOptions> optionsAccessor) : base(serviceProvider, optionsAccessor.Value)
|
||||
{
|
||||
if (optionsAccessor == null) throw new ArgumentNullException(nameof(optionsAccessor));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 获取
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public Byte[]? Get(String key) => base.Get<Byte[]?>(key);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task<Byte[]?> GetAsync(String key, CancellationToken token = default) => Task.Run(() => base.Get<Byte[]>(key), token);
|
||||
|
||||
/// <summary>
|
||||
/// 设置
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public void Set(String key, Byte[] value, DistributedCacheEntryOptions options)
|
||||
{
|
||||
if (key == null) throw new ArgumentNullException(nameof(key));
|
||||
if (value == null) throw new ArgumentNullException(nameof(value));
|
||||
|
||||
if (options == null)
|
||||
base.Set(key, value);
|
||||
else
|
||||
if (options.AbsoluteExpiration != null)
|
||||
base.Set(key, value, options.AbsoluteExpiration.Value - DateTime.Now);
|
||||
else if (options.AbsoluteExpirationRelativeToNow != null)
|
||||
base.Set(key, value, options.AbsoluteExpirationRelativeToNow.Value);
|
||||
else if (options.SlidingExpiration != null)
|
||||
base.Set(key, value, options.SlidingExpiration.Value);
|
||||
else
|
||||
base.Set(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步设置
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="options"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task SetAsync(String key, Byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => Task.Run(() => Set(key, value, options), token);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public void Refresh(String key) => base.SetExpire(key, Expire);
|
||||
|
||||
/// <summary>
|
||||
/// 异步刷新
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public Task RefreshAsync(String key, CancellationToken token = default) => Task.Run(() => Refresh(key), token);
|
||||
|
||||
/// <summary>
|
||||
/// 删除
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
public new void Remove(String key) => base.Remove(key);
|
||||
|
||||
/// <summary>
|
||||
/// 异步删除
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public Task RemoveAsync(String key, CancellationToken token = default) => Task.Run(() => base.Remove(key), token);
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Caching.Services;
|
||||
|
||||
namespace ThingsGateway.NewLife.Redis.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Redis分布式缓存扩展
|
||||
/// </summary>
|
||||
public static class RedisCacheServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加Redis分布式缓存,应用内可使用RedisCache/FullRedis/Redis/IDistributedCache/ICache/ICacheProvider
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
/// <param name="setupAction"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, Action<RedisOptions> setupAction)
|
||||
{
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
if (setupAction == null)
|
||||
throw new ArgumentNullException(nameof(setupAction));
|
||||
|
||||
|
||||
services.AddOptions();
|
||||
services.Configure(setupAction);
|
||||
services.AddSingleton(sp => new RedisCache(sp, sp.GetRequiredService<IOptions<RedisOptions>>()));
|
||||
services.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<RedisCache>());
|
||||
|
||||
services.TryAddSingleton<FullRedis>(sp => sp.GetRequiredService<RedisCache>());
|
||||
services.TryAddSingleton<ICache>(p =>
|
||||
{
|
||||
|
||||
var result = p.GetRequiredService<RedisCache>();
|
||||
Cache.Default = result;
|
||||
return result;
|
||||
});
|
||||
services.TryAddSingleton<ThingsGateway.NewLife.Caching.Redis>(p => p.GetRequiredService<RedisCache>());
|
||||
|
||||
// 注册Redis缓存服务
|
||||
services.TryAddSingleton(p =>
|
||||
{
|
||||
var redis = p.GetRequiredService<RedisCache>();
|
||||
var provider = new RedisCacheProvider(p);
|
||||
if (provider.Cache is not ThingsGateway.NewLife.Caching.Redis) provider.Cache = redis;
|
||||
provider.RedisQueue ??= redis;
|
||||
|
||||
return provider;
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.DataProtection.KeyManagement;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ThingsGateway.NewLife.Redis.Extensions;
|
||||
|
||||
/// <summary>Redis数据保护扩展</summary>
|
||||
public static class RedisDataProtectionBuilderExtensions
|
||||
{
|
||||
private const String DataProtectionKeysName = "DataProtection-Keys";
|
||||
|
||||
///// <summary>存储数据保护Key到Redis,自动识别已注入到容器的FullRedis或Redis单例</summary>
|
||||
///// <param name="builder"></param>
|
||||
///// <returns></returns>
|
||||
///// <exception cref="ArgumentNullException"></exception>
|
||||
//public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder)
|
||||
//{
|
||||
// if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||
|
||||
// var redis = builder.Services.LastOrDefault(e => e.ServiceType == typeof(FullRedis))?.ImplementationInstance as NewLife.Caching.Redis;
|
||||
// redis ??= builder.Services.LastOrDefault(e => e.ServiceType == typeof(NewLife.Caching.Redis))?.ImplementationInstance as NewLife.Caching.Redis;
|
||||
// if (redis == null) throw new ArgumentNullException(nameof(redis));
|
||||
|
||||
// return PersistKeysToRedisInternal(builder, redis, DataProtectionKeysName);
|
||||
//}
|
||||
|
||||
/// <summary>存储数据保护Key到Redis</summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="redis"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key = DataProtectionKeysName)
|
||||
{
|
||||
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||
if (redis == null) throw new ArgumentNullException(nameof(redis));
|
||||
|
||||
return PersistKeysToRedisInternal(builder, redis, key);
|
||||
}
|
||||
|
||||
private static IDataProtectionBuilder PersistKeysToRedisInternal(IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key)
|
||||
{
|
||||
builder.Services.Configure(delegate (KeyManagementOptions options)
|
||||
{
|
||||
options.XmlRepository = new RedisXmlRepository(redis, key);
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>存储数据保护Key到Redis</summary>
|
||||
/// <param name="builder"></param>
|
||||
/// <param name="redisFactory"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key = DataProtectionKeysName)
|
||||
{
|
||||
if (builder == null) throw new ArgumentNullException(nameof(builder));
|
||||
if (redisFactory == null) throw new ArgumentNullException(nameof(redisFactory));
|
||||
|
||||
builder.Services.Configure(delegate (KeyManagementOptions options)
|
||||
{
|
||||
options.XmlRepository = new RedisXmlRepository(redisFactory, key);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,69 @@
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
using Microsoft.AspNetCore.DataProtection.Repositories;
|
||||
|
||||
using System.Xml.Linq;
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.NewLife.Redis.Extensions;
|
||||
|
||||
/// <summary>在Redis中存储Xml</summary>
|
||||
public class RedisXmlRepository : IXmlRepository
|
||||
{
|
||||
private readonly ThingsGateway.NewLife.Caching.Redis? _redis;
|
||||
private readonly Func<ThingsGateway.NewLife.Caching.Redis>? _redisFactory;
|
||||
|
||||
private readonly String _key;
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="redis"></param>
|
||||
/// <param name="key"></param>
|
||||
public RedisXmlRepository(ThingsGateway.NewLife.Caching.Redis redis, String key)
|
||||
{
|
||||
_redis = redis;
|
||||
_key = key;
|
||||
|
||||
XTrace.WriteLine("DataProtection使用Redis持久化密钥,Key={0}", key);
|
||||
}
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="redisFactory"></param>
|
||||
/// <param name="key"></param>
|
||||
public RedisXmlRepository(Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key)
|
||||
{
|
||||
_redisFactory = redisFactory;
|
||||
_key = key;
|
||||
|
||||
XTrace.WriteLine("DataProtection使用Redis持久化密钥,Key={0}", key);
|
||||
}
|
||||
|
||||
/// <summary>获取所有元素</summary>
|
||||
/// <returns></returns>
|
||||
public IReadOnlyCollection<XElement> GetAllElements() => GetAllElementsCore().ToList().AsReadOnly();
|
||||
|
||||
/// <summary>遍历元素</summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<XElement> GetAllElementsCore()
|
||||
{
|
||||
var rds = _redis ?? _redisFactory!();
|
||||
var list = rds.GetList<String>(_key) ?? [];
|
||||
foreach (var item in list)
|
||||
{
|
||||
yield return XElement.Parse(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>存储元素</summary>
|
||||
/// <param name="element"></param>
|
||||
/// <param name="friendlyName"></param>
|
||||
public void StoreElement(XElement element, String friendlyName)
|
||||
{
|
||||
var rds = _redis ?? _redisFactory!();
|
||||
var list = rds.GetList<String>(_key);
|
||||
list.Add(element.ToString(SaveOptions.DisableFormatting));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
27
src/Admin/ThingsGateway.NewLife.X/Algorithms/AlignModes.cs
Normal file
27
src/Admin/ThingsGateway.NewLife.X/Algorithms/AlignModes.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace ThingsGateway.NewLife.Algorithms;
|
||||
|
||||
/// <summary>
|
||||
/// 对齐模型。数据采样时X轴对齐
|
||||
/// </summary>
|
||||
public enum AlignModes
|
||||
{
|
||||
/// <summary>
|
||||
/// 不对齐,原始值
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// 左对齐
|
||||
/// </summary>
|
||||
Left,
|
||||
|
||||
/// <summary>
|
||||
/// 中间对齐
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// 右对齐
|
||||
/// </summary>
|
||||
Right,
|
||||
}
|
||||
116
src/Admin/ThingsGateway.NewLife.X/Algorithms/AverageSampling.cs
Normal file
116
src/Admin/ThingsGateway.NewLife.X/Algorithms/AverageSampling.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Algorithms;
|
||||
|
||||
/// <summary>
|
||||
/// 平均值采样算法
|
||||
/// </summary>
|
||||
public class AverageSampling : ISampling
|
||||
{
|
||||
/// <summary>
|
||||
/// 对齐模式。每个桶X轴对齐方式
|
||||
/// </summary>
|
||||
public AlignModes AlignMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插值填充算法
|
||||
/// </summary>
|
||||
public IInterpolation? Interpolation { get; set; } = new LinearInterpolation();
|
||||
|
||||
/// <summary>
|
||||
/// 降采样处理。保留边界两个点
|
||||
/// </summary>
|
||||
/// <param name="data">原始数据</param>
|
||||
/// <param name="threshold">阈值,采样数</param>
|
||||
/// <returns></returns>
|
||||
public TimePoint[] Down(TimePoint[] data, Int32 threshold)
|
||||
{
|
||||
//if (data == null || data.Length < 2) return data;
|
||||
if (data.Length < 2) return data;
|
||||
if (threshold < 2 || threshold >= data.Length) return data;
|
||||
|
||||
var buckets = SamplingHelper.SplitByAverage(data.Length, threshold, true);
|
||||
|
||||
// 每个桶选择一个点作为代表
|
||||
var sampled = new TimePoint[buckets.Length];
|
||||
for (var i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
var item = buckets[i];
|
||||
TimePoint point = default;
|
||||
var vs = 0.0;
|
||||
for (var j = item.Start; j < item.End; j++)
|
||||
{
|
||||
vs += data[j].Value;
|
||||
}
|
||||
point.Value = vs / (item.End - item.Start);
|
||||
|
||||
// 对齐
|
||||
point.Time = AlignMode switch
|
||||
{
|
||||
AlignModes.Right => data[item.End - 1].Time,
|
||||
AlignModes.Center => data[(Int32)Math.Round((item.Start + item.End - 1) / 2.0)].Time,
|
||||
_ => data[item.Start].Time,
|
||||
};
|
||||
sampled[i] = point;
|
||||
}
|
||||
|
||||
return sampled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 混合处理,降采样和插值,不保留边界节点
|
||||
/// </summary>
|
||||
/// <param name="data">原始数据</param>
|
||||
/// <param name="size">桶大小。如60/3600/86400</param>
|
||||
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
|
||||
/// <returns></returns>
|
||||
public TimePoint[] Process(TimePoint[] data, Int32 size, Int32 offset = 0)
|
||||
{
|
||||
//if (data == null || data.Length < 2) return data;
|
||||
if (data.Length < 2) return data;
|
||||
if (size <= 1) return data;
|
||||
if (Interpolation == null) throw new ArgumentNullException(nameof(Interpolation));
|
||||
|
||||
var xs = new Int64[data.Length];
|
||||
for (var i = 0; i < data.Length; i++) xs[i] = data[i].Time;
|
||||
|
||||
var buckets = SamplingHelper.SplitByFixedSize(xs, size, offset);
|
||||
|
||||
// 每个桶选择一个点作为代表
|
||||
var sampled = new TimePoint[buckets.Length];
|
||||
var last = 0;
|
||||
for (var i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
// 断层,插值
|
||||
var item = buckets[i];
|
||||
if (item.Start < 0)
|
||||
{
|
||||
// 取last用于插值起点,如果不存在,可以取0点
|
||||
// 此时End指向下一个有效点,即使下一个桶也是断层
|
||||
sampled[i].Time = i * size;
|
||||
sampled[i].Value = Interpolation.Process(data, last, item.End, i);
|
||||
continue;
|
||||
}
|
||||
|
||||
TimePoint point = default;
|
||||
var vs = 0.0;
|
||||
for (var j = item.Start; j < item.End; j++)
|
||||
{
|
||||
vs += data[j].Value;
|
||||
}
|
||||
last = item.End - 1;
|
||||
point.Value = vs / (item.End - item.Start);
|
||||
|
||||
// 对齐
|
||||
point.Time = AlignMode switch
|
||||
{
|
||||
AlignModes.Right => (i + 1) * size - 1,
|
||||
AlignModes.Center => data[(Int32)Math.Round((i + 0.5) * size)].Time,
|
||||
_ => i * size,
|
||||
};
|
||||
sampled[i] = point;
|
||||
}
|
||||
|
||||
return sampled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Algorithms;
|
||||
|
||||
/// <summary>
|
||||
/// 插值算法
|
||||
/// </summary>
|
||||
public interface IInterpolation
|
||||
{
|
||||
/// <summary>
|
||||
/// 插值处理
|
||||
/// </summary>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="prev">上一个点索引</param>
|
||||
/// <param name="next">下一个点索引</param>
|
||||
/// <param name="current">当前点时间值</param>
|
||||
/// <returns></returns>
|
||||
Double Process(TimePoint[] data, Int32 prev, Int32 next, Int64 current);
|
||||
}
|
||||
138
src/Admin/ThingsGateway.NewLife.X/Algorithms/ISampling.cs
Normal file
138
src/Admin/ThingsGateway.NewLife.X/Algorithms/ISampling.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Algorithms;
|
||||
|
||||
/// <summary>
|
||||
/// 采样接口。负责降采样和插值处理,用于处理时序数据
|
||||
/// </summary>
|
||||
public interface ISampling
|
||||
{
|
||||
/// <summary>
|
||||
/// 对齐模式。每个桶X轴对齐方式
|
||||
/// </summary>
|
||||
AlignModes AlignMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 插值填充算法
|
||||
/// </summary>
|
||||
IInterpolation? Interpolation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 降采样处理
|
||||
/// </summary>
|
||||
/// <param name="data">原始数据</param>
|
||||
/// <param name="threshold">阈值,采样数</param>
|
||||
/// <returns></returns>
|
||||
TimePoint[] Down(TimePoint[] data, Int32 threshold);
|
||||
|
||||
/// <summary>
|
||||
/// 混合处理,降采样和插值
|
||||
/// </summary>
|
||||
/// <param name="data">原始数据</param>
|
||||
/// <param name="size">桶大小。如60/3600/86400</param>
|
||||
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
|
||||
/// <returns></returns>
|
||||
TimePoint[] Process(TimePoint[] data, Int32 size, Int32 offset = 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 采样助手
|
||||
/// </summary>
|
||||
public static class SamplingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 按照指定桶数平均分,可指定保留头尾
|
||||
/// </summary>
|
||||
/// <param name="dataLength"></param>
|
||||
/// <param name="threshold"></param>
|
||||
/// <param name="retainEdge"></param>
|
||||
/// <returns></returns>
|
||||
public static IndexRange[] SplitByAverage(Int32 dataLength, Int32 threshold, Boolean retainEdge = true)
|
||||
{
|
||||
if (dataLength == 0) throw new ArgumentNullException(nameof(dataLength));
|
||||
if (threshold <= 2) throw new ArgumentNullException(nameof(threshold));
|
||||
|
||||
var buckets = new IndexRange[threshold];
|
||||
if (retainEdge)
|
||||
{
|
||||
var step = (Double)(dataLength - 2) / (threshold - 2);
|
||||
var v = 0d;
|
||||
for (var i = 1; i < threshold - 1; i++)
|
||||
{
|
||||
buckets[i].Start = (Int32)Math.Round(v) + 1;
|
||||
buckets[i].End = (Int32)Math.Round(v += step) + 1;
|
||||
if (buckets[i].End > dataLength - 1) buckets[i].End = dataLength - 1;
|
||||
}
|
||||
buckets[0].Start = 0;
|
||||
buckets[0].End = 1;
|
||||
buckets[threshold - 1].Start = dataLength - 1;
|
||||
buckets[threshold - 1].End = dataLength - 1 + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var step = (Double)dataLength / threshold;
|
||||
var v = 0d;
|
||||
for (var i = 0; i < threshold; i++)
|
||||
{
|
||||
buckets[i].Start = (Int32)Math.Round(v);
|
||||
buckets[i].End = (Int32)Math.Round(v += step);
|
||||
if (buckets[i].End > dataLength) buckets[i].End = dataLength;
|
||||
}
|
||||
}
|
||||
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按照固定时间间隔,拆分数据轴为多个桶
|
||||
/// </summary>
|
||||
/// <param name="data">原始数据</param>
|
||||
/// <param name="size">桶大小。如60/3600/86400</param>
|
||||
/// <param name="offset">偏移量。时间不是对齐零点时使用</param>
|
||||
/// <returns></returns>
|
||||
public static IndexRange[] SplitByFixedSize(Int64[] data, Int32 size, Int32 offset = 0)
|
||||
{
|
||||
if (data == null || data.Length == 0) throw new ArgumentNullException(nameof(data));
|
||||
if (size <= 0) throw new ArgumentNullException(nameof(size));
|
||||
if (offset >= size) throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
|
||||
// 计算首尾的两个桶的值
|
||||
var start = data[0] / size * size + offset;
|
||||
if (start > data[0]) start -= size;
|
||||
var last = data[^1];
|
||||
var end = last / size * size + offset;
|
||||
if (end > last) end -= size;
|
||||
|
||||
var buckets = new IndexRange[(end - start) / size + 1];
|
||||
|
||||
// 计算每个桶的头尾
|
||||
var p = 0;
|
||||
var idx = 0;
|
||||
for (var time = start; time <= end; p++)
|
||||
{
|
||||
IndexRange r = default;
|
||||
r.Start = -1;
|
||||
r.End = -1;
|
||||
var next = time + size;
|
||||
|
||||
// 顺序遍历原始数据,这里假设原始数据为升序
|
||||
for (; idx < data.Length; idx++)
|
||||
{
|
||||
// 如果超过了当前桶的结尾,则换下一个桶
|
||||
if (data[idx] >= next)
|
||||
{
|
||||
r.End = idx;
|
||||
break;
|
||||
}
|
||||
|
||||
if (r.Start < 0 && time <= data[idx]) r.Start = idx;
|
||||
}
|
||||
if (r.End < 0) r.End = idx;
|
||||
|
||||
buckets[p] = r;
|
||||
time = next;
|
||||
}
|
||||
|
||||
return buckets.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Algorithms;
|
||||
|
||||
/// <summary>
|
||||
/// 线性插值
|
||||
/// </summary>
|
||||
public class LinearInterpolation : IInterpolation
|
||||
{
|
||||
/// <summary>
|
||||
/// 插值处理
|
||||
/// </summary>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="prev">上一个点索引</param>
|
||||
/// <param name="next">下一个点索引</param>
|
||||
/// <param name="current">当前点时间值</param>
|
||||
/// <returns></returns>
|
||||
public Double Process(TimePoint[] data, Int32 prev, Int32 next, Int64 current)
|
||||
{
|
||||
var dt = (data[next].Value - data[prev].Value) / (data[next].Time - data[prev].Time);
|
||||
return data[prev].Value + (current - data[prev].Time) * dt;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ThingsGateway.NewLife.Buffers;
|
||||
|
||||
internal sealed class BufferSegment : ReadOnlySequenceSegment<Byte>
|
||||
{
|
||||
private IMemoryOwner<Byte>? _memoryOwner;
|
||||
|
||||
private Byte[]? _array;
|
||||
|
||||
private BufferSegment? _next;
|
||||
|
||||
private Int32 _end;
|
||||
|
||||
public Int32 End
|
||||
{
|
||||
get
|
||||
{
|
||||
return _end;
|
||||
}
|
||||
set
|
||||
{
|
||||
_end = value;
|
||||
base.Memory = AvailableMemory[..value];
|
||||
}
|
||||
}
|
||||
|
||||
public BufferSegment? NextSegment
|
||||
{
|
||||
get
|
||||
{
|
||||
return _next;
|
||||
}
|
||||
set
|
||||
{
|
||||
base.Next = value;
|
||||
_next = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal Object? MemoryOwner => ((Object)_memoryOwner) ?? ((Object)_array);
|
||||
|
||||
public Memory<Byte> AvailableMemory { get; private set; }
|
||||
|
||||
public Int32 Length => End;
|
||||
|
||||
public Int32 WritableBytes
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => AvailableMemory.Length - End;
|
||||
}
|
||||
|
||||
public void SetOwnedMemory(IMemoryOwner<Byte> memoryOwner)
|
||||
{
|
||||
_memoryOwner = memoryOwner;
|
||||
AvailableMemory = memoryOwner.Memory;
|
||||
}
|
||||
|
||||
public void SetOwnedMemory(Byte[] arrayPoolBuffer)
|
||||
{
|
||||
_array = arrayPoolBuffer;
|
||||
AvailableMemory = arrayPoolBuffer;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
ResetMemory();
|
||||
base.Next = null;
|
||||
base.RunningIndex = 0L;
|
||||
_next = null;
|
||||
}
|
||||
|
||||
public void ResetMemory()
|
||||
{
|
||||
var memoryOwner = _memoryOwner;
|
||||
if (memoryOwner != null)
|
||||
{
|
||||
_memoryOwner = null;
|
||||
memoryOwner.Dispose();
|
||||
}
|
||||
else if (_array != null)
|
||||
{
|
||||
ArrayPool<Byte>.Shared.Return(_array);
|
||||
_array = null;
|
||||
}
|
||||
base.Memory = default;
|
||||
_end = 0;
|
||||
AvailableMemory = default;
|
||||
}
|
||||
|
||||
public void SetNext(BufferSegment segment)
|
||||
{
|
||||
NextSegment = segment;
|
||||
segment = this;
|
||||
while (segment.Next != null)
|
||||
{
|
||||
segment.NextSegment.RunningIndex = segment.RunningIndex + segment.Length;
|
||||
segment = segment.NextSegment;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static Int64 GetLength(BufferSegment startSegment, Int32 startIndex, BufferSegment endSegment, Int32 endIndex) => endSegment.RunningIndex + (UInt32)endIndex - (startSegment.RunningIndex + (UInt32)startIndex);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static Int64 GetLength(Int64 startPosition, BufferSegment endSegment, Int32 endIndex) => endSegment.RunningIndex + (UInt32)endIndex - startPosition;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public static class SpanHelper
|
||||
{
|
||||
if (bytes.IsEmpty) return String.Empty;
|
||||
|
||||
#if NET45
|
||||
#if NET452
|
||||
return encoding.GetString(bytes.ToArray());
|
||||
#else
|
||||
fixed (Byte* bytes2 = &MemoryMarshal.GetReference(bytes))
|
||||
|
||||
377
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanReader.cs
Normal file
377
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanReader.cs
Normal file
@@ -0,0 +1,377 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Buffers;
|
||||
|
||||
/// <summary>Span读取器</summary>
|
||||
/// <remarks>
|
||||
/// 引用结构的Span读取器确保高性能无GC读取。
|
||||
/// 支持Stream扩展,当数据不足时,自动从数据流中读取,常用于解析Redis/MySql等协议。
|
||||
/// </remarks>
|
||||
public ref struct SpanReader
|
||||
{
|
||||
#region 属性
|
||||
private ReadOnlySpan<Byte> _span;
|
||||
/// <summary>数据片段</summary>
|
||||
public ReadOnlySpan<Byte> Span => _span;
|
||||
|
||||
private Int32 _index;
|
||||
/// <summary>已读取字节数</summary>
|
||||
public Int32 Position { get => _index; set => _index = value; }
|
||||
|
||||
/// <summary>总容量</summary>
|
||||
public Int32 Capacity => _span.Length;
|
||||
|
||||
/// <summary>空闲容量</summary>
|
||||
public Int32 FreeCapacity => _span.Length - _index;
|
||||
|
||||
/// <summary>是否小端字节序。默认true</summary>
|
||||
public Boolean IsLittleEndian { get; set; } = true;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化。暂时兼容旧版,后面使用主构造函数</summary>
|
||||
/// <param name="span"></param>
|
||||
public SpanReader(ReadOnlySpan<Byte> span) => _span = span;
|
||||
|
||||
/// <summary>实例化。暂时兼容旧版,后面删除</summary>
|
||||
/// <param name="span"></param>
|
||||
public SpanReader(Span<Byte> span) => _span = span;
|
||||
|
||||
/// <summary>实例化Span读取器</summary>
|
||||
/// <param name="data"></param>
|
||||
public SpanReader(IPacket data)
|
||||
{
|
||||
_data = data;
|
||||
_span = data.GetSpan();
|
||||
_total = data.Total;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 扩容增强
|
||||
/// <summary>最大容量。多次从数据流读取数据时,受限于此最大值</summary>
|
||||
public Int32 MaxCapacity { get; set; }
|
||||
|
||||
private readonly Stream? _stream;
|
||||
private readonly Int32 _bufferSize;
|
||||
private IPacket? _data;
|
||||
private Int32 _total;
|
||||
|
||||
/// <summary>实例化Span读取器。支持从数据流中读取更多数据,突破大小限制</summary>
|
||||
/// <remarks>
|
||||
/// 解析网络协议时,有时候数据帧较大,超过特定缓冲区大小,导致无法一次性读取完整数据帧。
|
||||
/// 加入数据流参数后,在读取数据不足时,SpanReader会自动从数据流中读取一批数据。
|
||||
/// </remarks>
|
||||
/// <param name="stream">数据流。一般是网络流</param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="bufferSize"></param>
|
||||
public SpanReader(Stream stream, IPacket? data = null, Int32 bufferSize = 8192)
|
||||
{
|
||||
_stream = stream;
|
||||
_bufferSize = bufferSize;
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
_data = data;
|
||||
_span = data.GetSpan();
|
||||
_total = data.Total;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 基础方法
|
||||
/// <summary>告知有多少数据已从缓冲区读取</summary>
|
||||
/// <param name="count"></param>
|
||||
public void Advance(Int32 count)
|
||||
{
|
||||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
if (count > 0) EnsureSpace(count);
|
||||
|
||||
if (_index + count > _span.Length) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
_index += count;
|
||||
}
|
||||
|
||||
/// <summary>返回要写入到的Span,其大小按 sizeHint 参数指定至少为所请求的大小</summary>
|
||||
/// <param name="sizeHint"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public ReadOnlySpan<Byte> GetSpan(Int32 sizeHint = 0)
|
||||
{
|
||||
if (sizeHint > FreeCapacity) throw new ArgumentOutOfRangeException(nameof(sizeHint));
|
||||
|
||||
return _span[_index..];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 读取方法
|
||||
/// <summary>确保缓冲区中有足够的空间。</summary>
|
||||
/// <param name="size">需要的字节数。</param>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public void EnsureSpace(Int32 size)
|
||||
{
|
||||
// 检查剩余空间大小,不足时,再从数据流中读取。此时需要注意,创建新的OwnerPacket后,需要先把之前剩余的一点数据拷贝过去,然后再读取Stream
|
||||
var remain = FreeCapacity;
|
||||
if (remain < size && _stream != null)
|
||||
{
|
||||
// 申请指定大小的数据包缓冲区,至少达到缓冲区大小,但不超过最大容量
|
||||
var idx = 0;
|
||||
var bsize = size;
|
||||
if (MaxCapacity > 0)
|
||||
{
|
||||
if (bsize < _bufferSize) bsize = _bufferSize;
|
||||
if (bsize > MaxCapacity - _total) bsize = MaxCapacity - _total;
|
||||
}
|
||||
var pk = new OwnerPacket(bsize);
|
||||
if (_data != null && remain > 0)
|
||||
{
|
||||
if (!_data.TryGetArray(out var arr)) throw new NotSupportedException();
|
||||
|
||||
arr.AsSpan(_index, remain).CopyTo(pk.Buffer);
|
||||
idx += remain;
|
||||
}
|
||||
|
||||
_data.TryDispose();
|
||||
_data = pk;
|
||||
_index = 0;
|
||||
|
||||
// 多次读取,直到满足需求。不要超过最大容量,否则可能读取到下一个数据帧的数据
|
||||
_stream.ReadExactly(pk.Buffer, pk.Offset + idx, pk.Length - idx);
|
||||
idx = pk.Length;
|
||||
//while (idx < size)
|
||||
//{
|
||||
// var n = _stream.Read(pk.Buffer, pk.Offset + idx, pk.Length - idx);
|
||||
// if (n <= 0) break;
|
||||
|
||||
// idx += n;
|
||||
//}
|
||||
if (idx < size)
|
||||
throw new InvalidOperationException("Not enough data to read.");
|
||||
pk.Resize(idx);
|
||||
|
||||
_span = pk.GetSpan();
|
||||
_total += idx - remain;
|
||||
}
|
||||
|
||||
if (_index + size > _span.Length)
|
||||
throw new InvalidOperationException("Not enough data to read.");
|
||||
}
|
||||
|
||||
/// <summary>读取单个字节</summary>
|
||||
/// <returns></returns>
|
||||
public Byte ReadByte()
|
||||
{
|
||||
var size = sizeof(Byte);
|
||||
EnsureSpace(size);
|
||||
var result = _span[_index];
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取Int16整数</summary>
|
||||
/// <returns></returns>
|
||||
public Int16 ReadInt16()
|
||||
{
|
||||
var size = sizeof(Int16);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadInt16LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadInt16BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取UInt16整数</summary>
|
||||
/// <returns></returns>
|
||||
public UInt16 ReadUInt16()
|
||||
{
|
||||
var size = sizeof(UInt16);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadUInt16BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取Int32整数</summary>
|
||||
/// <returns></returns>
|
||||
public Int32 ReadInt32()
|
||||
{
|
||||
var size = sizeof(Int32);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadInt32LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadInt32BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取UInt32整数</summary>
|
||||
/// <returns></returns>
|
||||
public UInt32 ReadUInt32()
|
||||
{
|
||||
var size = sizeof(UInt32);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadUInt32BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取Int64整数</summary>
|
||||
/// <returns></returns>
|
||||
public Int64 ReadInt64()
|
||||
{
|
||||
var size = sizeof(Int64);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadInt64LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadInt64BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取UInt64整数</summary>
|
||||
/// <returns></returns>
|
||||
public UInt64 ReadUInt64()
|
||||
{
|
||||
var size = sizeof(UInt64);
|
||||
EnsureSpace(size);
|
||||
var result = IsLittleEndian ?
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(_span.Slice(_index, size)) :
|
||||
BinaryPrimitives.ReadUInt64BigEndian(_span.Slice(_index, size));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取单精度浮点数</summary>
|
||||
/// <returns></returns>
|
||||
public unsafe Single ReadSingle()
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
|
||||
return BitConverter.Int32BitsToSingle(ReadInt32());
|
||||
#else
|
||||
var result = ReadInt32();
|
||||
return Unsafe.ReadUnaligned<Single>(ref Unsafe.As<Int32, Byte>(ref result));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>读取双精度浮点数</summary>
|
||||
/// <returns></returns>
|
||||
public Double ReadDouble()
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
|
||||
return BitConverter.Int64BitsToDouble(ReadInt64());
|
||||
#else
|
||||
var result = ReadInt64();
|
||||
return Unsafe.ReadUnaligned<Double>(ref Unsafe.As<Int64, Byte>(ref result));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>读取字符串。支持定长、全部和长度前缀</summary>
|
||||
/// <param name="length">需要读取的长度。-1表示读取全部,默认0表示读取7位压缩编码整数长度</param>
|
||||
/// <param name="encoding">字符串编码,默认UTF8</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public String ReadString(Int32 length = 0, Encoding? encoding = null)
|
||||
{
|
||||
if (length < 0)
|
||||
length = _span.Length - _index;
|
||||
else if (length == 0)
|
||||
length = ReadEncodedInt();
|
||||
if (length == 0) return String.Empty;
|
||||
|
||||
EnsureSpace(length);
|
||||
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
var result = encoding.GetString(_span.Slice(_index, length));
|
||||
_index += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取字节数组</summary>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
public ReadOnlySpan<Byte> ReadBytes(Int32 length)
|
||||
{
|
||||
EnsureSpace(length);
|
||||
|
||||
var result = _span.Slice(_index, length);
|
||||
_index += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取字节数组</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 Read(Span<Byte> data)
|
||||
{
|
||||
var length = data.Length;
|
||||
EnsureSpace(length);
|
||||
|
||||
var result = _span.Slice(_index, length);
|
||||
result.CopyTo(data);
|
||||
_index += length;
|
||||
return length;
|
||||
}
|
||||
|
||||
/// <summary>读取数据包。直接对内部数据包进行切片</summary>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
public IPacket ReadPacket(Int32 length)
|
||||
{
|
||||
if (_data == null) throw new InvalidOperationException("No data stream to read!");
|
||||
|
||||
//EnsureSpace(length);
|
||||
|
||||
var result = _data.Slice(_index, length);
|
||||
_index += length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>读取结构体</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Read<T>() where T : struct
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureSpace(size);
|
||||
|
||||
var result = MemoryMarshal.Read<T>(_span.Slice(_index));
|
||||
_index += size;
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 扩展读取
|
||||
/// <summary>以压缩格式读取32位整数</summary>
|
||||
/// <returns></returns>
|
||||
public Int32 ReadEncodedInt()
|
||||
{
|
||||
Byte b;
|
||||
UInt32 rs = 0;
|
||||
Byte n = 0;
|
||||
while (true)
|
||||
{
|
||||
var bt = ReadByte();
|
||||
b = (Byte)bt;
|
||||
|
||||
// 必须转为Int32,否则可能溢出
|
||||
rs |= (UInt32)((b & 0x7f) << n);
|
||||
if ((b & 0x80) == 0) break;
|
||||
|
||||
n += 7;
|
||||
if (n >= 32) throw new FormatException("The number value is too large to read in compressed format!");
|
||||
}
|
||||
return (Int32)rs;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
338
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanWriter.cs
Normal file
338
src/Admin/ThingsGateway.NewLife.X/Buffers/SpanWriter.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Buffers;
|
||||
|
||||
/// <summary>Span写入器</summary>
|
||||
/// <param name="buffer"></param>
|
||||
public ref struct SpanWriter(Span<Byte> buffer)
|
||||
{
|
||||
#region 属性
|
||||
private readonly Span<Byte> _span = buffer;
|
||||
/// <summary>数据片段</summary>
|
||||
public Span<Byte> Span => _span;
|
||||
|
||||
private Int32 _index;
|
||||
/// <summary>已写入字节数</summary>
|
||||
public Int32 Position { get => _index; set => _index = value; }
|
||||
|
||||
/// <summary>总容量</summary>
|
||||
public readonly Int32 Capacity => _span.Length;
|
||||
|
||||
/// <summary>空闲容量</summary>
|
||||
public readonly Int32 FreeCapacity => _span.Length - _index;
|
||||
|
||||
/// <summary>是否小端字节序。默认true</summary>
|
||||
public Boolean IsLittleEndian { get; set; } = true;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化Span读取器</summary>
|
||||
/// <param name="data"></param>
|
||||
public SpanWriter(IPacket data) : this(data.GetSpan()) { }
|
||||
#endregion
|
||||
|
||||
#region 基础方法
|
||||
/// <summary>告知有多少数据已写入缓冲区</summary>
|
||||
/// <param name="count"></param>
|
||||
public void Advance(Int32 count)
|
||||
{
|
||||
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
if (_index + count > _span.Length) throw new ArgumentOutOfRangeException(nameof(count));
|
||||
|
||||
_index += count;
|
||||
}
|
||||
|
||||
/// <summary>返回要写入到的Span,其大小按 sizeHint 参数指定至少为所请求的大小</summary>
|
||||
/// <param name="sizeHint"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public readonly Span<Byte> GetSpan(Int32 sizeHint = 0)
|
||||
{
|
||||
if (sizeHint > FreeCapacity) throw new ArgumentOutOfRangeException(nameof(sizeHint));
|
||||
|
||||
return _span[_index..];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 写入方法
|
||||
/// <summary>确保缓冲区中有足够的空间。</summary>
|
||||
/// <param name="size">需要的字节数。</param>
|
||||
private readonly void EnsureSpace(Int32 size)
|
||||
{
|
||||
if (_index + size > _span.Length)
|
||||
throw new InvalidOperationException("Not enough data to write.");
|
||||
}
|
||||
|
||||
/// <summary>写入字节</summary>
|
||||
public Int32 WriteByte(Int32 value) => Write((Byte)value);
|
||||
|
||||
/// <summary>写入字节</summary>
|
||||
/// <param name="value">要写入的字节值。</param>
|
||||
public Int32 Write(Byte value)
|
||||
{
|
||||
var size = sizeof(Byte);
|
||||
EnsureSpace(size);
|
||||
_span[_index] = value;
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入 16 位整数。</summary>
|
||||
/// <param name="value">要写入的整数值。</param>
|
||||
public Int32 Write(Int16 value)
|
||||
{
|
||||
var size = sizeof(Int16);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteInt16LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteInt16BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入无符号 16 位整数。</summary>
|
||||
/// <param name="value">要写入的无符号整数值。</param>
|
||||
public Int32 Write(UInt16 value)
|
||||
{
|
||||
var size = sizeof(UInt16);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteUInt16BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入 32 位整数。</summary>
|
||||
/// <param name="value">要写入的整数值。</param>
|
||||
public Int32 Write(Int32 value)
|
||||
{
|
||||
var size = sizeof(Int32);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteInt32LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteInt32BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入无符号 32 位整数。</summary>
|
||||
/// <param name="value">要写入的无符号整数值。</param>
|
||||
public Int32 Write(UInt32 value)
|
||||
{
|
||||
var size = sizeof(UInt32);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteUInt32BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入 64 位整数。</summary>
|
||||
/// <param name="value">要写入的整数值。</param>
|
||||
public Int32 Write(Int64 value)
|
||||
{
|
||||
var size = sizeof(Int64);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteInt64LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteInt64BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入无符号 64 位整数。</summary>
|
||||
/// <param name="value">要写入的无符号整数值。</param>
|
||||
public Int32 Write(UInt64 value)
|
||||
{
|
||||
var size = sizeof(UInt64);
|
||||
EnsureSpace(size);
|
||||
if (IsLittleEndian)
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(_span[_index..], value);
|
||||
else
|
||||
BinaryPrimitives.WriteUInt64BigEndian(_span[_index..], value);
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>写入单精度浮点数。</summary>
|
||||
/// <param name="value">要写入的浮点值。</param>
|
||||
public unsafe Int32 Write(Single value)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
|
||||
return Write(BitConverter.SingleToInt32Bits(value));
|
||||
#else
|
||||
return Write(*(Int32*)&value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>写入双精度浮点数。</summary>
|
||||
/// <param name="value">要写入的浮点值。</param>
|
||||
public unsafe Int32 Write(Double value)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP
|
||||
return Write(BitConverter.DoubleToInt64Bits(value));
|
||||
#else
|
||||
return Write(*(Int64*)&value);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>写入字符串。支持定长、全部和长度前缀</summary>
|
||||
/// <param name="value">要写入的字符串</param>
|
||||
/// <param name="length">最大长度。字节数,-1表示写入全部,默认0表示写入7位压缩编码整数长度。不足时填充字节0,超长时截取</param>
|
||||
/// <param name="encoding">字符串编码,默认UTF8</param>
|
||||
/// <returns>返回写入字节数,包括头部长度和字符串部分</returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public Int32 Write(String value, Int32 length = 0, Encoding? encoding = null)
|
||||
{
|
||||
var p = _index;
|
||||
encoding ??= Encoding.UTF8;
|
||||
if (length < 0)
|
||||
{
|
||||
// 写入字符串全部内容
|
||||
var count = encoding.GetBytes(value.AsSpan(), _span[_index..]);
|
||||
_index += count;
|
||||
|
||||
return _index - p;
|
||||
}
|
||||
else if (length == 0)
|
||||
{
|
||||
// 先写入长度,再写入内容
|
||||
if (value.IsNullOrEmpty())
|
||||
{
|
||||
WriteEncodedInt(0);
|
||||
return _index - p;
|
||||
}
|
||||
|
||||
length = encoding.GetByteCount(value);
|
||||
WriteEncodedInt(length);
|
||||
EnsureSpace(length);
|
||||
|
||||
var count = encoding.GetBytes(value.AsSpan(), _span[_index..]);
|
||||
_index += count;
|
||||
|
||||
return _index - p;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 写入指定长度,不足是填充字节0,超长时截取
|
||||
var span = GetSpan(length);
|
||||
if (span.Length > length) span = span[..length];
|
||||
|
||||
// 输出缓冲区不能过小,否则报错。大小足够时,直接把字符串写入到目标
|
||||
var source = value.AsSpan();
|
||||
var max = encoding.GetMaxByteCount(source.Length);
|
||||
if (max <= length)
|
||||
encoding.GetBytes(source, span);
|
||||
else
|
||||
{
|
||||
// 目标大小可能不足,申请临时缓冲区,输出后做局部拷贝
|
||||
var buf = Pool.Shared.Rent(max);
|
||||
var count = encoding.GetBytes(source, buf);
|
||||
|
||||
// 局部拷贝,仅拷贝需要部分,抛弃超长部分
|
||||
new Span<Byte>(buf, 0, length).CopyTo(span);
|
||||
|
||||
Pool.Shared.Return(buf, true);
|
||||
}
|
||||
|
||||
_index += length;
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>写入字节数组</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public Int32 Write(Byte[] value)
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
|
||||
value.CopyTo(_span[_index..]);
|
||||
_index += value.Length;
|
||||
|
||||
return value.Length;
|
||||
}
|
||||
|
||||
/// <summary>写入Span</summary>
|
||||
/// <param name="span"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 Write(ReadOnlySpan<Byte> span)
|
||||
{
|
||||
span.CopyTo(_span[_index..]);
|
||||
_index += span.Length;
|
||||
|
||||
return span.Length;
|
||||
}
|
||||
|
||||
/// <summary>写入Span</summary>
|
||||
/// <param name="span"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 Write(Span<Byte> span)
|
||||
{
|
||||
span.CopyTo(_span[_index..]);
|
||||
_index += span.Length;
|
||||
|
||||
return span.Length;
|
||||
}
|
||||
|
||||
/// <summary>写入结构体</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 Write<T>(T value) where T : struct
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureSpace(size);
|
||||
#if NET8_0_OR_GREATER
|
||||
MemoryMarshal.Write(_span.Slice(_index, size), in value);
|
||||
#else
|
||||
MemoryMarshal.Write(_span.Slice(_index, size), ref value);
|
||||
#endif
|
||||
_index += size;
|
||||
return size;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 扩展写入
|
||||
/// <summary>写入7位压缩编码整数</summary>
|
||||
/// <remarks>
|
||||
/// 以7位压缩格式写入32位整数,小于7位用1个字节,小于14位用2个字节。
|
||||
/// 由每次写入的一个字节的第一位标记后面的字节是否还是当前数据,所以每个字节实际可利用存储空间只有后7位。
|
||||
/// </remarks>
|
||||
/// <param name="value">数值</param>
|
||||
/// <returns>实际写入字节数</returns>
|
||||
public Int32 WriteEncodedInt(Int64 value)
|
||||
{
|
||||
var span = _span[_index..];
|
||||
|
||||
var count = 0;
|
||||
var num = (UInt32)value;
|
||||
while (num >= 0x80)
|
||||
{
|
||||
span[count++] = (Byte)(num | 0x80);
|
||||
num >>= 7;
|
||||
}
|
||||
span[count++] = (Byte)num;
|
||||
|
||||
_index += count;
|
||||
|
||||
return count;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using ThingsGateway.NewLife.Messaging;
|
||||
|
||||
namespace ThingsGateway.NewLife.Caching;
|
||||
|
||||
/// <summary>缓存</summary>
|
||||
@@ -100,9 +102,9 @@ public abstract class Cache : DisposeBase, ICache
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="keys"></param>
|
||||
/// <returns></returns>
|
||||
public virtual IDictionary<String, T?> GetAll<T>(IEnumerable<String> keys)
|
||||
public virtual IDictionary<String, T> GetAll<T>(IEnumerable<String> keys)
|
||||
{
|
||||
var dic = new Dictionary<String, T?>();
|
||||
var dic = new Dictionary<String, T>();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
dic[key] = Get<T>(key);
|
||||
@@ -152,6 +154,14 @@ public abstract class Cache : DisposeBase, ICache
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public virtual ICollection<T> GetSet<T>(String key) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>获取事件总线,可发布消息或订阅消息</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="topic">事件主题</param>
|
||||
/// <param name="clientId">客户标识/消息分组</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public virtual IEventBus<T> GetEventBus<T>(String topic, String clientId = "") => throw new NotSupportedException();
|
||||
#endregion
|
||||
|
||||
#region 高级操作
|
||||
@@ -310,7 +320,7 @@ public abstract class Cache : DisposeBase, ICache
|
||||
if (!rlock.Acquire(msTimeout, msExpire))
|
||||
{
|
||||
if (throwOnFailure) throw new InvalidOperationException($"Lock [{key}] failed! msTimeout={msTimeout}");
|
||||
rlock.Dispose();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -322,101 +332,14 @@ public abstract class Cache : DisposeBase, ICache
|
||||
/// <summary>已重载。</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => Name;
|
||||
#endregion
|
||||
#if NET6_0_OR_GREATER
|
||||
#region 集合
|
||||
/// <inheritdoc/>
|
||||
public virtual void HashAdd<T>(string key, string hashKey, T value)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
if (exist.ContainsKey(hashKey))//如果包含Key
|
||||
exist[hashKey] = value;//重新赋值
|
||||
else exist.TryAdd(hashKey, value);//加上新的值
|
||||
Set(key, exist);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual bool HashSet<T>(string key, Dictionary<string, T> dic)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var it in dic)
|
||||
{
|
||||
if (exist.ContainsKey(it.Key))//如果包含Key
|
||||
exist[it.Key] = it.Value;//重新赋值
|
||||
else exist.Add(it.Key, it.Value);//加上新的值
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual int HashDel<T>(string key, params string[] fields)
|
||||
{
|
||||
var result = 0;
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (field != null && exist.ContainsKey(field))//如果包含Key
|
||||
{
|
||||
exist.Remove(field);//删除
|
||||
result++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual List<T> HashGet<T>(string key, params string[] fields)
|
||||
{
|
||||
var list = new List<T>();
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (exist.TryGetValue(field, out var data))//如果包含Key
|
||||
{
|
||||
list.Add(data);
|
||||
}
|
||||
else { list.Add(default); }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual T HashGetOne<T>(string key, string field)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
exist.TryGetValue(field, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual IDictionary<string, T> HashGetAll<T>(string key)
|
||||
{
|
||||
var data = GetDictionary<T>(key);
|
||||
return data;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void DelByPattern(string pattern)
|
||||
{
|
||||
var keys = Keys;//获取所有key
|
||||
foreach (var item in keys.ToList())
|
||||
{
|
||||
if (item.StartsWith(pattern))//如果匹配
|
||||
Remove(item);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
public abstract void HashAdd<T>(string key, string hashKey, T value);
|
||||
public abstract bool HashSet<T>(string key, Dictionary<string, T> dic);
|
||||
public abstract int HashDel<T>(string key, params string[] fields);
|
||||
public abstract List<T> HashGet<T>(string key, params string[] fields);
|
||||
public abstract T HashGetOne<T>(string key, string field);
|
||||
public abstract IDictionary<string, T> HashGetAll<T>(string key);
|
||||
public abstract long DelByPattern(string v);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public class CacheLock : DisposeBase
|
||||
/// <summary>
|
||||
/// 是否持有锁
|
||||
/// </summary>
|
||||
private Boolean _hasLock;
|
||||
private Boolean _hasLock = false;
|
||||
|
||||
/// <summary>键</summary>
|
||||
public String Key { get; set; }
|
||||
|
||||
@@ -202,7 +202,6 @@ public interface ICache
|
||||
IDisposable? AcquireLock(String key, Int32 msTimeout, Int32 msExpire, Boolean throwOnFailure);
|
||||
#endregion
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
#region 集合
|
||||
/// <inheritdoc/>
|
||||
public void HashAdd<T>(string key, string hashKey, T value);
|
||||
@@ -226,9 +225,8 @@ public interface ICache
|
||||
/// 按前缀删除
|
||||
/// </summary>
|
||||
/// <param name="v"></param>
|
||||
void DelByPattern(string v);
|
||||
long DelByPattern(string v);
|
||||
|
||||
#endregion
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Messaging;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
|
||||
namespace ThingsGateway.NewLife.Caching;
|
||||
@@ -34,7 +37,7 @@ public class MemoryCache : Cache
|
||||
|
||||
#region 静态默认实现
|
||||
/// <summary>默认缓存</summary>
|
||||
public static ICache Instance { get; set; } = new MemoryCache();
|
||||
public static MemoryCache Instance { get; set; } = new MemoryCache();
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
@@ -52,9 +55,10 @@ public class MemoryCache : Cache
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_clearTimer.TryDispose();
|
||||
_clearTimer?.TryDispose();
|
||||
_clearTimer = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 缓存属性
|
||||
@@ -68,7 +72,6 @@ public class MemoryCache : Cache
|
||||
|
||||
#region 方法
|
||||
|
||||
|
||||
#if !NET452
|
||||
|
||||
/// <summary>返回全部</summary>
|
||||
@@ -85,35 +88,6 @@ public class MemoryCache : Cache
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取或添加缓存项</summary>
|
||||
/// <typeparam name="T">值类型</typeparam>
|
||||
/// <param name="key">键</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <param name="expire">过期时间,秒</param>
|
||||
/// <returns></returns>
|
||||
public virtual T? GetOrAdd<T>(String key, T value, Int32 expire = -1)
|
||||
{
|
||||
if (expire < 0) expire = Expire;
|
||||
|
||||
CacheItem? item = null;
|
||||
do
|
||||
{
|
||||
if (_cache.TryGetValue(key, out item) && item != null)
|
||||
{
|
||||
if (!item.Expired) return item.Visit<T>();
|
||||
|
||||
item.Set(value, expire);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
item ??= new CacheItem(value, expire);
|
||||
} while (!_cache.TryAdd(key, item));
|
||||
|
||||
Interlocked.Increment(ref _count);
|
||||
|
||||
return item.Visit<T>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 基本操作
|
||||
@@ -444,6 +418,19 @@ public class MemoryCache : Cache
|
||||
throw new InvalidCastException($"Unable to convert the value of [{key}] from {item.TypeCode} to {typeof(ICollection<T>)}");
|
||||
}
|
||||
|
||||
/// <summary>获取事件总线,可发布消息或订阅消息</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="topic">事件主题</param>
|
||||
/// <param name="clientId">客户标识/消息分组</param>
|
||||
/// <returns></returns>
|
||||
public override IEventBus<T> GetEventBus<T>(String topic, String clientId = "")
|
||||
{
|
||||
var key = $"eventbus:{topic}";
|
||||
var item = GetOrAddItem(key, k => new QueueEventBus<T>(this, topic));
|
||||
return item.Visit<IEventBus<T>>() ??
|
||||
throw new InvalidCastException($"Unable to convert the value of [{topic}] from {item.TypeCode} to {typeof(IEventBus<T>)}");
|
||||
}
|
||||
|
||||
/// <summary>获取 或 添加 缓存项</summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="valueFactory"></param>
|
||||
@@ -750,6 +737,224 @@ public class MemoryCache : Cache
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 持久化
|
||||
private const String MAGIC = "NewLifeCache";
|
||||
private const Byte _Ver = 1;
|
||||
/// <summary>保存到数据流</summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
public void Save(Stream stream)
|
||||
{
|
||||
var bn = new Binary
|
||||
{
|
||||
Stream = stream,
|
||||
EncodeInt = true,
|
||||
};
|
||||
|
||||
// 头部,幻数、版本和标记
|
||||
bn.Write(MAGIC.GetBytes(), 0, MAGIC.Length);
|
||||
bn.Write(_Ver);
|
||||
bn.Write(0);
|
||||
|
||||
bn.WriteSize(_cache.Count);
|
||||
foreach (var item in _cache)
|
||||
{
|
||||
var ci = item.Value;
|
||||
|
||||
// Key+Expire+Empty
|
||||
// Key+Expire+TypeCode+Value
|
||||
// Key+Expire+TypeCode+Type+Length+Value
|
||||
bn.Write(item.Key);
|
||||
bn.Write((Int32)(ci.ExpiredTime / 1000));
|
||||
|
||||
var value = ci.Value;
|
||||
var type = value?.GetType();
|
||||
if (type == null)
|
||||
{
|
||||
bn.Write((Byte)TypeCode.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
var code = type.GetTypeCode();
|
||||
bn.Write((Byte)code);
|
||||
|
||||
if (code != TypeCode.Object)
|
||||
bn.Write(value);
|
||||
else
|
||||
{
|
||||
bn.Write(type.FullName);
|
||||
if (value != null) bn.Write(Binary.FastWrite(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从数据流加载</summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
public void Load(Stream stream)
|
||||
{
|
||||
var bn = new Binary
|
||||
{
|
||||
Stream = stream,
|
||||
EncodeInt = true,
|
||||
};
|
||||
|
||||
// 头部,幻数、版本和标记
|
||||
var magic = bn.ReadBytes(MAGIC.Length).ToStr();
|
||||
if (magic != MAGIC) throw new InvalidDataException();
|
||||
|
||||
var ver = bn.Read<Byte>();
|
||||
_ = bn.Read<Byte>();
|
||||
|
||||
// 版本兼容
|
||||
if (ver > _Ver) throw new InvalidDataException($"MemoryCache[ver={_Ver}] Unable to support newer versions [{ver}]");
|
||||
|
||||
var count = bn.ReadSize();
|
||||
while (count-- > 0)
|
||||
{
|
||||
// Key+Expire+Empty
|
||||
// Key+Expire+TypeCode+Value
|
||||
// Key+Expire+TypeCode+Type+Length+Value
|
||||
var key = bn.Read<String>();
|
||||
var exp = bn.Read<Int32>();
|
||||
var code = (TypeCode)bn.ReadByte();
|
||||
|
||||
Object? value = null;
|
||||
if (code == TypeCode.Empty)
|
||||
{
|
||||
}
|
||||
else if (code != TypeCode.Object)
|
||||
{
|
||||
var type = Type.GetType("System." + code);
|
||||
if (type != null) value = bn.Read(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
var typeName = bn.Read<String>();
|
||||
//var type = Type.GetType(typeName);
|
||||
var type = typeName?.GetTypeEx();
|
||||
|
||||
var pk = bn.Read<IPacket>();
|
||||
value = pk;
|
||||
if (type != null && pk != null)
|
||||
{
|
||||
var bn2 = new Binary() { Stream = pk.GetStream(), EncodeInt = true };
|
||||
value = bn2.Read(type);
|
||||
}
|
||||
}
|
||||
|
||||
if (key != null) Set(key, value, exp - (Int32)(Runtime.TickCount64 / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>保存到文件</summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="compressed"></param>
|
||||
/// <returns></returns>
|
||||
public Int64 Save(String file, Boolean compressed) => file.AsFile().OpenWrite(compressed, s => Save(s));
|
||||
|
||||
/// <summary>从文件加载</summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="compressed"></param>
|
||||
/// <returns></returns>
|
||||
public Int64 Load(String file, Boolean compressed) => file.AsFile().OpenRead(compressed, s => Load(s));
|
||||
#endregion
|
||||
|
||||
#region 集合
|
||||
/// <inheritdoc/>
|
||||
public override void HashAdd<T>(string key, string hashKey, T value)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
if (exist.ContainsKey(hashKey))//如果包含Key
|
||||
exist[hashKey] = value;//重新赋值
|
||||
else exist.TryAdd(hashKey, value);//加上新的值
|
||||
Set(key, exist);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool HashSet<T>(string key, Dictionary<string, T> dic)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var it in dic)
|
||||
{
|
||||
if (exist.ContainsKey(it.Key))//如果包含Key
|
||||
exist[it.Key] = it.Value;//重新赋值
|
||||
else exist.Add(it.Key, it.Value);//加上新的值
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int HashDel<T>(string key, params string[] fields)
|
||||
{
|
||||
var result = 0;
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (field != null && exist.ContainsKey(field))//如果包含Key
|
||||
{
|
||||
exist.Remove(field);//删除
|
||||
result++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override List<T> HashGet<T>(string key, params string[] fields)
|
||||
{
|
||||
var list = new List<T>();
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
foreach (var field in fields)
|
||||
{
|
||||
if (exist.TryGetValue(field, out var data))//如果包含Key
|
||||
{
|
||||
list.Add(data);
|
||||
}
|
||||
else { list.Add(default); }
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override T HashGetOne<T>(string key, string field)
|
||||
{
|
||||
//获取字典
|
||||
var exist = GetDictionary<T>(key);
|
||||
exist.TryGetValue(field, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDictionary<string, T> HashGetAll<T>(string key)
|
||||
{
|
||||
var data = GetDictionary<T>(key);
|
||||
return data;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public override long DelByPattern(string pattern)
|
||||
{
|
||||
var keys = Keys;//获取所有key
|
||||
long count = 0;
|
||||
foreach (var item in keys.ToList())
|
||||
{
|
||||
if (item.StartsWith(pattern))//如果匹配
|
||||
count += Remove(item);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>生产者消费者</summary>
|
||||
@@ -881,4 +1086,4 @@ public class MemoryQueue<T> : DisposeBase, IProducerConsumer<T>
|
||||
/// <param name="keys"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 Acknowledge(params String[] keys) => 0;
|
||||
}
|
||||
}
|
||||
97
src/Admin/ThingsGateway.NewLife.X/Caching/QueueEventBus.cs
Normal file
97
src/Admin/ThingsGateway.NewLife.X/Caching/QueueEventBus.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Messaging;
|
||||
|
||||
namespace ThingsGateway.NewLife.Caching;
|
||||
|
||||
/// <summary>消息队列事件总线。通过消息队列来发布和订阅消息</summary>
|
||||
/// <remarks>实例化消息队列事件总线</remarks>
|
||||
public class QueueEventBus<TEvent>(ICache cache, String topic) : EventBus<TEvent>
|
||||
{
|
||||
private IProducerConsumer<TEvent>? _queue;
|
||||
private CancellationTokenSource? _source;
|
||||
|
||||
/// <summary>销毁</summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(Boolean disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_source?.TryDispose();
|
||||
}
|
||||
|
||||
/// <summary>初始化</summary>
|
||||
[MemberNotNull(nameof(_queue))]
|
||||
protected virtual void Init()
|
||||
{
|
||||
if (_queue != null) return;
|
||||
|
||||
_queue = cache.GetQueue<TEvent>(topic);
|
||||
}
|
||||
|
||||
/// <summary>发布消息到消息队列</summary>
|
||||
/// <param name="event">事件</param>
|
||||
/// <param name="context">上下文</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
public override Task<Int32> PublishAsync(TEvent @event, IEventContext<TEvent>? context = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Init();
|
||||
var rs = _queue.Add(@event);
|
||||
|
||||
return Task.FromResult(rs);
|
||||
}
|
||||
|
||||
/// <summary>订阅消息。启动大循环,从消息队列订阅消息,再分发到本地订阅者</summary>
|
||||
/// <param name="handler">处理器</param>
|
||||
/// <param name="clientId">客户标识。每个客户只能订阅一次,重复订阅将会挤掉前一次订阅</param>
|
||||
public override Boolean Subscribe(IEventHandler<TEvent> handler, String clientId = "")
|
||||
{
|
||||
if (_source == null)
|
||||
{
|
||||
var source = new CancellationTokenSource();
|
||||
if (Interlocked.CompareExchange(ref _source, source, null) == null)
|
||||
{
|
||||
Init();
|
||||
_ = Task.Run(() => ConsumeMessage(_source));
|
||||
}
|
||||
}
|
||||
|
||||
return base.Subscribe(handler, clientId);
|
||||
}
|
||||
|
||||
/// <summary>从队列中消费消息,经事件总线送给设备会话</summary>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task ConsumeMessage(CancellationTokenSource source)
|
||||
{
|
||||
DefaultSpan.Current = null;
|
||||
var cancellationToken = source.Token;
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var msg = await _queue!.TakeOneAsync(15, cancellationToken).ConfigureAwait(false);
|
||||
if (msg != null)
|
||||
{
|
||||
// 发布到事件总线
|
||||
await DispatchAsync(msg, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(1_000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (OperationCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
150
src/Admin/ThingsGateway.NewLife.X/Caching/Readme.MD
Normal file
150
src/Admin/ThingsGateway.NewLife.X/Caching/Readme.MD
Normal file
@@ -0,0 +1,150 @@
|
||||
缓存架构以ICache接口为核心,包括MemoryCache、Redis和DbCache实现!
|
||||
后续例程与使用说明均以Redis为例,各缓存实现类似。
|
||||
|
||||
### 内存缓存 MemoryCache
|
||||
MemoryCache核心是并发字典ConcurrentDictionary,由于省去了序列化和网络通信,使得它具有千万级超高性能。
|
||||
MemoryCache支持过期时间,默认容量10万个,未过期key超过该值后,每60秒根据LRU清理溢出部分。
|
||||
常用于进程内千万级以下数据缓存场景。
|
||||
|
||||
```csharp
|
||||
// 缓存默认实现Cache.Default是MemoryCache,可修改
|
||||
//var ic = Cache.Default;
|
||||
//var ic = new MemoryCache();
|
||||
```
|
||||
|
||||
### 基础 Redis
|
||||
Redis实现标准协议以及基础字符串操作,完整实现由独立开源项目[NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis)提供。
|
||||
采取连接池加同步阻塞架构,具有超低延迟(200~600us)以及超高吞吐量的特点。
|
||||
在物流行业大数据实时计算中广泛应有,经过日均100亿次调用量验证。
|
||||
|
||||
```csharp
|
||||
// 实例化Redis,默认端口6379可以省略,密码有两种写法
|
||||
//var ic = new Redis("127.0.0.1", 7);
|
||||
var ic = new Redis("pass@127.0.0.1:6379", 7);
|
||||
//var ic = new Redis("server=127.0.0.1:6379;password=pass", 7);
|
||||
ic.Log = XTrace.Log; // 调试日志。正式使用时注释
|
||||
```
|
||||
|
||||
### 数据库 DbCache
|
||||
DbCache属于实验性质,采用数据库存储数据,默认SQLite。
|
||||
|
||||
### 基本操作
|
||||
在基本操作之前,我们先做一些准备工作:
|
||||
+ 新建控制台项目,并在入口函数开头加上 `XTrace.UseConsole();` ,这是为了方便查看调试日志
|
||||
+ 具体测试代码之前,需要加上前面MemoryCache或Redis的实例化代码
|
||||
+ 准备一个模型类User
|
||||
```csharp
|
||||
class User
|
||||
{
|
||||
public String Name { get; set; }
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
添删改查:
|
||||
```csharp
|
||||
var user = new User { Name = "NewLife", CreateTime = DateTime.Now };
|
||||
ic.Set("user", user, 3600);
|
||||
var user2 = ic.Get<User>("user");
|
||||
XTrace.WriteLine("Json: {0}", ic.Get<String>("user"));
|
||||
if (ic.ContainsKey("user")) XTrace.WriteLine("存在!");
|
||||
ic.Remove("user");
|
||||
```
|
||||
|
||||
执行结果:
|
||||
```csharp
|
||||
14:14:25.990 1 N - SELECT 7
|
||||
14:14:25.992 1 N - => OK
|
||||
14:14:26.008 1 N - SETEX user 3600 [53]
|
||||
14:14:26.021 1 N - => OK
|
||||
14:14:26.042 1 N - GET user
|
||||
14:14:26.048 1 N - => [53]
|
||||
14:14:26.064 1 N - GET user
|
||||
14:14:26.065 1 N - => [53]
|
||||
14:14:26.066 1 N - Json: {"Name":"NewLife","CreateTime":"2018-09-25 14:14:25"}
|
||||
14:14:26.067 1 N - EXISTS user
|
||||
14:14:26.068 1 N - => 1
|
||||
14:14:26.068 1 N - 存在!
|
||||
14:14:26.069 1 N - DEL user
|
||||
14:14:26.070 1 N - => 1
|
||||
```
|
||||
|
||||
保存复杂对象时,默认采用Json序列化,所以上面可以按字符串把结果取回来,发现正是Json字符串。
|
||||
Redis的strings,实质上就是带有长度前缀的二进制数据,[53]表示一段53字节长度的二进制数据。
|
||||
|
||||
### 集合操作
|
||||
GetAll/SetAll 在Redis上是很常用的批量操作,同时获取或设置多个key,一般有10倍以上吞吐量。
|
||||
|
||||
批量操作:
|
||||
```csharp
|
||||
var dic = new Dictionary<String, Object>
|
||||
{
|
||||
["name"] = "NewLife",
|
||||
["time"] = DateTime.Now,
|
||||
["count"] = 1234
|
||||
};
|
||||
ic.SetAll(dic, 120);
|
||||
|
||||
var vs = ic.GetAll<String>(dic.Keys);
|
||||
XTrace.WriteLine(vs.Join(",", e => $"{e.Key}={e.Value}"));
|
||||
```
|
||||
|
||||
执行结果:
|
||||
```csharp
|
||||
MSET name NewLife time 2018-09-25 15:56:26 count 1234
|
||||
=> OK
|
||||
EXPIRE name 120
|
||||
EXPIRE time 120
|
||||
EXPIRE count 120
|
||||
MGET name time count
|
||||
name=NewLife,time=2018-09-25 15:56:26,count=1234
|
||||
```
|
||||
|
||||
集合操作里面还有 `GetList/GetDictionary/GetQueue/GetSet` 四个类型集合,分别代表Redis的列表、哈希、队列、Set集合等。
|
||||
基础版Redis不支持这四个集合,完整版[NewLife.Redis](https://github.com/NewLifeX/NewLife.Redis)支持,MemoryCache则直接支持。
|
||||
|
||||
### 高级操作
|
||||
+ Add 添加,当key不存在时添加,已存在时返回false。
|
||||
+ Replace 替换,替换已有值为新值,返回旧值。
|
||||
+ Increment 累加,原子操作
|
||||
+ Decrement 递减,原子操作
|
||||
|
||||
高级操作:
|
||||
```csharp
|
||||
var flag = ic.Add("count", 5678);
|
||||
XTrace.WriteLine(flag ? "Add成功" : "Add失败");
|
||||
var ori = ic.Replace("count", 777);
|
||||
var count = ic.Get<Int32>("count");
|
||||
XTrace.WriteLine("count由{0}替换为{1}", ori, count);
|
||||
|
||||
ic.Increment("count", 11);
|
||||
var count2 = ic.Decrement("count", 10);
|
||||
XTrace.WriteLine("count={0}", count2);
|
||||
```
|
||||
|
||||
执行结果:
|
||||
```csharp
|
||||
SETNX count 5678
|
||||
=> 0
|
||||
Add失败
|
||||
GETSET count 777
|
||||
=> 1234
|
||||
GET count
|
||||
=> 777
|
||||
count由1234替换为777
|
||||
INCRBY count 11
|
||||
=> 788
|
||||
DECRBY count 10
|
||||
=> 778
|
||||
count=778
|
||||
```
|
||||
|
||||
### 性能测试
|
||||
Bench 会分根据线程数分多组进行添删改压力测试。
|
||||
rand 参数,是否随机产生key/value。
|
||||
batch 批大小,分批执行读写操作,借助GetAll/SetAll进行优化。
|
||||
|
||||
Redis默认设置AutoPipeline=100,无分批时打开管道操作,对添删改优化。
|
||||
|
||||
### 容器化部署
|
||||
支持从环境变量 `Redis_{Name}` 中加载配置,用于容器化部署。
|
||||
@@ -0,0 +1,248 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
#if NETCOREAPP
|
||||
using System.Text.Json;
|
||||
#endif
|
||||
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
/// <summary>集合扩展</summary>
|
||||
public static class CollectionHelper
|
||||
{
|
||||
|
||||
/// <summary>集合转为数组,加锁确保安全</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="collection"></param>
|
||||
/// <returns></returns>
|
||||
public static T[] ToArray<T>(this ICollection<T> collection)
|
||||
{
|
||||
//if (collection == null) return null;
|
||||
|
||||
lock (collection)
|
||||
{
|
||||
var count = collection.Count;
|
||||
if (count == 0) return [];
|
||||
|
||||
var arr = new T[count];
|
||||
collection.CopyTo(arr, 0);
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>集合转为数组</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<TKey> ToKeyArray<TKey, TValue>(this IDictionary<TKey, TValue> collection, Int32 index = 0) where TKey : notnull
|
||||
{
|
||||
//if (collection == null) return null;
|
||||
|
||||
if (collection is ConcurrentDictionary<TKey, TValue> cdiv && cdiv.Keys is IList<TKey> list) return list;
|
||||
|
||||
if (collection.Count == 0) return [];
|
||||
lock (collection)
|
||||
{
|
||||
var arr = new TKey[collection.Count - index];
|
||||
collection.Keys.CopyTo(arr, index);
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>集合转为数组</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<TValue> ToValueArray<TKey, TValue>(this IDictionary<TKey, TValue> collection, Int32 index = 0) where TKey : notnull
|
||||
{
|
||||
//if (collection == null) return null;
|
||||
|
||||
//if (collection is ConcurrentDictionary<TKey, TValue> cdiv) return cdiv.Values as IList<TValue>;
|
||||
if (collection is ConcurrentDictionary<TKey, TValue> cdiv && cdiv.Values is IList<TValue> list) return list;
|
||||
|
||||
if (collection.Count == 0) return [];
|
||||
lock (collection)
|
||||
{
|
||||
var arr = new TValue[collection.Count - index];
|
||||
collection.Values.CopyTo(arr, index);
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>目标匿名参数对象转为名值字典</summary>
|
||||
/// <param name="source"></param>
|
||||
/// <returns></returns>
|
||||
public static IDictionary<String, Object?> ToDictionary(this Object source)
|
||||
{
|
||||
//!! 即使传入为空,也返回字典,而不是null,避免业务层需要大量判空
|
||||
//if (target == null) return null;
|
||||
#pragma warning disable CA1859 // 尽可能使用具体类型以提高性能
|
||||
if (source is IDictionary<String, Object?> dic) return dic;
|
||||
#pragma warning restore CA1859 // 尽可能使用具体类型以提高性能
|
||||
var type = source?.GetType();
|
||||
if (type?.IsBaseType() == true)
|
||||
throw new InvalidDataException("source is not Object");
|
||||
|
||||
dic = new NullableDictionary<String, Object?>(StringComparer.OrdinalIgnoreCase);
|
||||
if (source != null)
|
||||
{
|
||||
// 修正字符串字典的支持问题
|
||||
if (source is IDictionary dic2)
|
||||
{
|
||||
foreach (var item in dic2)
|
||||
{
|
||||
if (item is DictionaryEntry de)
|
||||
dic[de.Key + ""] = de.Value;
|
||||
}
|
||||
}
|
||||
#if NETCOREAPP
|
||||
else if (source is JsonElement element && element.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var item in element.EnumerateObject())
|
||||
{
|
||||
Object? v = item.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => item.Value.ToDictionary(),
|
||||
JsonValueKind.Array => ToArray(item.Value),
|
||||
JsonValueKind.String => item.Value.GetString(),
|
||||
JsonValueKind.Number when item.Value.GetRawText().Contains('.') => item.Value.GetDouble(),
|
||||
JsonValueKind.Number => item.Value.GetInt64(),
|
||||
JsonValueKind.True or JsonValueKind.False => item.Value.GetBoolean(),
|
||||
_ => item.Value.GetString(),
|
||||
};
|
||||
if (v is Int64 n && n < Int32.MaxValue) v = (Int32)n;
|
||||
dic[item.Name] = v;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
foreach (var pi in source.GetType().GetProperties(true))
|
||||
{
|
||||
var name = SerialHelper.GetName(pi);
|
||||
if (source is IModel src)
|
||||
dic[name] = src[name];
|
||||
else
|
||||
dic[name] = source.GetValue(pi);
|
||||
}
|
||||
|
||||
// 增加扩展属性
|
||||
if (source is IExtend ext && ext.Items != null)
|
||||
{
|
||||
foreach (var item in ext.Items)
|
||||
{
|
||||
dic[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
#if NETCOREAPP
|
||||
/// <summary>Json对象转为数组</summary>
|
||||
/// <param name="element"></param>
|
||||
/// <returns></returns>
|
||||
public static IList<Object?> ToArray(this JsonElement element)
|
||||
{
|
||||
var list = new List<Object?>();
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
Object? v = item.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => item.ToDictionary(),
|
||||
JsonValueKind.Array => ToArray(item),
|
||||
JsonValueKind.String => item.GetString(),
|
||||
JsonValueKind.Number when item.GetRawText().Contains('.') => item.GetDouble(),
|
||||
JsonValueKind.Number => item.GetInt64(),
|
||||
JsonValueKind.True or JsonValueKind.False => item.GetBoolean(),
|
||||
_ => item.GetString(),
|
||||
};
|
||||
if (v is Int64 n && n < Int32.MaxValue) v = (Int32)n;
|
||||
list.Add(v);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>合并字典参数</summary>
|
||||
/// <param name="dic">字典</param>
|
||||
/// <param name="target">目标对象</param>
|
||||
/// <param name="overwrite">是否覆盖同名参数</param>
|
||||
/// <param name="excludes">排除项</param>
|
||||
/// <returns></returns>
|
||||
public static IDictionary<String, Object?> Merge(this IDictionary<String, Object?> dic, Object target, Boolean overwrite = true, String[]? excludes = null)
|
||||
{
|
||||
if (target?.GetType().IsBaseType() != false) return dic;
|
||||
|
||||
var exs = excludes != null ? new HashSet<String>(excludes, StringComparer.OrdinalIgnoreCase) : null;
|
||||
foreach (var item in target.ToDictionary())
|
||||
{
|
||||
if (exs?.Contains(item.Key) != true)
|
||||
{
|
||||
if (overwrite || !dic.ContainsKey(item.Key)) dic[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
/// <summary>转为可空字典</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <param name="collection"></param>
|
||||
/// <param name="comparer"></param>
|
||||
/// <returns></returns>
|
||||
public static IDictionary<TKey, TValue> ToNullable<TKey, TValue>(this IDictionary<TKey, TValue> collection, IEqualityComparer<TKey>? comparer = null) where TKey : notnull
|
||||
{
|
||||
//if (collection == null) return null;
|
||||
|
||||
if (collection is NullableDictionary<TKey, TValue> dic && (comparer == null || dic.Comparer == comparer)) return dic;
|
||||
|
||||
if (comparer == null)
|
||||
return new NullableDictionary<TKey, TValue>(collection);
|
||||
else
|
||||
return new NullableDictionary<TKey, TValue>(collection, comparer);
|
||||
}
|
||||
|
||||
/// <summary>从队列里面获取指定个数元素</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="collection">消费集合</param>
|
||||
/// <param name="count">元素个数</param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<T> Take<T>(this Queue<T> collection, Int32 count)
|
||||
{
|
||||
if (collection == null) yield break;
|
||||
|
||||
while (count-- > 0 && collection.Count > 0)
|
||||
{
|
||||
yield return collection.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从消费集合里面获取指定个数元素</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="collection">消费集合</param>
|
||||
/// <param name="count">元素个数</param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<T> Take<T>(this IProducerConsumerCollection<T> collection, Int32 count)
|
||||
{
|
||||
if (collection == null) yield break;
|
||||
|
||||
while (count-- > 0 && collection.TryTake(out var item))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/Admin/ThingsGateway.NewLife.X/Collections/ICluster.cs
Normal file
76
src/Admin/ThingsGateway.NewLife.X/Collections/ICluster.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace ThingsGateway.NewLife.Collections
|
||||
{
|
||||
/// <summary>集群管理</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
public interface ICluster<TKey, TValue>
|
||||
{
|
||||
/// <summary>最后使用资源</summary>
|
||||
KeyValuePair<TKey, TValue> Current { get; }
|
||||
|
||||
/// <summary>资源列表</summary>
|
||||
Func<IEnumerable<TKey>> GetItems { get; set; }
|
||||
|
||||
/// <summary>打开</summary>
|
||||
Boolean Open();
|
||||
|
||||
/// <summary>关闭</summary>
|
||||
/// <param name="reason">关闭原因。便于日志分析</param>
|
||||
/// <returns>是否成功</returns>
|
||||
Boolean Close(String reason);
|
||||
|
||||
/// <summary>从集群中获取资源</summary>
|
||||
/// <returns></returns>
|
||||
TValue Get();
|
||||
|
||||
/// <summary>归还</summary>
|
||||
/// <param name="value"></param>
|
||||
Boolean Put(TValue value);
|
||||
}
|
||||
|
||||
/// <summary>集群助手</summary>
|
||||
public static class ClusterHelper
|
||||
{
|
||||
/// <summary>借助集群资源处理事务</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="cluster"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
public static TResult Invoke<TKey, TValue, TResult>(this ICluster<TKey, TValue> cluster, Func<TValue, TResult> func)
|
||||
{
|
||||
var item = default(TValue);
|
||||
try
|
||||
{
|
||||
item = cluster.Get();
|
||||
return func(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cluster.Put(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>借助集群资源处理事务</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
/// <typeparam name="TResult"></typeparam>
|
||||
/// <param name="cluster"></param>
|
||||
/// <param name="func"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<TResult> InvokeAsync<TKey, TValue, TResult>(this ICluster<TKey, TValue> cluster, Func<TValue, Task<TResult>> func)
|
||||
{
|
||||
var item = default(TValue);
|
||||
try
|
||||
{
|
||||
item = cluster.Get();
|
||||
return await func(item).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
cluster.Put(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace ThingsGateway.NewLife.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// 字典数据源接口。定义该模型类支持输出名值字典,便于序列化传输
|
||||
/// </summary>
|
||||
public interface IDictionarySource
|
||||
{
|
||||
/// <summary>
|
||||
/// 把对象转为名值字典,便于序列化传输
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IDictionary<String, Object?> ToDictionary();
|
||||
}
|
||||
@@ -94,7 +94,7 @@ public static class Pool
|
||||
{
|
||||
//if (ms == null) return null;
|
||||
|
||||
var buf = returnResult ? ms.ToArray() : Array.Empty<byte>();
|
||||
var buf = returnResult ? ms.ToArray() : Empty;
|
||||
|
||||
Pool.MemoryStream.Return(ms);
|
||||
|
||||
@@ -132,5 +132,11 @@ public static class Pool
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ByteArray
|
||||
/// <summary>字节数组共享存储</summary>
|
||||
public static ArrayPool<Byte> Shared { get; set; } = ArrayPool<Byte>.Shared;
|
||||
|
||||
/// <summary>空数组</summary>
|
||||
public static Byte[] Empty { get; } = [];
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -151,4 +151,4 @@ public class Pool<T> : IPool<T> where T : class
|
||||
/// <returns></returns>
|
||||
protected virtual T? OnCreate() => typeof(T).CreateInstance() as T;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
|
||||
namespace ThingsGateway.NewLife.Collections
|
||||
{
|
||||
/// <summary>主动式消息服务</summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public interface IQueueService<T>
|
||||
{
|
||||
/// <summary>发布消息</summary>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="value">消息</param>
|
||||
/// <returns></returns>
|
||||
Int32 Public(String topic, T value);
|
||||
|
||||
/// <summary>订阅</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
Boolean Subscribe(String clientId, String topic);
|
||||
|
||||
/// <summary>取消订阅</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
Boolean UnSubscribe(String clientId, String topic);
|
||||
|
||||
/// <summary>消费消息</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="count">要拉取的消息数</param>
|
||||
/// <returns></returns>
|
||||
T[] Consume(String clientId, String topic, Int32 count);
|
||||
}
|
||||
|
||||
/// <summary>轻量级主动式消息服务</summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public class QueueService<T> : IQueueService<T>
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>数据存储</summary>
|
||||
public ICache Cache { get; set; } = MemoryCache.Instance;
|
||||
|
||||
/// <summary>每个主题的所有订阅者</summary>
|
||||
private readonly ConcurrentDictionary<String, ConcurrentDictionary<String, IProducerConsumer<T>>> _topics = new();
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>发布消息</summary>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="value">消息</param>
|
||||
/// <returns></returns>
|
||||
public Int32 Public(String topic, T value)
|
||||
{
|
||||
var rs = 0;
|
||||
if (_topics.TryGetValue(topic, out var clients))
|
||||
{
|
||||
// 向每个订阅者推送
|
||||
foreach (var item in clients)
|
||||
{
|
||||
var queue = item.Value;
|
||||
rs += queue.Add(new[] { value });
|
||||
}
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>订阅</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
public Boolean Subscribe(String clientId, String topic)
|
||||
{
|
||||
var dic = _topics.GetOrAdd(topic, k => new ConcurrentDictionary<String, IProducerConsumer<T>>());
|
||||
if (dic.ContainsKey(clientId)) return false;
|
||||
|
||||
// 创建队列
|
||||
var queue = Cache.GetQueue<T>($"{topic}_{clientId}");
|
||||
return dic.TryAdd(clientId, queue);
|
||||
}
|
||||
|
||||
/// <summary>取消订阅</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
public Boolean UnSubscribe(String clientId, String topic)
|
||||
{
|
||||
if (_topics.TryGetValue(topic, out var clients))
|
||||
{
|
||||
return clients.TryRemove(clientId, out _);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>消费消息</summary>
|
||||
/// <param name="clientId">客户标识</param>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="count"></param>
|
||||
/// <returns></returns>
|
||||
public T[] Consume(String clientId, String topic, Int32 count)
|
||||
{
|
||||
if (_topics.TryGetValue(topic, out var clients))
|
||||
{
|
||||
if (clients.TryGetValue(clientId, out var queue))
|
||||
{
|
||||
return queue.Take(count).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
|
||||
namespace ThingsGateway.NewLife.Extension;
|
||||
|
||||
/// <summary>工具类</summary>
|
||||
@@ -175,7 +177,7 @@ public static class ConvertUtility
|
||||
public class DefaultConvert
|
||||
{
|
||||
private static readonly DateTime _dt1970 = new(1970, 1, 1);
|
||||
private static readonly DateTimeOffset _dto1970 = new(new DateTime(1970, 1, 1));
|
||||
private static readonly DateTimeOffset _dto1970 = new(new DateTime(1970, 1, 1), TimeSpan.Zero);
|
||||
private static readonly Int64 _maxSeconds = (Int64)(DateTime.MaxValue - DateTime.MinValue).TotalSeconds;
|
||||
private static readonly Int64 _maxMilliseconds = (Int64)(DateTime.MaxValue - DateTime.MinValue).TotalMilliseconds;
|
||||
|
||||
@@ -338,7 +340,6 @@ public class DefaultConvert
|
||||
}
|
||||
}
|
||||
|
||||
//暂时不做处理 先处理异常转换
|
||||
try
|
||||
{
|
||||
// 转换接口
|
||||
@@ -450,12 +451,12 @@ public class DefaultConvert
|
||||
// 凑够8字节
|
||||
if (buf.Length < 8)
|
||||
{
|
||||
var bts = ArrayPool<Byte>.Shared.Rent(8);
|
||||
var bts = Pool.Shared.Rent(8);
|
||||
Buffer.BlockCopy(buf, 0, bts, 0, buf.Length);
|
||||
|
||||
var dec = BitConverter.ToDouble(bts, 0).ToDecimal();
|
||||
|
||||
ArrayPool<Byte>.Shared.Return(bts);
|
||||
Pool.Shared.Return(bts);
|
||||
|
||||
return dec;
|
||||
}
|
||||
@@ -500,16 +501,35 @@ public class DefaultConvert
|
||||
}
|
||||
|
||||
// 特殊处理字符串,也是最常见的
|
||||
var str = value.ToString().Trim();
|
||||
if (str.IsNullOrEmpty()) return defaultValue;
|
||||
if (value is String str)
|
||||
{
|
||||
str = str.Trim();
|
||||
if (str.IsNullOrEmpty()) return defaultValue;
|
||||
|
||||
if (Boolean.TryParse(str, out var b)) return b;
|
||||
if (Boolean.TryParse(str, out var b)) return b;
|
||||
|
||||
if (String.Equals(str, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (String.Equals(str, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
if (String.Equals(str, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (String.Equals(str, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
if (Int32.TryParse(str, out var n)) return n != 0;
|
||||
return Int32.TryParse(str, out var n) ? n != 0 : defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
var str2 = value.ToString()?.Trim();
|
||||
if (!str2.IsNullOrEmpty() && Boolean.TryParse(str2, out var n2))
|
||||
{
|
||||
return n2;
|
||||
}
|
||||
|
||||
if (String.Equals(str2, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (String.Equals(str2, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
|
||||
|
||||
if (!str2.IsNullOrEmpty() && Int32.TryParse(str2, out var n3))
|
||||
{
|
||||
return n3 != 0;
|
||||
}
|
||||
|
||||
}
|
||||
try
|
||||
{
|
||||
// 转换接口
|
||||
@@ -519,9 +539,7 @@ public class DefaultConvert
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 转字符串再转整数,作为兜底方案
|
||||
var str2 = value.ToString();
|
||||
return !str2.IsNullOrEmpty() && Boolean.TryParse(str2.Trim(), out var n2) ? n2 : defaultValue;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>转为时间日期,转换失败时返回最小时间。支持字符串、整数(Unix秒)</summary>
|
||||
@@ -672,12 +690,14 @@ public class DefaultConvert
|
||||
// 去掉逗号分隔符
|
||||
var ch = input[i];
|
||||
if (ch == ',' || ch == '_' || ch == ' ') continue;
|
||||
|
||||
// 支持前缀正号。Redis响应中就会返回带正号的整数
|
||||
if (ch == '+')
|
||||
{
|
||||
if (idx == 0) continue;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 支持负数
|
||||
if (ch == '-' && idx > 0) return 0;
|
||||
|
||||
@@ -696,7 +716,7 @@ public class DefaultConvert
|
||||
return idx;
|
||||
}
|
||||
|
||||
/// <summary>去掉时间日期指定位置后面部分,可指定毫秒ms、秒s、分m、小时h</summary>
|
||||
/// <summary>去掉时间日期指定位置后面部分,可指定毫秒ms、秒s、分m、小时h、纳秒ns</summary>
|
||||
/// <param name="value">时间日期</param>
|
||||
/// <param name="format">格式字符串,默认s格式化到秒,ms格式化到毫秒</param>
|
||||
/// <returns></returns>
|
||||
@@ -706,6 +726,7 @@ public class DefaultConvert
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
"us" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond, value.Kind),
|
||||
"ns" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond / 100 * 100, value.Kind),
|
||||
#endif
|
||||
"ms" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Kind),
|
||||
"s" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Kind),
|
||||
@@ -907,9 +928,10 @@ public class DefaultConvert
|
||||
/// <returns></returns>
|
||||
public virtual String GetMessage(Exception ex)
|
||||
{
|
||||
// 部分异常ToString可能报错,例如System.Data.SqlClient.SqlException
|
||||
try
|
||||
{
|
||||
var msg = ex + "";
|
||||
var msg = ex + string.Empty;
|
||||
if (msg.IsNullOrEmpty()) return ex.Message;
|
||||
|
||||
var ss = msg.Split(Environment.NewLine);
|
||||
|
||||
@@ -3,7 +3,8 @@ using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
#nullable enable
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.NewLife;
|
||||
|
||||
/// <summary>具有是否已释放和释放后事件的接口</summary>
|
||||
@@ -94,7 +95,7 @@ public abstract class DisposeBase : IDisposable2
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NewLife.Log.XTrace.WriteException(ex);
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@@ -150,5 +151,4 @@ public static class DisposeHelper
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
}
|
||||
@@ -69,4 +69,4 @@ public class Gen2GcCallback : CriticalFinalizerObject
|
||||
}
|
||||
GC.ReRegisterForFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
@@ -9,9 +7,11 @@ using System.Runtime.Versioning;
|
||||
using System.Security;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Json.Extension;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Model;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
using ThingsGateway.NewLife.Windows;
|
||||
|
||||
#if NETFRAMEWORK
|
||||
@@ -42,7 +42,7 @@ public interface IMachineInfo
|
||||
///
|
||||
/// 刷新信息成本较高,建议采用单例模式
|
||||
/// </remarks>
|
||||
public class MachineInfo
|
||||
public class MachineInfo : IExtend
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>系统名称</summary>
|
||||
@@ -116,6 +116,13 @@ public class MachineInfo
|
||||
[DisplayName("电池剩余")]
|
||||
public Double Battery { get; set; }
|
||||
|
||||
private readonly Dictionary<String, Object?> _items = [];
|
||||
IDictionary<String, Object?> IExtend.Items => _items;
|
||||
|
||||
/// <summary>获取 或 设置 扩展属性数据</summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public Object? this[String key] { get => _items.TryGetValue(key, out var obj) ? obj : null; set => _items[key] = value; }
|
||||
#endregion
|
||||
|
||||
#region 全局静态
|
||||
@@ -127,28 +134,44 @@ public class MachineInfo
|
||||
|
||||
//static MachineInfo() => RegisterAsync().Wait(100);
|
||||
|
||||
private static Task<MachineInfo>? _task;
|
||||
/// <summary>异步注册一个初始化后的机器信息实例</summary>
|
||||
/// <returns></returns>
|
||||
public static MachineInfo Register()
|
||||
public static Task<MachineInfo> RegisterAsync()
|
||||
{
|
||||
if (Current != null) return Current;
|
||||
if (_task != null) return _task;
|
||||
|
||||
return _task = Task.Factory.StartNew(() =>
|
||||
{
|
||||
return Register();
|
||||
});
|
||||
}
|
||||
|
||||
private static MachineInfo Register()
|
||||
{
|
||||
var set = Setting.Current;
|
||||
var dataPath = set.DataPath;
|
||||
if (dataPath.IsNullOrEmpty()) dataPath = "Data";
|
||||
|
||||
// 文件缓存,加快机器信息获取。在Linux下,可能StarAgent以root权限写入缓存文件,其它应用以普通用户访问
|
||||
var file = Path.GetTempPath().CombinePath("machine_info.json");
|
||||
var json = "";
|
||||
var file2 = dataPath.CombinePath("machine_info.json").GetBasePath();
|
||||
var json = string.Empty;
|
||||
if (Current == null)
|
||||
{
|
||||
var f = file;
|
||||
if (!File.Exists(f)) f = file2;
|
||||
if (File.Exists(f))
|
||||
{
|
||||
try
|
||||
{
|
||||
//XTrace.WriteLine("Load MachineInfo {0}", f);
|
||||
json = File.ReadAllText(f);
|
||||
Current = json.FromJsonNetString<MachineInfo>();
|
||||
Current = json.ToJsonEntity<MachineInfo>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,27 +181,33 @@ public class MachineInfo
|
||||
mi.Init();
|
||||
Current = mi;
|
||||
|
||||
// 注册到对象容器
|
||||
ObjectContainer.Current.AddSingleton(mi);
|
||||
|
||||
try
|
||||
{
|
||||
var json2 = mi.ToJsonNetString();
|
||||
var json2 = mi.ToJson(true);
|
||||
if (json != json2)
|
||||
{
|
||||
File.WriteAllText(file2.EnsureDirectory(true), json2);
|
||||
File.WriteAllText(file.EnsureDirectory(true), json2);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
|
||||
return mi;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>获取当前信息,如果未设置则等待异步注册结果</summary>
|
||||
/// <returns></returns>
|
||||
public static MachineInfo GetCurrent() => Current ?? Register();
|
||||
|
||||
/// <summary>从对象容器中获取一个已注册机器信息实例</summary>
|
||||
/// <returns></returns>
|
||||
public static MachineInfo? Resolve() => ObjectContainer.Current.Resolve<MachineInfo>();
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
@@ -186,9 +215,9 @@ public class MachineInfo
|
||||
public void Init()
|
||||
{
|
||||
var osv = Environment.OSVersion;
|
||||
if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + "";
|
||||
if (OSVersion.IsNullOrEmpty()) OSVersion = osv.Version + string.Empty;
|
||||
if (OSName.IsNullOrEmpty()) OSName = (osv + "").TrimStart("Microsoft").TrimEnd(OSVersion).Trim();
|
||||
if (Guid.IsNullOrEmpty()) Guid = "";
|
||||
if (Guid.IsNullOrEmpty()) Guid = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -210,7 +239,7 @@ public class MachineInfo
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
|
||||
// 裁剪不可见字符,顺带去掉两头空白
|
||||
@@ -234,7 +263,7 @@ public class MachineInfo
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,43 +272,43 @@ public class MachineInfo
|
||||
#endif
|
||||
private void LoadWindowsInfo()
|
||||
{
|
||||
var str = "";
|
||||
var str = string.Empty;
|
||||
|
||||
// 从注册表读取 MachineGuid
|
||||
#if NETFRAMEWORK || NET6_0_OR_GREATER
|
||||
using var Cryptography = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
||||
if (Cryptography != null) str = Cryptography.GetValue("MachineGuid") + "";
|
||||
var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
||||
if (reg != null) str = reg.GetValue("MachineGuid") + string.Empty;
|
||||
if (str.IsNullOrEmpty())
|
||||
{
|
||||
using var Registry64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
||||
using var Registry64Cryptography = Registry64?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
||||
if (Registry64Cryptography != null) str = Registry64Cryptography.GetValue("MachineGuid") + "";
|
||||
reg = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
||||
reg = reg?.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography");
|
||||
if (reg != null) str = reg.GetValue("MachineGuid") + string.Empty;
|
||||
}
|
||||
|
||||
if (!str.IsNullOrEmpty()) Guid = str;
|
||||
|
||||
using var HardwareConfig = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
|
||||
if (HardwareConfig != null)
|
||||
reg = Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig");
|
||||
if (reg != null)
|
||||
{
|
||||
str = (HardwareConfig.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
|
||||
str = (reg.GetValue("LastConfig") + "")?.Trim('{', '}').ToUpper();
|
||||
|
||||
// UUID取不到时返回 FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
|
||||
if (!str.IsNullOrEmpty() && !str.EqualIgnoreCase("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")) UUID = str;
|
||||
}
|
||||
|
||||
using var BIOS = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS") ?? Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
|
||||
|
||||
if (BIOS != null)
|
||||
reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\BIOS");
|
||||
reg ??= Registry.LocalMachine.OpenSubKey(@"SYSTEM\HardwareConfig\Current");
|
||||
if (reg != null)
|
||||
{
|
||||
Product = (BIOS.GetValue("SystemProductName") + "").Replace("System Product Name", null);
|
||||
if (Product.IsNullOrEmpty()) Product = BIOS.GetValue("BaseBoardProduct") + "";
|
||||
Product = (reg.GetValue("SystemProductName") + "").Replace("System Product Name", null);
|
||||
if (Product.IsNullOrEmpty()) Product = reg.GetValue("BaseBoardProduct") + string.Empty;
|
||||
|
||||
Vendor = BIOS.GetValue("SystemManufacturer") + "";
|
||||
if (Vendor.IsNullOrEmpty()) Vendor = BIOS.GetValue("ASUSTeK COMPUTER INC.") + "";
|
||||
Vendor = reg.GetValue("SystemManufacturer") + string.Empty;
|
||||
if (Vendor.IsNullOrEmpty()) Vendor = reg.GetValue("ASUSTeK COMPUTER INC.") + string.Empty;
|
||||
}
|
||||
|
||||
using var CentralProcessor = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
if (CentralProcessor != null) Processor = CentralProcessor.GetValue("ProcessorNameString") + "";
|
||||
reg = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
if (reg != null) Processor = reg.GetValue("ProcessorNameString") + string.Empty;
|
||||
|
||||
// 旧版系统(如win2008)没有UUID的注册表项,需要用wmic查询。也可能因为过去的某个BUG,导致GUID跟UUID相等
|
||||
if (UUID.IsNullOrEmpty() || UUID == Guid || Vendor.IsNullOrEmpty())
|
||||
@@ -329,13 +358,13 @@ public class MachineInfo
|
||||
var reg2 = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
|
||||
if (reg2 != null)
|
||||
{
|
||||
OSName = reg2.GetValue("ProductName") + "";
|
||||
OSVersion = reg2.GetValue("ReleaseId") + "";
|
||||
OSName = reg2.GetValue("ProductName") + string.Empty;
|
||||
OSVersion = reg2.GetValue("ReleaseId") + string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) NewLife.Log.XTrace.WriteException(ex);
|
||||
if (XTrace.Log.Level <= LogLevel.Debug) XTrace.WriteException(ex);
|
||||
}
|
||||
}
|
||||
//#elif NET6_0_OR_GREATER
|
||||
@@ -446,7 +475,7 @@ public class MachineInfo
|
||||
// Guid = str;
|
||||
|
||||
// DMI信息位于 /sys/class/dmi/id/ 目录,可以直接读取,不需要执行dmidecode命令
|
||||
var uuid = "";
|
||||
var uuid = string.Empty;
|
||||
var file = "/sys/class/dmi/id/product_uuid";
|
||||
if (!File.Exists(file)) file = "/etc/uuid";
|
||||
if (!File.Exists(file)) file = "/proc/serial_num"; // miui12支持/proc/serial_num
|
||||
@@ -906,7 +935,7 @@ public class MachineInfo
|
||||
var str = "powershell.exe".Execute(args, 3_000) ?? String.Empty;
|
||||
if (!String.IsNullOrWhiteSpace(str))
|
||||
{
|
||||
foreach (var item in JObject.Parse(str)!)
|
||||
foreach (var item in str.DecodeJson()!)
|
||||
{
|
||||
dic[item.Key] = item.Value?.ToString() ?? String.Empty;
|
||||
}
|
||||
@@ -971,7 +1000,7 @@ public class MachineInfo
|
||||
{
|
||||
try
|
||||
{
|
||||
dic[item.Name] = item.GetValue(null) + "";
|
||||
dic[item.Name] = item.GetValue(null) + string.Empty;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -985,7 +1014,7 @@ public class MachineInfo
|
||||
{
|
||||
try
|
||||
{
|
||||
dic[item.Name] = item.GetValue(null) + "";
|
||||
dic[item.Name] = item.GetValue(null) + string.Empty;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
@@ -1137,7 +1166,7 @@ public class MachineInfo
|
||||
public Int64 ToLong() => (Int64)(((UInt64)High << 32) | Low);
|
||||
}
|
||||
|
||||
private sealed class SystemTime
|
||||
private class SystemTime
|
||||
{
|
||||
public Int64 IdleTime;
|
||||
public Int64 TotalTime;
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Runtime;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
|
||||
namespace ThingsGateway.NewLife;
|
||||
|
||||
@@ -65,9 +66,11 @@ public static class Runtime
|
||||
|
||||
#region 系统特性
|
||||
/// <summary>是否Mono环境</summary>
|
||||
public static Boolean Mono { get; } = Type.GetType("Mono.Runtime") != null;
|
||||
public static Boolean Mono { get; }
|
||||
|
||||
/// <summary>是否Unity环境</summary>
|
||||
public static Boolean Unity { get; }
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
private static Boolean? _IsWeb;
|
||||
/// <summary>是否Web环境</summary>
|
||||
@@ -132,13 +135,17 @@ public static class Runtime
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>获取当前UTC时间。基于全局时间提供者,在星尘应用中会屏蔽服务器时间差</summary>
|
||||
/// <returns></returns>
|
||||
public static DateTimeOffset UtcNow => TimerScheduler.GlobalTimeProvider.GetUtcNow();
|
||||
|
||||
private static Int32 _ProcessId;
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>当前进程Id</summary>
|
||||
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = Environment.ProcessId;
|
||||
#else
|
||||
/// <summary>当前进程Id</summary>
|
||||
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = Process.GetCurrentProcess().Id;
|
||||
public static Int32 ProcessId => _ProcessId > 0 ? _ProcessId : _ProcessId = ProcessHelper.GetProcessId();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
|
||||
150
src/Admin/ThingsGateway.NewLife.X/Common/SysConfig.cs
Normal file
150
src/Admin/ThingsGateway.NewLife.X/Common/SysConfig.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.NewLife.Configuration;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Security;
|
||||
|
||||
namespace ThingsGateway.NewLife.Common;
|
||||
|
||||
/// <summary>系统设置。提供系统名称、版本等基本设置</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/sysconfig
|
||||
/// </remarks>
|
||||
[DisplayName("系统设置")]
|
||||
public class SysConfig : Config<SysConfig>
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>系统名称</summary>
|
||||
[DisplayName("系统名称")]
|
||||
[Description("用于标识系统的英文名")]
|
||||
public String Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>系统版本</summary>
|
||||
[DisplayName("系统版本")]
|
||||
public String Version { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>显示名称</summary>
|
||||
[DisplayName("显示名称")]
|
||||
[Description("用户可见的名称")]
|
||||
public String DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>公司</summary>
|
||||
[DisplayName("公司")]
|
||||
public String Company { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>应用实例。单应用多实例部署时用于唯一标识实例节点</summary>
|
||||
[DisplayName("应用实例。单应用多实例部署时用于唯一标识实例节点")]
|
||||
public Int32 Instance { get; set; }
|
||||
|
||||
/// <summary>开发者模式</summary>
|
||||
[DisplayName("开发者模式")]
|
||||
public Boolean Develop { get; set; } = true;
|
||||
|
||||
/// <summary>启用</summary>
|
||||
[DisplayName("启用")]
|
||||
public Boolean Enable { get; set; } = true;
|
||||
|
||||
/// <summary>安装时间</summary>
|
||||
[DisplayName("安装时间")]
|
||||
public DateTime InstallTime { get; set; } = DateTime.Now;
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>加载后触发</summary>
|
||||
protected override void OnLoaded()
|
||||
{
|
||||
if (IsNew)
|
||||
{
|
||||
var asmx = SysAssembly;
|
||||
|
||||
Name = asmx?.Name ?? "NewLife.Cube";
|
||||
Version = asmx?.Version ?? "0.1";
|
||||
DisplayName = (asmx?.Title ?? asmx?.Name) ?? "魔方平台";
|
||||
Company = asmx?.Company ?? "新生命开发团队";
|
||||
//Address = "新生命开发团队";
|
||||
|
||||
if (DisplayName.IsNullOrEmpty()) DisplayName = "系统设置";
|
||||
}
|
||||
|
||||
// 强制设置
|
||||
var name = GetSysName();
|
||||
if (!name.IsNullOrEmpty()) Name = name;
|
||||
|
||||
// 本地实例,取IPv4地址后两段
|
||||
if (Instance <= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ip = NetHelper.MyIP();
|
||||
if (ip != null)
|
||||
{
|
||||
var buf = ip.GetAddressBytes();
|
||||
Instance = (buf[2] << 8) | buf[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = Rand.Next(1, 1024);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 异常时随机
|
||||
Instance = Rand.Next(1, 1024);
|
||||
}
|
||||
}
|
||||
|
||||
base.OnLoaded();
|
||||
}
|
||||
|
||||
/// <summary>获取系统名</summary>
|
||||
/// <returns></returns>
|
||||
public static String? GetSysName()
|
||||
{
|
||||
// 从命令参数或环境变量获取系统名称,强制覆盖SysConfig,方便星尘发布根据命令行控制系统名称
|
||||
var name = string.Empty;
|
||||
// 命令参数
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
if (args[i].EqualIgnoreCase("-Name") && i + 1 < args.Length)
|
||||
{
|
||||
name = args[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 环境变量
|
||||
if (name.IsNullOrEmpty()) name = Runtime.GetEnvironmentVariable("Name");
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// <summary>系统主程序集</summary>
|
||||
public static AssemblyX? SysAssembly
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var asm = AssemblyX.Entry;
|
||||
if (asm != null) return asm;
|
||||
|
||||
var sm = Assembly.GetCallingAssembly();
|
||||
if (sm != null) return AssemblyX.Create(sm);
|
||||
|
||||
var list = AssemblyX.GetMyAssemblies();
|
||||
|
||||
// 最后编译那一个
|
||||
list = list.OrderByDescending(e => e.Compile)
|
||||
.ThenByDescending(e => e.Name.EndsWithIgnoreCase("Web"))
|
||||
.ToList();
|
||||
|
||||
return list.FirstOrDefault();
|
||||
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -66,4 +66,4 @@ public abstract class TimeProvider
|
||||
/// <returns></returns>
|
||||
public TimeSpan GetElapsedTime(Int64 startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp());
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
64
src/Admin/ThingsGateway.NewLife.X/Compression/SevenZip.cs
Normal file
64
src/Admin/ThingsGateway.NewLife.X/Compression/SevenZip.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.NewLife.Compression;
|
||||
|
||||
/// <summary>7Zip</summary>
|
||||
public class SevenZip
|
||||
{
|
||||
#region 基础
|
||||
private static readonly String _7z = null!;
|
||||
|
||||
static SevenZip()
|
||||
{
|
||||
var p = string.Empty;
|
||||
var set = Setting.Current;
|
||||
|
||||
// 附近文件
|
||||
if (p.IsNullOrEmpty())
|
||||
{
|
||||
p = "7z.exe".GetFullPath();
|
||||
if (!File.Exists(p)) p = set.PluginPath.CombinePath("7z.exe").GetFullPath();
|
||||
if (!File.Exists(p)) p = "7z/7z.exe".GetFullPath();
|
||||
if (!File.Exists(p)) p = "../7z/7z.exe".GetFullPath();
|
||||
if (!File.Exists(p)) p = string.Empty;
|
||||
}
|
||||
|
||||
if (!p.IsNullOrEmpty()) _7z = p.GetFullPath();
|
||||
|
||||
XTrace.WriteLine("7Z目录 {0}", _7z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 压缩/解压缩
|
||||
/// <summary>压缩文件</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="destFile"></param>
|
||||
/// <returns></returns>
|
||||
public void Compress(String path, String destFile)
|
||||
{
|
||||
if (Directory.Exists(path)) path = path.GetFullPath().EnsureEnd("\\") + "*";
|
||||
|
||||
Run($"a \"{destFile}\" \"{path}\" -mx9 -ssw");
|
||||
}
|
||||
|
||||
/// <summary>解压缩文件</summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="destDir"></param>
|
||||
/// <param name="overwrite">是否覆盖目标同名文件</param>
|
||||
/// <returns></returns>
|
||||
public void Extract(String file, String destDir, Boolean overwrite = false)
|
||||
{
|
||||
destDir.EnsureDirectory(false);
|
||||
|
||||
var args = $"x \"{file}\" -o\"{destDir}\" -y -r";
|
||||
if (overwrite)
|
||||
args += " -aoa";
|
||||
else
|
||||
args += " -aos";
|
||||
|
||||
Run(args);
|
||||
}
|
||||
|
||||
private Int32 Run(String args) => _7z.Run(args, 5000);
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ThingsGateway.NewLife.Configuration;
|
||||
|
||||
/// <summary>复合配置提供者。常用于本地配置与网络配置的混合</summary>
|
||||
public class CompositeConfigProvider : IConfigProvider
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>日志提供者集合</summary>
|
||||
/// <remarks>为了线程安全,使用数组</remarks>
|
||||
public IConfigProvider[] Configs { get; set; } //= new IConfigProvider[0];
|
||||
|
||||
/// <summary>名称</summary>
|
||||
public String Name { get; set; }
|
||||
|
||||
/// <summary>根元素</summary>
|
||||
public IConfigSection Root { get => Configs[0].Root; set => throw new NotImplementedException(); }
|
||||
|
||||
/// <summary>所有键</summary>
|
||||
public ICollection<String> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
var ks = new List<String>();
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
if (cfg.Keys != null)
|
||||
{
|
||||
foreach (var item in cfg.Keys)
|
||||
{
|
||||
if (!ks.Contains(item)) ks.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ks;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>是否新的配置文件</summary>
|
||||
public Boolean IsNew { get => Configs[0].IsNew; set => Configs[0].IsNew = value; }
|
||||
|
||||
/// <summary>返回获取配置的委托</summary>
|
||||
public GetConfigCallback GetConfig => key => GetSection(key)?.Value;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
///// <summary>实例化</summary>
|
||||
//public CompositeConfigProvider() => Name = GetType().Name.TrimEnd("ConfigProvider");
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="configProvider1"></param>
|
||||
/// <param name="configProvider2"></param>
|
||||
public CompositeConfigProvider(IConfigProvider configProvider1, IConfigProvider configProvider2)
|
||||
{
|
||||
Name = GetType().Name.TrimEnd("ConfigProvider");
|
||||
|
||||
Configs = [configProvider1, configProvider2];
|
||||
}
|
||||
|
||||
/// <summary>添加</summary>
|
||||
/// <param name="configProviders"></param>
|
||||
public void Add(params IConfigProvider[] configProviders)
|
||||
{
|
||||
var list = new List<IConfigProvider>(Configs);
|
||||
list.AddRange(configProviders);
|
||||
|
||||
Configs = list.ToArray();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 取值
|
||||
/// <summary>获取 或 设置 配置值</summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns></returns>
|
||||
public String? this[String key]
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
var value = cfg[key];
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
//cfg[key] = value;
|
||||
var section = cfg.GetSection(key);
|
||||
if (section != null) section.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>查找配置项。可得到子级和配置</summary>
|
||||
/// <param name="key">配置名</param>
|
||||
/// <returns></returns>
|
||||
public IConfigSection? GetSection(String key)
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
var section = cfg.GetSection(key);
|
||||
if (section != null) return section;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>从数据源加载数据到配置树</summary>
|
||||
public Boolean LoadAll()
|
||||
{
|
||||
var rs = false;
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
rs |= cfg.LoadAll();
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>保存配置树到数据源</summary>
|
||||
public Boolean SaveAll()
|
||||
{
|
||||
var rs = false;
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
rs |= cfg.SaveAll();
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>加载配置到模型</summary>
|
||||
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
|
||||
/// <param name="path">路径。配置树位置,配置中心等多对象混合使用时</param>
|
||||
/// <returns></returns>
|
||||
public T? Load<T>(String? path = null) where T : new()
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
var model = cfg.Load<T>(path);
|
||||
if (model != null) return model;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>保存模型实例</summary>
|
||||
/// <typeparam name="T">模型</typeparam>
|
||||
/// <param name="model">模型实例</param>
|
||||
/// <param name="path">路径。配置树位置</param>
|
||||
public Boolean Save<T>(T model, String? path = null)
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
var rs = cfg.Save(model, path);
|
||||
if (rs) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 绑定
|
||||
private readonly ConcurrentDictionary<Object, String> _models = [];
|
||||
private readonly ConcurrentDictionary<Object, ModelWrap> _models2 = [];
|
||||
/// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
|
||||
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
|
||||
/// <param name="model">模型实例</param>
|
||||
/// <param name="autoReload">是否自动更新。默认true</param>
|
||||
/// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
|
||||
public virtual void Bind<T>(T model, Boolean autoReload = true, String? path = null)
|
||||
{
|
||||
if (model == null) return;
|
||||
|
||||
// 如果有命名空间则使用指定层级数据源
|
||||
path ??= String.Empty;
|
||||
var source = GetSection(path);
|
||||
if (source != null)
|
||||
{
|
||||
if (model is IConfigMapping map)
|
||||
map.MapConfig(this, source);
|
||||
else
|
||||
source.MapTo(model, this);
|
||||
}
|
||||
|
||||
if (autoReload)
|
||||
{
|
||||
_models.TryAdd(model, path);
|
||||
}
|
||||
|
||||
AddChanged();
|
||||
}
|
||||
|
||||
/// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
|
||||
/// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
|
||||
/// <param name="model">模型实例</param>
|
||||
/// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
|
||||
/// <param name="onChange">配置改变时执行的委托</param>
|
||||
public virtual void Bind<T>(T model, String? path, Action<IConfigSection> onChange)
|
||||
{
|
||||
if (model == null) return;
|
||||
|
||||
// 如果有命名空间则使用指定层级数据源
|
||||
path ??= String.Empty;
|
||||
var source = GetSection(path);
|
||||
if (source != null)
|
||||
{
|
||||
if (model is IConfigMapping map)
|
||||
map.MapConfig(this, source);
|
||||
else
|
||||
source.MapTo(model, this);
|
||||
}
|
||||
|
||||
if (onChange != null)
|
||||
{
|
||||
_models2.TryAdd(model, new ModelWrap(path, onChange));
|
||||
}
|
||||
|
||||
AddChanged();
|
||||
}
|
||||
|
||||
private record ModelWrap(String Path, Action<IConfigSection> OnChange);
|
||||
|
||||
/// <summary>通知绑定对象,配置数据有改变</summary>
|
||||
protected virtual void NotifyChange()
|
||||
{
|
||||
foreach (var item in _models)
|
||||
{
|
||||
var model = item.Key;
|
||||
var source = GetSection(item.Value);
|
||||
if (source != null)
|
||||
{
|
||||
if (model is IConfigMapping map)
|
||||
map.MapConfig(this, source);
|
||||
else
|
||||
source.MapTo(model, this);
|
||||
}
|
||||
}
|
||||
foreach (var item in _models2)
|
||||
{
|
||||
var model = item.Key;
|
||||
var source = GetSection(item.Value.Path);
|
||||
if (source != null) item.Value.OnChange(source);
|
||||
}
|
||||
|
||||
// 通过事件通知外部
|
||||
_Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 配置变化
|
||||
private Int32 _count;
|
||||
|
||||
private event EventHandler? _Changed;
|
||||
/// <summary>配置改变事件。执行了某些动作,可能导致配置数据发生改变时触发</summary>
|
||||
public event EventHandler Changed
|
||||
{
|
||||
add
|
||||
{
|
||||
_Changed += value;
|
||||
|
||||
// 首次注册事件时,向内部提供者注册事件
|
||||
AddChanged();
|
||||
}
|
||||
remove
|
||||
{
|
||||
// 最后一次取消注册时,向内部提供者取消注册
|
||||
if (Interlocked.Decrement(ref _count) == 0)
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
cfg.Changed -= OnChange;
|
||||
}
|
||||
}
|
||||
|
||||
_Changed -= value;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddChanged()
|
||||
{
|
||||
if (Interlocked.Increment(ref _count) == 1)
|
||||
{
|
||||
foreach (var cfg in Configs)
|
||||
{
|
||||
cfg.Changed += OnChange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChange(Object? sender, EventArgs e) => NotifyChange();
|
||||
#endregion
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
/// 如未指定提供者,则使用全局默认,此时将根据全局代码配置或环境变量配置使用不同提供者,实现配置信息整体转移。
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public sealed class ConfigAttribute : Attribute
|
||||
public class ConfigAttribute : Attribute
|
||||
{
|
||||
/// <summary>提供者。内置ini/xml/json/http,一般不指定,使用全局默认</summary>
|
||||
public String? Provider { get; set; }
|
||||
@@ -23,3 +23,48 @@ public sealed class ConfigAttribute : Attribute
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>http配置特性</summary>
|
||||
/// <remarks>
|
||||
/// 声明配置模型使用哪一种配置提供者,以及所需要的文件名和分类名。
|
||||
/// 如未指定提供者,则使用全局默认,此时将根据全局代码配置或环境变量配置使用不同提供者,实现配置信息整体转移。
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class HttpConfigAttribute : ConfigAttribute
|
||||
{
|
||||
/// <summary>应用标识</summary>
|
||||
public String Server { get; set; }
|
||||
|
||||
/// <summary>服务操作</summary>
|
||||
public String Action { get; set; }
|
||||
|
||||
/// <summary>应用标识</summary>
|
||||
public String AppId { get; set; }
|
||||
|
||||
/// <summary>应用密钥</summary>
|
||||
public String? Secret { get; set; }
|
||||
|
||||
/// <summary>作用域。获取指定作用域下的配置值,生产、开发、测试 等</summary>
|
||||
public String? Scope { get; set; }
|
||||
|
||||
/// <summary>本地缓存配置数据。即使网络断开,仍然能够加载使用本地数据,默认Encrypted</summary>
|
||||
public ConfigCacheLevel CacheLevel { get; set; }
|
||||
|
||||
/// <summary>指定配置名</summary>
|
||||
/// <param name="server">服务器地址</param>
|
||||
/// <param name="action">服务操作</param>
|
||||
/// <param name="name">配置名。可以是文件名或分类名</param>
|
||||
/// <param name="appId">应用标识</param>
|
||||
/// <param name="secret">应用密钥</param>
|
||||
/// <param name="scope">作用域。获取指定作用域下的配置值,生产、开发、测试 等</param>
|
||||
/// <param name="cacheLevel">本地缓存配置数据。即使网络断开,仍然能够加载使用本地数据,默认Encrypted</param>
|
||||
public HttpConfigAttribute(String name, String server, String action, String appId, String? secret = null, String? scope = null, ConfigCacheLevel cacheLevel = ConfigCacheLevel.Encrypted) : base(name, "http")
|
||||
{
|
||||
Server = server;
|
||||
Action = action;
|
||||
AppId = appId;
|
||||
Secret = secret;
|
||||
Scope = scope;
|
||||
CacheLevel = cacheLevel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace ThingsGateway.NewLife.Configuration
|
||||
{
|
||||
/// <summary>配置数据缓存等级</summary>
|
||||
public enum ConfigCacheLevel
|
||||
{
|
||||
/// <summary>不缓存</summary>
|
||||
NoCache,
|
||||
|
||||
/// <summary>Json格式缓存</summary>
|
||||
Json,
|
||||
|
||||
/// <summary>加密缓存</summary>
|
||||
Encrypted,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
|
||||
namespace ThingsGateway.NewLife.Configuration;
|
||||
@@ -13,7 +12,7 @@ public abstract class FileConfigProvider : ConfigProvider
|
||||
#region 属性
|
||||
/// <summary>文件名。最高优先级,优先于模型特性指定的文件名</summary>
|
||||
public String? FileName { get; set; }
|
||||
|
||||
public static String DataPath { get; set; } = "Configuration";
|
||||
/// <summary>更新周期。默认5秒</summary>
|
||||
public Int32 Period { get; set; } = 5;
|
||||
#endregion
|
||||
@@ -34,6 +33,8 @@ public abstract class FileConfigProvider : ConfigProvider
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
|
||||
|
||||
/// <summary>初始化</summary>
|
||||
/// <param name="value"></param>
|
||||
public override void Init(String value)
|
||||
@@ -45,7 +46,7 @@ public abstract class FileConfigProvider : ConfigProvider
|
||||
{
|
||||
// 加上配置目录
|
||||
var str = value;
|
||||
if (!str.StartsWithIgnoreCase("Config/", "Config\\")) str = "Config".CombinePath(str);
|
||||
if (!str.StartsWithIgnoreCase($"{DataPath}/", $"{DataPath}\\")) str = $"{DataPath}".CombinePath(str);
|
||||
|
||||
FileName = str;
|
||||
}
|
||||
@@ -126,8 +127,8 @@ public abstract class FileConfigProvider : ConfigProvider
|
||||
protected virtual void OnWrite(String fileName, IConfigSection section)
|
||||
{
|
||||
var str = GetString(section);
|
||||
var old = "";
|
||||
if (File.Exists(fileName)) old = File.ReadAllText(fileName)?.Trim() ?? "";
|
||||
var old = string.Empty;
|
||||
if (File.Exists(fileName)) old = File.ReadAllText(fileName)?.Trim() ?? string.Empty;
|
||||
|
||||
if (str != null && str != old)
|
||||
{
|
||||
@@ -206,7 +207,7 @@ public abstract class FileConfigProvider : ConfigProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NewLife.Log.XTrace.WriteException(ex);
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -304,7 +304,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record ModelWrap(String Path, Action<IConfigSection> OnChange);
|
||||
private record ModelWrap(String Path, Action<IConfigSection> OnChange);
|
||||
|
||||
/// <summary>通知绑定对象,配置数据有改变</summary>
|
||||
protected virtual void NotifyChange()
|
||||
@@ -340,7 +340,7 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
|
||||
static ConfigProvider()
|
||||
{
|
||||
// 支持从命令行参数和环境变量设定默认配置提供者
|
||||
var str = "";
|
||||
var str = string.Empty;
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
@@ -350,11 +350,12 @@ public abstract class ConfigProvider : DisposeBase, IConfigProvider
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (str.IsNullOrEmpty()) str = ThingsGateway.NewLife.Runtime.GetEnvironmentVariable("DefaultConfig");
|
||||
if (str.IsNullOrEmpty()) str = NewLife.Runtime.GetEnvironmentVariable("DefaultConfig");
|
||||
if (!str.IsNullOrEmpty()) DefaultProvider = str;
|
||||
|
||||
Register<InIConfigProvider>("ini");
|
||||
Register<XmlConfigProvider>("xml");
|
||||
Register<JsonConfigProvider>("json");
|
||||
Register<XmlConfigProvider>("config");
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class InIConfigProvider : FileConfigProvider
|
||||
var lines = File.ReadAllLines(fileName);
|
||||
|
||||
var currentSection = section;
|
||||
var remark = "";
|
||||
var remark = string.Empty;
|
||||
foreach (var item in lines)
|
||||
{
|
||||
var str = item.Trim();
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Configuration;
|
||||
|
||||
/// <summary>Json文件配置提供者</summary>
|
||||
/// <remarks>
|
||||
/// 支持从不同配置文件加载到不同配置模型
|
||||
/// </remarks>
|
||||
public class JsonConfigProvider : FileConfigProvider
|
||||
{
|
||||
#region 静态
|
||||
/// <summary>加载本地配置文件得到配置提供者</summary>
|
||||
/// <param name="fileName">配置文件名,默认appsettings.json</param>
|
||||
/// <returns></returns>
|
||||
public static JsonConfigProvider LoadAppSettings(String? fileName = null)
|
||||
{
|
||||
if (fileName.IsNullOrEmpty()) fileName = "appsettings.json";
|
||||
|
||||
// 读取本地配置
|
||||
var jsonConfig = new JsonConfigProvider { FileName = fileName };
|
||||
|
||||
return jsonConfig;
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>初始化</summary>
|
||||
/// <param name="value"></param>
|
||||
public override void Init(String value)
|
||||
{
|
||||
// 加上默认后缀
|
||||
if (!value.IsNullOrEmpty() && Path.GetExtension(value).IsNullOrEmpty()) value += ".json";
|
||||
|
||||
base.Init(value);
|
||||
}
|
||||
|
||||
/// <summary>读取配置文件</summary>
|
||||
/// <param name="fileName">文件名</param>
|
||||
/// <param name="section">配置段</param>
|
||||
protected override void OnRead(String fileName, IConfigSection section)
|
||||
{
|
||||
var txt = File.ReadAllText(fileName);
|
||||
|
||||
// 预处理注释
|
||||
txt = TrimComment(txt);
|
||||
|
||||
var src = txt.DecodeJson();
|
||||
if (src != null) Map(src, section);
|
||||
}
|
||||
|
||||
/// <summary>获取字符串形式</summary>
|
||||
/// <param name="section">配置段</param>
|
||||
/// <returns></returns>
|
||||
public override String GetString(IConfigSection? section = null)
|
||||
{
|
||||
section ??= Root;
|
||||
|
||||
var rs = new Dictionary<String, Object?>();
|
||||
Map(section, rs);
|
||||
|
||||
var jw = new JsonWriter
|
||||
{
|
||||
//IgnoreNullValues = false,
|
||||
//IgnoreComment = false,
|
||||
//Indented = true,
|
||||
//SmartIndented = true,
|
||||
};
|
||||
jw.Options.IgnoreNullValues = false;
|
||||
jw.Options.WriteIndented = true;
|
||||
|
||||
jw.Write(rs);
|
||||
|
||||
return jw.GetString();
|
||||
|
||||
//var js = new Json();
|
||||
//js.Write(rs);
|
||||
|
||||
//return js.GetBytes().ToStr();
|
||||
}
|
||||
|
||||
#region 辅助
|
||||
/// <summary>字典映射到配置树</summary>
|
||||
/// <param name="src"></param>
|
||||
/// <param name="section"></param>
|
||||
protected virtual void Map(IDictionary<String, Object?> src, IConfigSection section)
|
||||
{
|
||||
foreach (var item in src)
|
||||
{
|
||||
var name = item.Key;
|
||||
if (name[0] == '#') continue;
|
||||
|
||||
var cfg = section.GetOrAddChild(name);
|
||||
var cname = "#" + name;
|
||||
if (src.TryGetValue(cname, out var comment) && comment != null) cfg.Comment = comment + string.Empty;
|
||||
|
||||
// 支持字典
|
||||
if (item.Value is IDictionary<String, Object?> dic)
|
||||
Map(dic, cfg);
|
||||
else if (item.Value is IList<Object> list)
|
||||
{
|
||||
cfg.Childs = new List<IConfigSection>();
|
||||
foreach (var elm in list)
|
||||
{
|
||||
// 复杂对象
|
||||
if (elm is IDictionary<String, Object?> dic2)
|
||||
{
|
||||
var cfg2 = new ConfigSection();
|
||||
Map(dic2, cfg2);
|
||||
cfg.Childs.Add(cfg2);
|
||||
}
|
||||
// 简单基元类型
|
||||
else
|
||||
{
|
||||
var key = elm?.GetType()?.Name;
|
||||
if (!key.IsNullOrEmpty())
|
||||
{
|
||||
var cfg2 = new ConfigSection
|
||||
{
|
||||
Key = key,
|
||||
Value = elm + "",
|
||||
};
|
||||
cfg.Childs.Add(cfg2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
cfg.SetValue(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>配置树映射到字典</summary>
|
||||
/// <param name="section"></param>
|
||||
/// <param name="dst"></param>
|
||||
protected virtual void Map(IConfigSection section, IDictionary<String, Object?> dst)
|
||||
{
|
||||
if (section.Childs == null) return;
|
||||
|
||||
foreach (var item in section.Childs.ToArray())
|
||||
{
|
||||
//// 注释
|
||||
//if (!item.Comment.IsNullOrEmpty()) dst["#" + item.Key] = item.Comment;
|
||||
|
||||
var key = item.Key + string.Empty;
|
||||
var cs = item.Childs;
|
||||
if (cs != null)
|
||||
{
|
||||
// 数组
|
||||
if (cs.Count == 0 || cs.Count > 0 && cs[0].Key == null || cs.Count >= 2 && cs[0].Key == cs[1].Key)
|
||||
{
|
||||
Object? val = null;
|
||||
|
||||
// 普通基元类型数组
|
||||
if (cs.Count > 0)
|
||||
{
|
||||
var childs = cs[0].Childs;
|
||||
if (childs == null || childs.Count == 0) val = cs.Select(e => e.Value).ToArray();
|
||||
}
|
||||
if (val == null)
|
||||
{
|
||||
var list = new List<Object>();
|
||||
foreach (var elm in cs)
|
||||
{
|
||||
var rs = new Dictionary<String, Object?>();
|
||||
Map(elm, rs);
|
||||
list.Add(rs);
|
||||
}
|
||||
val = list;
|
||||
}
|
||||
dst[key] = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
var rs = new Dictionary<String, Object?>();
|
||||
Map(item, rs);
|
||||
|
||||
dst[key] = rs;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
dst[key] = item.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理json字符串中的注释,避免json解析错误
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static String TrimComment(String text)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
// 以下处理多行注释 “/**/” 放在一行的情况
|
||||
var p = text.IndexOf("/*");
|
||||
if (p < 0) break;
|
||||
|
||||
var p2 = text.IndexOf("*/", p + 2);
|
||||
if (p2 < 0) break;
|
||||
|
||||
text = text[..p] + text[(p2 + 2)..];
|
||||
}
|
||||
|
||||
// 增加 \r以及\n的处理, 处理类似如下json转换时的错误:==>{"key":"http://*:5000" \n /*注释*/}<==
|
||||
var lines = text.Split("\r\n", "\n", "\r");
|
||||
text = lines
|
||||
.Where(e => !e.IsNullOrEmpty() && !e.TrimStart().StartsWith("//"))
|
||||
// 没考虑到链接中带双斜杠的,以下导致链接的内容被干掉
|
||||
//.Select(e =>
|
||||
//{
|
||||
// // 单行注释 “//” 放在最后的情况
|
||||
// var p0 = e.IndexOf("//");
|
||||
// if (p0 > 0) return e.Substring(0, p0);
|
||||
|
||||
// return e;
|
||||
//})
|
||||
.Join(Environment.NewLine);
|
||||
|
||||
return text;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
@@ -45,11 +45,11 @@ public class XmlConfigProvider : FileConfigProvider
|
||||
if (reader.NodeType == XmlNodeType.EndElement) reader.ReadEndElement();
|
||||
}
|
||||
|
||||
private static void ReadNode(XmlReader reader, IConfigSection section)
|
||||
private void ReadNode(XmlReader reader, IConfigSection section)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var remark = "";
|
||||
var remark = string.Empty;
|
||||
if (reader.NodeType == XmlNodeType.Comment) remark = reader.Value;
|
||||
while (reader.NodeType is XmlNodeType.Comment or XmlNodeType.Whitespace) reader.Skip();
|
||||
if (reader.NodeType != XmlNodeType.Element) break;
|
||||
@@ -126,7 +126,7 @@ public class XmlConfigProvider : FileConfigProvider
|
||||
return ms.ToStr();
|
||||
}
|
||||
|
||||
private static void WriteNode(XmlWriter writer, String name, IConfigSection section)
|
||||
private void WriteNode(XmlWriter writer, String name, IConfigSection section)
|
||||
{
|
||||
if (section.Childs == null) return;
|
||||
|
||||
@@ -169,7 +169,7 @@ public class XmlConfigProvider : FileConfigProvider
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteAttributeNode(XmlWriter writer, String name, IConfigSection section)
|
||||
private void WriteAttributeNode(XmlWriter writer, String name, IConfigSection section)
|
||||
{
|
||||
writer.WriteStartElement(name);
|
||||
//writer.WriteStartAttribute(name);
|
||||
|
||||
52
src/Admin/ThingsGateway.NewLife.X/Data/DbRow.cs
Normal file
52
src/Admin/ThingsGateway.NewLife.X/Data/DbRow.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据行</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/dbtable
|
||||
/// </remarks>
|
||||
/// <remarks>构造数据行</remarks>
|
||||
/// <param name="table"></param>
|
||||
/// <param name="index"></param>
|
||||
public readonly struct DbRow(DbTable table, Int32 index) : IModel
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>数据表</summary>
|
||||
[XmlIgnore, IgnoreDataMember]
|
||||
public readonly DbTable Table { get; } = table;
|
||||
|
||||
/// <summary>行索引</summary>
|
||||
public readonly Int32 Index { get; } = index;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 索引器
|
||||
/// <summary>基于列索引访问</summary>
|
||||
/// <param name="column"></param>
|
||||
/// <returns></returns>
|
||||
public readonly Object? this[Int32 column]
|
||||
{
|
||||
get => Table.Rows?[Index][column];
|
||||
set
|
||||
{
|
||||
var rows = Table.Rows;
|
||||
if (rows != null) rows[Index][column] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>基于列名访问</summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public Object? this[String name] { get => this[Table.GetColumn(name)]; set => this[Table.GetColumn(name)] = value; }
|
||||
#endregion
|
||||
|
||||
#region 高级扩展
|
||||
/// <summary>读取指定行的字段值</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public readonly T? Get<T>(String name) => Table.Get<T>(Index, name);
|
||||
#endregion
|
||||
}
|
||||
845
src/Admin/ThingsGateway.NewLife.X/Data/DbTable.cs
Normal file
845
src/Admin/ThingsGateway.NewLife.X/Data/DbTable.cs
Normal file
@@ -0,0 +1,845 @@
|
||||
using System.Collections;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
using ThingsGateway.NewLife.IO;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据表</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/dbtable
|
||||
/// </remarks>
|
||||
public class DbTable : IEnumerable<DbRow>, ICloneable, IAccessor
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>数据列</summary>
|
||||
public String[] Columns { get; set; } = [];
|
||||
|
||||
/// <summary>数据列类型</summary>
|
||||
[XmlIgnore, IgnoreDataMember]
|
||||
public Type[] Types { get; set; } = [];
|
||||
|
||||
/// <summary>数据行</summary>
|
||||
public IList<Object?[]> Rows { get; set; } = [];
|
||||
|
||||
/// <summary>总行数</summary>
|
||||
public Int32 Total { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
#endregion
|
||||
|
||||
#region 从数据库读取
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="dr"></param>
|
||||
public void Read(IDataReader dr)
|
||||
{
|
||||
ReadHeader(dr);
|
||||
ReadData(dr);
|
||||
}
|
||||
|
||||
/// <summary>读取头部</summary>
|
||||
/// <param name="dr"></param>
|
||||
public void ReadHeader(IDataReader dr)
|
||||
{
|
||||
var count = dr.FieldCount;
|
||||
|
||||
// 字段
|
||||
var cs = new String[count];
|
||||
var ts = new Type[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
cs[i] = dr.GetName(i);
|
||||
ts[i] = dr.GetFieldType(i);
|
||||
}
|
||||
Columns = cs;
|
||||
Types = ts;
|
||||
}
|
||||
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="dr">数据读取器</param>
|
||||
/// <param name="fields">要读取的字段序列</param>
|
||||
public void ReadData(IDataReader dr, Int32[]? fields = null)
|
||||
{
|
||||
// 字段
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
|
||||
fields ??= Enumerable.Range(0, cs.Length).ToArray();
|
||||
|
||||
// 数据
|
||||
var rs = new List<Object?[]>();
|
||||
while (dr.Read())
|
||||
{
|
||||
var row = new Object?[fields.Length];
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
// MySql在读取0000时间数据时会报错
|
||||
try
|
||||
{
|
||||
var val = dr[fields[i]];
|
||||
|
||||
if (val == DBNull.Value) val = GetDefault(ts[i].GetTypeCode());
|
||||
row[i] = val;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
rs.Add(row);
|
||||
}
|
||||
Rows = rs;
|
||||
|
||||
Total = rs.Count;
|
||||
}
|
||||
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="dr"></param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
public async Task ReadAsync(DbDataReader dr, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ReadHeader(dr);
|
||||
await ReadDataAsync(dr, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="dr">数据读取器</param>
|
||||
/// <param name="fields">要读取的字段序列</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
public async Task ReadDataAsync(DbDataReader dr, Int32[]? fields = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 字段
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
|
||||
fields ??= Enumerable.Range(0, cs.Length).ToArray();
|
||||
|
||||
// 数据
|
||||
var rs = new List<Object?[]>();
|
||||
while (await dr.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var row = new Object?[fields.Length];
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
// MySql在读取0000时间数据时会报错
|
||||
try
|
||||
{
|
||||
var val = dr[fields[i]];
|
||||
|
||||
if (val == DBNull.Value) val = GetDefault(ts[i].GetTypeCode());
|
||||
row[i] = val;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
rs.Add(row);
|
||||
}
|
||||
Rows = rs;
|
||||
|
||||
Total = rs.Count;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DataTable互转
|
||||
/// <summary>从DataTable读取数据</summary>
|
||||
/// <param name="dataTable">数据表</param>
|
||||
public Int32 Read(DataTable dataTable)
|
||||
{
|
||||
if (dataTable == null) throw new ArgumentNullException(nameof(dataTable));
|
||||
|
||||
var cs = new List<String>();
|
||||
var ts = new List<Type>();
|
||||
foreach (var item in dataTable.Columns)
|
||||
{
|
||||
if (item is DataColumn dc)
|
||||
{
|
||||
cs.Add(dc.ColumnName);
|
||||
ts.Add(dc.DataType);
|
||||
}
|
||||
}
|
||||
Columns = cs.ToArray();
|
||||
Types = ts.ToArray();
|
||||
|
||||
var rs = new List<Object?[]>();
|
||||
foreach (var item in dataTable.Rows)
|
||||
{
|
||||
if (item is DataRow dr)
|
||||
rs.Add(dr.ItemArray);
|
||||
}
|
||||
Rows = rs;
|
||||
|
||||
return rs.Count;
|
||||
}
|
||||
|
||||
/// <summary>转换为DataTable</summary>
|
||||
/// <returns></returns>
|
||||
public DataTable ToDataTable() => Write(new DataTable());
|
||||
|
||||
/// <summary>转换为DataTable</summary>
|
||||
/// <param name="dataTable">数据表</param>
|
||||
/// <returns></returns>
|
||||
public DataTable Write(DataTable dataTable)
|
||||
{
|
||||
if (dataTable == null) throw new ArgumentNullException(nameof(dataTable));
|
||||
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
|
||||
for (var i = 0; i < cs.Length; i++)
|
||||
{
|
||||
var dc = new DataColumn(cs[i], ts[i]);
|
||||
|
||||
dataTable.Columns.Add(dc);
|
||||
}
|
||||
|
||||
var rs = Rows;
|
||||
if (rs != null)
|
||||
{
|
||||
for (var i = 0; i < rs.Count; i++)
|
||||
{
|
||||
var dr = dataTable.NewRow();
|
||||
dr.ItemArray = rs[i];
|
||||
|
||||
dataTable.Rows.Add(dr);
|
||||
}
|
||||
}
|
||||
|
||||
return dataTable;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 二进制读取
|
||||
private const Byte _Ver = 3;
|
||||
private const String MAGIC = "NewLifeDbTable";
|
||||
|
||||
/// <summary>从数据流读取</summary>
|
||||
/// <param name="stream"></param>
|
||||
public void Read(Stream stream)
|
||||
{
|
||||
var bn = new Binary
|
||||
{
|
||||
FullTime = true,
|
||||
EncodeInt = true,
|
||||
Stream = stream,
|
||||
};
|
||||
|
||||
// 读取头部
|
||||
ReadHeader(bn);
|
||||
|
||||
// 读取全部数据
|
||||
ReadData(bn, Total);
|
||||
}
|
||||
|
||||
/// <summary>读取头部</summary>
|
||||
/// <param name="bn"></param>
|
||||
public void ReadHeader(Binary bn)
|
||||
{
|
||||
// 头部,幻数、版本和标记
|
||||
var magic = bn.ReadBytes(MAGIC.Length).ToStr();
|
||||
if (magic != MAGIC) throw new InvalidDataException();
|
||||
|
||||
var ver = bn.Read<Byte>();
|
||||
_ = bn.Read<Byte>();
|
||||
|
||||
// 版本兼容
|
||||
if (ver > _Ver) throw new InvalidDataException($"DbTable[ver={_Ver}] Unable to support newer versions [{ver}]");
|
||||
|
||||
// v3开始支持FullTime
|
||||
if (ver < 3) bn.FullTime = false;
|
||||
|
||||
// 读取头部
|
||||
var count = bn.Read<Int32>();
|
||||
var cs = new String[count];
|
||||
var ts = new Type[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
cs[i] = bn.Read<String>() + string.Empty;
|
||||
|
||||
// 复杂类型写入类型字符串
|
||||
var tc = (TypeCode)bn.Read<Byte>();
|
||||
if (tc != TypeCode.Object)
|
||||
ts[i] = Type.GetType("System." + tc) ?? typeof(Object);
|
||||
else if (ver >= 2)
|
||||
ts[i] = Type.GetType(bn.Read<String>() + "") ?? typeof(Object);
|
||||
}
|
||||
Columns = cs;
|
||||
Types = ts;
|
||||
|
||||
Total = bn.ReadBytes(4).ToInt();
|
||||
}
|
||||
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="bn"></param>
|
||||
/// <param name="rows"></param>
|
||||
/// <returns></returns>
|
||||
public void ReadData(Binary bn, Int32 rows)
|
||||
{
|
||||
if (rows <= 0) return;
|
||||
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
var rs = new List<Object?[]>(rows);
|
||||
for (var k = 0; k < rows; k++)
|
||||
{
|
||||
var row = new Object?[ts.Length];
|
||||
for (var i = 0; i < ts.Length; i++)
|
||||
{
|
||||
row[i] = bn.Read(ts[i]);
|
||||
}
|
||||
rs.Add(row);
|
||||
}
|
||||
Rows = rs;
|
||||
}
|
||||
|
||||
/// <summary>读取</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean Read(IPacket pk)
|
||||
{
|
||||
if (pk == null || pk.Length == 0) return false;
|
||||
|
||||
Read(pk.GetStream());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>读取</summary>
|
||||
/// <param name="buffer"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean Read(Byte[] buffer, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (count < 0) count = buffer.Length - offset;
|
||||
|
||||
var ms = new MemoryStream(buffer, offset, count);
|
||||
Read(ms);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>从文件加载</summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="compressed">是否压缩</param>
|
||||
/// <returns></returns>
|
||||
public Int64 LoadFile(String file, Boolean compressed = false) => file.AsFile().OpenRead(compressed, s => Read(s));
|
||||
|
||||
Boolean IAccessor.Read(Stream stream, Object? context)
|
||||
{
|
||||
Read(stream);
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 二进制写入
|
||||
/// <summary>写入数据流</summary>
|
||||
/// <param name="stream"></param>
|
||||
public void Write(Stream stream)
|
||||
{
|
||||
var bn = new Binary
|
||||
{
|
||||
FullTime = true,
|
||||
EncodeInt = true,
|
||||
Stream = stream,
|
||||
};
|
||||
|
||||
// 写入数据体
|
||||
var rs = Rows;
|
||||
if (Total == 0 && rs != null) Total = rs.Count;
|
||||
|
||||
// 写入头部
|
||||
WriteHeader(bn);
|
||||
|
||||
// 写入数据行
|
||||
WriteData(bn);
|
||||
}
|
||||
|
||||
/// <summary>写入头部到数据流</summary>
|
||||
/// <param name="bn"></param>
|
||||
public void WriteHeader(Binary bn)
|
||||
{
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
|
||||
// 头部,幻数、版本和标记
|
||||
var buf = MAGIC.GetBytes();
|
||||
bn.Write(buf, 0, buf.Length);
|
||||
bn.Write(_Ver);
|
||||
bn.Write(0);
|
||||
|
||||
// 写入头部
|
||||
var count = cs.Length;
|
||||
bn.Write(count);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
bn.Write(cs[i]);
|
||||
|
||||
// 复杂类型写入类型字符串
|
||||
var code = ts[i].GetTypeCode();
|
||||
bn.Write((Byte)code);
|
||||
if (code == TypeCode.Object) bn.Write(ts[i].FullName);
|
||||
}
|
||||
|
||||
// 数据行数
|
||||
bn.Write(Total.GetBytes(), 0, 4);
|
||||
}
|
||||
|
||||
/// <summary>写入数据部分到数据流</summary>
|
||||
/// <param name="bn"></param>
|
||||
public void WriteData(Binary bn)
|
||||
{
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
var rs = Rows;
|
||||
if (rs == null) return;
|
||||
|
||||
// 写入数据
|
||||
foreach (var row in rs)
|
||||
{
|
||||
for (var i = 0; i < row.Length; i++)
|
||||
{
|
||||
bn.Write(row[i], ts[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>写入数据部分到数据流</summary>
|
||||
/// <param name="bn"></param>
|
||||
/// <param name="fields">要写入的字段序列</param>
|
||||
public void WriteData(Binary bn, Int32[] fields)
|
||||
{
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
var rs = Rows;
|
||||
if (rs == null) return;
|
||||
|
||||
// 写入数据,按照指定的顺序
|
||||
foreach (var row in rs)
|
||||
{
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
{
|
||||
// 找到目标顺序,实际上几乎不可能出现-1
|
||||
var idx = fields[i];
|
||||
if (idx >= 0)
|
||||
bn.Write(row[idx], ts[idx]);
|
||||
else
|
||||
bn.Write(null, ts[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>转数据包</summary>
|
||||
/// <returns></returns>
|
||||
public IPacket ToPacket()
|
||||
{
|
||||
// 不确定所需大小,只能使用内存流,再包装为数据包。
|
||||
// 头部预留8个字节,方便网络传输时添加协议头。
|
||||
var ms = new MemoryStream
|
||||
{
|
||||
Position = 8
|
||||
};
|
||||
|
||||
Write(ms);
|
||||
|
||||
ms.Position = 8;
|
||||
|
||||
// 包装为数据包,直接窃取内存流内部的缓冲区
|
||||
return new ArrayPacket(ms);
|
||||
}
|
||||
|
||||
/// <summary>保存到文件</summary>
|
||||
/// <param name="file"></param>
|
||||
/// <param name="compressed">是否压缩</param>
|
||||
/// <returns></returns>
|
||||
public void SaveFile(String file, Boolean compressed = false) => file.AsFile().OpenWrite(compressed, s => Write(s));
|
||||
|
||||
Boolean IAccessor.Write(Stream stream, Object? context)
|
||||
{
|
||||
Write(stream);
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Json序列化
|
||||
/// <summary>转Json字符串</summary>
|
||||
/// <param name="indented">是否缩进。默认false</param>
|
||||
/// <param name="nullValue">是否写空值。默认true</param>
|
||||
/// <param name="camelCase">是否驼峰命名。默认false</param>
|
||||
/// <returns></returns>
|
||||
public String ToJson(Boolean indented = false, Boolean nullValue = true, Boolean camelCase = false)
|
||||
{
|
||||
// 先转为名值对象的数组,再进行序列化
|
||||
var list = ToDictionary();
|
||||
return list.ToJson(indented, nullValue, camelCase);
|
||||
}
|
||||
|
||||
/// <summary>转为字典数组形式</summary>
|
||||
/// <returns></returns>
|
||||
public IList<IDictionary<String, Object?>> ToDictionary()
|
||||
{
|
||||
var list = new List<IDictionary<String, Object?>>();
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var rows = Rows;
|
||||
|
||||
if (rows != null)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var dic = new Dictionary<String, Object?>();
|
||||
for (var i = 0; i < cs.Length; i++)
|
||||
{
|
||||
dic[cs[i]] = row[i];
|
||||
}
|
||||
list.Add(dic);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Xml序列化
|
||||
/// <summary>转Xml字符串</summary>
|
||||
/// <returns></returns>
|
||||
public String GetXml()
|
||||
{
|
||||
//var doc = new XmlDocument();
|
||||
//var root = doc.CreateElement("DbTable");
|
||||
//doc.AppendChild(root);
|
||||
|
||||
//foreach (var row in Rows)
|
||||
//{
|
||||
// var dr = doc.CreateElement("Table");
|
||||
// for (var i = 0; i < Columns.Length; i++)
|
||||
// {
|
||||
// var elm = doc.CreateElement(Columns[i]);
|
||||
// elm.InnerText = row[i] + string.Empty;
|
||||
// dr.AppendChild(elm);
|
||||
// }
|
||||
// root.AppendChild(dr);
|
||||
//}
|
||||
|
||||
//return doc.OuterXml;
|
||||
|
||||
var ms = new MemoryStream();
|
||||
WriteXml(ms).Wait(15_000);
|
||||
|
||||
return ms.ToArray().ToStr();
|
||||
}
|
||||
|
||||
/// <summary>以Xml格式写入数据流中</summary>
|
||||
/// <param name="stream"></param>
|
||||
public async Task WriteXml(Stream stream)
|
||||
{
|
||||
var set = new XmlWriterSettings
|
||||
{
|
||||
OmitXmlDeclaration = true,
|
||||
ConformanceLevel = ConformanceLevel.Auto,
|
||||
Indent = true,
|
||||
Async = true,
|
||||
};
|
||||
using var writer = XmlWriter.Create(stream, set);
|
||||
|
||||
await writer.WriteStartDocumentAsync().ConfigureAwait(false);
|
||||
await writer.WriteStartElementAsync(null, "DbTable", null).ConfigureAwait(false);
|
||||
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var ts = Types ?? throw new ArgumentNullException(nameof(Types));
|
||||
var rows = Rows;
|
||||
|
||||
if (rows != null)
|
||||
{
|
||||
foreach (var row in rows)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Table", null).ConfigureAwait(false);
|
||||
for (var i = 0; i < cs.Length; i++)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, cs[i], null).ConfigureAwait(false);
|
||||
|
||||
if (ts[i] == typeof(Boolean))
|
||||
writer.WriteValue(row[i].ToBoolean());
|
||||
else if (ts[i] == typeof(DateTime))
|
||||
writer.WriteValue(new DateTimeOffset(row[i].ChangeType<DateTime>()));
|
||||
else if (ts[i] == typeof(DateTimeOffset))
|
||||
writer.WriteValue(row[i].ChangeType<DateTimeOffset>());
|
||||
else if (row[i] is IFormattable ft)
|
||||
await writer.WriteStringAsync(ft + "").ConfigureAwait(false);
|
||||
else
|
||||
await writer.WriteStringAsync(row[i] + "").ConfigureAwait(false);
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
await writer.WriteEndDocumentAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Csv序列化
|
||||
/// <summary>保存到Csv文件</summary>
|
||||
/// <param name="file"></param>
|
||||
public void SaveCsv(String file)
|
||||
{
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var rows = Rows;
|
||||
|
||||
using var csv = new CsvFile(file, true);
|
||||
csv.WriteLine(cs);
|
||||
if (rows != null) csv.WriteAll(rows);
|
||||
}
|
||||
|
||||
/// <summary>从Csv文件加载</summary>
|
||||
/// <param name="file"></param>
|
||||
public void LoadCsv(String file)
|
||||
{
|
||||
using var csv = new CsvFile(file, false);
|
||||
var cs = csv.ReadLine();
|
||||
if (cs != null) Columns = cs;
|
||||
Rows = csv.ReadAll().Cast<Object?[]>().ToList();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 反射
|
||||
/// <summary>写入模型列表</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="models"></param>
|
||||
public void WriteModels<T>(IEnumerable<T> models)
|
||||
{
|
||||
// 可用属性
|
||||
var pis = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
pis = pis.Where(e => e.PropertyType.IsBaseType()).ToArray();
|
||||
|
||||
Rows = [];
|
||||
foreach (var item in models)
|
||||
{
|
||||
// 头部
|
||||
if (Columns == null || Columns.Length == 0)
|
||||
{
|
||||
Columns = pis.Select(e => SerialHelper.GetName(e)).ToArray();
|
||||
Types = pis.Select(e => e.PropertyType).ToArray();
|
||||
}
|
||||
|
||||
var row = new Object?[Columns.Length];
|
||||
for (var i = 0; i < row.Length; i++)
|
||||
{
|
||||
// 反射取值
|
||||
if (pis[i].CanRead)
|
||||
{
|
||||
if (item is IModel ext)
|
||||
row[i] = ext[pis[i].Name];
|
||||
else if (item != null)
|
||||
row[i] = item.GetValue(pis[i]);
|
||||
}
|
||||
}
|
||||
Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>数据表转模型列表。普通反射,便于DAL查询后转任意模型列表</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<T> ReadModels<T>()
|
||||
{
|
||||
foreach (var model in ReadModels(typeof(T)))
|
||||
{
|
||||
yield return (T)model;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>数据表转模型列表。普通反射,便于DAL查询后转任意模型列表</summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Object> ReadModels(Type type)
|
||||
{
|
||||
var cs = Columns ?? throw new ArgumentNullException(nameof(Columns));
|
||||
var rows = Rows;
|
||||
if (rows == null) yield break;
|
||||
|
||||
// 可用属性
|
||||
var pis = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
var dic = pis.ToDictionary(e => SerialHelper.GetName(e), e => e, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var model = type.CreateInstance();
|
||||
if (model == null) continue;
|
||||
|
||||
for (var i = 0; i < row.Length; i++)
|
||||
{
|
||||
// 扩展赋值,或 反射赋值
|
||||
if (dic.TryGetValue(cs[i], out var pi) && pi.CanWrite)
|
||||
{
|
||||
var val = row[i].ChangeType(pi.PropertyType);
|
||||
if (model is IModel ext)
|
||||
ext[pi.Name] = val;
|
||||
else
|
||||
model.SetValue(pi, val);
|
||||
}
|
||||
}
|
||||
|
||||
yield return model;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 获取
|
||||
/// <summary>读取指定行的字段值</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="row"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public T? Get<T>(Int32 row, String name) => !TryGet<T>(row, name, out var value) ? default : value;
|
||||
|
||||
/// <summary>尝试读取指定行的字段值</summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="row"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean TryGet<T>(Int32 row, String name, out T? value)
|
||||
{
|
||||
value = default;
|
||||
var rs = Rows;
|
||||
if (rs == null) return false;
|
||||
|
||||
if (row < 0 || row >= rs.Count || name.IsNullOrEmpty()) return false;
|
||||
|
||||
var col = GetColumn(name);
|
||||
if (col < 0) return false;
|
||||
|
||||
value = rs[row][col].ChangeType<T>();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>根据名称找字段序号</summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
public Int32 GetColumn(String name)
|
||||
{
|
||||
var cs = Columns;
|
||||
if (cs == null) return -1;
|
||||
|
||||
for (var i = 0; i < cs.Length; i++)
|
||||
{
|
||||
if (cs[i].EqualIgnoreCase(name)) return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
/// <summary>数据集</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"DbTable[{Columns?.Length}][{Rows?.Count}]";
|
||||
|
||||
private static Dictionary<TypeCode, Object?>? _Defs;
|
||||
private static Object? GetDefault(TypeCode tc)
|
||||
{
|
||||
if (_Defs == null)
|
||||
{
|
||||
var dic = new Dictionary<TypeCode, Object?>();
|
||||
foreach (var item in Enum.GetValues(typeof(TypeCode)))
|
||||
{
|
||||
if (item is not TypeCode tc2) continue;
|
||||
|
||||
Object? val = null;
|
||||
val = tc2 switch
|
||||
{
|
||||
TypeCode.Boolean => false,
|
||||
TypeCode.Char => (Char)0,
|
||||
TypeCode.SByte => (SByte)0,
|
||||
TypeCode.Byte => (Byte)0,
|
||||
TypeCode.Int16 => (Int16)0,
|
||||
TypeCode.UInt16 => (UInt16)0,
|
||||
TypeCode.Int32 => 0,
|
||||
TypeCode.UInt32 => (UInt32)0,
|
||||
TypeCode.Int64 => (Int64)0,
|
||||
TypeCode.UInt64 => (UInt64)0,
|
||||
TypeCode.Single => (Single)0,
|
||||
TypeCode.Double => (Double)0,
|
||||
TypeCode.Decimal => (Decimal)0,
|
||||
TypeCode.DateTime => DateTime.MinValue,
|
||||
_ => null,
|
||||
};
|
||||
dic[tc2] = val;
|
||||
}
|
||||
_Defs = dic;
|
||||
}
|
||||
|
||||
return _Defs.TryGetValue(tc, out var obj) ? obj : null;
|
||||
}
|
||||
|
||||
Object ICloneable.Clone() => Clone();
|
||||
|
||||
/// <summary>克隆</summary>
|
||||
/// <returns></returns>
|
||||
public DbTable Clone()
|
||||
{
|
||||
var dt = new DbTable
|
||||
{
|
||||
Columns = Columns,
|
||||
Types = Types,
|
||||
Rows = Rows,
|
||||
Total = Total
|
||||
};
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
/// <summary>获取数据行</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public DbRow GetRow(Int32 index) => new(this, index);
|
||||
#endregion
|
||||
|
||||
#region 枚举
|
||||
/// <summary>获取枚举</summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerator<DbRow> GetEnumerator() => new DbEnumerator { Table = this };
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
private struct DbEnumerator : IEnumerator<DbRow>
|
||||
{
|
||||
public DbTable Table { get; set; }
|
||||
|
||||
private Int32 _row;
|
||||
private DbRow _Current;
|
||||
public readonly DbRow Current => _Current;
|
||||
|
||||
readonly Object IEnumerator.Current => _Current;
|
||||
|
||||
public Boolean MoveNext()
|
||||
{
|
||||
var rs = Table.Rows;
|
||||
if (rs == null || rs.Count == 0) return false;
|
||||
|
||||
if (_row < 0 || _row >= rs.Count)
|
||||
{
|
||||
_Current = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
_Current = new DbRow(Table, _row);
|
||||
|
||||
_row++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_Current = default;
|
||||
_row = -1;
|
||||
}
|
||||
|
||||
public readonly void Dispose() { }
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
121
src/Admin/ThingsGateway.NewLife.X/Data/GeoHash.cs
Normal file
121
src/Admin/ThingsGateway.NewLife.X/Data/GeoHash.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Text;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>经纬坐标的一维编码表示</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/geo_hash
|
||||
///
|
||||
/// 一维编码表示一个矩形区域,前缀表示更大区域,例如北京wx4fbzdvs80包含在wx4fbzdvs里面。
|
||||
/// 这个特性可以用于附近地点搜索。
|
||||
/// GeoHash编码位数及距离关系:
|
||||
/// 1位,+-2500km;
|
||||
/// 2位,+-630km;
|
||||
/// 3位,+-78km;
|
||||
/// 4位,+-20km;
|
||||
/// 5位,+-2.4km;
|
||||
/// 6位,+-610m;
|
||||
/// 7位,+-76m;
|
||||
/// 8位,+-19m;
|
||||
/// 9位,+-2m;
|
||||
/// </remarks>
|
||||
public static class GeoHash
|
||||
{
|
||||
#region 属性
|
||||
private static readonly Int32[] BITS = [16, 8, 4, 2, 1];
|
||||
private const String _base32 = "0123456789bcdefghjkmnpqrstuvwxyz";
|
||||
private static readonly Dictionary<Char, Int32> _decode = new();
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
static GeoHash()
|
||||
{
|
||||
for (var i = 0; i < _base32.Length; i++)
|
||||
{
|
||||
_decode[_base32[i]] = i;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>编码坐标点为GeoHash字符串</summary>
|
||||
/// <param name="longitude">经度</param>
|
||||
/// <param name="latitude">纬度</param>
|
||||
/// <param name="charCount">字符个数。默认9位字符编码,精度2米</param>
|
||||
/// <returns></returns>
|
||||
public static String Encode(Double longitude, Double latitude, Int32 charCount = 9)
|
||||
{
|
||||
Double[] longitudeRange = [-180, 180];
|
||||
Double[] latitudeRange = [-90, 90];
|
||||
|
||||
var isEvenBit = true;
|
||||
UInt64 bits = 0;
|
||||
var len = charCount * 5;
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
bits <<= 1;
|
||||
|
||||
// 轮流占用信息位
|
||||
var value = isEvenBit ? longitude : latitude;
|
||||
var rang = isEvenBit ? longitudeRange : latitudeRange;
|
||||
|
||||
var mid = (rang[0] + rang[1]) / 2;
|
||||
if (value >= mid)
|
||||
{
|
||||
bits |= 0x1;
|
||||
rang[0] = mid;
|
||||
}
|
||||
else
|
||||
{
|
||||
rang[1] = mid;
|
||||
}
|
||||
|
||||
isEvenBit = !isEvenBit;
|
||||
}
|
||||
|
||||
bits <<= (64 - len);
|
||||
|
||||
// base32编码
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < charCount; i++)
|
||||
{
|
||||
var pointer = (Int32)((bits & 0xf800000000000000L) >> 59);
|
||||
sb.Append(_base32[pointer]);
|
||||
bits <<= 5;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>解码GeoHash字符串为坐标点</summary>
|
||||
/// <param name="geohash"></param>
|
||||
/// <returns></returns>
|
||||
public static (Double Longitude, Double Latitude) Decode(String geohash)
|
||||
{
|
||||
Double[] latitudeRange = [-90, 90];
|
||||
Double[] longitudeRange = [-180, 180];
|
||||
|
||||
var isEvenBit = true;
|
||||
for (var i = 0; i < geohash.Length; i++)
|
||||
{
|
||||
var ch = _decode[geohash[i]];
|
||||
for (var j = 0; j < 5; j++)
|
||||
{
|
||||
// 轮流解码信息位
|
||||
var rang = isEvenBit ? longitudeRange : latitudeRange;
|
||||
var mid = (rang[0] + rang[1]) / 2;
|
||||
if ((ch & BITS[j]) != 0)
|
||||
rang[0] = mid;
|
||||
else
|
||||
rang[1] = mid;
|
||||
|
||||
isEvenBit = !isEvenBit;
|
||||
}
|
||||
}
|
||||
|
||||
var longitude = (longitudeRange[0] + longitudeRange[1]) / 2;
|
||||
var latitude = (latitudeRange[0] + latitudeRange[1]) / 2;
|
||||
|
||||
return (longitude, latitude);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
21
src/Admin/ThingsGateway.NewLife.X/Data/IData.cs
Normal file
21
src/Admin/ThingsGateway.NewLife.X/Data/IData.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Net;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据帧接口。用于网络通信领域,定义数据帧的必要字段</summary>
|
||||
public interface IData
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>原始数据包</summary>
|
||||
IPacket? Packet { get; set; }
|
||||
|
||||
/// <summary>远程地址</summary>
|
||||
IPEndPoint? Remote { get; set; }
|
||||
|
||||
/// <summary>解码后的消息</summary>
|
||||
Object? Message { get; set; }
|
||||
|
||||
/// <summary>用户自定义数据</summary>
|
||||
Object? UserState { get; set; }
|
||||
#endregion
|
||||
}
|
||||
16
src/Admin/ThingsGateway.NewLife.X/Data/IExtend.cs
Normal file
16
src/Admin/ThingsGateway.NewLife.X/Data/IExtend.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>具有可读写的扩展数据</summary>
|
||||
/// <remarks>
|
||||
/// 仅限于扩展属性,不包括基本属性,区别于 IModel
|
||||
/// </remarks>
|
||||
public interface IExtend
|
||||
{
|
||||
/// <summary>数据项</summary>
|
||||
IDictionary<String, Object?> Items { get; }
|
||||
|
||||
/// <summary>设置 或 获取 数据项</summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
Object? this[String key] { get; set; }
|
||||
}
|
||||
74
src/Admin/ThingsGateway.NewLife.X/Data/IFilter.cs
Normal file
74
src/Admin/ThingsGateway.NewLife.X/Data/IFilter.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据过滤器</summary>
|
||||
public interface IFilter
|
||||
{
|
||||
/// <summary>下一个过滤器</summary>
|
||||
IFilter? Next { get; }
|
||||
|
||||
/// <summary>对封包执行过滤器</summary>
|
||||
/// <param name="context"></param>
|
||||
void Execute(FilterContext context);
|
||||
}
|
||||
|
||||
/// <summary>过滤器上下文</summary>
|
||||
public class FilterContext
|
||||
{
|
||||
/// <summary>封包</summary>
|
||||
public virtual IPacket? Packet { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>过滤器助手</summary>
|
||||
public static class FilterHelper
|
||||
{
|
||||
/// <summary>在链条里面查找指定类型的过滤器</summary>
|
||||
/// <param name="filter"></param>
|
||||
/// <param name="filterType"></param>
|
||||
/// <returns></returns>
|
||||
public static IFilter? Find(this IFilter filter, Type filterType)
|
||||
{
|
||||
if (filter == null || filterType == null) return null;
|
||||
|
||||
if (filter.GetType() == filterType) return filter;
|
||||
|
||||
return filter.Next?.Find(filterType);
|
||||
}
|
||||
|
||||
///// <summary>在开头插入过滤器</summary>
|
||||
///// <param name="filter"></param>
|
||||
///// <param name="newFilter"></param>
|
||||
///// <returns></returns>
|
||||
//public static IFilter Add(this IFilter filter, IFilter newFilter)
|
||||
//{
|
||||
// if (filter == null || newFilter == null) return filter;
|
||||
|
||||
// newFilter.Next = filter;
|
||||
|
||||
// return newFilter;
|
||||
//}
|
||||
}
|
||||
|
||||
/// <summary>数据过滤器基类</summary>
|
||||
public abstract class FilterBase : IFilter
|
||||
{
|
||||
/// <summary>下一个过滤器</summary>
|
||||
public IFilter? Next { get; set; }
|
||||
|
||||
///// <summary>实例化过滤器</summary>
|
||||
///// <param name="next"></param>
|
||||
//public FilterBase(IFilter next) { Next = next; }
|
||||
|
||||
/// <summary>对封包执行过滤器</summary>
|
||||
/// <param name="context"></param>
|
||||
public virtual void Execute(FilterContext context)
|
||||
{
|
||||
if (!OnExecute(context) || context.Packet == null) return;
|
||||
|
||||
Next?.Execute(context);
|
||||
}
|
||||
|
||||
/// <summary>执行过滤</summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns>返回是否执行下一个过滤器</returns>
|
||||
protected abstract Boolean OnExecute(FilterContext context);
|
||||
}
|
||||
16
src/Admin/ThingsGateway.NewLife.X/Data/IModel.cs
Normal file
16
src/Admin/ThingsGateway.NewLife.X/Data/IModel.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>模型数据接口,支持索引器读写属性</summary>
|
||||
/// <remarks>
|
||||
/// 可借助反射取得属性列表成员,从而对实体模型属性进行读写操作,避免反射带来的负担。
|
||||
/// 常用于WebApi模型类以及XCode数据实体类,也用于魔方接口拷贝。
|
||||
///
|
||||
/// 逐步替代 IExtend 的大部分使用场景
|
||||
/// </remarks>
|
||||
public interface IModel
|
||||
{
|
||||
/// <summary>设置 或 获取 数据项</summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
Object? this[String key] { get; set; }
|
||||
}
|
||||
900
src/Admin/ThingsGateway.NewLife.X/Data/IPacket.cs
Normal file
900
src/Admin/ThingsGateway.NewLife.X/Data/IPacket.cs
Normal file
@@ -0,0 +1,900 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据包接口。几乎内存共享理念,统一提供数据包,内部可能是内存池、数组和旧版Packet等多种实现</summary>
|
||||
/// <remarks>
|
||||
/// 常用于网络编程和协议解析,为了避免大量内存分配和拷贝,采用数据包对象池,复用内存。
|
||||
/// 数据包接口一般由结构体实现,提升GC性能。
|
||||
///
|
||||
/// 特别需要注意内存管理权转移问题,一般由调用栈的上部负责释放内存。
|
||||
/// Socket非阻塞事件接收时,负责申请与释放内存,数据处理是调用栈下游;
|
||||
/// Socket阻塞接收时,接收函数内部申请内存,外部使用方释放内存,管理权甚至在此次传递给消息层;
|
||||
///
|
||||
/// 作为过渡期,旧版Packet也会实现该接口,以便逐步替换。
|
||||
/// </remarks>
|
||||
public interface IPacket
|
||||
{
|
||||
/// <summary>数据长度。仅当前数据包,不包括Next</summary>
|
||||
Int32 Length { get; }
|
||||
|
||||
/// <summary>下一个链式包</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
IPacket? Next { get; set; }
|
||||
|
||||
/// <summary>总长度。包括Next链的长度</summary>
|
||||
Int32 Total { get; }
|
||||
|
||||
/// <summary>获取/设置 指定位置的字节</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
Byte this[Int32 index] { get; set; }
|
||||
|
||||
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
Span<Byte> GetSpan();
|
||||
|
||||
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
Memory<Byte> GetMemory();
|
||||
|
||||
/// <summary>切片得到新数据包</summary>
|
||||
/// <remarks>引用相同内存块或缓冲区,减少内存分配</remarks>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <returns></returns>
|
||||
IPacket Slice(Int32 offset, Int32 count = -1);
|
||||
|
||||
/// <summary>切片得到新数据包,同时转移内存管理权</summary>
|
||||
/// <remarks>
|
||||
/// 引用相同内存块或缓冲区,减少内存分配。
|
||||
/// 如果原数据包只切一次给新包,可以转移内存管理权,由新数据包负责释放;
|
||||
/// 如果原数据包需要切多次,不要转移内存管理权,由原数据包负责释放。
|
||||
/// </remarks>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <param name="transferOwner">转移所有权。若为true则由新数据包负责归还缓冲区,只能转移一次。并非所有数据包都支持</param>
|
||||
/// <returns></returns>
|
||||
IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner);
|
||||
|
||||
/// <summary>尝试获取缓冲区。仅本片段,不包括Next</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
Boolean TryGetArray(out ArraySegment<Byte> segment);
|
||||
}
|
||||
|
||||
/// <summary>拥有管理权的数据包。使用完以后需要释放</summary>
|
||||
public interface IOwnerPacket : IPacket, IDisposable { }
|
||||
|
||||
/// <summary>内存包辅助类</summary>
|
||||
public static class PacketHelper
|
||||
{
|
||||
/// <summary>附加一个包到当前包链的末尾</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="next"></param>
|
||||
public static IPacket Append(this IPacket pk, IPacket next)
|
||||
{
|
||||
if (next == null) return pk;
|
||||
|
||||
var p = pk;
|
||||
while (p.Next != null) p = p.Next;
|
||||
p.Next = next;
|
||||
|
||||
return pk;
|
||||
}
|
||||
|
||||
/// <summary>附加一个包到当前包链的末尾</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="next"></param>
|
||||
public static IPacket Append(this IPacket pk, Byte[] next) => Append(pk, new ArrayPacket(next));
|
||||
|
||||
/// <summary>转字符串</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="encoding"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <returns></returns>
|
||||
public static String ToStr(this IPacket pk, Encoding? encoding = null, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
// 总是有异常数据,这里屏蔽异常
|
||||
if (pk == null) return null!;
|
||||
|
||||
if (pk.Next == null)
|
||||
{
|
||||
if (count < 0) count = pk.Length - offset;
|
||||
var span = pk.GetSpan();
|
||||
if (span.Length > count) span = span[..count];
|
||||
|
||||
return span.ToStr(encoding);
|
||||
}
|
||||
|
||||
if (count < 0) count = pk.Total - offset;
|
||||
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
for (var p = pk; p != null; p = p.Next)
|
||||
{
|
||||
var span = p.GetSpan();
|
||||
if (span.Length > count) span = span[..count];
|
||||
|
||||
sb.Append(span.ToStr(encoding));
|
||||
|
||||
count -= span.Length;
|
||||
if (count <= 0) break;
|
||||
}
|
||||
return sb.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>以十六进制编码表示</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="maxLength">最大显示多少个字节。默认-1显示全部</param>
|
||||
/// <param name="separate">分隔符</param>
|
||||
/// <param name="groupSize">分组大小,为0时对每个字节应用分隔符,否则对每个分组使用</param>
|
||||
/// <returns></returns>
|
||||
public static String ToHex(this IPacket pk, Int32 maxLength = 32, String? separate = null, Int32 groupSize = 0)
|
||||
{
|
||||
if (pk.Length == 0) return String.Empty;
|
||||
|
||||
if (pk.Next == null)
|
||||
return pk.GetSpan().ToHex(separate, groupSize, maxLength);
|
||||
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
for (var p = pk; p != null; p = p.Next)
|
||||
{
|
||||
sb.Append(p.GetSpan().ToHex(separate, groupSize, maxLength));
|
||||
|
||||
maxLength -= p.Length;
|
||||
if (maxLength <= 0) break;
|
||||
}
|
||||
return sb.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>写入数据流,netfx中可能有二次拷贝</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="stream"></param>
|
||||
public static void CopyTo(this IPacket pk, Stream stream)
|
||||
{
|
||||
for (var p = pk; p != null; p = p.Next)
|
||||
{
|
||||
if (p.TryGetArray(out var segment))
|
||||
stream.Write(segment.Array!, segment.Offset, segment.Count);
|
||||
else
|
||||
stream.Write(p.GetMemory());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>异步拷贝</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task CopyToAsync(this IPacket pk, Stream stream, CancellationToken cancellationToken = default)
|
||||
{
|
||||
for (var p = pk; p != null; p = p.Next)
|
||||
{
|
||||
if (p.TryGetArray(out var segment))
|
||||
await stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken).ConfigureAwait(false);
|
||||
else
|
||||
await stream.WriteAsync(p.GetMemory(), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取数据流</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public static Stream GetStream(this IPacket pk)
|
||||
{
|
||||
var ms = new MemoryStream(pk.Total);
|
||||
pk.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>返回数据段,可能有拷贝</summary>
|
||||
/// <returns></returns>
|
||||
public static ArraySegment<Byte> ToSegment(this IPacket pk)
|
||||
{
|
||||
if (pk.Next == null && pk.TryGetArray(out var segment)) return segment;
|
||||
|
||||
var ms = Pool.MemoryStream.Get();
|
||||
pk.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
return new ArraySegment<Byte>(ms.Return(true));
|
||||
}
|
||||
|
||||
/// <summary>返回数据段集合,可能有拷贝</summary>
|
||||
/// <returns></returns>
|
||||
public static IList<ArraySegment<Byte>> ToSegments(this IPacket pk)
|
||||
{
|
||||
// 初始4元素,优化扩容
|
||||
var list = new List<ArraySegment<Byte>>(4);
|
||||
|
||||
for (var p = pk; p != null; p = p.Next)
|
||||
{
|
||||
if (p.TryGetArray(out var seg))
|
||||
list.Add(seg);
|
||||
else
|
||||
list.Add(new ArraySegment<Byte>(p.GetSpan().ToArray(), 0, p.Length));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>返回字节数组。无差别复制,一定返回新数组</summary>
|
||||
/// <returns></returns>
|
||||
public static Byte[] ToArray(this IPacket pk)
|
||||
{
|
||||
if (pk.Next == null) return pk.GetSpan().ToArray();
|
||||
|
||||
// 链式包输出
|
||||
var ms = Pool.MemoryStream.Get();
|
||||
pk.CopyTo(ms);
|
||||
|
||||
return ms.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>从封包中读取指定数据区,读取全部时直接返回缓冲区,以提升性能</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="offset">相对于数据包的起始位置,实际上是数组的Offset+offset</param>
|
||||
/// <param name="count">字节个数</param>
|
||||
/// <returns></returns>
|
||||
public static Byte[] ReadBytes(this IPacket pk, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (pk.Next == null)
|
||||
{
|
||||
if (count < 0) count = pk.Length - offset;
|
||||
|
||||
if (pk.TryGetArray(out var seg))
|
||||
{
|
||||
// 读取全部
|
||||
if (offset == 0 && count == pk.Length)
|
||||
{
|
||||
if (seg.Offset == 0 && seg.Count == seg.Array!.Length) return seg.Array;
|
||||
}
|
||||
|
||||
return seg.Array!.ReadBytes(seg.Offset + offset, count);
|
||||
}
|
||||
|
||||
var span = pk.GetSpan();
|
||||
return span.Slice(offset, count).ToArray();
|
||||
}
|
||||
|
||||
return pk.ToArray().ReadBytes(offset, count);
|
||||
}
|
||||
|
||||
/// <summary>深度克隆一份数据包,拷贝数据区</summary>
|
||||
/// <returns></returns>
|
||||
public static IPacket Clone(this IPacket pk)
|
||||
{
|
||||
if (pk.Next == null)
|
||||
{
|
||||
// 需要深度拷贝,避免重用缓冲区
|
||||
return new ArrayPacket(pk.GetSpan().ToArray());
|
||||
}
|
||||
|
||||
// 链式包输出
|
||||
var ms = new MemoryStream();
|
||||
pk.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
return new ArrayPacket(ms);
|
||||
}
|
||||
|
||||
/// <summary>尝试获取内存片段。非链式数据包时直接返回</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <param name="span"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean TryGetSpan(this IPacket pk, out Span<Byte> span)
|
||||
{
|
||||
if (pk.Next == null)
|
||||
{
|
||||
span = pk.GetSpan();
|
||||
return true;
|
||||
}
|
||||
|
||||
span = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>扩展头部,用于填充包头,减少内存分配</summary>
|
||||
/// <param name="pk">数据包</param>
|
||||
/// <param name="size">要扩大的头部大小,不包括负载数据</param>
|
||||
/// <returns>扩展后的数据包</returns>
|
||||
public static IPacket ExpandHeader(this IPacket? pk, Int32 size)
|
||||
{
|
||||
if (pk is ArrayPacket ap && ap.Offset >= size)
|
||||
{
|
||||
return new ArrayPacket(ap.Buffer, ap.Offset - size, ap.Length + size) { Next = ap.Next };
|
||||
}
|
||||
else if (pk is OwnerPacket owner && owner.Offset >= size)
|
||||
{
|
||||
return new OwnerPacket(owner, size);
|
||||
}
|
||||
|
||||
return new OwnerPacket(size) { Next = pk };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>所有权内存包。具有所有权管理,不再使用时释放</summary>
|
||||
/// <remarks>
|
||||
/// 使用时务必明确所有权归属,用完后及时释放。
|
||||
/// </remarks>
|
||||
public class OwnerPacket : MemoryManager<Byte>, IPacket, IOwnerPacket
|
||||
{
|
||||
#region 属性
|
||||
private Byte[] _buffer;
|
||||
/// <summary>缓冲区</summary>
|
||||
public Byte[] Buffer => _buffer;
|
||||
|
||||
private Int32 _offset;
|
||||
/// <summary>数据偏移</summary>
|
||||
public Int32 Offset => _offset;
|
||||
|
||||
private Int32 _length;
|
||||
/// <summary>数据长度</summary>
|
||||
public Int32 Length => _length;
|
||||
|
||||
/// <summary>获取/设置 指定位置的字节</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public Byte this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
return Next[p];
|
||||
}
|
||||
|
||||
return _buffer[_offset + index];
|
||||
}
|
||||
set
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
Next[p] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[_offset + index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>下一个链式包</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IPacket? Next { get; set; }
|
||||
|
||||
/// <summary>总长度</summary>
|
||||
public Int32 Total => Length + (Next?.Total ?? 0);
|
||||
|
||||
private Boolean _hasOwner;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化指定长度的内存包,从共享内存池中借出</summary>
|
||||
/// <param name="length">长度</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public OwnerPacket(Int32 length)
|
||||
{
|
||||
if (length < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
|
||||
|
||||
_buffer = ArrayPool<Byte>.Shared.Rent(length);
|
||||
_offset = 0;
|
||||
_length = length;
|
||||
_hasOwner = true;
|
||||
}
|
||||
|
||||
/// <summary>实例化内存包,指定内存所有者和长度</summary>
|
||||
/// <param name="buffer">缓冲区</param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="length">长度</param>
|
||||
/// <param name="hasOwner">是否转移所有权</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public OwnerPacket(Byte[] buffer, Int32 offset, Int32 length, Boolean hasOwner)
|
||||
{
|
||||
if (offset < 0 || length < 0 || offset + length > buffer.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
|
||||
|
||||
_buffer = buffer;
|
||||
_offset = offset;
|
||||
_length = length;
|
||||
_hasOwner = hasOwner;
|
||||
}
|
||||
|
||||
/// <summary>从另一个内存包创建新内存包,共用缓冲区</summary>
|
||||
/// <param name="owner"></param>
|
||||
/// <param name="expandSize"></param>
|
||||
public OwnerPacket(OwnerPacket owner, Int32 expandSize)
|
||||
{
|
||||
_buffer = owner.Buffer;
|
||||
_offset = owner.Offset - expandSize;
|
||||
_length = owner.Length + expandSize;
|
||||
Next = owner.Next;
|
||||
|
||||
// 转移所有权
|
||||
_hasOwner = owner._hasOwner;
|
||||
owner._hasOwner = false;
|
||||
}
|
||||
|
||||
/// <summary>销毁释放</summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(Boolean disposing)
|
||||
{
|
||||
if (!_hasOwner) return;
|
||||
_hasOwner = true;
|
||||
|
||||
var buffer = _buffer;
|
||||
if (buffer != null)
|
||||
{
|
||||
// 释放内存所有者以后,直接置空,避免重复使用
|
||||
_buffer = null!;
|
||||
|
||||
ArrayPool<Byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
Next.TryDispose();
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public override Span<Byte> GetSpan() => new(_buffer, _offset, _length);
|
||||
|
||||
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public Memory<Byte> GetMemory() => new(_buffer, _offset, _length);
|
||||
|
||||
/// <summary>重新设置数据包大小。一般用于申请缓冲区并读取数据后设置为实际大小</summary>
|
||||
/// <param name="size"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public OwnerPacket Resize(Int32 size)
|
||||
{
|
||||
if (size < 0) throw new ArgumentNullException(nameof(size));
|
||||
|
||||
if (Next == null)
|
||||
{
|
||||
if (size > _buffer.Length) throw new ArgumentOutOfRangeException(nameof(size));
|
||||
|
||||
_length = size;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (size >= _length) throw new NotSupportedException();
|
||||
|
||||
_length = size;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>切片得到新数据包</summary>
|
||||
/// <remarks>引用相同内存块,减少内存分配</remarks>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
public IPacket Slice(Int32 offset, Int32 count) => Slice(offset, count, true);
|
||||
|
||||
/// <summary>切片得到新数据包,同时转移内存管理权</summary>
|
||||
/// <remarks>引用相同内存块,减少内存分配</remarks>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <param name="transferOwner">转移所有权。若为true则由新数据包负责归还缓冲区,只能转移一次</param>
|
||||
public IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner)
|
||||
{
|
||||
// 释放后无法再次使用
|
||||
if (_buffer == null) throw new InvalidDataException();
|
||||
|
||||
var buffer = _buffer;
|
||||
var start = _offset + offset;
|
||||
var remain = _length - offset;
|
||||
var hasOwner = _hasOwner && transferOwner;
|
||||
|
||||
// 超出范围
|
||||
if (count > Total - offset) throw new ArgumentOutOfRangeException(nameof(count), "count must be non-negative and less than or equal to the memory owner's length.");
|
||||
|
||||
// 单个数据包
|
||||
if (Next == null)
|
||||
{
|
||||
// 转移管理权
|
||||
if (transferOwner) _hasOwner = false;
|
||||
|
||||
if (count < 0 || count > remain) count = remain;
|
||||
return new OwnerPacket(buffer, start, count, hasOwner);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果当前段用完,则取下一段。当前包自己负责释放
|
||||
if (remain <= 0) return Next.Slice(offset - _length, count, transferOwner);
|
||||
|
||||
// 转移管理权
|
||||
if (transferOwner) _hasOwner = false;
|
||||
|
||||
// 当前包用一截,剩下的全部。转移管理权后,Next随新包一起释放
|
||||
if (count < 0) return new OwnerPacket(buffer, start, remain, hasOwner) { Next = Next };
|
||||
|
||||
// 当前包可以读完。转移管理权后,Next失去释放机会
|
||||
if (count <= remain) return new OwnerPacket(buffer, start, count, hasOwner);
|
||||
|
||||
// 当前包用一截,剩下的再截取。转移管理权后,Next再次转移管理权,随新包一起释放
|
||||
return new OwnerPacket(buffer, start, remain, hasOwner) { Next = Next.Slice(0, count - remain, transferOwner) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>尝试获取缓冲区。仅本片段,不包括Next</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
protected override Boolean TryGetArray(out ArraySegment<Byte> segment)
|
||||
{
|
||||
segment = new ArraySegment<Byte>(_buffer, _offset, _length);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>尝试获取数据段</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
Boolean IPacket.TryGetArray(out ArraySegment<Byte> segment) => TryGetArray(out segment);
|
||||
|
||||
/// <summary>释放所有权,不再使用</summary>
|
||||
public void Free()
|
||||
{
|
||||
_buffer = null!;
|
||||
Next = null;
|
||||
}
|
||||
|
||||
/// <summary>钉住内存</summary>
|
||||
/// <param name="elementIndex"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
public override MemoryHandle Pin(Int32 elementIndex = 0) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>取消钉内存</summary>
|
||||
/// <exception cref="NotImplementedException"></exception>
|
||||
public override void Unpin() => throw new NotImplementedException();
|
||||
|
||||
#region 重载运算符
|
||||
/// <summary>已重载</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"[{_buffer.Length}]({_offset}, {_length})<{Total}>";
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>内存包</summary>
|
||||
/// <remarks>
|
||||
/// 内存包可能来自内存池,失去所有权时已被释放,因此不应该长期持有。
|
||||
/// </remarks>
|
||||
public struct MemoryPacket : IPacket
|
||||
{
|
||||
#region 属性
|
||||
private readonly Memory<Byte> _memory;
|
||||
/// <summary>内存</summary>
|
||||
public readonly Memory<Byte> Memory => _memory;
|
||||
|
||||
private readonly Int32 _length;
|
||||
/// <summary>数据长度</summary>
|
||||
public readonly Int32 Length => _length;
|
||||
|
||||
/// <summary>获取/设置 指定位置的字节</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public Byte this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
return Next[p];
|
||||
}
|
||||
|
||||
return _memory.Span[index];
|
||||
}
|
||||
set
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
Next[p] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_memory.Span[index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>下一个链式包</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IPacket? Next { get; set; }
|
||||
|
||||
/// <summary>总长度</summary>
|
||||
public readonly Int32 Total => Length + (Next?.Total ?? 0);
|
||||
#endregion
|
||||
|
||||
/// <summary>实例化内存包,指定内存和长度</summary>
|
||||
/// <param name="memory">内存</param>
|
||||
/// <param name="length">长度</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public MemoryPacket(Memory<Byte> memory, Int32 length)
|
||||
{
|
||||
if (length < 0 || length > memory.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be non-negative and less than or equal to the memory owner's length.");
|
||||
|
||||
_memory = memory;
|
||||
_length = length;
|
||||
}
|
||||
|
||||
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public readonly Span<Byte> GetSpan() => _memory.Span[.._length];
|
||||
|
||||
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public readonly Memory<Byte> GetMemory() => _memory[.._length];
|
||||
|
||||
/// <summary>切片得到新数据包,共用内存块</summary>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
public IPacket Slice(Int32 offset, Int32 count) => Slice(offset, count, true);
|
||||
|
||||
/// <summary>切片得到新数据包,共用内存块</summary>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <param name="transferOwner">转移所有权。不支持</param>
|
||||
public IPacket Slice(Int32 offset, Int32 count, Boolean transferOwner)
|
||||
{
|
||||
// 带有Next时,不支持Slice
|
||||
if (Next != null) throw new NotSupportedException("Slice with Next");
|
||||
|
||||
var remain = _length - offset;
|
||||
if (count < 0 || count > remain) count = remain;
|
||||
if (offset == 0 && count == _length) return this;
|
||||
|
||||
if (offset == 0)
|
||||
return new MemoryPacket(_memory, count);
|
||||
|
||||
return new MemoryPacket(_memory[offset..], count);
|
||||
}
|
||||
|
||||
/// <summary>尝试获取缓冲区。仅本片段,不包括Next</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public readonly Boolean TryGetArray(out ArraySegment<Byte> segment) => MemoryMarshal.TryGetArray(GetMemory(), out segment);
|
||||
|
||||
/// <summary>已重载</summary>
|
||||
/// <returns></returns>
|
||||
public override readonly String ToString() => $"[{_memory.Length}](0, {_length})<{Total}>";
|
||||
}
|
||||
|
||||
/// <summary>字节数组包</summary>
|
||||
public struct ArrayPacket : IPacket
|
||||
{
|
||||
#region 属性
|
||||
private Byte[] _buffer;
|
||||
/// <summary>缓冲区</summary>
|
||||
public readonly Byte[] Buffer => _buffer;
|
||||
|
||||
private readonly Int32 _offset;
|
||||
/// <summary>数据偏移</summary>
|
||||
public readonly Int32 Offset => _offset;
|
||||
|
||||
private readonly Int32 _length;
|
||||
/// <summary>数据长度</summary>
|
||||
public readonly Int32 Length => _length;
|
||||
|
||||
/// <summary>下一个链式包</summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public IPacket? Next { get; set; }
|
||||
|
||||
/// <summary>总长度</summary>
|
||||
public readonly Int32 Total => Length + (Next?.Total ?? 0);
|
||||
|
||||
/// <summary>空数组</summary>
|
||||
public static ArrayPacket Empty = new([]);
|
||||
#endregion
|
||||
|
||||
#region 索引
|
||||
/// <summary>获取/设置 指定位置的字节</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public Byte this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
return Next[p];
|
||||
}
|
||||
|
||||
return _buffer[_offset + index];
|
||||
}
|
||||
set
|
||||
{
|
||||
var p = index - _length;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
Next[p] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer[_offset + index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>通过指定字节数组来实例化数据包</summary>
|
||||
/// <param name="buf"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
public ArrayPacket(Byte[] buf, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (count < 0) count = buf.Length - offset;
|
||||
|
||||
_buffer = buf;
|
||||
_offset = offset;
|
||||
_length = count;
|
||||
}
|
||||
|
||||
/// <summary>从可扩展内存流实例化,尝试窃取内存流内部的字节数组,失败后拷贝</summary>
|
||||
/// <remarks>因数据包内数组窃取自内存流,需要特别小心,避免多线程共用。常用于内存流转数据包,而内存流不再使用</remarks>
|
||||
/// <param name="stream"></param>
|
||||
public ArrayPacket(Stream stream)
|
||||
{
|
||||
if (stream is MemoryStream ms)
|
||||
{
|
||||
#if !NET45
|
||||
// 尝试抠了内部存储区,下面代码需要.Net 4.6支持
|
||||
if (ms.TryGetBuffer(out var seg))
|
||||
{
|
||||
if (seg.Array == null) throw new InvalidDataException();
|
||||
|
||||
_buffer = seg.Array;
|
||||
_offset = seg.Offset + (Int32)ms.Position;
|
||||
_length = seg.Count - (Int32)ms.Position;
|
||||
return;
|
||||
}
|
||||
// GetBuffer窃取内部缓冲区后,无法得知真正的起始位置index,可能导致错误取数
|
||||
// public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible)
|
||||
|
||||
//try
|
||||
//{
|
||||
// Set(ms.GetBuffer(), (Int32)ms.Position, (Int32)(ms.Length - ms.Position));
|
||||
//}
|
||||
//catch (UnauthorizedAccessException) { }
|
||||
#endif
|
||||
}
|
||||
|
||||
var buf = new Byte[stream.Length - stream.Position];
|
||||
var count = stream.Read(buf, 0, buf.Length);
|
||||
_buffer = buf;
|
||||
_offset = 0;
|
||||
_length = count;
|
||||
|
||||
// 必须确保数据流位置不变
|
||||
if (count > 0) stream.Seek(-count, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
/// <summary>从数据段实例化数据包</summary>
|
||||
/// <param name="segment"></param>
|
||||
public ArrayPacket(ArraySegment<Byte> segment) : this(segment.Array!, segment.Offset, segment.Count) { }
|
||||
#endregion
|
||||
|
||||
/// <summary>获取分片包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public readonly Span<Byte> GetSpan() => new(_buffer, _offset, _length);
|
||||
|
||||
/// <summary>获取内存包。在管理权生命周期内短暂使用</summary>
|
||||
/// <returns></returns>
|
||||
public readonly Memory<Byte> GetMemory() => new(_buffer, _offset, _length);
|
||||
|
||||
/// <summary>切片得到新数据包,共用缓冲区</summary>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
IPacket IPacket.Slice(Int32 offset, Int32 count) => (this as IPacket).Slice(offset, count, true);
|
||||
|
||||
/// <summary>切片得到新数据包,共用缓冲区</summary>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <param name="transferOwner">转移所有权。仅对Next有效</param>
|
||||
IPacket IPacket.Slice(Int32 offset, Int32 count, Boolean transferOwner)
|
||||
{
|
||||
if (count == 0) return Empty;
|
||||
|
||||
var remain = _length - offset;
|
||||
var next = Next;
|
||||
if (next != null && remain <= 0) return next.Slice(offset - _length, count, transferOwner);
|
||||
|
||||
return Slice(offset, count, transferOwner);
|
||||
}
|
||||
|
||||
/// <summary>切片得到新数据包,共用缓冲区,无内存分配</summary>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">个数。默认-1表示到末尾</param>
|
||||
/// <param name="transferOwner">转移所有权。仅对Next有效</param>
|
||||
public ArrayPacket Slice(Int32 offset, Int32 count = -1, Boolean transferOwner = false)
|
||||
{
|
||||
if (count == 0) return Empty;
|
||||
|
||||
var start = Offset + offset;
|
||||
var remain = _length - offset;
|
||||
|
||||
var next = Next;
|
||||
if (next == null)
|
||||
{
|
||||
// count 是 offset 之后的个数
|
||||
if (count < 0 || count > remain) count = remain;
|
||||
if (count < 0) count = 0;
|
||||
|
||||
return new ArrayPacket(_buffer, start, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果当前段用完,则取下一段。强转ArrayPacket,如果不是则抛出异常
|
||||
if (remain <= 0)
|
||||
return (ArrayPacket)next.Slice(offset - _length, count, transferOwner);
|
||||
|
||||
// 当前包用一截,剩下的全部
|
||||
if (count < 0)
|
||||
return new ArrayPacket(_buffer, start, remain) { Next = next };
|
||||
|
||||
// 当前包可以读完
|
||||
if (count <= remain)
|
||||
return new ArrayPacket(_buffer, start, count);
|
||||
|
||||
// 当前包用一截,剩下的再截取
|
||||
return new ArrayPacket(_buffer, start, remain) { Next = next.Slice(0, count - remain, transferOwner) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>尝试获取缓冲区。仅本片段,不包括Next</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public readonly Boolean TryGetArray(out ArraySegment<Byte> segment)
|
||||
{
|
||||
segment = new ArraySegment<Byte>(_buffer, _offset, _length);
|
||||
return true;
|
||||
}
|
||||
|
||||
#region 重载运算符
|
||||
/// <summary>重载类型转换,字节数组直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator ArrayPacket(Byte[] value) => new(value);
|
||||
|
||||
/// <summary>重载类型转换,一维数组直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator ArrayPacket(ArraySegment<Byte> value) => new(value.Array!, value.Offset, value.Count);
|
||||
|
||||
/// <summary>重载类型转换,字符串直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator ArrayPacket(String value) => new(value.GetBytes());
|
||||
|
||||
/// <summary>已重载</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"[{_buffer.Length}]({_offset}, {_length})<{Total}>";
|
||||
#endregion
|
||||
}
|
||||
112
src/Admin/ThingsGateway.NewLife.X/Data/IPacketEncoder.cs
Normal file
112
src/Admin/ThingsGateway.NewLife.X/Data/IPacketEncoder.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据包编码器接口</summary>
|
||||
public interface IPacketEncoder
|
||||
{
|
||||
/// <summary>数值转数据包</summary>
|
||||
/// <param name="value">数值对象</param>
|
||||
/// <returns></returns>
|
||||
IPacket? Encode(Object value);
|
||||
|
||||
/// <summary>数据包转对象</summary>
|
||||
/// <param name="data">数据包</param>
|
||||
/// <param name="type">目标类型</param>
|
||||
/// <returns></returns>
|
||||
Object? Decode(IPacket data, Type type);
|
||||
}
|
||||
|
||||
/// <summary>编码器扩展</summary>
|
||||
public static class PackerEncoderExtensions
|
||||
{
|
||||
/// <summary>数据包转对象</summary>
|
||||
/// <typeparam name="T">目标类型</typeparam>
|
||||
/// <param name="encoder"></param>
|
||||
/// <param name="data">数据包</param>
|
||||
/// <returns></returns>
|
||||
public static T? Decode<T>(this IPacketEncoder encoder, IPacket data) => (T?)encoder.Decode(data, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>默认数据包编码器。基础类型直接转,复杂类型Json序列化</summary>
|
||||
public class DefaultPacketEncoder : IPacketEncoder
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>Json序列化主机</summary>
|
||||
public IJsonHost JsonHost { get; set; } = JsonHelper.Default;
|
||||
|
||||
/// <summary>解码出错时抛出异常。默认false不抛出异常,仅返回默认值</summary>
|
||||
public Boolean ThrowOnError { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>数值转数据包</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public virtual IPacket? Encode(Object value)
|
||||
{
|
||||
if (value == null) return null!;
|
||||
|
||||
if (value is IPacket pk) return pk;
|
||||
if (value is Byte[] buf) return (ArrayPacket)buf;
|
||||
if (value is IAccessor acc) return acc.ToPacket();
|
||||
|
||||
var str = OnEncode(value);
|
||||
|
||||
return (ArrayPacket)str.GetBytes();
|
||||
}
|
||||
|
||||
/// <summary>编码为字符串。复杂类型采用Json序列化</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual String? OnEncode(Object value)
|
||||
{
|
||||
var type = value.GetType();
|
||||
return type.GetTypeCode() switch
|
||||
{
|
||||
TypeCode.Object => JsonHost.Write(value),
|
||||
TypeCode.String => value as String,
|
||||
TypeCode.DateTime => ((DateTime)value).ToString("yyyy-MM-dd HH:mm:ss.fff"),
|
||||
_ => value + "",
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>数据包转对象</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Object? Decode(IPacket data, Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type == typeof(IPacket)) return data;
|
||||
if (type == typeof(Packet)) return data is Packet pk ? pk : data.ReadBytes();
|
||||
if (type == typeof(Byte[])) return data.ReadBytes();
|
||||
if (type.As<IAccessor>()) return type.AccessorRead(data);
|
||||
|
||||
// 可空类型
|
||||
if (data.Length == 0 && type.IsNullable()) return null;
|
||||
|
||||
var str = data.ToStr();
|
||||
return OnDecode(str, type);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (ThrowOnError) throw;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>字符串解码为对象。复杂类型采用Json反序列化</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual Object? OnDecode(String value, Type type)
|
||||
{
|
||||
if (type.GetTypeCode() == TypeCode.String) return value;
|
||||
if (type.IsBaseType()) return value.ChangeType(type);
|
||||
|
||||
return JsonHost.Read(value, type);
|
||||
}
|
||||
}
|
||||
24
src/Admin/ThingsGateway.NewLife.X/Data/IndexRange.cs
Normal file
24
src/Admin/ThingsGateway.NewLife.X/Data/IndexRange.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace ThingsGateway.NewLife.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// 范围
|
||||
/// </summary>
|
||||
public struct IndexRange
|
||||
{
|
||||
/// <summary>
|
||||
/// 开始,包含
|
||||
/// </summary>
|
||||
public Int32 Start;
|
||||
|
||||
/// <summary>
|
||||
/// 结束,不包含
|
||||
/// </summary>
|
||||
public Int32 End;
|
||||
|
||||
/// <summary>
|
||||
/// 已重载
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"({Start}, {End})";
|
||||
}
|
||||
}
|
||||
544
src/Admin/ThingsGateway.NewLife.X/Data/Packet.cs
Normal file
544
src/Admin/ThingsGateway.NewLife.X/Data/Packet.cs
Normal file
@@ -0,0 +1,544 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>数据包。表示数据区Data的指定范围(Offset, Count)。</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/packet
|
||||
/// 设计于.NET2.0时代,功能上类似于NETCore的Span/Memory。
|
||||
/// Packet的设计目标就是网络库零拷贝,所以Slice切片是其最重要功能。
|
||||
/// </remarks>
|
||||
public class Packet : IPacket
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>数据</summary>
|
||||
public Byte[] Data { get; private set; }
|
||||
|
||||
/// <summary>偏移</summary>
|
||||
public Int32 Offset { get; private set; }
|
||||
|
||||
/// <summary>长度</summary>
|
||||
public Int32 Count { get; private set; }
|
||||
|
||||
Int32 IPacket.Length => Count;
|
||||
|
||||
/// <summary>下一个链式包</summary>
|
||||
public Packet? Next { get; set; }
|
||||
|
||||
/// <summary>总长度</summary>
|
||||
public Int32 Total => Count + (Next != null ? Next.Total : 0);
|
||||
|
||||
IPacket? IPacket.Next { get => Next; set => Next = (value as Packet) ?? throw new InvalidDataException(); }
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>根据数据区实例化</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
public Packet(Byte[] data, Int32 offset = 0, Int32 count = -1) => Set(data, offset, count);
|
||||
|
||||
/// <summary>根据数组段实例化</summary>
|
||||
/// <param name="seg"></param>
|
||||
public Packet(ArraySegment<Byte> seg)
|
||||
{
|
||||
if (seg.Array == null) throw new ArgumentNullException(nameof(seg));
|
||||
|
||||
Set(seg.Array, seg.Offset, seg.Count);
|
||||
}
|
||||
|
||||
/// <summary>从可扩展内存流实例化,尝试窃取内存流内部的字节数组,失败后拷贝</summary>
|
||||
/// <remarks>因数据包内数组窃取自内存流,需要特别小心,避免多线程共用。常用于内存流转数据包,而内存流不再使用</remarks>
|
||||
/// <param name="stream"></param>
|
||||
public Packet(Stream stream)
|
||||
{
|
||||
if (stream is MemoryStream ms)
|
||||
{
|
||||
#if !NET45
|
||||
// 尝试抠了内部存储区,下面代码需要.Net 4.6支持
|
||||
if (ms.TryGetBuffer(out var seg))
|
||||
{
|
||||
if (seg.Array == null) throw new ArgumentNullException(nameof(seg));
|
||||
|
||||
Set(seg.Array, seg.Offset + (Int32)ms.Position, seg.Count - (Int32)ms.Position);
|
||||
return;
|
||||
}
|
||||
// GetBuffer窃取内部缓冲区后,无法得知真正的起始位置index,可能导致错误取数
|
||||
// public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible)
|
||||
|
||||
//try
|
||||
//{
|
||||
// Set(ms.GetBuffer(), (Int32)ms.Position, (Int32)(ms.Length - ms.Position));
|
||||
//}
|
||||
//catch (UnauthorizedAccessException) { }
|
||||
#endif
|
||||
}
|
||||
|
||||
//Set(stream.ToArray());
|
||||
|
||||
var buf = new Byte[stream.Length - stream.Position];
|
||||
var count = stream.Read(buf, 0, buf.Length);
|
||||
Set(buf, 0, count);
|
||||
|
||||
// 必须确保数据流位置不变
|
||||
if (count > 0) stream.Seek(-count, SeekOrigin.Current);
|
||||
}
|
||||
|
||||
/// <summary>从Span实例化</summary>
|
||||
/// <param name="span"></param>
|
||||
public Packet(Span<Byte> span) => Set(span.ToArray());
|
||||
|
||||
/// <summary>从Memory实例化</summary>
|
||||
/// <param name="memory"></param>
|
||||
public Packet(Memory<Byte> memory)
|
||||
{
|
||||
if (MemoryMarshal.TryGetArray<Byte>(memory, out var segment))
|
||||
{
|
||||
Set(segment.Array!, segment.Offset, segment.Count);
|
||||
}
|
||||
else
|
||||
{
|
||||
Set(memory.ToArray());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 索引
|
||||
/// <summary>获取/设置 指定位置的字节</summary>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public Byte this[Int32 index]
|
||||
{
|
||||
get
|
||||
{
|
||||
var p = index - Count;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
return Next[p];
|
||||
}
|
||||
|
||||
return Data[Offset + index];
|
||||
}
|
||||
set
|
||||
{
|
||||
var p = index - Count;
|
||||
if (p >= 0)
|
||||
{
|
||||
if (Next == null) throw new IndexOutOfRangeException(nameof(index));
|
||||
|
||||
Next[p] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Data[Offset + index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>设置新的数据区</summary>
|
||||
/// <param name="data">数据区</param>
|
||||
/// <param name="offset">偏移</param>
|
||||
/// <param name="count">字节个数</param>
|
||||
[MemberNotNull(nameof(Data))]
|
||||
public virtual void Set(Byte[] data, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
Data = data;
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
Offset = 0;
|
||||
Count = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Offset = offset;
|
||||
|
||||
if (count < 0) count = data.Length - offset;
|
||||
Count = count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>截取子数据区</summary>
|
||||
/// <param name="offset">相对偏移</param>
|
||||
/// <param name="count">字节个数</param>
|
||||
/// <returns></returns>
|
||||
public Packet Slice(Int32 offset, Int32 count = -1)
|
||||
{
|
||||
var start = Offset + offset;
|
||||
var remain = Count - offset;
|
||||
|
||||
if (Next == null)
|
||||
{
|
||||
// count 是 offset 之后的个数
|
||||
if (count < 0 || count > remain) count = remain;
|
||||
if (count < 0) count = 0;
|
||||
|
||||
return new Packet(Data, start, count);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果当前段用完,则取下一段
|
||||
if (remain <= 0) return Next.Slice(offset - Count, count);
|
||||
|
||||
// 当前包用一截,剩下的全部
|
||||
if (count < 0) return new Packet(Data, start, remain) { Next = Next };
|
||||
|
||||
// 当前包可以读完
|
||||
if (count <= remain) return new Packet(Data, start, count);
|
||||
|
||||
// 当前包用一截,剩下的再截取
|
||||
return new Packet(Data, start, remain) { Next = Next.Slice(0, count - remain) };
|
||||
}
|
||||
}
|
||||
|
||||
IPacket IPacket.Slice(Int32 offset, Int32 count) => Slice(offset, count);
|
||||
IPacket IPacket.Slice(Int32 offset, Int32 count, Boolean transferOwner) => Slice(offset, count);
|
||||
|
||||
/// <summary>查找目标数组</summary>
|
||||
/// <param name="data">目标数组</param>
|
||||
/// <param name="offset">本数组起始偏移</param>
|
||||
/// <param name="count">本数组搜索个数</param>
|
||||
/// <returns></returns>
|
||||
public Int32 IndexOf(Byte[] data, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
var start = offset;
|
||||
var length = data.Length;
|
||||
|
||||
if (count < 0 || count > Total - offset) count = Total - offset;
|
||||
|
||||
// 快速查找
|
||||
if (Next == null)
|
||||
{
|
||||
if (start >= Count) return -1;
|
||||
|
||||
//#if NETCOREAPP3_1_OR_GREATER
|
||||
// var s1 = new Span<Byte>(Data, Offset + offset, count);
|
||||
// var p = s1.IndexOf(data);
|
||||
// return p >= 0 ? (p + offset) : -1;
|
||||
//#endif
|
||||
var p = Data.IndexOf(data, Offset + start, count);
|
||||
return p >= 0 ? (p - Offset) : -1;
|
||||
}
|
||||
|
||||
// 已匹配字节数
|
||||
var win = 0;
|
||||
// 索引加上data剩余字节数必须小于count,否则就是已匹配
|
||||
for (var i = 0; i + length - win <= count; i++)
|
||||
{
|
||||
if (this[start + i] == data[win])
|
||||
{
|
||||
win++;
|
||||
|
||||
// 全部匹配,退出
|
||||
if (win >= length) return (start + i) - length + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
//win = 0; // 只要有一个不匹配,马上清零
|
||||
// 不能直接清零,那样会导致数据丢失,需要逐位探测,窗口一个个字节滑动
|
||||
i -= win;
|
||||
win = 0;
|
||||
|
||||
// 本段分析未匹配,递归下一段
|
||||
if (start + i == Count && Next != null)
|
||||
{
|
||||
var p = Next.IndexOf(data, 0, count - i);
|
||||
if (p >= 0) return (start + i) + p;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>附加一个包到当前包链的末尾</summary>
|
||||
/// <param name="pk"></param>
|
||||
public Packet Append(Packet pk)
|
||||
{
|
||||
if (pk == null) return this;
|
||||
|
||||
var p = this;
|
||||
while (p.Next != null) p = p.Next;
|
||||
p.Next = pk;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>返回字节数组。无差别复制,一定返回新数组</summary>
|
||||
/// <returns></returns>
|
||||
public virtual Byte[] ToArray()
|
||||
{
|
||||
//if (Offset == 0 && (Count < 0 || Offset + Count == Data.Length) && Next == null) return Data;
|
||||
|
||||
if (Next == null) return Data.ReadBytes(Offset, Count);
|
||||
|
||||
// 链式包输出
|
||||
var ms = Pool.MemoryStream.Get();
|
||||
CopyTo(ms);
|
||||
|
||||
return ms.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>从封包中读取指定数据区,读取全部时直接返回缓冲区,以提升性能</summary>
|
||||
/// <param name="offset">相对于数据包的起始位置,实际上是数组的Offset+offset</param>
|
||||
/// <param name="count">字节个数</param>
|
||||
/// <returns></returns>
|
||||
public Byte[] ReadBytes(Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
// 读取全部
|
||||
if (offset == 0 && count < 0)
|
||||
{
|
||||
if (Offset == 0 && (Count < 0 || Offset + Count == Data.Length) && Next == null) return Data;
|
||||
|
||||
return ToArray();
|
||||
}
|
||||
|
||||
if (Next == null) return Data.ReadBytes(Offset + offset, count < 0 || count > Count ? Count : count);
|
||||
|
||||
// 当前包足够长
|
||||
if (count >= 0 && offset + count <= Count) return Data.ReadBytes(Offset + offset, count);
|
||||
|
||||
// 链式包输出
|
||||
if (count < 0) count = Total - offset;
|
||||
var ms = Pool.MemoryStream.Get();
|
||||
|
||||
// 遍历
|
||||
var cur = this;
|
||||
while (cur != null && count > 0)
|
||||
{
|
||||
var len = cur.Count;
|
||||
// 当前包不够用
|
||||
if (len < offset)
|
||||
offset -= len;
|
||||
else if (cur.Data != null)
|
||||
{
|
||||
len -= offset;
|
||||
if (len > count) len = count;
|
||||
ms.Write(cur.Data, cur.Offset + offset, len);
|
||||
|
||||
offset = 0;
|
||||
count -= len;
|
||||
}
|
||||
|
||||
cur = cur.Next;
|
||||
}
|
||||
return ms.Return(true);
|
||||
|
||||
//// 以上算法太复杂,直接来
|
||||
//return ToArray().ReadBytes(offset, count);
|
||||
}
|
||||
|
||||
/// <summary>返回数据段</summary>
|
||||
/// <returns></returns>
|
||||
public ArraySegment<Byte> ToSegment()
|
||||
{
|
||||
if (Next == null) return new ArraySegment<Byte>(Data, Offset, Count);
|
||||
|
||||
return new ArraySegment<Byte>(ToArray());
|
||||
}
|
||||
|
||||
/// <summary>返回数据段集合</summary>
|
||||
/// <returns></returns>
|
||||
public IList<ArraySegment<Byte>> ToSegments()
|
||||
{
|
||||
// 初始4元素,优化扩容
|
||||
var list = new List<ArraySegment<Byte>>(4);
|
||||
|
||||
for (var pk = this; pk != null; pk = pk.Next)
|
||||
{
|
||||
list.Add(new ArraySegment<Byte>(pk.Data, pk.Offset, pk.Count));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>转为Span</summary>
|
||||
/// <returns></returns>
|
||||
public Span<Byte> AsSpan()
|
||||
{
|
||||
if (Next == null) return new Span<Byte>(Data, Offset, Count);
|
||||
|
||||
return new Span<Byte>(ToArray());
|
||||
}
|
||||
|
||||
/// <summary>转为Memory</summary>
|
||||
/// <returns></returns>
|
||||
public Memory<Byte> AsMemory()
|
||||
{
|
||||
if (Next == null) return new Memory<Byte>(Data, Offset, Count);
|
||||
|
||||
return new Memory<Byte>(ToArray());
|
||||
}
|
||||
|
||||
Span<Byte> IPacket.GetSpan() => AsSpan();
|
||||
Memory<Byte> IPacket.GetMemory() => AsMemory();
|
||||
|
||||
/// <summary>获取封包的数据流形式</summary>
|
||||
/// <returns></returns>
|
||||
public virtual MemoryStream GetStream()
|
||||
{
|
||||
if (Next == null) return new MemoryStream(Data, Offset, Count, false, true);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
|
||||
return ms;
|
||||
}
|
||||
|
||||
/// <summary>把封包写入到数据流</summary>
|
||||
/// <param name="stream"></param>
|
||||
public void CopyTo(Stream stream)
|
||||
{
|
||||
stream.Write(Data, Offset, Count);
|
||||
Next?.CopyTo(stream);
|
||||
}
|
||||
|
||||
/// <summary>把封包写入到目标数组</summary>
|
||||
/// <param name="buffer">目标数组</param>
|
||||
/// <param name="offset">目标数组的偏移量</param>
|
||||
/// <param name="count">目标数组的字节数</param>
|
||||
public void WriteTo(Byte[] buffer, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (count < 0) count = Total;
|
||||
var len = count;
|
||||
if (len > Count) len = Count;
|
||||
Buffer.BlockCopy(Data, Offset, buffer, offset, len);
|
||||
|
||||
offset += len;
|
||||
count -= len;
|
||||
if (count > 0) Next?.WriteTo(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <summary>异步复制到目标数据流</summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
public async Task CopyToAsync(Stream stream)
|
||||
{
|
||||
await stream.WriteAsync(Data, Offset, Count).ConfigureAwait(false);
|
||||
if (Next != null) await Next.CopyToAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>异步复制到目标数据流</summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
await stream.WriteAsync(Data, Offset, Count, cancellationToken).ConfigureAwait(false);
|
||||
if (Next != null) await Next.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>深度克隆一份数据包,拷贝数据区</summary>
|
||||
/// <returns></returns>
|
||||
public Packet Clone()
|
||||
{
|
||||
if (Next == null) return new Packet(Data.ReadBytes(Offset, Count));
|
||||
|
||||
// 链式包输出
|
||||
var ms = Pool.MemoryStream.Get();
|
||||
CopyTo(ms);
|
||||
|
||||
return new Packet(ms.Return(true));
|
||||
}
|
||||
|
||||
/// <summary>尝试获取缓冲区</summary>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean TryGetArray(out ArraySegment<Byte> segment)
|
||||
{
|
||||
if (Next == null)
|
||||
{
|
||||
segment = new ArraySegment<Byte>(Data, Offset, Count);
|
||||
return true;
|
||||
}
|
||||
|
||||
segment = default;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>以字符串表示</summary>
|
||||
/// <param name="encoding">字符串编码,默认URF-8</param>
|
||||
/// <param name="offset"></param>
|
||||
/// <param name="count"></param>
|
||||
/// <returns></returns>
|
||||
public String ToStr(Encoding? encoding = null, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (Data == null) return String.Empty;
|
||||
|
||||
encoding ??= Encoding.UTF8;
|
||||
if (count < 0) count = Total - offset;
|
||||
|
||||
if (Next == null) return Data.ToStr(encoding, Offset + offset, count);
|
||||
|
||||
return ReadBytes(offset, count).ToStr(encoding);
|
||||
}
|
||||
|
||||
/// <summary>以十六进制编码表示</summary>
|
||||
/// <param name="maxLength">最大显示多少个字节。默认-1显示全部</param>
|
||||
/// <param name="separate">分隔符</param>
|
||||
/// <param name="groupSize">分组大小,为0时对每个字节应用分隔符,否则对每个分组使用</param>
|
||||
/// <returns></returns>
|
||||
public String ToHex(Int32 maxLength = 32, String? separate = null, Int32 groupSize = 0)
|
||||
{
|
||||
if (Data == null) return String.Empty;
|
||||
|
||||
var hex = ReadBytes(0, maxLength).ToHex(separate, groupSize);
|
||||
|
||||
return (maxLength == -1 || Count <= maxLength) ? hex : String.Concat(hex, "...");
|
||||
}
|
||||
|
||||
/// <summary>转为Base64编码</summary>
|
||||
/// <returns></returns>
|
||||
public String ToBase64()
|
||||
{
|
||||
if (Data == null) return String.Empty;
|
||||
|
||||
if (Next == null) Data.ToBase64(Offset, Count);
|
||||
|
||||
return ToArray().ToBase64();
|
||||
}
|
||||
|
||||
/// <summary>读取无符号短整数</summary>
|
||||
/// <param name="isLittleEndian"></param>
|
||||
/// <returns></returns>
|
||||
public UInt16 ReadUInt16(Boolean isLittleEndian = true) => Data.ToUInt16(Offset, isLittleEndian);
|
||||
|
||||
/// <summary>读取无符号整数</summary>
|
||||
/// <param name="isLittleEndian"></param>
|
||||
/// <returns></returns>
|
||||
public UInt32 ReadUInt32(Boolean isLittleEndian = true) => Data.ToUInt32(Offset, isLittleEndian);
|
||||
#endregion
|
||||
|
||||
#region 重载运算符
|
||||
/// <summary>重载类型转换,字节数组直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator Packet(Byte[] value) => value == null ? null! : new(value);
|
||||
|
||||
/// <summary>重载类型转换,一维数组直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator Packet(ArraySegment<Byte> value) => new(value);
|
||||
|
||||
/// <summary>重载类型转换,字符串直接转为Packet对象</summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static implicit operator Packet(String value) => new(value.GetBytes());
|
||||
|
||||
/// <summary>已重载</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"[{Data.Length}]({Offset}, {Count})" + (Next == null ? "" : $"<{Total}>");
|
||||
#endregion
|
||||
}
|
||||
148
src/Admin/ThingsGateway.NewLife.X/Data/PageParameter.cs
Normal file
148
src/Admin/ThingsGateway.NewLife.X/Data/PageParameter.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System.Runtime.Serialization;
|
||||
using System.Web.Script.Serialization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>分页参数信息。可携带统计和数据权限扩展查询等信息</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/page_parameter
|
||||
/// </remarks>
|
||||
public class PageParameter
|
||||
{
|
||||
#region 核心属性
|
||||
private String? _Sort;
|
||||
/// <summary>获取 或 设置 排序字段,前台接收,便于做SQL安全性校验</summary>
|
||||
/// <remarks>
|
||||
/// 一般用于接收单个排序字段,可以带上Asc/Desc,这里会自动拆分。
|
||||
/// 极少数情况下,前端需要传递多个字段排序,这时候可以使用OrderBy。
|
||||
///
|
||||
/// OrderBy优先级更高,且支持手写复杂排序语句(不做SQL安全性校验)。
|
||||
/// 如果设置Sort,OrderBy将被清空。
|
||||
/// </remarks>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public virtual String? Sort
|
||||
{
|
||||
get => _Sort;
|
||||
set
|
||||
{
|
||||
_Sort = value;
|
||||
|
||||
// 自动识别带有Asc/Desc的排序
|
||||
if (!_Sort.IsNullOrEmpty() && !_Sort.Contains(','))
|
||||
{
|
||||
_Sort = _Sort.Trim();
|
||||
var p = _Sort.LastIndexOf(' ');
|
||||
if (p > 0)
|
||||
{
|
||||
var dir = _Sort[(p + 1)..];
|
||||
if (dir.EqualIgnoreCase("asc"))
|
||||
{
|
||||
Desc = false;
|
||||
_Sort = _Sort[..p].Trim();
|
||||
}
|
||||
else if (dir.EqualIgnoreCase("desc"))
|
||||
{
|
||||
Desc = true;
|
||||
_Sort = _Sort[..p].Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OrderBy = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取 或 设置 是否降序</summary>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public virtual Boolean Desc { get; set; }
|
||||
|
||||
/// <summary>获取 或 设置 页面索引。从1开始,默认1</summary>
|
||||
/// <remarks>如果设定了开始行,分页时将不再使用PageIndex</remarks>
|
||||
public virtual Int32 PageIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>获取 或 设置 页面大小。默认20,若为0表示不分页</summary>
|
||||
public virtual Int32 PageSize { get; set; } = 20;
|
||||
#endregion
|
||||
|
||||
#region 扩展属性
|
||||
/// <summary>获取 或 设置 总记录数</summary>
|
||||
public virtual Int64 TotalCount { get; set; }
|
||||
|
||||
/// <summary>获取 页数</summary>
|
||||
public virtual Int64 PageCount
|
||||
{
|
||||
get
|
||||
{
|
||||
// 如果PageSize小于等于0,则直接返回1
|
||||
if (PageSize <= 0) return 1;
|
||||
|
||||
var count = TotalCount / PageSize;
|
||||
if ((TotalCount % PageSize) != 0) count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取 或 设置 自定义排序字句。常用于用户自定义排序,不经过SQL安全性校验</summary>
|
||||
/// <remarks>
|
||||
/// OrderBy优先级更高,且支持手写复杂排序语句(不做SQL安全性校验)。
|
||||
/// 如果设置Sort,OrderBy将被清空。
|
||||
/// </remarks>
|
||||
public virtual String? OrderBy { get; set; }
|
||||
|
||||
/// <summary>获取 或 设置 开始行</summary>
|
||||
/// <remarks>如果设定了开始行,分页时将不再使用PageIndex</remarks>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public virtual Int64 StartRow { get; set; } = -1;
|
||||
|
||||
/// <summary>获取 或 设置 是否获取总记录数,默认false</summary>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public Boolean RetrieveTotalCount { get; set; }
|
||||
|
||||
/// <summary>获取 或 设置 状态。用于传递统计、扩展查询等用户数据</summary>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public virtual Object? State { get; set; }
|
||||
|
||||
/// <summary>获取 或 设置 是否获取统计,默认false</summary>
|
||||
[XmlIgnore, ScriptIgnore, IgnoreDataMember]
|
||||
public Boolean RetrieveState { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
/// <summary>实例化分页参数</summary>
|
||||
public PageParameter() { }
|
||||
|
||||
/// <summary>通过另一个分页参数来实例化当前分页参数</summary>
|
||||
/// <param name="pm"></param>
|
||||
public PageParameter(PageParameter pm) => CopyFrom(pm);
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>从另一个分页参数拷贝到当前分页参数</summary>
|
||||
/// <param name="pm"></param>
|
||||
/// <returns></returns>
|
||||
public virtual PageParameter CopyFrom(PageParameter pm)
|
||||
{
|
||||
if (pm == null) return this;
|
||||
|
||||
OrderBy = pm.OrderBy;
|
||||
Sort = pm.Sort;
|
||||
Desc = pm.Desc;
|
||||
PageIndex = pm.PageIndex;
|
||||
PageSize = pm.PageSize;
|
||||
StartRow = pm.StartRow;
|
||||
|
||||
TotalCount = pm.TotalCount;
|
||||
RetrieveTotalCount = pm.RetrieveTotalCount;
|
||||
State = pm.State;
|
||||
RetrieveState = pm.RetrieveState;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>获取表示分页参数唯一性的键值,可用作缓存键</summary>
|
||||
/// <returns></returns>
|
||||
public virtual String GetKey() => $"{PageIndex}-{PageCount}-{OrderBy}";
|
||||
#endregion
|
||||
}
|
||||
120
src/Admin/ThingsGateway.NewLife.X/Data/RingBuffer.cs
Normal file
120
src/Admin/ThingsGateway.NewLife.X/Data/RingBuffer.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>环形缓冲区。用于协议组包设计</summary>
|
||||
public class RingBuffer
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>容量</summary>
|
||||
public Int32 Capacity => _data.Length;
|
||||
|
||||
/// <summary>头指针。写入位置</summary>
|
||||
public Int32 Head { get; set; }
|
||||
|
||||
/// <summary>尾指针。读取位置</summary>
|
||||
public Int32 Tail { get; set; }
|
||||
|
||||
/// <summary>数据长度</summary>
|
||||
public Int32 Length => Head >= Tail ? (Head - Tail) : (Head + _data.Length - Tail);
|
||||
|
||||
private Byte[] _data;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>使用默认容量1024来初始化</summary>
|
||||
public RingBuffer() : this(1024) { }
|
||||
|
||||
/// <summary>实例化环形缓冲区</summary>
|
||||
/// <param name="capacity">容量。合理的容量能够减少扩容</param>
|
||||
public RingBuffer(Int32 capacity) => _data = new Byte[capacity];
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>扩容,确保容量</summary>
|
||||
/// <param name="capacity"></param>
|
||||
public void EnsureCapacity(Int32 capacity)
|
||||
{
|
||||
if (capacity <= Capacity) return;
|
||||
|
||||
// 分配新空间,全量拷贝。比分块拷贝要低效一些,但是代码简单直接
|
||||
var data = new Byte[capacity];
|
||||
if (Length > 0)
|
||||
Buffer.BlockCopy(_data, 0, data, 0, _data.Length);
|
||||
_data = data;
|
||||
}
|
||||
|
||||
private void CheckCapacity(Int32 capacity)
|
||||
{
|
||||
var len = _data.Length;
|
||||
|
||||
// 两倍增长
|
||||
while (len < capacity) len *= 2;
|
||||
|
||||
EnsureCapacity(len);
|
||||
}
|
||||
|
||||
/// <summary>写入数据</summary>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="offset">偏移量</param>
|
||||
/// <param name="count">个数</param>
|
||||
public void Write(Byte[] data, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (count < 0) count = data.Length - offset;
|
||||
|
||||
CheckCapacity(Length + count);
|
||||
|
||||
var len = _data.Length - Head;
|
||||
if (len > count) len = count;
|
||||
|
||||
Buffer.BlockCopy(data, offset, _data, Head, len);
|
||||
|
||||
count -= len;
|
||||
Head += len;
|
||||
if (Head == _data.Length) Head = 0;
|
||||
|
||||
// 还有数据,移到开头
|
||||
if (count > 0)
|
||||
{
|
||||
Buffer.BlockCopy(data, offset, _data, Head, len);
|
||||
|
||||
Head = count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读取数据</summary>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="offset">偏移量</param>
|
||||
/// <param name="count">个数</param>
|
||||
/// <returns></returns>
|
||||
public Int32 Read(Byte[] data, Int32 offset = 0, Int32 count = -1)
|
||||
{
|
||||
if (count < 0) count = data.Length - offset;
|
||||
|
||||
var len = Length;
|
||||
if (len > count) len = count;
|
||||
if (Tail + len > _data.Length) len = _data.Length - Tail;
|
||||
|
||||
Buffer.BlockCopy(_data, Tail, data, offset, len);
|
||||
|
||||
var rs = len;
|
||||
count -= len;
|
||||
Tail += len;
|
||||
if (Tail == _data.Length) Tail = 0;
|
||||
|
||||
// 还有数据,移到开头
|
||||
if (count > 0)
|
||||
{
|
||||
offset += len;
|
||||
len = Length;
|
||||
if (len > count) len = count;
|
||||
|
||||
Buffer.BlockCopy(_data, 0, data, offset, len);
|
||||
|
||||
rs += len;
|
||||
count -= len;
|
||||
Tail += len;
|
||||
}
|
||||
|
||||
return rs;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
372
src/Admin/ThingsGateway.NewLife.X/Data/Snowflake.cs
Normal file
372
src/Admin/ThingsGateway.NewLife.X/Data/Snowflake.cs
Normal file
@@ -0,0 +1,372 @@
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Model;
|
||||
using ThingsGateway.NewLife.Security;
|
||||
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>雪花算法。分布式Id,业务内必须确保单例</summary>
|
||||
/// <remarks>
|
||||
/// 文档 https://newlifex.com/core/snow_flake
|
||||
///
|
||||
/// 使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增。
|
||||
/// 1bit保留 + 41bit时间戳 + 10bit机器 + 12bit序列号
|
||||
///
|
||||
/// 内置自动选择机器workerId,IP+进程+线程,无法绝对保证唯一,从而导致整体生成的雪花Id有一定几率重复。
|
||||
/// 如果想要绝对唯一,建议在外部设置唯一的workerId,再结合单例使用,此时确保最终生成的Id绝对不重复!
|
||||
/// 高要求场合,推荐使用Redis自增序数作为workerId,在大型分布式系统中亦能保证绝对唯一。
|
||||
/// 已提供JoinCluster方法,用于把当前对象加入集群,确保workerId唯一。
|
||||
///
|
||||
/// 务必请保证Snowflake对象的唯一性,Snowflake确保本对象生成的Id绝对唯一,但如果有多个Snowflake对象,可能会生成重复Id。
|
||||
/// 特别在使用XCode等数据中间件时,要确保每张表只有一个Snowflake实例。
|
||||
/// </remarks>
|
||||
public class Snowflake
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>开始时间戳。首次使用前设置,否则无效,默认1970-1-1</summary>
|
||||
/// <remarks>
|
||||
/// 该时间戳默认已带有时区偏移,不管是为本地时间还是UTC时间生成雪花Id,都是一样的时间大小。
|
||||
/// 默认值本质上就是UTC 1970-1-1,转本地时间是为了方便解析雪花Id时得到的时间就是本地时间,最大兼容已有业务。
|
||||
/// 在星尘和IoT的自动分表场景中,一般需要用本地时间来作为分表依据,所以默认值是本地时间。
|
||||
/// </remarks>
|
||||
public DateTime StartTimestamp { get; set; } = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).ToLocalTime();
|
||||
|
||||
/// <summary>机器Id,取10位</summary>
|
||||
/// <remarks>
|
||||
/// 内置默认取IP+进程+线程,不能保证绝对唯一,要求高的场合建议外部保证workerId唯一。
|
||||
/// 一般借助Redis自增序数作为workerId,确保绝对唯一。
|
||||
/// 如果应用接入星尘,将自动从星尘配置中心获取workerId,确保全局唯一。
|
||||
/// </remarks>
|
||||
public Int32 WorkerId { get; set; }
|
||||
|
||||
private Int32 _Sequence;
|
||||
/// <summary>序列号,取12位。进程内静态,避免多个实例生成重复Id</summary>
|
||||
public Int32 Sequence => _Sequence;
|
||||
|
||||
/// <summary>全局机器Id。若设置,所有雪花实例都将使用该Id,可以由星尘配置中心提供本应用全局唯一机器码,且跨多环境唯一</summary>
|
||||
public static Int32 GlobalWorkerId { get; set; }
|
||||
|
||||
/// <summary>workerId分配集群。配置后可确保所有实例化的雪花对象得到唯一workerId,建议使用Redis</summary>
|
||||
public static ICache? Cluster { get; set; }
|
||||
|
||||
//private Int64 _msStart;
|
||||
//private Stopwatch _watch = null!;
|
||||
private Int64 _lastTime;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
private static Int32 _gid;
|
||||
private static readonly Int32 _instance;
|
||||
static Snowflake()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从容器中获取缓存提供者,查找Redis作为集群WorkerId分配器
|
||||
var provider = ObjectContainer.Provider?.GetService<ICacheProvider>();
|
||||
if (provider != null && provider.Cache != provider.InnerCache && provider.Cache is not MemoryCache)
|
||||
Cluster = provider.Cache;
|
||||
|
||||
var ip = NetHelper.MyIP();
|
||||
if (ip != null)
|
||||
{
|
||||
var buf = ip.GetAddressBytes();
|
||||
_instance = (buf[2] << 8) | buf[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
_instance = Rand.Next(1, 1024);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 异常时随机
|
||||
_instance = Rand.Next(1, 1024);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 核心方法
|
||||
private Boolean _inited;
|
||||
private void Init()
|
||||
{
|
||||
if (_inited) return;
|
||||
lock (this)
|
||||
{
|
||||
if (_inited) return;
|
||||
|
||||
// 记录雪花算法初始化埋点,及时发现算法使用错误
|
||||
using var span = DefaultTracer.Instance?.NewSpan("Snowflake-Init", new { id = Interlocked.Increment(ref _gid) });
|
||||
|
||||
if (WorkerId <= 0 && GlobalWorkerId > 0) WorkerId = GlobalWorkerId & 0x3FF;
|
||||
if (WorkerId <= 0 && Cluster != null) JoinCluster(Cluster);
|
||||
|
||||
// 初始化WorkerId,取5位实例加上5位进程,确保同一台机器的WorkerId不同
|
||||
if (WorkerId <= 0)
|
||||
{
|
||||
var nodeId = _instance;
|
||||
var pid = ProcessHelper.GetProcessId();
|
||||
var tid = Environment.CurrentManagedThreadId;
|
||||
//WorkerId = ((nodeId & 0x1F) << 5) | (pid & 0x1F);
|
||||
//WorkerId = (nodeId ^ pid ^ tid) & 0x3FF;
|
||||
WorkerId = ((nodeId & 0x1F) << 5) | ((pid ^ tid) & 0x1F);
|
||||
}
|
||||
|
||||
//// 记录此时距离起点的毫秒数以及开机嘀嗒数
|
||||
//if (_watch == null)
|
||||
//{
|
||||
// var now = ConvertKind(DateTime.Now);
|
||||
// _msStart = (Int64)(now - StartTimestamp).TotalMilliseconds;
|
||||
// _watch = Stopwatch.StartNew();
|
||||
//}
|
||||
|
||||
//span?.AppendTag($"WorkerId={WorkerId} StartTimestamp={StartTimestamp.ToFullString()} _msStart={_msStart}");
|
||||
span?.AppendTag($"WorkerId={WorkerId} StartTimestamp={StartTimestamp.ToFullString()}");
|
||||
|
||||
_inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>获取下一个Id</summary>
|
||||
/// <remarks>基于当前时间,转StartTimestamp所属时区后,生成Id</remarks>
|
||||
/// <returns></returns>
|
||||
public virtual Int64 NewId()
|
||||
{
|
||||
Init();
|
||||
|
||||
// 此时嘀嗒数减去起点嘀嗒数,加上起点毫秒数
|
||||
var ms = (Int64)(ConvertKind(DateTime.Now) - StartTimestamp).TotalMilliseconds;
|
||||
//var ms = _watch.ElapsedMilliseconds + _msStart;
|
||||
var wid = WorkerId & (-1 ^ (-1 << 10));
|
||||
|
||||
var origin = Volatile.Read(ref _lastTime);
|
||||
//!!! 避免时间倒退
|
||||
if (ms < origin)
|
||||
{
|
||||
var t = origin - ms;
|
||||
// 在夏令时地区,时间可能回拨1个小时
|
||||
if (t > 3600_000 + 10_000) throw new InvalidOperationException($"Time reversal too large ({t}ms). To ensure uniqueness, Snowflake refuses to generate a new Id");
|
||||
|
||||
// 暂时使用上次时间,即未来时间
|
||||
ms = origin;
|
||||
}
|
||||
|
||||
// 核心理念:时间不同时序号置零,时间相同时序号递增
|
||||
var seq = 0;
|
||||
lock (this)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (ms > _lastTime)
|
||||
{
|
||||
_Sequence = 0;
|
||||
_lastTime = ms;
|
||||
seq = 0;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
ms = _lastTime;
|
||||
seq = Interlocked.Increment(ref _Sequence);
|
||||
if (seq < 4096) break;
|
||||
|
||||
ms++;
|
||||
_Sequence = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//var seq = 0;
|
||||
//while (true)
|
||||
//{
|
||||
// if (ms > origin)
|
||||
// {
|
||||
// lock (this)
|
||||
// {
|
||||
// origin = Volatile.Read(ref _lastTime);
|
||||
// if (ms > origin)
|
||||
// {
|
||||
// Volatile.Write(ref _Sequence, 0);
|
||||
// // 1,空闲时走这里。跟上次时间不同,抢夺当前坑位(序号0)。每毫秒只有1次机会
|
||||
// if (Interlocked.CompareExchange(ref _lastTime, ms, origin) == origin)
|
||||
// {
|
||||
// seq = 0;
|
||||
// break;
|
||||
// }
|
||||
|
||||
// // 抢夺失败,必须用新的时间,原来时间已经错过,无法得到唯一序号
|
||||
// origin = Volatile.Read(ref _lastTime);
|
||||
// //ms = origin;
|
||||
// }
|
||||
// ms = origin;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 2,繁忙时走这里。时间相同,递增序列号,较小序列号直接采用。每毫秒有4095次机会
|
||||
// seq = Interlocked.Increment(ref _Sequence);
|
||||
// if (seq < 4096) break;
|
||||
|
||||
// // 3,极度繁忙时走这里。4096之外的“幸运儿”集体加锁,重置序列号和时间,准备再来抢一次。很少业务会走到这里,只可能是积压数据冲击
|
||||
// origin = Volatile.Read(ref _lastTime);
|
||||
// if (ms == origin)
|
||||
// {
|
||||
// lock (this)
|
||||
// {
|
||||
// origin = Volatile.Read(ref _lastTime);
|
||||
// if (ms == origin)
|
||||
// {
|
||||
// // 时间不允许后退,否则可能生成重复Id。算法在每毫秒上生成4096个Id,等待被回拨的时间追上
|
||||
// //origin = Volatile.Read(ref _lastTime);
|
||||
// ms++;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// XTrace.WriteLine("ms.notEqual2 ms={0}, origin={1}", ms, origin);
|
||||
// ms = origin;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// XTrace.WriteLine("ms.notEqual1 ms={0}, origin={1}", ms, origin);
|
||||
// ms = origin;
|
||||
// }
|
||||
//}
|
||||
|
||||
seq &= (-1 ^ (-1 << 12));
|
||||
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
|
||||
}
|
||||
|
||||
/// <summary>获取指定时间的Id,带上节点和序列号。可用于根据业务时间构造插入Id</summary>
|
||||
/// <remarks>
|
||||
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
|
||||
///
|
||||
/// 如果为指定毫秒时间生成多个Id(超过4096),则可能重复。
|
||||
/// </remarks>
|
||||
/// <param name="time">时间</param>
|
||||
/// <returns></returns>
|
||||
public virtual Int64 NewId(DateTime time)
|
||||
{
|
||||
Init();
|
||||
|
||||
time = ConvertKind(time);
|
||||
|
||||
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
|
||||
var wid = WorkerId & (-1 ^ (-1 << 10));
|
||||
var seq = Interlocked.Increment(ref _Sequence) & (-1 ^ (-1 << 12));
|
||||
|
||||
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
|
||||
}
|
||||
|
||||
/// <summary>获取指定时间的Id,传入唯一业务id(取模为10位)。可用于物联网数据采集,每1024个传感器为一组,每组每毫秒多个Id</summary>
|
||||
/// <remarks>
|
||||
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
|
||||
///
|
||||
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
|
||||
/// 为了避免主键重复,可以使用传感器id作为workerId。
|
||||
/// uid需要取模为10位,即按1024分组,每组每毫秒最多生成4096个Id。
|
||||
///
|
||||
/// 如果为指定分组在特定毫秒时间生成多个Id(超过4096),则可能重复。
|
||||
/// </remarks>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="uid">唯一业务id。例如传感器id</param>
|
||||
/// <returns></returns>
|
||||
public virtual Int64 NewId(DateTime time, Int32 uid)
|
||||
{
|
||||
Init();
|
||||
|
||||
time = ConvertKind(time);
|
||||
|
||||
// 业务id作为workerId,保留12位序列号。即传感器按1024分组,每组每毫秒最多生成4096个Id
|
||||
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
|
||||
var wid = uid & (-1 ^ (-1 << 10));
|
||||
var seq = Interlocked.Increment(ref _Sequence) & (-1 ^ (-1 << 12));
|
||||
|
||||
return (ms << (10 + 12)) | (Int64)(wid << 12) | (Int64)seq;
|
||||
}
|
||||
|
||||
/// <summary>获取指定时间的Id,传入唯一业务id(22位)。可用于物联网数据采集,每4194304个传感器一组,每组每毫秒1个Id</summary>
|
||||
/// <remarks>
|
||||
/// 基于指定时间,转StartTimestamp所属时区后,生成Id。
|
||||
///
|
||||
/// 在物联网数据采集中,数据分析需要,更多希望能够按照采集时间去存储。
|
||||
/// 为了避免主键重复,可以使用传感器id作为workerId。
|
||||
/// 再配合upsert写入数据,如果同一个毫秒内传感器有多行数据,则只会插入一行。
|
||||
///
|
||||
/// 如果为指定业务id在特定毫秒时间生成多个Id(超过1个),则可能重复。
|
||||
/// </remarks>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="uid">唯一业务id。例如传感器id</param>
|
||||
/// <returns></returns>
|
||||
public virtual Int64 NewId22(DateTime time, Int32 uid)
|
||||
{
|
||||
Init();
|
||||
|
||||
time = ConvertKind(time);
|
||||
|
||||
// 业务id作为workerId,不保留序列号。即传感器按4194304(1<<22)分组,每组每毫秒最多生成1个Id
|
||||
var ms = (Int64)(time - StartTimestamp).TotalMilliseconds;
|
||||
var wid = uid & (-1 ^ (-1 << 22));
|
||||
|
||||
return (ms << (10 + 12)) | (Int64)wid;
|
||||
}
|
||||
|
||||
/// <summary>时间转为Id,不带节点和序列号。可用于构建时间片段查询</summary>
|
||||
/// <remarks>
|
||||
/// 基于指定时间,转StartTimestamp所属时区后,生成不带WorkerId和序列号的Id。
|
||||
/// 一般用于构建时间片段查询,例如查询某个时间段内的数据,把时间片段转为雪花Id片段。
|
||||
/// </remarks>
|
||||
/// <param name="time">时间</param>
|
||||
/// <returns></returns>
|
||||
public virtual Int64 GetId(DateTime time)
|
||||
{
|
||||
time = ConvertKind(time);
|
||||
var t = (Int64)(time - StartTimestamp).TotalMilliseconds;
|
||||
return t << (10 + 12);
|
||||
}
|
||||
|
||||
/// <summary>解析雪花Id,得到时间、WorkerId和序列号</summary>
|
||||
/// <remarks>
|
||||
/// 其中的时间是StartTimestamp所属时区的时间。
|
||||
/// </remarks>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="time">时间</param>
|
||||
/// <param name="workerId">节点</param>
|
||||
/// <param name="sequence">序列号</param>
|
||||
/// <returns></returns>
|
||||
public virtual Boolean TryParse(Int64 id, out DateTime time, out Int32 workerId, out Int32 sequence)
|
||||
{
|
||||
time = StartTimestamp.AddMilliseconds(id >> (10 + 12));
|
||||
workerId = (Int32)((id >> 12) & 0x3FF);
|
||||
sequence = (Int32)(id & 0x0FFF);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>把输入时间转为开始时间戳的类型,便于相减</summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
public DateTime ConvertKind(DateTime time)
|
||||
{
|
||||
// 如果待转换时间未指定时区,则直接返回
|
||||
if (time.Kind == DateTimeKind.Unspecified) return time;
|
||||
|
||||
return StartTimestamp.Kind switch
|
||||
{
|
||||
DateTimeKind.Utc => time.ToUniversalTime(),
|
||||
DateTimeKind.Local => time.ToLocalTime(),
|
||||
_ => time,
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 集群扩展
|
||||
/// <summary>加入集群。由集群统一分配WorkerId,确保唯一,从而保证生成的雪花Id绝对唯一</summary>
|
||||
/// <param name="cache"></param>
|
||||
/// <param name="key"></param>
|
||||
public virtual void JoinCluster(ICache cache, String key = "SnowflakeWorkerId")
|
||||
{
|
||||
var wid = (Int32)cache.Increment(key, 1);
|
||||
WorkerId = wid & 0x3FF;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
39
src/Admin/ThingsGateway.NewLife.X/Data/TimePoint.cs
Normal file
39
src/Admin/ThingsGateway.NewLife.X/Data/TimePoint.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace ThingsGateway.NewLife.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 时序点,用于时序数据计算
|
||||
/// </summary>
|
||||
public struct TimePoint
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间
|
||||
/// </summary>
|
||||
public Int64 Time;
|
||||
|
||||
/// <summary>
|
||||
/// 数值
|
||||
/// </summary>
|
||||
public Double Value;
|
||||
|
||||
/// <summary>
|
||||
/// 已重载
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"({Time}, {Value})";
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 时序点,用于时序数据计算
|
||||
///// </summary>
|
||||
//public struct LongTimePoint
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 时间
|
||||
// /// </summary>
|
||||
// public Int64 Time;
|
||||
|
||||
// /// <summary>
|
||||
// /// 数值
|
||||
// /// </summary>
|
||||
// public Double Value;
|
||||
//}
|
||||
@@ -17,19 +17,19 @@ public class WeakAction<TArgs>
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>目标对象。弱引用,使得调用方对象可以被GC回收</summary>
|
||||
private readonly WeakReference? Target;
|
||||
readonly WeakReference? Target;
|
||||
|
||||
/// <summary>委托方法</summary>
|
||||
private readonly MethodBase Method;
|
||||
readonly MethodBase Method;
|
||||
|
||||
/// <summary>经过包装的新的委托</summary>
|
||||
private readonly Action<TArgs> Handler;
|
||||
readonly Action<TArgs> Handler;
|
||||
|
||||
/// <summary>取消注册的委托</summary>
|
||||
private Action<Action<TArgs>>? UnHandler;
|
||||
Action<Action<TArgs>>? UnHandler;
|
||||
|
||||
/// <summary>是否只使用一次,如果只使用一次,执行委托后马上取消注册</summary>
|
||||
private readonly Boolean Once;
|
||||
readonly Boolean Once;
|
||||
#endregion
|
||||
|
||||
#region 扩展属性
|
||||
|
||||
@@ -1,86 +1,87 @@
|
||||
namespace ThingsGateway.NewLife;
|
||||
|
||||
/// <summary>数据位助手</summary>
|
||||
public static class BitHelper
|
||||
namespace ThingsGateway.NewLife
|
||||
{
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="flag"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 SetBit(this UInt16 value, Int32 position, Boolean flag)
|
||||
/// <summary>数据位助手</summary>
|
||||
public static class BitHelper
|
||||
{
|
||||
return SetBits(value, position, 1, (flag ? (Byte)1 : (Byte)0));
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="flag"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 SetBit(this UInt16 value, Int32 position, Boolean flag)
|
||||
{
|
||||
return SetBits(value, position, 1, (flag ? (Byte)1 : (Byte)0));
|
||||
}
|
||||
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="bits"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 SetBits(this UInt16 value, Int32 position, Int32 length, UInt16 bits)
|
||||
{
|
||||
if (length <= 0 || position >= 16) return value;
|
||||
|
||||
var mask = (2 << (length - 1)) - 1;
|
||||
|
||||
value &= (UInt16)~(mask << position);
|
||||
value |= (UInt16)((bits & mask) << position);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="flag"></param>
|
||||
/// <returns></returns>
|
||||
public static Byte SetBit(this Byte value, Int32 position, Boolean flag)
|
||||
{
|
||||
if (position >= 8) return value;
|
||||
|
||||
var mask = (2 << (1 - 1)) - 1;
|
||||
|
||||
value &= (Byte)~(mask << position);
|
||||
value |= (Byte)(((flag ? 1 : 0) & mask) << position);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean GetBit(this UInt16 value, Int32 position)
|
||||
{
|
||||
return GetBits(value, position, 1) == 1;
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 GetBits(this UInt16 value, Int32 position, Int32 length)
|
||||
{
|
||||
if (length <= 0 || position >= 16) return 0;
|
||||
|
||||
var mask = (2 << (length - 1)) - 1;
|
||||
|
||||
return (UInt16)((value >> position) & mask);
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean GetBit(this Byte value, Int32 position)
|
||||
{
|
||||
if (position >= 8) return false;
|
||||
|
||||
var mask = (2 << (1 - 1)) - 1;
|
||||
|
||||
return ((Byte)((value >> position) & mask)) == 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <param name="bits"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 SetBits(this UInt16 value, Int32 position, Int32 length, UInt16 bits)
|
||||
{
|
||||
if (length <= 0 || position >= 16) return value;
|
||||
|
||||
var mask = (2 << (length - 1)) - 1;
|
||||
|
||||
value &= (UInt16)~(mask << position);
|
||||
value |= (UInt16)((bits & mask) << position);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>设置数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="flag"></param>
|
||||
/// <returns></returns>
|
||||
public static Byte SetBit(this Byte value, Int32 position, Boolean flag)
|
||||
{
|
||||
if (position >= 8) return value;
|
||||
|
||||
var mask = (2 << (1 - 1)) - 1;
|
||||
|
||||
value &= (Byte)~(mask << position);
|
||||
value |= (Byte)(((flag ? 1 : 0) & mask) << position);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean GetBit(this UInt16 value, Int32 position)
|
||||
{
|
||||
return GetBits(value, position, 1) == 1;
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt16 GetBits(this UInt16 value, Int32 position, Int32 length)
|
||||
{
|
||||
if (length <= 0 || position >= 16) return 0;
|
||||
|
||||
var mask = (2 << (length - 1)) - 1;
|
||||
|
||||
return (UInt16)((value >> position) & mask);
|
||||
}
|
||||
|
||||
/// <summary>获取数据位</summary>
|
||||
/// <param name="value">数值</param>
|
||||
/// <param name="position"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean GetBit(this Byte value, Int32 position)
|
||||
{
|
||||
if (position >= 8) return false;
|
||||
|
||||
var mask = (2 << (1 - 1)) - 1;
|
||||
|
||||
return ((Byte)((value >> position) & mask)) == 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ThingsGateway.NewLife.Extension;
|
||||
namespace ThingsGateway.NewLife.DictionaryExtensions;
|
||||
|
||||
/// <summary>并发字典扩展</summary>
|
||||
public static class ConcurrentDictionaryExtensions
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
/// <summary>从并发字典中删除</summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
@@ -13,6 +13,18 @@ public static class ConcurrentDictionaryExtensions
|
||||
/// <returns></returns>
|
||||
public static Boolean Remove<TKey, TValue>(this ConcurrentDictionary<TKey, TValue> dict, TKey key) where TKey : notnull => dict.TryRemove(key, out _);
|
||||
|
||||
#if !NET6_0_OR_GREATER
|
||||
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> pairs, TKey key, TValue value)
|
||||
{
|
||||
if (!pairs.ContainsKey(key))
|
||||
{
|
||||
pairs.Add(key, value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static int RemoveWhere<TKey, TValue>(this IDictionary<TKey, TValue> pairs, Func<KeyValuePair<TKey, TValue>, bool> func)
|
||||
{
|
||||
@@ -97,4 +109,4 @@ public static class ConcurrentDictionaryExtensions
|
||||
return dict;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using System.Net;
|
||||
|
||||
|
||||
namespace ThingsGateway.NewLife.Extension;
|
||||
|
||||
/// <summary>网络结点扩展</summary>
|
||||
public static class EndPointExtensions
|
||||
{
|
||||
public static String ToAddress(this EndPoint endpoint)
|
||||
{
|
||||
return ((IPEndPoint)endpoint).ToAddress();
|
||||
}
|
||||
|
||||
|
||||
public static String ToAddress(this IPEndPoint endpoint)
|
||||
{
|
||||
return String.Format("{0}:{1}", endpoint.Address, endpoint.Port);
|
||||
}
|
||||
private static readonly String[] SplitColon = new String[] { ":" };
|
||||
|
||||
public static IPEndPoint ToEndPoint(this String address)
|
||||
{
|
||||
var array = address.Split(SplitColon, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (array.Length != 2)
|
||||
{
|
||||
throw new Exception("Invalid endpoint address: " + address);
|
||||
}
|
||||
var ip = IPAddress.Parse(array[0]);
|
||||
var port = Int32.Parse(array[1]);
|
||||
return new IPEndPoint(ip, port);
|
||||
}
|
||||
|
||||
private static readonly String[] SplitComma = new String[] { "," };
|
||||
|
||||
public static IEnumerable<IPEndPoint> ToEndPoints(this String addresses)
|
||||
{
|
||||
var array = addresses.Split(SplitComma, StringSplitOptions.RemoveEmptyEntries);
|
||||
var list = new List<IPEndPoint>();
|
||||
foreach (var item in array)
|
||||
{
|
||||
list.Add(item.ToEndPoint());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ThingsGateway.NewLife.Extension;
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
/// <summary>扩展List,支持遍历中修改元素</summary>
|
||||
public static class ListExtension
|
||||
@@ -24,4 +24,4 @@ public static class ListExtension
|
||||
|
||||
return list.ToArray().Where(e => match(e)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,17 @@ namespace ThingsGateway.NewLife;
|
||||
/// </remarks>
|
||||
public static class ProcessHelper
|
||||
{
|
||||
|
||||
|
||||
public static int GetProcessId()
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
return Environment.ProcessId;
|
||||
#else
|
||||
return ProcessHelper.GetProcessId();
|
||||
#endif
|
||||
}
|
||||
|
||||
#region 进程查找
|
||||
/// <summary>获取二级进程名。默认一级,如果是dotnet/java则取二级</summary>
|
||||
/// <param name="process"></param>
|
||||
@@ -187,6 +198,10 @@ public static class ProcessHelper
|
||||
{
|
||||
if (process?.GetHasExited() != false) return process;
|
||||
|
||||
var span = DefaultSpan.Current;
|
||||
//XTrace.WriteLine("安全,温柔一刀!PID={0}/{1}", process.Id, process.ProcessName);
|
||||
span?.AppendTag($"SafetyKill,温柔一刀!PID={process.Id}/{process.ProcessName}");
|
||||
|
||||
// 杀进程,如果命令未成功则马上退出(后续强杀),否则循环检测并等待
|
||||
try
|
||||
{
|
||||
@@ -211,8 +226,9 @@ public static class ProcessHelper
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
span?.AppendTag(ex.Message);
|
||||
}
|
||||
|
||||
//if (!process.GetHasExited()) process.Kill();
|
||||
@@ -228,6 +244,9 @@ public static class ProcessHelper
|
||||
{
|
||||
if (process?.GetHasExited() != false) return process;
|
||||
|
||||
var span = DefaultSpan.Current;
|
||||
//XTrace.WriteLine("强杀,大力出奇迹!PID={0}/{1}", process.Id, process.ProcessName);
|
||||
span?.AppendTag($"ForceKill,大力出奇迹!PID={process.Id}/{process.ProcessName}");
|
||||
|
||||
// 终止指定的进程及启动的子进程,如nginx等
|
||||
// 在Core 3.0, Core 3.1, 5, 6, 7, 8, 9 中支持此重载
|
||||
@@ -240,8 +259,9 @@ public static class ProcessHelper
|
||||
process.Kill();
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
span?.AppendTag(ex.Message);
|
||||
}
|
||||
|
||||
if (process.GetHasExited()) return process;
|
||||
@@ -260,9 +280,9 @@ public static class ProcessHelper
|
||||
Process.Start("taskkill", $"/t /f /pid {process.Id}").WaitForExit(msWait);
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
span?.AppendTag(ex.Message);
|
||||
}
|
||||
|
||||
// 兜底再来一次
|
||||
@@ -276,8 +296,9 @@ public static class ProcessHelper
|
||||
process.Kill();
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
span?.AppendTag(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +354,7 @@ public static class ProcessHelper
|
||||
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
using var p = new Process();
|
||||
var p = new Process();
|
||||
var si = p.StartInfo;
|
||||
si.FileName = fileName;
|
||||
if (arguments != null) si.Arguments = arguments;
|
||||
|
||||
@@ -5,7 +5,7 @@ using ThingsGateway.NewLife.Reflection;
|
||||
|
||||
namespace ThingsGateway.NewLife.Extension;
|
||||
|
||||
internal sealed class SpeakProvider
|
||||
class SpeakProvider
|
||||
{
|
||||
private const String typeName = "System.Speech.Synthesis.SpeechSynthesizer";
|
||||
private Type? _type;
|
||||
@@ -38,15 +38,14 @@ internal sealed class SpeakProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NewLife.Log.XTrace.WriteException(ex);
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
|
||||
if (_type == null) XTrace.WriteLine("找不到语音库System.Speech,需要从nuget引用");
|
||||
}
|
||||
|
||||
private Object? synth;
|
||||
|
||||
private void EnsureSynth()
|
||||
void EnsureSynth()
|
||||
{
|
||||
if (synth == null && _type != null)
|
||||
{
|
||||
@@ -57,7 +56,7 @@ internal sealed class SpeakProvider
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NewLife.Log.XTrace.WriteException(ex);
|
||||
XTrace.WriteException(ex);
|
||||
_type = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,8 +321,8 @@ public static class StringHelper
|
||||
/// <returns></returns>
|
||||
public static String EnsureStart(this String? str, String start)
|
||||
{
|
||||
if (String.IsNullOrEmpty(start)) return str + "";
|
||||
if (String.IsNullOrEmpty(str) || str == null) return start + "";
|
||||
if (String.IsNullOrEmpty(start)) return str + string.Empty;
|
||||
if (String.IsNullOrEmpty(str) || str == null) return start + string.Empty;
|
||||
|
||||
if (str.StartsWith(start, StringComparison.OrdinalIgnoreCase)) return str;
|
||||
|
||||
@@ -335,8 +335,8 @@ public static class StringHelper
|
||||
/// <returns></returns>
|
||||
public static String EnsureEnd(this String? str, String end)
|
||||
{
|
||||
if (String.IsNullOrEmpty(end)) return str + "";
|
||||
if (String.IsNullOrEmpty(str) || str == null) return end + "";
|
||||
if (String.IsNullOrEmpty(end)) return str + string.Empty;
|
||||
if (String.IsNullOrEmpty(str) || str == null) return end + string.Empty;
|
||||
|
||||
if (str.EndsWith(end, StringComparison.OrdinalIgnoreCase)) return str;
|
||||
|
||||
@@ -846,14 +846,14 @@ public static class StringHelper
|
||||
#endregion
|
||||
|
||||
#region 文字转语音
|
||||
private static ThingsGateway.NewLife.Extension.SpeakProvider? _provider;
|
||||
private static NewLife.Extension.SpeakProvider? _provider;
|
||||
//private static System.Speech.Synthesis.SpeechSynthesizer _provider;
|
||||
[MemberNotNull(nameof(_provider))]
|
||||
private static void Init()
|
||||
static void Init()
|
||||
{
|
||||
//_provider = new Speech.Synthesis.SpeechSynthesizer();
|
||||
//_provider.SetOutputToDefaultAudioDevice();
|
||||
_provider ??= new ThingsGateway.NewLife.Extension.SpeakProvider();
|
||||
_provider ??= new NewLife.Extension.SpeakProvider();
|
||||
}
|
||||
|
||||
/// <summary>调用语音引擎说出指定话</summary>
|
||||
@@ -903,4 +903,4 @@ public static class StringHelper
|
||||
return value;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,5 @@
|
||||
|
||||
global using System.Buffers;
|
||||
|
||||
global using ThingsGateway.NewLife.DictionaryExtensions;
|
||||
global using ThingsGateway.NewLife.Extension;
|
||||
|
||||
38
src/Admin/ThingsGateway.NewLife.X/Http/ControllerHandler.cs
Normal file
38
src/Admin/ThingsGateway.NewLife.X/Http/ControllerHandler.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
|
||||
using ThingsGateway.NewLife.Model;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Remoting;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>控制器处理器</summary>
|
||||
public class ControllerHandler : IHttpHandler
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>控制器类型</summary>
|
||||
public Type? ControllerType { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>处理请求</summary>
|
||||
/// <param name="context"></param>
|
||||
public virtual void ProcessRequest(IHttpContext context)
|
||||
{
|
||||
var type = ControllerType;
|
||||
if (type == null) return;
|
||||
|
||||
var ss = context.Path.Split('/');
|
||||
var methodName = ss.Length >= 3 ? ss[2] : null;
|
||||
|
||||
// 优先使用服务提供者创建控制器对象,以便控制器构造函数注入
|
||||
var controller = context.ServiceProvider?.CreateInstance(type) ?? type.CreateInstance();
|
||||
|
||||
var method = methodName == null ? null : type.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
if (method == null) throw new ApiException(ApiCode.NotFound, $"Cannot find operation [{methodName}] within controller [{type.FullName}]");
|
||||
|
||||
var result = controller.InvokeWithParams(method, context.Parameters as IDictionary);
|
||||
if (result != null)
|
||||
context.Response.SetResult(result);
|
||||
}
|
||||
}
|
||||
59
src/Admin/ThingsGateway.NewLife.X/Http/DnsHttpHandler.cs
Normal file
59
src/Admin/ThingsGateway.NewLife.X/Http/DnsHttpHandler.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using ThingsGateway.NewLife.Net;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>支持优化Dns解析的HttpClient处理器</summary>
|
||||
public class DnsHttpHandler : DelegatingHandler
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>DNS解析器</summary>
|
||||
public IDnsResolver Resolver { get; set; } = DnsResolver.Instance;
|
||||
#endregion
|
||||
|
||||
/// <summary>实例化一个支持APM的HttpClient处理器</summary>
|
||||
/// <param name="innerHandler"></param>
|
||||
public DnsHttpHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }
|
||||
|
||||
/// <summary>发送请求</summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
if (uri == null) return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 调用自定义DNS解析器
|
||||
var addrs = Resolver?.Resolve(uri.Host);
|
||||
if (addrs?.Length > 0)
|
||||
{
|
||||
var addr = addrs[0];
|
||||
|
||||
// 从请求中读取当前使用的IP索引,轮询使用。因为HttpClient调用失败后会重试,这里分配新的IP
|
||||
#if NET6_0_OR_GREATER
|
||||
var key = new HttpRequestOptionsKey<Int32>("dnsIndex");
|
||||
if (!request.Options.TryGetValue(key, out var idx)) idx = 0;
|
||||
|
||||
addr = addrs[idx % addrs.Length];
|
||||
request.Options.Set(key, ++idx);
|
||||
#else
|
||||
var idx = request.Properties.TryGetValue("dnsIndex", out var obj) ? obj.ToInt() : 0;
|
||||
addr = addrs[idx % addrs.Length];
|
||||
request.Properties["dnsIndex"] = ++idx;
|
||||
#endif
|
||||
|
||||
// 先固定Host
|
||||
request.Headers.Host ??= uri.Host;
|
||||
|
||||
var builder = new UriBuilder(uri)
|
||||
{
|
||||
Host = addr + "",
|
||||
};
|
||||
|
||||
// 再把Uri换成IP
|
||||
request.RequestUri = builder.Uri;
|
||||
}
|
||||
|
||||
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
46
src/Admin/ThingsGateway.NewLife.X/Http/FormFile.cs
Normal file
46
src/Admin/ThingsGateway.NewLife.X/Http/FormFile.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>表单部分</summary>
|
||||
public class FormFile
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>名称</summary>
|
||||
public String Name { get; set; } = null!;
|
||||
|
||||
/// <summary>内容部署</summary>
|
||||
public String? ContentDisposition { get; set; }
|
||||
|
||||
/// <summary>内容类型</summary>
|
||||
public String? ContentType { get; set; }
|
||||
|
||||
/// <summary>文件名</summary>
|
||||
public String? FileName { get; set; }
|
||||
|
||||
/// <summary>数据</summary>
|
||||
public Byte[]? Data { get; set; }
|
||||
|
||||
/// <summary>长度</summary>
|
||||
public Int64 Length => Data?.Length ?? 0;
|
||||
#endregion
|
||||
|
||||
/// <summary>打开数据读取流</summary>
|
||||
/// <returns></returns>
|
||||
public Stream? OpenReadStream() => Data == null ? null : new MemoryStream(Data);
|
||||
|
||||
/// <summary>保存到文件</summary>
|
||||
/// <param name="fileName"></param>
|
||||
public void SaveToFile(String? fileName = null)
|
||||
{
|
||||
if (fileName.IsNullOrEmpty()) fileName = FileName;
|
||||
if (fileName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(fileName));
|
||||
if (Data == null) throw new ArgumentNullException(nameof(Data));
|
||||
|
||||
fileName.EnsureDirectory(true);
|
||||
|
||||
using var fs = File.OpenWrite(fileName.GetFullPath());
|
||||
//Data.CopyTo(fs);
|
||||
fs.Write(Data);
|
||||
fs.SetLength(fs.Position);
|
||||
fs.Flush();
|
||||
}
|
||||
}
|
||||
143
src/Admin/ThingsGateway.NewLife.X/Http/HttpBase.cs
Normal file
143
src/Admin/ThingsGateway.NewLife.X/Http/HttpBase.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Buffers;
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http请求响应基类</summary>
|
||||
public abstract class HttpBase : IDisposable
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>协议版本</summary>
|
||||
public String Version { get; set; } = "1.1";
|
||||
|
||||
/// <summary>内容长度</summary>
|
||||
public Int32 ContentLength { get; set; } = -1;
|
||||
|
||||
/// <summary>内容类型</summary>
|
||||
public String? ContentType { get; set; }
|
||||
|
||||
/// <summary>请求或响应的主体部分</summary>
|
||||
public IPacket? Body { get; set; }
|
||||
|
||||
/// <summary>主体长度</summary>
|
||||
public Int32 BodyLength => Body == null ? 0 : Body.Total;
|
||||
|
||||
/// <summary>是否已完整。头部未指定长度,或指定长度后内容已满足</summary>
|
||||
public Boolean IsCompleted => ContentLength < 0 || ContentLength <= BodyLength;
|
||||
|
||||
/// <summary>头部集合</summary>
|
||||
public IDictionary<String, String> Headers { get; set; } = new NullableDictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>获取/设置 头部</summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
public String this[String key] { get => Headers[key] + string.Empty; set => Headers[key] = value; }
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>释放</summary>
|
||||
public void Dispose() => Body.TryDispose();
|
||||
#endregion
|
||||
|
||||
#region 解析
|
||||
/// <summary>快速验证协议头,剔除非HTTP协议。仅排除,验证通过不一定就是HTTP协议</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public static Boolean FastValidHeader(ReadOnlySpan<Byte> data)
|
||||
{
|
||||
// 性能优化,Http头部第一行以请求谓语或响应版本开头,然后是一个空格。最长谓语Options/Connect,版本HTTP/1.1,不超过10个字符
|
||||
if (data.Length > 10) data = data[..10];
|
||||
var p = data.IndexOf((Byte)' ');
|
||||
return p >= 0;
|
||||
}
|
||||
|
||||
private static readonly Byte[] NewLine = [(Byte)'\r', (Byte)'\n'];
|
||||
private static readonly Byte[] NewLine2 = [(Byte)'\r', (Byte)'\n', (Byte)'\r', (Byte)'\n'];
|
||||
/// <summary>分析请求头。截取Body时获取缓冲区所有权</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean Parse(IPacket pk)
|
||||
{
|
||||
var data = pk.GetSpan();
|
||||
if (!FastValidHeader(data)) return false;
|
||||
|
||||
// 识别整个请求头
|
||||
var p = data.IndexOf(NewLine2);
|
||||
if (p < 0) return false;
|
||||
|
||||
// 分析头部
|
||||
var header = data[..(p + 2)];
|
||||
var firstLine = string.Empty;
|
||||
while (true)
|
||||
{
|
||||
var p2 = header.IndexOf(NewLine);
|
||||
if (p2 < 0) break;
|
||||
|
||||
var line = header[..p2];
|
||||
if (firstLine.IsNullOrEmpty())
|
||||
firstLine = line.ToStr();
|
||||
else
|
||||
{
|
||||
var p3 = line.IndexOf((Byte)':');
|
||||
if (p3 > 0)
|
||||
{
|
||||
var name = line[..p3].Trim((Byte)' ').ToStr();
|
||||
var value = line[(p3 + 1)..].Trim((Byte)' ').ToStr();
|
||||
Headers[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (p2 + 2 == header.Length) break;
|
||||
header = header[(p2 + 2)..];
|
||||
}
|
||||
|
||||
// 截取主体,获取所有权
|
||||
Body = pk.Slice(p + 4, -1, true);
|
||||
|
||||
ContentLength = Headers["Content-Length"].ToInt(-1);
|
||||
ContentType = Headers["Content-Type"];
|
||||
|
||||
// 分析第一行
|
||||
if (!OnParse(firstLine)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>分析第一行</summary>
|
||||
/// <param name="firstLine"></param>
|
||||
protected abstract Boolean OnParse(String firstLine);
|
||||
#endregion
|
||||
|
||||
#region 读写
|
||||
/// <summary>创建请求响应包</summary>
|
||||
/// <remarks>数据来自缓冲池,使用者用完返回数据包后应该释放,以便把缓冲区放回池里</remarks>
|
||||
/// <returns></returns>
|
||||
public virtual IOwnerPacket Build()
|
||||
{
|
||||
var body = Body;
|
||||
var len = body != null ? body.Total : 0;
|
||||
|
||||
var header = BuildHeader(len);
|
||||
|
||||
// 从内存池申请缓冲区,Slice后管理权转移,外部使用完以后释放
|
||||
//using var pk = new ArrayPacket(Encoding.UTF8.GetByteCount(header) + len);
|
||||
len += Encoding.UTF8.GetByteCount(header);
|
||||
var pk = new OwnerPacket(len);
|
||||
var writer = new SpanWriter(pk.GetSpan());
|
||||
|
||||
writer.Write(header, -1);
|
||||
|
||||
if (body != null) writer.Write(body.GetSpan());
|
||||
|
||||
return pk.Resize(writer.Position);
|
||||
}
|
||||
|
||||
/// <summary>创建头部</summary>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract String BuildHeader(Int32 length);
|
||||
#endregion
|
||||
}
|
||||
783
src/Admin/ThingsGateway.NewLife.X/Http/HttpHelper.cs
Normal file
783
src/Admin/ThingsGateway.NewLife.X/Http/HttpHelper.cs
Normal file
@@ -0,0 +1,783 @@
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Caching;
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Net;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
using ThingsGateway.NewLife.Xml;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http帮助类</summary>
|
||||
public static class HttpHelper
|
||||
{
|
||||
/// <summary>性能跟踪器</summary>
|
||||
public static ITracer? Tracer { get; set; } = DefaultTracer.Instance;
|
||||
|
||||
/// <summary>Http过滤器</summary>
|
||||
public static IHttpFilter? Filter { get; set; }
|
||||
|
||||
/// <summary>默认用户浏览器UserAgent。用于内部创建的HttpClient请求</summary>
|
||||
public static String? DefaultUserAgent { get; set; }
|
||||
|
||||
static HttpHelper()
|
||||
{
|
||||
var asm = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||
//if (asm != null) agent = $"{asm.GetName().Name}/{asm.GetName().Version}";
|
||||
if (asm != null)
|
||||
{
|
||||
var aname = asm.GetName();
|
||||
var os = Environment.OSVersion?.ToString().TrimStart("Microsoft ");
|
||||
if (!os.IsNullOrEmpty() && Encoding.UTF8.GetByteCount(os) == os.Length)
|
||||
DefaultUserAgent = $"{aname.Name}/{aname.Version} ({os})";
|
||||
else
|
||||
DefaultUserAgent = $"{aname.Name}/{aname.Version}";
|
||||
}
|
||||
}
|
||||
|
||||
#region 默认封装
|
||||
/// <summary>设置浏览器UserAgent。默认使用应用名和版本</summary>
|
||||
/// <param name="client"></param>
|
||||
/// <returns></returns>
|
||||
public static HttpClient SetUserAgent(this HttpClient client)
|
||||
{
|
||||
var userAgent = DefaultUserAgent;
|
||||
if (!userAgent.IsNullOrEmpty()) client.DefaultRequestHeaders.UserAgent.ParseAdd(userAgent);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>为HttpClient创建Socket处理器,默认设置连接生命为5分钟,有效反映DNS网络更改</summary>
|
||||
/// <remarks>
|
||||
/// PooledConnectionLifetime 属性定义池中的最大连接生存期,从建立连接的时间跟踪其年龄,而不考虑其空闲时间或活动时间。
|
||||
/// 在主动用于服务请求时,连接不会被拆毁。此生存期非常有用,以便定期重新建立连接,以便更好地反映 DNS 或其他网络更改。
|
||||
/// </remarks>
|
||||
/// <param name="useProxy">是否使用代理</param>
|
||||
/// <param name="useCookie">是否使用Cookie</param>
|
||||
/// <returns></returns>
|
||||
public static HttpMessageHandler CreateHandler(Boolean useProxy, Boolean useCookie) => CreateHandler(useProxy, useCookie, false);
|
||||
|
||||
/// <summary>为HttpClient创建Socket处理器,默认设置连接生命为5分钟,有效反映DNS网络更改</summary>
|
||||
/// <remarks>
|
||||
/// PooledConnectionLifetime 属性定义池中的最大连接生存期,从建立连接的时间跟踪其年龄,而不考虑其空闲时间或活动时间。
|
||||
/// 在主动用于服务请求时,连接不会被拆毁。此生存期非常有用,以便定期重新建立连接,以便更好地反映 DNS 或其他网络更改。
|
||||
/// </remarks>
|
||||
/// <param name="useProxy">是否使用代理</param>
|
||||
/// <param name="useCookie">是否使用Cookie</param>
|
||||
/// <param name="ignoreSSL">是否忽略证书检验</param>
|
||||
/// <returns></returns>
|
||||
public static HttpMessageHandler CreateHandler(Boolean useProxy, Boolean useCookie, Boolean ignoreSSL)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
var hander = new SocketsHttpHandler
|
||||
{
|
||||
UseProxy = useProxy,
|
||||
UseCookies = useCookie,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
||||
ConnectCallback = ConnectCallback,
|
||||
};
|
||||
|
||||
if (ignoreSSL)
|
||||
{
|
||||
#pragma warning disable CA5359 // 请勿禁用证书验证
|
||||
hander.SslOptions = new SslClientAuthenticationOptions
|
||||
{
|
||||
RemoteCertificateValidationCallback = (_, _, _, _) => true
|
||||
};
|
||||
#pragma warning restore CA5359 // 请勿禁用证书验证
|
||||
}
|
||||
|
||||
return hander;
|
||||
#elif NETCOREAPP3_0_OR_GREATER
|
||||
var hander = new SocketsHttpHandler
|
||||
{
|
||||
UseProxy = useProxy,
|
||||
UseCookies = useCookie,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
|
||||
};
|
||||
|
||||
if (ignoreSSL)
|
||||
{
|
||||
hander.SslOptions = new SslClientAuthenticationOptions
|
||||
{
|
||||
RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
|
||||
};
|
||||
}
|
||||
|
||||
return hander;
|
||||
#else
|
||||
var hander = new HttpClientHandler
|
||||
{
|
||||
UseProxy = useProxy,
|
||||
UseCookies = useCookie,
|
||||
AutomaticDecompression = DecompressionMethods.GZip
|
||||
};
|
||||
|
||||
if (ignoreSSL)
|
||||
{
|
||||
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
|
||||
}
|
||||
|
||||
return hander;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>连接回调,内部创建Socket,解决DNS解析缓存问题</summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
static async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
var dep = context.DnsEndPoint;
|
||||
var method = context.InitialRequestMessage.Method?.ToString() ?? "Connect";
|
||||
using var span = Tracer?.NewSpan($"net:{dep.Host}:{dep.Port}:{method}");
|
||||
|
||||
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
NoDelay = true
|
||||
};
|
||||
try
|
||||
{
|
||||
var ep = context.DnsEndPoint;
|
||||
var addrs = NetUri.ParseAddress(ep.Host);
|
||||
span?.AppendTag($"addrs={addrs?.Join()}");
|
||||
if (addrs?.Length > 0)
|
||||
await socket.ConnectAsync(addrs, ep.Port, cancellationToken).ConfigureAwait(false);
|
||||
else
|
||||
await socket.ConnectAsync(ep, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
span?.SetError(ex, null);
|
||||
|
||||
if (ex is SocketException se)
|
||||
Tracer?.NewError($"socket:SocketError-{se.SocketErrorCode}", se);
|
||||
|
||||
socket.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new NetworkStream(socket, ownsSocket: true);
|
||||
}
|
||||
#endif
|
||||
#endregion
|
||||
|
||||
#region Http封包解包
|
||||
/// <summary>创建请求包</summary>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="headers"></param>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public static IPacket MakeRequest(String method, Uri uri, IDictionary<String, Object?>? headers, IPacket? pk)
|
||||
{
|
||||
var count = pk?.Total ?? 0;
|
||||
if (method.IsNullOrEmpty()) method = count > 0 ? "POST" : "GET";
|
||||
|
||||
// 分解主机和资源
|
||||
var host = string.Empty;
|
||||
if (uri == null) uri = new Uri("/");
|
||||
|
||||
if (uri.Scheme.EqualIgnoreCase("http", "ws"))
|
||||
{
|
||||
if (uri.Port == 80)
|
||||
host = uri.Host;
|
||||
else
|
||||
host = $"{uri.Host}:{uri.Port}";
|
||||
}
|
||||
else if (uri.Scheme.EqualIgnoreCase("https"))
|
||||
{
|
||||
if (uri.Port == 443)
|
||||
host = uri.Host;
|
||||
else
|
||||
host = $"{uri.Host}:{uri.Port}";
|
||||
}
|
||||
|
||||
// 构建头部
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
sb.AppendFormat("{0} {1} HTTP/1.1\r\n", method, uri.PathAndQuery);
|
||||
sb.AppendFormat("Host:{0}\r\n", host);
|
||||
|
||||
//if (Compressed) sb.AppendLine("Accept-Encoding:gzip, deflate");
|
||||
//if (KeepAlive) sb.AppendLine("Connection:keep-alive");
|
||||
//if (!UserAgent.IsNullOrEmpty()) sb.AppendFormat("User-Agent:{0}\r\n", UserAgent);
|
||||
|
||||
// 内容长度
|
||||
if (count > 0) sb.AppendFormat("Content-Length:{0}\r\n", count);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var item in headers)
|
||||
{
|
||||
sb.AppendFormat("{0}:{1}\r\n", item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("\r\n");
|
||||
|
||||
//return sb.ToString();
|
||||
var rs = new ArrayPacket(sb.Return(true).GetBytes())
|
||||
{
|
||||
Next = pk
|
||||
};
|
||||
return rs;
|
||||
}
|
||||
|
||||
/// <summary>创建响应包</summary>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="headers"></param>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public static IPacket MakeResponse(HttpStatusCode code, IDictionary<String, Object?>? headers, IPacket? pk)
|
||||
{
|
||||
// 构建头部
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
sb.AppendFormat("HTTP/1.1 {0} {1}\r\n", (Int32)code, code);
|
||||
|
||||
// 内容长度
|
||||
var count = pk?.Total ?? 0;
|
||||
if (count > 0) sb.AppendFormat("Content-Length:{0}\r\n", count);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var item in headers)
|
||||
{
|
||||
sb.AppendFormat("{0}:{1}\r\n", item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("\r\n");
|
||||
|
||||
//return sb.ToString();
|
||||
var rs = new ArrayPacket(sb.Return(true).GetBytes())
|
||||
{
|
||||
Next = pk
|
||||
};
|
||||
return rs;
|
||||
}
|
||||
|
||||
private static readonly Byte[] NewLine = [(Byte)'\r', (Byte)'\n', (Byte)'\r', (Byte)'\n'];
|
||||
/// <summary>分析头部</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public static IDictionary<String, Object> ParseHeader(Packet pk)
|
||||
{
|
||||
// 客户端收到响应,服务端收到请求
|
||||
var headers = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var p = pk.IndexOf(NewLine);
|
||||
if (p < 0) return headers;
|
||||
|
||||
// 截取
|
||||
var lines = pk.ReadBytes(0, p).ToStr().Split("\r\n");
|
||||
// 重构
|
||||
p += 4;
|
||||
pk.Set(pk.Data, pk.Offset + p, pk.Count - p);
|
||||
|
||||
// 分析头部
|
||||
headers.Clear();
|
||||
var line = lines[0];
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
line = lines[i];
|
||||
p = line.IndexOf(':');
|
||||
if (p > 0) headers[line[..p]] = line[(p + 1)..].Trim();
|
||||
}
|
||||
|
||||
line = lines[0];
|
||||
var ss = line.Split(' ');
|
||||
// 分析请求方法 GET / HTTP/1.1
|
||||
if (ss.Length >= 3 && ss[2].StartsWithIgnoreCase("HTTP/"))
|
||||
{
|
||||
headers["Method"] = ss[0];
|
||||
|
||||
// 构造资源路径
|
||||
var host = headers.TryGetValue("Host", out var s) ? s : string.Empty;
|
||||
var uri = $"http://{host}";
|
||||
//var uri = "{0}://{1}".F(IsSSL ? "https" : "http", host);
|
||||
//if (host.IsNullOrEmpty() || !host.Contains(":"))
|
||||
//{
|
||||
// var port = Local.Port;
|
||||
// if (IsSSL && port != 443 || !IsSSL && port != 80) uri += ":" + port;
|
||||
//}
|
||||
uri += ss[1];
|
||||
headers["Url"] = new Uri(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 分析响应码
|
||||
var code = ss[1].ToInt();
|
||||
if (code > 0) headers["StatusCode"] = (HttpStatusCode)code;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 高级功能扩展
|
||||
/// <summary>异步提交Json</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<String> PostJsonAsync(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpContent? content = null;
|
||||
//if (data != null)
|
||||
{
|
||||
content = data is String str
|
||||
? new StringContent(str, Encoding.UTF8, "application/json")
|
||||
: new StringContent(data.ToJson(), Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
//if (headers == null && client.DefaultRequestHeaders.Accept.Count == 0) client.DefaultRequestHeaders.Accept.ParseAdd("application/json");
|
||||
|
||||
return await PostAsync(client, requestUri, content, headers, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>同步提交Json</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <returns></returns>
|
||||
public static String PostJson(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null) => client.PostJsonAsync(requestUri, data, headers).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>异步提交Xml</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<String> PostXmlAsync(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpContent? content = null;
|
||||
//if (data != null)
|
||||
{
|
||||
content = data is String str
|
||||
? new StringContent(str, Encoding.UTF8, "application/xml")
|
||||
: new StringContent(data.ToXml(), Encoding.UTF8, "application/xml");
|
||||
}
|
||||
|
||||
//if (headers == null && client.DefaultRequestHeaders.Accept.Count == 0) client.DefaultRequestHeaders.Accept.ParseAdd("application/xml");
|
||||
//client.AddHeaders(headers);
|
||||
|
||||
return await PostAsync(client, requestUri, content, headers, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>同步提交Xml</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">数据</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <returns></returns>
|
||||
public static String PostXml(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null) => client.PostXmlAsync(requestUri, data, headers).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>异步提交表单,名值对传输字典参数</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">名值对数据。匿名对象或字典</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public static async Task<String> PostFormAsync(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
HttpContent? content = null;
|
||||
//if (data != null)
|
||||
{
|
||||
//content = data is String str
|
||||
// ? new StringContent(str, Encoding.UTF8, "application/x-www-form-urlencoded")
|
||||
// : (
|
||||
// data is IDictionary<String, String?> dic
|
||||
// ? new FormUrlEncodedContent(dic)
|
||||
// : new FormUrlEncodedContent(data.ToDictionary().ToDictionary(e => e.Key, e => e.Value + ""))
|
||||
// );
|
||||
|
||||
if (data is String str)
|
||||
{
|
||||
content = new StringContent(str, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||
}
|
||||
#if NET6_0_OR_GREATER
|
||||
else if (data is IDictionary<String?, String?> dic)
|
||||
{
|
||||
content = new FormUrlEncodedContent(dic);
|
||||
}
|
||||
else
|
||||
{
|
||||
var list = new List<KeyValuePair<String?, String?>>();
|
||||
//var dic2 = new Dictionary<String, String?>();
|
||||
foreach (var item in data.ToDictionary())
|
||||
{
|
||||
//dic2[item.Key + ""] = item.Value + string.Empty;
|
||||
list.Add(new KeyValuePair<String?, String?>(item.Key, item.Value + ""));
|
||||
}
|
||||
content = new FormUrlEncodedContent(list);
|
||||
}
|
||||
#else
|
||||
else if (data is IDictionary<String, String> dic)
|
||||
{
|
||||
content = new FormUrlEncodedContent(dic);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dic2 = new Dictionary<String, String>();
|
||||
foreach (var item in data.ToDictionary())
|
||||
{
|
||||
dic2[item.Key + ""] = item.Value + string.Empty;
|
||||
}
|
||||
content = new FormUrlEncodedContent(dic2);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return await PostAsync(client, requestUri, content, headers, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>同步提交表单,名值对传输字典参数</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">名值对数据。匿名对象或字典</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <returns></returns>
|
||||
public static String PostForm(this HttpClient client, String requestUri, Object data, IDictionary<String, String>? headers = null) => client.PostFormAsync(requestUri, data, headers).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>异步提交多段表单数据,含文件流</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="data">名值对数据。匿名对象或字典,支持文件流</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
public static async Task<String> PostMultipartFormAsync(this HttpClient client, String requestUri, Object data, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var content = new MultipartFormDataContent();
|
||||
|
||||
foreach (var item in data.ToDictionary())
|
||||
{
|
||||
//if (item.Value == null) continue;
|
||||
|
||||
if (item.Value is FileStream fs)
|
||||
content.Add(new StreamContent(fs), item.Key, Path.GetFileName(fs.Name));
|
||||
else if (item.Value is Stream stream)
|
||||
content.Add(new StreamContent(stream), item.Key);
|
||||
else if (item.Value is String str)
|
||||
content.Add(new StringContent(str), item.Key);
|
||||
else if (item.Value is Byte[] buf)
|
||||
content.Add(new ByteArrayContent(buf), item.Key);
|
||||
else if (item.Value?.GetType().IsBaseType() != false)
|
||||
content.Add(new StringContent(item.Value + ""), item.Key);
|
||||
else
|
||||
content.Add(new StringContent(item.Value.ToJson()), item.Key);
|
||||
}
|
||||
|
||||
return await PostAsync(client, requestUri, content, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>同步获取字符串</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="headers">附加头部</param>
|
||||
/// <returns></returns>
|
||||
public static String GetString(this HttpClient client, String requestUri, IDictionary<String, String>? headers = null)
|
||||
{
|
||||
if (headers != null) client.AddHeaders(headers);
|
||||
#if NET6_0_OR_GREATER
|
||||
using var source = new CancellationTokenSource(client.Timeout);
|
||||
return client.GetStringAsync(requestUri, source.Token).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
#else
|
||||
return client.GetStringAsync(requestUri).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
#endif
|
||||
}
|
||||
|
||||
private static async Task<String> PostAsync(HttpClient client, String requestUri, HttpContent content, IDictionary<String, String>? headers, CancellationToken cancellationToken)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, requestUri)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var item in headers)
|
||||
{
|
||||
request.Headers.Add(item.Key, item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置接受 mediaType
|
||||
if (content.Headers.TryGetValues("Content-Type", out var vs))
|
||||
{
|
||||
// application/json; charset=utf-8
|
||||
var type = vs.FirstOrDefault()?.Split(';').FirstOrDefault();
|
||||
if (type.EqualIgnoreCase("application/json", "application/xml")) request.Headers.Accept.ParseAdd(type);
|
||||
}
|
||||
|
||||
// 开始跟踪,注入TraceId
|
||||
using var span = Tracer?.NewSpan(request);
|
||||
//if (span != null) span.SetTag(content.ReadAsStringAsync().Result);
|
||||
var filter = Filter;
|
||||
try
|
||||
{
|
||||
if (filter != null) await filter.OnRequest(client, request, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (filter != null) await filter.OnResponse(client, response, request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
var result = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
// 增加埋点数据
|
||||
span?.AppendTag(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 跟踪异常
|
||||
span?.SetError(ex, null);
|
||||
|
||||
if (filter != null) await filter.OnError(client, ex, request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient AddHeaders(this HttpClient client, IDictionary<String, String> headers)
|
||||
{
|
||||
//if (client == null) return null;
|
||||
if (headers == null || headers.Count == 0) return client;
|
||||
|
||||
foreach (var item in headers)
|
||||
{
|
||||
//判断请求头中是否已存在,存在先删除,再添加
|
||||
if (client.DefaultRequestHeaders.Contains(item.Key))
|
||||
client.DefaultRequestHeaders.Remove(item.Key);
|
||||
client.DefaultRequestHeaders.Add(item.Key, item.Value);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/// <summary>下载文件</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="fileName">目标文件名</param>
|
||||
public static async Task DownloadFileAsync(this HttpClient client, String requestUri, String fileName)
|
||||
{
|
||||
var rs = await client.GetStreamAsync(requestUri).ConfigureAwait(false);
|
||||
fileName.EnsureDirectory(true);
|
||||
using var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
await rs.CopyToAsync(fs).ConfigureAwait(false);
|
||||
await fs.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>下载文件</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="fileName">目标文件名</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
public static async Task DownloadFileAsync(this HttpClient client, String requestUri, String fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
var rs = await client.GetStreamAsync(requestUri, cancellationToken).ConfigureAwait(false);
|
||||
fileName.EnsureDirectory(true);
|
||||
using var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
await rs.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
#elif NETSTANDARD2_1_OR_GREATER || NETCOREAPP
|
||||
var rs = await client.GetStreamAsync(requestUri).ConfigureAwait(false);
|
||||
fileName.EnsureDirectory(true);
|
||||
using var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
await rs.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
var rs = await client.GetStreamAsync(requestUri).ConfigureAwait(false);
|
||||
fileName.EnsureDirectory(true);
|
||||
using var fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
await rs.CopyToAsync(fs, 81920, cancellationToken).ConfigureAwait(false);
|
||||
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>上传文件以及表单数据</summary>
|
||||
/// <param name="client">Http客户端</param>
|
||||
/// <param name="requestUri">请求资源地址</param>
|
||||
/// <param name="fileName">目标文件名</param>
|
||||
/// <param name="data">其它表单数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
public static async Task<String> UploadFileAsync(this HttpClient client, String requestUri, String fileName, Object? data = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var content = new MultipartFormDataContent();
|
||||
if (!fileName.IsNullOrEmpty())
|
||||
content.Add(new StreamContent(fileName.AsFile().OpenRead()), "file", Path.GetFileName(fileName));
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
foreach (var item in data.ToDictionary())
|
||||
{
|
||||
//if (item.Value == null) continue;
|
||||
|
||||
if (item.Value is String str)
|
||||
content.Add(new StringContent(str), item.Key);
|
||||
else if (item.Value is Byte[] buf)
|
||||
content.Add(new ByteArrayContent(buf), item.Key);
|
||||
else if (item.Value?.GetType().IsBaseType() != false)
|
||||
content.Add(new StringContent(item.Value + ""), item.Key);
|
||||
else
|
||||
content.Add(new StringContent(item.Value.ToJson()), item.Key);
|
||||
}
|
||||
}
|
||||
|
||||
return await PostAsync(client, requestUri, content, null, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region WebSocket
|
||||
/// <summary>从队列消费消息并推送到WebSocket客户端</summary>
|
||||
/// <param name="socket">WebSocket实例</param>
|
||||
/// <param name="queue">队列</param>
|
||||
/// <param name="onProcess">数据处理委托</param>
|
||||
/// <param name="source">取消通知源</param>
|
||||
/// <returns></returns>
|
||||
public static async Task ConsumeAndPushAsync(this WebSocket socket, IProducerConsumer<String> queue, Func<String, Byte[]>? onProcess, CancellationTokenSource source)
|
||||
{
|
||||
DefaultSpan.Current = null;
|
||||
var token = source.Token;
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested && socket.Connected)
|
||||
{
|
||||
var msg = await queue.TakeOneAsync(30, token).ConfigureAwait(false);
|
||||
if (msg != null)
|
||||
{
|
||||
var buf = onProcess != null ? onProcess(msg) : msg.GetBytes();
|
||||
socket.Send(buf, WebSocketMessageType.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(100, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从队列消费消息并推送到WebSocket客户端</summary>
|
||||
/// <param name="socket">WebSocket实例</param>
|
||||
/// <param name="host">缓存主机</param>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="source">取消通知源</param>
|
||||
/// <returns></returns>
|
||||
public static Task ConsumeAndPushAsync(this WebSocket socket, ICache host, String topic, CancellationTokenSource source) => ConsumeAndPushAsync(socket, host.GetQueue<String>(topic), null, source);
|
||||
|
||||
/// <summary>从队列消费消息并推送到WebSocket客户端</summary>
|
||||
/// <param name="socket">WebSocket实例</param>
|
||||
/// <param name="queue">队列</param>
|
||||
/// <param name="onProcess">数据处理委托</param>
|
||||
/// <param name="source">取消通知源</param>
|
||||
/// <returns></returns>
|
||||
public static async Task ConsumeAndPushAsync(this System.Net.WebSockets.WebSocket socket, IProducerConsumer<String> queue, Func<String, Byte[]>? onProcess, CancellationTokenSource source)
|
||||
{
|
||||
DefaultSpan.Current = null;
|
||||
var token = source.Token;
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested && socket.State == System.Net.WebSockets.WebSocketState.Open)
|
||||
{
|
||||
var msg = await queue.TakeOneAsync(30, token).ConfigureAwait(false);
|
||||
if (msg != null)
|
||||
{
|
||||
var buf = onProcess != null ? onProcess(msg) : msg.GetBytes();
|
||||
|
||||
if (buf?.Length > 0)
|
||||
await socket.SendAsync(new ArraySegment<Byte>(buf), System.Net.WebSockets.WebSocketMessageType.Text, true, token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(100, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从队列消费消息并推送到WebSocket客户端</summary>
|
||||
/// <param name="socket">WebSocket实例</param>
|
||||
/// <param name="host">缓存主机</param>
|
||||
/// <param name="topic">主题</param>
|
||||
/// <param name="source">取消通知源</param>
|
||||
/// <returns></returns>
|
||||
public static Task ConsumeAndPushAsync(this System.Net.WebSockets.WebSocket socket, ICache host, String topic, CancellationTokenSource source) => ConsumeAndPushAsync(socket, host.GetQueue<String>(topic), null, source);
|
||||
|
||||
/// <summary>阻塞等待WebSocket关闭</summary>
|
||||
/// <param name="socket">WebSocket实例</param>
|
||||
/// <param name="onReceive">数据处理委托</param>
|
||||
/// <param name="source">取消通知源</param>
|
||||
/// <returns></returns>
|
||||
public static async Task WaitForClose(this System.Net.WebSockets.WebSocket socket, Action<String?>? onReceive, CancellationTokenSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
var buf = new Byte[4 * 1024];
|
||||
while (!source.IsCancellationRequested && socket.State == WebSocketState.Open)
|
||||
{
|
||||
var data = await socket.ReceiveAsync(new ArraySegment<Byte>(buf), source.Token).ConfigureAwait(false);
|
||||
if (data.MessageType == System.Net.WebSockets.WebSocketMessageType.Close) break;
|
||||
if (data.MessageType == System.Net.WebSockets.WebSocketMessageType.Text)
|
||||
{
|
||||
var str = buf.ToStr(null, 0, data.Count);
|
||||
if (!str.IsNullOrEmpty())
|
||||
onReceive?.Invoke(str);
|
||||
}
|
||||
}
|
||||
|
||||
if (!source.IsCancellationRequested) source.Cancel();
|
||||
|
||||
if (socket.State == WebSocketState.Open)
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "finish", default).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (OperationCanceledException) { }
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
XTrace.WriteLine("WebSocket异常 {0}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
197
src/Admin/ThingsGateway.NewLife.X/Http/HttpRequest.cs
Normal file
197
src/Admin/ThingsGateway.NewLife.X/Http/HttpRequest.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http请求</summary>
|
||||
public class HttpRequest : HttpBase
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>Http方法</summary>
|
||||
public String? Method { get; set; }
|
||||
|
||||
/// <summary>资源路径</summary>
|
||||
public Uri? RequestUri { get; set; }
|
||||
|
||||
/// <summary>目标主机</summary>
|
||||
public String? Host { get; set; }
|
||||
|
||||
/// <summary>保持连接</summary>
|
||||
public Boolean KeepAlive { get; set; }
|
||||
|
||||
/// <summary>文件集合</summary>
|
||||
public FormFile[]? Files { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>分析第一行</summary>
|
||||
/// <param name="firstLine"></param>
|
||||
protected override Boolean OnParse(String firstLine)
|
||||
{
|
||||
if (firstLine.IsNullOrEmpty()) return false;
|
||||
|
||||
var ss = firstLine.Split(' ');
|
||||
if (ss.Length < 3) return false;
|
||||
|
||||
// 分析请求方法 GET / HTTP/1.1
|
||||
if (ss.Length >= 3 && ss[2].StartsWithIgnoreCase("HTTP/"))
|
||||
{
|
||||
Method = ss[0];
|
||||
RequestUri = new Uri(ss[1], UriKind.RelativeOrAbsolute);
|
||||
Version = ss[2].TrimStart("HTTP/");
|
||||
}
|
||||
|
||||
Host = Headers["Host"];
|
||||
KeepAlive = Headers["Connection"].EqualIgnoreCase("keep-alive");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly Byte[] NewLine = [(Byte)'\r', (Byte)'\n'];
|
||||
private static readonly Byte[] NewLine2 = [(Byte)'\r', (Byte)'\n', (Byte)'\r', (Byte)'\n'];
|
||||
/// <summary>快速分析请求头,只分析第一行</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean FastParse(IPacket pk)
|
||||
{
|
||||
var data = pk.GetSpan();
|
||||
if (!FastValidHeader(data)) return false;
|
||||
|
||||
var p = data.IndexOf(NewLine);
|
||||
if (p < 0) return false;
|
||||
|
||||
var line = data.Slice(0, p).ToStr();
|
||||
|
||||
Body = pk.Slice(p + 2, -1, true);
|
||||
|
||||
// 分析第一行
|
||||
if (!OnParse(line)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>创建头部</summary>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
protected override String BuildHeader(Int32 length)
|
||||
{
|
||||
if (Method.IsNullOrEmpty()) Method = length > 0 ? "POST" : "GET";
|
||||
|
||||
// 分解主机和资源
|
||||
var uri = RequestUri ?? new Uri("/");
|
||||
|
||||
if (Host.IsNullOrEmpty())
|
||||
{
|
||||
var host = string.Empty;
|
||||
if (uri.Scheme.EqualIgnoreCase("http", "ws"))
|
||||
{
|
||||
if (uri.Port == 80)
|
||||
host = uri.Host;
|
||||
else
|
||||
host = $"{uri.Host}:{uri.Port}";
|
||||
}
|
||||
else if (uri.Scheme.EqualIgnoreCase("https", "wss"))
|
||||
{
|
||||
if (uri.Port == 443)
|
||||
host = uri.Host;
|
||||
else
|
||||
host = $"{uri.Host}:{uri.Port}";
|
||||
}
|
||||
Host = host;
|
||||
}
|
||||
|
||||
// 构建头部
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
sb.AppendFormat("{0} {1} HTTP/{2}\r\n", Method, uri.PathAndQuery, Version);
|
||||
sb.AppendFormat("Host: {0}\r\n", Host);
|
||||
|
||||
// 内容长度
|
||||
if (length > 0) Headers["Content-Length"] = length + string.Empty;
|
||||
if (!ContentType.IsNullOrEmpty()) Headers["Content-Type"] = ContentType;
|
||||
|
||||
if (KeepAlive) Headers["Connection"] = "keep-alive";
|
||||
|
||||
foreach (var item in Headers)
|
||||
{
|
||||
if (!item.Key.EqualIgnoreCase("Host"))
|
||||
sb.AppendFormat("{0}: {1}\r\n", item.Key, item.Value);
|
||||
}
|
||||
|
||||
sb.Append("\r\n");
|
||||
|
||||
return sb.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>分析表单数据</summary>
|
||||
public virtual IDictionary<String, Object> ParseFormData()
|
||||
{
|
||||
var dic = new Dictionary<String, Object>();
|
||||
if (ContentType.IsNullOrEmpty()) return dic;
|
||||
|
||||
var boundary = ContentType.Substring("boundary=", null);
|
||||
if (boundary.IsNullOrEmpty()) return dic;
|
||||
|
||||
var body = Body;
|
||||
if (body == null || body.Length == 0) return dic;
|
||||
var data = body.GetSpan();
|
||||
|
||||
/*
|
||||
* ------WebKitFormBoundary3ZXeqQWNjAzojVR7
|
||||
* Content-Disposition: form-data; name="name"
|
||||
*
|
||||
* 大石头
|
||||
* ------WebKitFormBoundary3ZXeqQWNjAzojVR7
|
||||
* Content-Disposition: form-data; name="password"
|
||||
*
|
||||
* 565656
|
||||
* ------WebKitFormBoundary3ZXeqQWNjAzojVR7
|
||||
* Content-Disposition: form-data; name="avatar"; filename="logo.png"
|
||||
* Content-Type: image/jpeg
|
||||
*
|
||||
*/
|
||||
|
||||
// 前面加两个横杠,作为分隔符。最后一行分隔符的末尾也有两个横杠
|
||||
var bd = ("--" + boundary + "\r\n").GetBytes();
|
||||
var bd2 = ("\r\n--" + boundary).GetBytes();
|
||||
do
|
||||
{
|
||||
// 找到边界
|
||||
var (s, e) = data.IndexOf(bd, bd2);
|
||||
if (e < 0) break;
|
||||
|
||||
// 截取一段,剩下的以bd开头作为新的data。这一段的开头结尾都有\r\n
|
||||
var part = data.Slice(s, e);
|
||||
data = data[(s + e)..];
|
||||
|
||||
var pHeader = part.IndexOf(NewLine2);
|
||||
var lines = part[..pHeader].ToStr().SplitAsDictionary(":", "\r\n");
|
||||
if (lines.TryGetValue("Content-Disposition", out var str))
|
||||
{
|
||||
var ss = str.SplitAsDictionary("=", ";", true);
|
||||
var file = new FormFile
|
||||
{
|
||||
Name = ss["name"],
|
||||
FileName = ss["filename"],
|
||||
ContentDisposition = ss["[0]"],
|
||||
};
|
||||
|
||||
if (lines.TryGetValue("Content-Type", out str))
|
||||
file.ContentType = str;
|
||||
|
||||
var fileData = part[(pHeader + NewLine2.Length)..];
|
||||
file.Data = fileData.ToArray();
|
||||
|
||||
if (!file.Name.IsNullOrEmpty()) dic[file.Name] = file.FileName.IsNullOrEmpty() ? fileData.ToStr() : file;
|
||||
}
|
||||
|
||||
// 判断是否最后一个分隔符
|
||||
if (data.Slice(bd2.Length, 2).ToStr() == "--") break;
|
||||
|
||||
} while (data.Length > 0);
|
||||
|
||||
return dic;
|
||||
}
|
||||
|
||||
/// <summary>已重载。</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"{Method} {RequestUri}";
|
||||
}
|
||||
144
src/Admin/ThingsGateway.NewLife.X/Http/HttpResponse.cs
Normal file
144
src/Admin/ThingsGateway.NewLife.X/Http/HttpResponse.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Net;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Remoting;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http响应</summary>
|
||||
public class HttpResponse : HttpBase
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>状态码</summary>
|
||||
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
|
||||
|
||||
/// <summary>状态描述</summary>
|
||||
public String? StatusDescription { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>分析第一行</summary>
|
||||
/// <param name="firstLine"></param>
|
||||
protected override Boolean OnParse(String firstLine)
|
||||
{
|
||||
if (firstLine.IsNullOrEmpty()) return false;
|
||||
|
||||
// HTTP/1.1 502 Bad Gateway
|
||||
if (!firstLine.StartsWith("HTTP/")) return false;
|
||||
|
||||
var ss = firstLine.Split(' ');
|
||||
//if (ss.Length < 3) throw new Exception("非法响应头 {0}".F(firstLine));
|
||||
if (ss.Length < 3) return false;
|
||||
|
||||
Version = ss[0].TrimStart("HTTP/");
|
||||
|
||||
// 分析响应码
|
||||
var code = ss[1].ToInt();
|
||||
if (code > 0) StatusCode = (HttpStatusCode)code;
|
||||
|
||||
StatusDescription = ss.Skip(2).Join(" ");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>创建请求响应包</summary>
|
||||
/// <returns></returns>
|
||||
public override IOwnerPacket Build()
|
||||
{
|
||||
// 如果响应异常,则使用响应描述作为内容
|
||||
if (StatusCode > HttpStatusCode.OK && Body == null && !StatusDescription.IsNullOrEmpty())
|
||||
{
|
||||
Body = (ArrayPacket)StatusDescription.GetBytes();
|
||||
}
|
||||
|
||||
return base.Build();
|
||||
}
|
||||
|
||||
/// <summary>创建头部</summary>
|
||||
/// <param name="length"></param>
|
||||
/// <returns></returns>
|
||||
protected override String BuildHeader(Int32 length)
|
||||
{
|
||||
// 构建头部
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
sb.AppendFormat("HTTP/{2} {0} {1}\r\n", (Int32)StatusCode, StatusCode, Version);
|
||||
|
||||
//// cors
|
||||
//sb.AppendFormat("Access-Control-Allow-Origin:{0}\r\n", "*");
|
||||
//sb.AppendFormat("Access-Control-Allow-Methods:{0}\r\n", "POST, GET");
|
||||
//sb.AppendFormat("Access-Control-Allow-Headers:{0}\r\n", "Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With");
|
||||
|
||||
// 内容长度
|
||||
if (length > 0)
|
||||
Headers["Content-Length"] = length + string.Empty;
|
||||
else if (!Headers.ContainsKey("Transfer-Encoding") && !Headers.ContainsKey("Upgrade"))
|
||||
Headers["Content-Length"] = "0";
|
||||
|
||||
if (!ContentType.IsNullOrEmpty()) Headers["Content-Type"] = ContentType;
|
||||
|
||||
foreach (var item in Headers)
|
||||
{
|
||||
sb.AppendFormat("{0}: {1}\r\n", item.Key, item.Value);
|
||||
}
|
||||
|
||||
sb.Append("\r\n");
|
||||
|
||||
return sb.Return(true);
|
||||
}
|
||||
|
||||
/// <summary>验证,如果失败则抛出异常</summary>
|
||||
public void Valid()
|
||||
{
|
||||
if (StatusCode != HttpStatusCode.OK) throw new Exception(StatusDescription ?? (StatusCode + ""));
|
||||
}
|
||||
|
||||
/// <summary>设置结果,影响Body和ContentType</summary>
|
||||
/// <param name="result"></param>
|
||||
/// <param name="contentType"></param>
|
||||
public void SetResult(Object result, String? contentType = null)
|
||||
{
|
||||
if (result == null) return;
|
||||
|
||||
if (result is Exception ex)
|
||||
{
|
||||
if (ex is ApiException aex)
|
||||
StatusCode = (HttpStatusCode)aex.Code;
|
||||
else
|
||||
StatusCode = HttpStatusCode.InternalServerError;
|
||||
|
||||
StatusDescription = ex.Message;
|
||||
}
|
||||
else if (result is IPacket pk)
|
||||
{
|
||||
if (contentType.IsNullOrEmpty()) contentType = "application/octet-stream";
|
||||
Body = pk;
|
||||
}
|
||||
else if (result is Byte[] buffer)
|
||||
{
|
||||
if (contentType.IsNullOrEmpty()) contentType = "application/octet-stream";
|
||||
Body = (ArrayPacket)buffer;
|
||||
}
|
||||
else if (result is Stream stream)
|
||||
{
|
||||
if (contentType.IsNullOrEmpty()) contentType = "application/octet-stream";
|
||||
Body = (ArrayPacket)stream.ReadBytes(-1);
|
||||
}
|
||||
else if (result is String str)
|
||||
{
|
||||
if (contentType.IsNullOrEmpty()) contentType = "text/html";
|
||||
Body = (ArrayPacket)str.GetBytes();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (contentType.IsNullOrEmpty()) contentType = "application/json";
|
||||
Body = (ArrayPacket)result.ToJson().GetBytes();
|
||||
}
|
||||
|
||||
if (ContentType.IsNullOrEmpty()) ContentType = contentType;
|
||||
}
|
||||
|
||||
/// <summary>已重载。</summary>
|
||||
/// <returns></returns>
|
||||
public override String ToString() => $"HTTP/{Version} {(Int32)StatusCode} {StatusDescription ?? (StatusCode + "")}";
|
||||
}
|
||||
133
src/Admin/ThingsGateway.NewLife.X/Http/HttpServer.cs
Normal file
133
src/Admin/ThingsGateway.NewLife.X/Http/HttpServer.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
using ThingsGateway.NewLife.Net;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http服务器</summary>
|
||||
[DisplayName("Http服务器")]
|
||||
public class HttpServer : NetServer, IHttpHost
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>Http响应头Server名称</summary>
|
||||
public String ServerName { get; set; }
|
||||
|
||||
/// <summary>路由映射</summary>
|
||||
public IDictionary<String, IHttpHandler> Routes { get; set; } = new Dictionary<String, IHttpHandler>(StringComparer.OrdinalIgnoreCase);
|
||||
#endregion
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
public HttpServer()
|
||||
{
|
||||
Name = "Http";
|
||||
Port = 80;
|
||||
ProtocolType = NetType.Http;
|
||||
|
||||
var ver = GetType().Assembly.GetName().Version ?? new Version();
|
||||
ServerName = $"NewLife-HttpServer/{ver.Major}.{ver.Minor}";
|
||||
}
|
||||
|
||||
///// <summary>创建会话</summary>
|
||||
///// <param name="session"></param>
|
||||
///// <returns></returns>
|
||||
//protected override INetSession CreateSession(ISocketSession session) => new HttpSession();
|
||||
|
||||
/// <summary>为会话创建网络数据处理器。可作为业务处理实现,也可以作为前置协议解析</summary>
|
||||
/// <param name="session"></param>
|
||||
/// <returns></returns>
|
||||
public override INetHandler? CreateHandler(INetSession session) => new HttpSession();
|
||||
|
||||
#region 方法
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map(String path, IHttpHandler handler) => Routes[path] = handler;
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map(String path, HttpProcessDelegate handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map<TResult>(String path, Func<TResult> handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map<TModel, TResult>(String path, Func<TModel, TResult> handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map<T1, T2, TResult>(String path, Func<T1, T2, TResult> handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map<T1, T2, T3, TResult>(String path, Func<T1, T2, T3, TResult> handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射路由处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public void Map<T1, T2, T3, T4, TResult>(String path, Func<T1, T2, T3, T4, TResult> handler) => Routes[path] = new DelegateHandler { Callback = handler };
|
||||
|
||||
/// <summary>映射控制器</summary>
|
||||
/// <typeparam name="TController"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
public void MapController<TController>(String? path = null) => MapController(typeof(TController), path);
|
||||
|
||||
/// <summary>映射控制器</summary>
|
||||
/// <param name="controllerType"></param>
|
||||
/// <param name="path"></param>
|
||||
public void MapController(Type controllerType, String? path = null)
|
||||
{
|
||||
if (path.IsNullOrEmpty()) path = "/" + controllerType.Name.TrimEnd("Controller");
|
||||
|
||||
var path2 = path.EnsureEnd("/*");
|
||||
Routes[path2] = new ControllerHandler { ControllerType = controllerType };
|
||||
}
|
||||
|
||||
/// <summary>映射静态文件</summary>
|
||||
/// <param name="path">映射路径,如 /js</param>
|
||||
/// <param name="contentPath">内容目录,如 /wwwroot/js</param>
|
||||
public void MapStaticFiles(String path, String contentPath)
|
||||
{
|
||||
path = path.EnsureEnd("/");
|
||||
var path2 = path.EnsureEnd("*");
|
||||
Routes[path2] = new StaticFilesHandler { Path = path, ContentPath = contentPath };
|
||||
}
|
||||
|
||||
private readonly Dictionary<String, String> _maps = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>匹配处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
public IHttpHandler? MatchHandler(String path, HttpRequest? request)
|
||||
{
|
||||
if (Routes.TryGetValue(path, out var handler)) return handler;
|
||||
|
||||
// 判断缓存
|
||||
if (_maps.TryGetValue(path, out var p) &&
|
||||
Routes.TryGetValue(p, out handler)) return handler;
|
||||
|
||||
// 模糊匹配
|
||||
foreach (var item in Routes)
|
||||
{
|
||||
if (item.Key.Contains('*') && item.Key.IsMatch(path))
|
||||
{
|
||||
if (Routes.TryGetValue(item.Key, out handler))
|
||||
{
|
||||
// 大于3段的路径不做缓存,避免动态Url引起缓存膨胀
|
||||
if (handler is StaticFilesHandler || path.Split('/').Length <= 3) _maps[path] = item.Key;
|
||||
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
302
src/Admin/ThingsGateway.NewLife.X/Http/HttpSession.cs
Normal file
302
src/Admin/ThingsGateway.NewLife.X/Http/HttpSession.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Net;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http会话</summary>
|
||||
public class HttpSession : INetHandler
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>请求</summary>
|
||||
public HttpRequest? Request { get; set; }
|
||||
|
||||
/// <summary>Http服务主机。不一定是HttpServer</summary>
|
||||
public IHttpHost? Host { get; set; }
|
||||
|
||||
/// <summary>最大请求长度。单位字节,默认1G</summary>
|
||||
public Int32 MaxRequestLength { get; set; } = 1 * 1024 * 1024 * 1024;
|
||||
|
||||
/// <summary>忽略的头部</summary>
|
||||
public static String[] ExcludeHeaders { get; set; } = [
|
||||
"traceparent", "Authorization", "Cookie"
|
||||
];
|
||||
|
||||
/// <summary>支持作为标签数据的内容类型</summary>
|
||||
public static String[] TagTypes { get; set; } = [
|
||||
"text/plain", "text/xml", "application/json", "application/xml", "application/x-www-form-urlencoded"
|
||||
];
|
||||
|
||||
private INetSession _session = null!;
|
||||
private WebSocket? _websocket;
|
||||
private MemoryStream? _cache;
|
||||
#endregion
|
||||
|
||||
#region 收发数据
|
||||
/// <summary>建立连接时初始化会话</summary>
|
||||
/// <param name="session">会话</param>
|
||||
public void Init(INetSession session)
|
||||
{
|
||||
_session = session;
|
||||
Host ??= session.Host as IHttpHost;
|
||||
}
|
||||
|
||||
/// <summary>处理客户端发来的数据</summary>
|
||||
/// <param name="data"></param>
|
||||
public void Process(IData data)
|
||||
{
|
||||
var pk = data.Packet;
|
||||
if (pk == null || pk.Length == 0) return;
|
||||
|
||||
// WebSocket 数据
|
||||
if (_websocket != null)
|
||||
{
|
||||
_websocket.Process(pk);
|
||||
|
||||
//base.OnReceive(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解码请求头,单个连接可能有多个请求
|
||||
var req = Request;
|
||||
var request = new HttpRequest();
|
||||
if (request.Parse(pk))
|
||||
{
|
||||
req = Request = request;
|
||||
|
||||
(_session as NetSession)?.WriteLog("{0} {1}", request.Method, request.RequestUri);
|
||||
|
||||
_websocket = null;
|
||||
OnNewRequest(request, data);
|
||||
|
||||
// 后面还有数据包,克隆缓冲区
|
||||
if (req.IsCompleted)
|
||||
_cache = null;
|
||||
else
|
||||
{
|
||||
// 限制最大请求为1G
|
||||
if (req.ContentLength > MaxRequestLength)
|
||||
{
|
||||
var rs = new HttpResponse { StatusCode = HttpStatusCode.RequestEntityTooLarge };
|
||||
|
||||
// 发送响应。用完后释放数据包,还给缓冲池
|
||||
using var res = rs.Build();
|
||||
_session.Send(res);
|
||||
_session.Dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_cache = new MemoryStream(req.ContentLength);
|
||||
req.Body?.CopyTo(_cache);
|
||||
//req.Body = req.Body.Clone();
|
||||
|
||||
// 请求主体数据来自缓冲区,要还回去
|
||||
req.Body.TryDispose();
|
||||
req.Body = null;
|
||||
}
|
||||
}
|
||||
else if (req != null)
|
||||
{
|
||||
if (_cache != null)
|
||||
{
|
||||
// 链式数据包
|
||||
//req.Body.Append(pk.Clone());
|
||||
pk.CopyTo(_cache);
|
||||
|
||||
if (_cache.Length >= req.ContentLength)
|
||||
{
|
||||
_cache.Position = 0;
|
||||
req.Body = new ArrayPacket(_cache);
|
||||
_cache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (req != null)
|
||||
{
|
||||
// 改变数据
|
||||
data.Message = req;
|
||||
data.Packet = req.Body;
|
||||
}
|
||||
|
||||
// 收到全部数据后,触发请求处理
|
||||
if (req?.IsCompleted == true)
|
||||
{
|
||||
var rs = ProcessRequest(req, data);
|
||||
if (rs != null)
|
||||
{
|
||||
var server = _session.Host as HttpServer;
|
||||
if (server?.ServerName.IsNullOrEmpty() == false && !rs.Headers.ContainsKey("Server"))
|
||||
rs.Headers["Server"] = server.ServerName;
|
||||
|
||||
var closing = !req.KeepAlive && _websocket == null;
|
||||
if (closing && !rs.Headers.ContainsKey("Connection")) rs.Headers["Connection"] = "close";
|
||||
|
||||
// 发送响应。用完后释放数据包,还给缓冲池
|
||||
using var res = rs.Build();
|
||||
_session.Send(res);
|
||||
|
||||
if (closing) _session.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// 请求主体数据来自缓冲区,要还回去
|
||||
if (req != null)
|
||||
{
|
||||
req.Body.TryDispose();
|
||||
req.Body = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>收到新的Http请求,只有头部</summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="data"></param>
|
||||
protected virtual void OnNewRequest(HttpRequest request, IData data) { }
|
||||
|
||||
/// <summary>处理Http请求</summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual HttpResponse ProcessRequest(HttpRequest request, IData data)
|
||||
{
|
||||
if (request?.RequestUri == null) return new HttpResponse { StatusCode = HttpStatusCode.NotFound };
|
||||
|
||||
// 匹配路由处理器
|
||||
var path = request.RequestUri.OriginalString;
|
||||
var p = path.IndexOf('?');
|
||||
if (p > 0) path = path[..p];
|
||||
|
||||
// 埋点
|
||||
using var span = _session.Host.Tracer?.NewSpan(path);
|
||||
if (span != null)
|
||||
{
|
||||
span.Tag = $"{_session.Remote.EndPoint} {request.Method} {request.RequestUri}";
|
||||
span.Detach(request.Headers);
|
||||
span.Value = request.ContentLength;
|
||||
|
||||
if (span is DefaultSpan ds && ds.TraceFlag > 0)
|
||||
{
|
||||
var flag = false;
|
||||
if (request.BodyLength > 0 &&
|
||||
request.Body?.Length < 8 * 1024 &&
|
||||
request.ContentType.EqualIgnoreCase(TagTypes))
|
||||
{
|
||||
var body = request.Body.GetSpan();
|
||||
if (body.Length > 1024) body = body[..1024];
|
||||
span.AppendTag("\r\n<=\r\n" + body.ToStr(null));
|
||||
flag = true;
|
||||
}
|
||||
|
||||
if (span.Tag.Length < 500)
|
||||
{
|
||||
if (!flag) span.AppendTag("\r\n<=");
|
||||
var vs = request.Headers.Where(e => !e.Key.EqualIgnoreCase(ExcludeHeaders)).ToDictionary(e => e.Key, e => e.Value + "");
|
||||
span.AppendTag("\r\n" + vs.Join(Environment.NewLine, e => $"{e.Key}:{e.Value}"));
|
||||
}
|
||||
else if (!flag)
|
||||
{
|
||||
span.AppendTag("\r\n<=\r\n");
|
||||
span.AppendTag($"ContentLength: {request.ContentLength}\r\n");
|
||||
span.AppendTag($"ContentType: {request.ContentType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 路径安全检查,防止越界
|
||||
if (path.Contains("..")) return new HttpResponse { StatusCode = HttpStatusCode.Forbidden };
|
||||
|
||||
var handler = Host?.MatchHandler(path, request);
|
||||
//if (handler == null) return new HttpResponse { StatusCode = HttpStatusCode.NotFound };
|
||||
|
||||
var context = new DefaultHttpContext(_session, request, path, handler)
|
||||
{
|
||||
ServiceProvider = _session as IServiceProvider
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
PrepareRequest(context);
|
||||
|
||||
//if (span != null && context.Parameters.Count > 0) span.SetError(null, context.Parameters);
|
||||
|
||||
// 处理 WebSocket 握手
|
||||
_websocket ??= WebSocket.Handshake(context);
|
||||
|
||||
if (handler != null)
|
||||
handler.ProcessRequest(context);
|
||||
else if (_websocket == null)
|
||||
return new HttpResponse { StatusCode = HttpStatusCode.NotFound };
|
||||
|
||||
// 根据状态码识别异常
|
||||
if (span != null)
|
||||
{
|
||||
var res = context.Response;
|
||||
span.Value += res.ContentLength;
|
||||
var code = res.StatusCode;
|
||||
if (code == HttpStatusCode.BadRequest || code > HttpStatusCode.NotFound)
|
||||
span.SetError(new HttpRequestException($"Http Error {(Int32)code} {code}"), null);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
span?.SetError(ex, null);
|
||||
context.Response.SetResult(ex);
|
||||
}
|
||||
|
||||
return context.Response;
|
||||
}
|
||||
|
||||
/// <summary>准备请求参数</summary>
|
||||
/// <param name="context"></param>
|
||||
protected virtual void PrepareRequest(IHttpContext context)
|
||||
{
|
||||
var req = context.Request;
|
||||
var ps = context.Parameters;
|
||||
|
||||
//// 头部参数
|
||||
//ps.Merge(req.Headers);
|
||||
|
||||
// 地址参数
|
||||
var uri = req.RequestUri;
|
||||
if (uri == null) return;
|
||||
|
||||
var url = uri.OriginalString;
|
||||
var p = url.IndexOf('?');
|
||||
if (p > 0)
|
||||
{
|
||||
var qs = url[(p + 1)..].SplitAsDictionary("=", "&")
|
||||
.ToDictionary(e => HttpUtility.UrlDecode(e.Key), e => HttpUtility.UrlDecode(e.Value));
|
||||
ps.Merge(qs);
|
||||
}
|
||||
|
||||
// POST提交参数,支持Url编码、表单提交、Json主体
|
||||
if (req.Method == "POST" && req.BodyLength > 0 && req.Body != null)
|
||||
{
|
||||
var body = req.Body.GetSpan();
|
||||
if (req.ContentType.StartsWithIgnoreCase("application/x-www-form-urlencoded", "application/x-www-urlencoded"))
|
||||
{
|
||||
var qs = body.ToStr().SplitAsDictionary("=", "&")
|
||||
.ToDictionary(e => HttpUtility.UrlDecode(e.Key), e => HttpUtility.UrlDecode(e.Value));
|
||||
ps.Merge(qs);
|
||||
}
|
||||
else if (req.ContentType.StartsWithIgnoreCase("multipart/form-data;"))
|
||||
{
|
||||
var dic = req.ParseFormData();
|
||||
var fs = dic.Values.Where(e => e is FormFile).Cast<FormFile>().ToArray();
|
||||
if (fs.Length > 0) req.Files = fs;
|
||||
ps.Merge(dic);
|
||||
}
|
||||
else if (body[0] == (Byte)'{' && body[^1] == (Byte)'}')
|
||||
{
|
||||
var js = body.ToStr().DecodeJson();
|
||||
if (js != null) ps.Merge(js);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
52
src/Admin/ThingsGateway.NewLife.X/Http/HttpTraceHandler.cs
Normal file
52
src/Admin/ThingsGateway.NewLife.X/Http/HttpTraceHandler.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>支持APM跟踪的HttpClient处理器</summary>
|
||||
public class HttpTraceHandler : DelegatingHandler
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>APM跟踪器</summary>
|
||||
public ITracer? Tracer { get; set; }
|
||||
|
||||
/// <summary>异常过滤器。仅记录满足条件的异常,默认空记录所有异常</summary>
|
||||
public Predicate<Exception>? ExceptionFilter { get; set; }
|
||||
#endregion
|
||||
|
||||
/// <summary>实例化一个支持APM的HttpClient处理器</summary>
|
||||
/// <param name="innerHandler"></param>
|
||||
public HttpTraceHandler(HttpMessageHandler innerHandler) : base(innerHandler) { }
|
||||
|
||||
/// <summary>发送请求</summary>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var uri = request.RequestUri;
|
||||
|
||||
// 如果父级已经做了ApiHelper.Invoke埋点,这里不需要再做一次
|
||||
var parent = DefaultSpan.Current;
|
||||
if (parent != null && parent.Tag == uri + "" || request.Headers.Contains("traceparent"))
|
||||
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var span = Tracer?.NewSpan(request);
|
||||
try
|
||||
{
|
||||
// 任何层级,只要是通用库代码,await时都应该调用ConfigureAwait(false)
|
||||
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (Tracer?.Resolver is DefaultTracerResolver resolver && resolver.RequestContentAsTag)
|
||||
span?.AppendTag(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ExceptionFilter == null || ExceptionFilter(ex))
|
||||
span?.SetError(ex, null);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/Admin/ThingsGateway.NewLife.X/Http/IHttpClientFactory.cs
Normal file
11
src/Admin/ThingsGateway.NewLife.X/Http/IHttpClientFactory.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ThingsGateway.NewLife.Http
|
||||
{
|
||||
/// <summary>HttpClient工厂</summary>
|
||||
public interface IHttpClientFactory
|
||||
{
|
||||
/// <summary>创建HttpClient</summary>
|
||||
/// <param name="name"></param>
|
||||
/// <returns></returns>
|
||||
HttpClient CreateClient(String name);
|
||||
}
|
||||
}
|
||||
108
src/Admin/ThingsGateway.NewLife.X/Http/IHttpContext.cs
Normal file
108
src/Admin/ThingsGateway.NewLife.X/Http/IHttpContext.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Net;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http上下文</summary>
|
||||
public interface IHttpContext
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>请求</summary>
|
||||
HttpRequest Request { get; }
|
||||
|
||||
/// <summary>响应</summary>
|
||||
HttpResponse Response { get; }
|
||||
|
||||
/// <summary>连接会话</summary>
|
||||
INetSession? Connection { get; }
|
||||
|
||||
/// <summary>Socket连接</summary>
|
||||
ISocketRemote? Socket { get; }
|
||||
|
||||
/// <summary>WebSocket连接</summary>
|
||||
WebSocket? WebSocket { get; }
|
||||
|
||||
/// <summary>执行路径</summary>
|
||||
String Path { get; }
|
||||
|
||||
/// <summary>处理器</summary>
|
||||
IHttpHandler? Handler { get; }
|
||||
|
||||
/// <summary>服务提供者</summary>
|
||||
IServiceProvider? ServiceProvider { get; }
|
||||
|
||||
/// <summary>请求参数</summary>
|
||||
IDictionary<String, Object?> Parameters { get; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>默认Http上下文</summary>
|
||||
public class DefaultHttpContext : IHttpContext
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>请求</summary>
|
||||
public HttpRequest Request { get; set; }
|
||||
|
||||
/// <summary>响应</summary>
|
||||
public HttpResponse Response { get; set; } = new HttpResponse();
|
||||
|
||||
/// <summary>连接会话</summary>
|
||||
public INetSession? Connection { get; set; }
|
||||
|
||||
/// <summary>Socket连接</summary>
|
||||
public ISocketRemote? Socket { get; set; }
|
||||
|
||||
/// <summary>WebSocket连接</summary>
|
||||
public WebSocket? WebSocket { get; set; }
|
||||
|
||||
/// <summary>执行路径</summary>
|
||||
public String Path { get; set; }
|
||||
|
||||
/// <summary>处理器</summary>
|
||||
public IHttpHandler? Handler { get; set; }
|
||||
|
||||
/// <summary>服务提供者</summary>
|
||||
public IServiceProvider? ServiceProvider { get; set; }
|
||||
|
||||
/// <summary>请求参数</summary>
|
||||
public IDictionary<String, Object?> Parameters { get; } = new NullableDictionary<String, Object?>(StringComparer.OrdinalIgnoreCase);
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="session"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public DefaultHttpContext(INetSession session, HttpRequest request, String path, IHttpHandler? handler)
|
||||
{
|
||||
Connection = session;
|
||||
Request = request;
|
||||
Path = path;
|
||||
Handler = handler;
|
||||
|
||||
Socket = session?.Session;
|
||||
}
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="handler"></param>
|
||||
public DefaultHttpContext(ISocketRemote socket, HttpRequest request, String path, IHttpHandler? handler)
|
||||
{
|
||||
Socket = socket;
|
||||
Request = request;
|
||||
Path = path;
|
||||
Handler = handler;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 静态
|
||||
[ThreadStatic]
|
||||
private static IHttpContext? _current;
|
||||
|
||||
/// <summary>当前上下文</summary>
|
||||
public static IHttpContext? Current { get => _current; set => _current = value; }
|
||||
#endregion
|
||||
}
|
||||
29
src/Admin/ThingsGateway.NewLife.X/Http/IHttpFilter.cs
Normal file
29
src/Admin/ThingsGateway.NewLife.X/Http/IHttpFilter.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http过滤器,拦截请求前后</summary>
|
||||
public interface IHttpFilter
|
||||
{
|
||||
/// <summary>请求前</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="request">请求消息</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
Task OnRequest(HttpClient client, HttpRequestMessage request, Object? state, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>获取响应后</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="response">响应消息</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
Task OnResponse(HttpClient client, HttpResponseMessage response, Object? state, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>发生错误时</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
Task OnError(HttpClient client, Exception exception, Object? state, CancellationToken cancellationToken);
|
||||
}
|
||||
60
src/Admin/ThingsGateway.NewLife.X/Http/IHttpHandler.cs
Normal file
60
src/Admin/ThingsGateway.NewLife.X/Http/IHttpHandler.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http处理器</summary>
|
||||
public interface IHttpHandler
|
||||
{
|
||||
/// <summary>处理请求</summary>
|
||||
/// <param name="context"></param>
|
||||
void ProcessRequest(IHttpContext context);
|
||||
}
|
||||
|
||||
/// <summary>Http请求处理委托</summary>
|
||||
/// <param name="context"></param>
|
||||
public delegate void HttpProcessDelegate(IHttpContext context);
|
||||
|
||||
/// <summary>委托Http处理器</summary>
|
||||
public class DelegateHandler : IHttpHandler
|
||||
{
|
||||
/// <summary>委托</summary>
|
||||
public Delegate? Callback { get; set; }
|
||||
|
||||
/// <summary>处理请求</summary>
|
||||
/// <param name="context"></param>
|
||||
public virtual void ProcessRequest(IHttpContext context)
|
||||
{
|
||||
var handler = Callback;
|
||||
if (handler is HttpProcessDelegate httpHandler)
|
||||
{
|
||||
httpHandler(context);
|
||||
}
|
||||
else if (handler != null)
|
||||
{
|
||||
var result = OnInvoke(handler, context);
|
||||
if (result != null) context.Response.SetResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>复杂调用</summary>
|
||||
/// <param name="handler"></param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual Object? OnInvoke(Delegate handler, IHttpContext context)
|
||||
{
|
||||
var mi = handler.Method;
|
||||
var pis = mi.GetParameters();
|
||||
if (pis.Length == 0) return handler.DynamicInvoke();
|
||||
|
||||
var parameters = context.Parameters;
|
||||
|
||||
var args = new Object?[pis.Length];
|
||||
for (var i = 0; i < pis.Length; i++)
|
||||
{
|
||||
if (parameters.TryGetValue(pis[i].Name + "", out var v))
|
||||
args[i] = v.ChangeType(pis[i].ParameterType);
|
||||
}
|
||||
|
||||
return handler.DynamicInvoke(args);
|
||||
}
|
||||
}
|
||||
11
src/Admin/ThingsGateway.NewLife.X/Http/IHttpHost.cs
Normal file
11
src/Admin/ThingsGateway.NewLife.X/Http/IHttpHost.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http主机服务</summary>
|
||||
public interface IHttpHost
|
||||
{
|
||||
/// <summary>匹配处理器</summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
IHttpHandler? MatchHandler(String path, HttpRequest? request);
|
||||
}
|
||||
52
src/Admin/ThingsGateway.NewLife.X/Http/StaticFilesHandler.cs
Normal file
52
src/Admin/ThingsGateway.NewLife.X/Http/StaticFilesHandler.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using ThingsGateway.NewLife.Remoting;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>静态文件处理器</summary>
|
||||
public class StaticFilesHandler : IHttpHandler
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>映射路径</summary>
|
||||
public String Path { get; set; } = null!;
|
||||
|
||||
/// <summary>内容目录</summary>
|
||||
public String ContentPath { get; set; } = null!;
|
||||
#endregion
|
||||
|
||||
/// <summary>处理请求</summary>
|
||||
/// <param name="context"></param>
|
||||
public virtual void ProcessRequest(IHttpContext context)
|
||||
{
|
||||
if (!context.Path.StartsWithIgnoreCase(Path)) throw new ApiException(ApiCode.NotFound, $"File {context.Path} not found");
|
||||
|
||||
var file = context.Path[Path.Length..];
|
||||
file = ContentPath.CombinePath(file);
|
||||
|
||||
// 路径安全检查,防止越界
|
||||
if (!file.GetFullPath().StartsWithIgnoreCase(ContentPath.GetFullPath()))
|
||||
throw new ApiException(ApiCode.NotFound, $"File {context.Path} not found");
|
||||
|
||||
var fi = file.AsFile();
|
||||
if (!fi.Exists) throw new ApiException(ApiCode.NotFound, $"File {context.Path} not found");
|
||||
|
||||
var contentType = fi.Extension switch
|
||||
{
|
||||
".htm" => "text/html",
|
||||
".html" => "text/html",
|
||||
".txt" => "text/plain",
|
||||
".log" => "text/plain",
|
||||
".xml" => "text/xml",
|
||||
".json" => "text/json",
|
||||
".js" => "text/javascript",
|
||||
".css" => "text/css",
|
||||
".png" => "image/png",
|
||||
".jpg" => "image/jpg",
|
||||
".gif" => "image/gif",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
// 确保使用完以后关闭文件流
|
||||
using var fs = fi.OpenRead();
|
||||
context.Response.SetResult(fs, contentType);
|
||||
}
|
||||
}
|
||||
471
src/Admin/ThingsGateway.NewLife.X/Http/TinyHttpClient.cs
Normal file
471
src/Admin/ThingsGateway.NewLife.X/Http/TinyHttpClient.cs
Normal file
@@ -0,0 +1,471 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Authentication;
|
||||
using System.Web;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Net;
|
||||
using ThingsGateway.NewLife.Reflection;
|
||||
using ThingsGateway.NewLife.Remoting;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>迷你Http客户端。支持https和302跳转</summary>
|
||||
/// <remarks>
|
||||
/// 基于Tcp连接设计,用于高吞吐的HTTP通信场景,功能较少,但一切均在掌控之中。
|
||||
/// 单个实例使用单个连接,建议外部使用ObjectPool建立连接池。
|
||||
/// </remarks>
|
||||
public class TinyHttpClient : DisposeBase
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>客户端</summary>
|
||||
public TcpClient? Client { get; set; }
|
||||
|
||||
/// <summary>基础地址</summary>
|
||||
public Uri? BaseAddress { get; set; }
|
||||
|
||||
/// <summary>保持连接</summary>
|
||||
public Boolean KeepAlive { get; set; }
|
||||
|
||||
/// <summary>超时时间。默认15s</summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>缓冲区大小。接收缓冲区默认64*1024</summary>
|
||||
public Int32 BufferSize { get; set; } = 64 * 1024;
|
||||
|
||||
/// <summary>Json序列化</summary>
|
||||
public IJsonHost JsonHost { get; set; } = JsonHelper.Default;
|
||||
|
||||
/// <summary>性能追踪</summary>
|
||||
public ITracer? Tracer { get; set; } = HttpHelper.Tracer;
|
||||
|
||||
private Stream? _stream;
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>实例化</summary>
|
||||
public TinyHttpClient() { }
|
||||
|
||||
/// <summary>实例化</summary>
|
||||
/// <param name="server"></param>
|
||||
public TinyHttpClient(String server) => BaseAddress = new Uri(server);
|
||||
|
||||
/// <summary>销毁</summary>
|
||||
/// <param name="disposing"></param>
|
||||
protected override void Dispose(Boolean disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
Client.TryDispose();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 核心方法
|
||||
/// <summary>获取网络数据流</summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<Stream> GetStreamAsync(Uri? uri)
|
||||
{
|
||||
var tc = Client;
|
||||
var ns = _stream;
|
||||
|
||||
// 判断连接是否可用
|
||||
var active = false;
|
||||
try
|
||||
{
|
||||
active = ns != null && tc?.Connected == true && ns.CanWrite && ns.CanRead;
|
||||
if (active) return ns!;
|
||||
|
||||
ns = tc?.GetStream();
|
||||
active = ns != null && tc?.Connected == true && ns.CanWrite && ns.CanRead;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// 如果连接不可用,则重新建立连接
|
||||
if (!active)
|
||||
{
|
||||
if (uri == null) throw new ArgumentNullException(nameof(uri));
|
||||
|
||||
var remote = new NetUri(NetType.Tcp, uri.Host, uri.Port);
|
||||
|
||||
tc.TryDispose();
|
||||
tc = new TcpClient { ReceiveTimeout = (Int32)Timeout.TotalMilliseconds };
|
||||
await tc.ConnectAsync(remote.GetAddresses(), remote.Port).ConfigureAwait(false);
|
||||
|
||||
Client = tc;
|
||||
ns = tc.GetStream();
|
||||
|
||||
if (BaseAddress == null) BaseAddress = new Uri(uri, "/");
|
||||
|
||||
active = true;
|
||||
}
|
||||
|
||||
// 支持SSL
|
||||
if (active)
|
||||
{
|
||||
if (uri?.Scheme.EqualIgnoreCase("https") == true)
|
||||
{
|
||||
if (ns == null) throw new InvalidOperationException(nameof(NetworkStream));
|
||||
|
||||
#pragma warning disable CA5359 // 请勿禁用证书验证
|
||||
var sslStream = new SslStream(ns, false, (sender, certificate, chain, sslPolicyErrors) => true);
|
||||
#pragma warning restore CA5359 // 请勿禁用证书验证
|
||||
#pragma warning disable CA5398 // 避免硬编码的 SslProtocols 值
|
||||
await sslStream.AuthenticateAsClientAsync(uri.Host, [], SslProtocols.Tls12, false).ConfigureAwait(false);
|
||||
#pragma warning restore CA5398 // 避免硬编码的 SslProtocols 值
|
||||
ns = sslStream;
|
||||
}
|
||||
|
||||
_stream = ns;
|
||||
}
|
||||
|
||||
return ns!;
|
||||
}
|
||||
|
||||
/// <summary>异步请求</summary>
|
||||
/// <param name="uri"></param>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<IOwnerPacket> SendDataAsync(Uri? uri, IPacket? request)
|
||||
{
|
||||
var ns = await GetStreamAsync(uri).ConfigureAwait(false);
|
||||
|
||||
// 发送
|
||||
if (request != null) await request.CopyToAsync(ns).ConfigureAwait(false);
|
||||
|
||||
// 接收
|
||||
var pk = new OwnerPacket(BufferSize);
|
||||
using var source = new CancellationTokenSource(Timeout);
|
||||
|
||||
#if NETCOREAPP || NETSTANDARD2_1
|
||||
var count = await ns.ReadAsync(pk.GetMemory(), source.Token).ConfigureAwait(false);
|
||||
#else
|
||||
var count = await ns.ReadAsync(pk.Buffer, 0, pk.Length, source.Token).ConfigureAwait(false);
|
||||
#endif
|
||||
|
||||
return pk.Resize(count);
|
||||
}
|
||||
|
||||
/// <summary>异步发出请求,并接收响应</summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<HttpResponse?> SendAsync(HttpRequest request)
|
||||
{
|
||||
// 构造请求
|
||||
var uri = request.RequestUri ?? throw new ArgumentNullException(nameof(request.RequestUri));
|
||||
var req = request.Build();
|
||||
|
||||
var res = new HttpResponse();
|
||||
IPacket? rs = null;
|
||||
var retry = 5;
|
||||
while (retry-- > 0)
|
||||
{
|
||||
// 发出请求
|
||||
var rs2 = await SendDataAsync(uri, req).ConfigureAwait(false);
|
||||
if (rs2 == null || rs2.Length == 0) return null;
|
||||
|
||||
// 解析响应
|
||||
if (!res.Parse(rs2)) return res;
|
||||
rs = res.Body;
|
||||
|
||||
// 跳转
|
||||
if (res.StatusCode is HttpStatusCode.Moved or HttpStatusCode.Redirect)
|
||||
{
|
||||
if (res.Headers.TryGetValue("Location", out var location) && !location.IsNullOrEmpty())
|
||||
{
|
||||
// 再次请求
|
||||
var uri2 = new Uri(location);
|
||||
|
||||
if (uri.Host != uri2.Host || uri.Scheme != uri2.Scheme) Client.TryDispose();
|
||||
|
||||
uri = uri2;
|
||||
request.RequestUri = uri;
|
||||
|
||||
req.TryDispose();
|
||||
req = request.Build();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// 释放数据包,还给缓冲池
|
||||
req.TryDispose();
|
||||
|
||||
if (res.StatusCode != HttpStatusCode.OK) throw new Exception($"{(Int32)res.StatusCode} {res.StatusDescription}");
|
||||
|
||||
// 如果没有收完数据包
|
||||
if (rs != null && res.ContentLength > 0 && rs.Length < res.ContentLength)
|
||||
{
|
||||
// 使用内存流拼接需要多次接收的数据包,降低逻辑复杂度
|
||||
var ms = new MemoryStream(res.ContentLength);
|
||||
await rs.CopyToAsync(ms).ConfigureAwait(false);
|
||||
|
||||
var total = rs.Length;
|
||||
while (total < res.ContentLength)
|
||||
{
|
||||
var pk = await SendDataAsync(null, null).ConfigureAwait(false);
|
||||
if (pk == null || pk.Length == 0) break;
|
||||
|
||||
pk.CopyTo(ms);
|
||||
|
||||
total += pk.Length;
|
||||
}
|
||||
|
||||
// 从内存流获取缓冲区,打包为数据包返回,避免再次内存分配
|
||||
ms.Position = 0;
|
||||
rs = new ArrayPacket(ms);
|
||||
}
|
||||
|
||||
// chunk编码
|
||||
if (rs != null && res.Headers.TryGetValue("Transfer-Encoding", out var s) && s.EqualIgnoreCase("chunked"))
|
||||
{
|
||||
// 如果不足则读取一个chunk,因为有可能第一个响应包只有头部
|
||||
if (rs.Length == 0)
|
||||
{
|
||||
rs.TryDispose();
|
||||
rs = await SendDataAsync(null, null).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
res.Body = await ReadChunkAsync(rs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
if (!KeepAlive) Client.TryDispose();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>读取分片,返回链式Packet</summary>
|
||||
/// <param name="body"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<IPacket> ReadChunkAsync(IPacket body)
|
||||
{
|
||||
// 使用内存流拼接需要多次接收的数据包,降低逻辑复杂度
|
||||
var ms = new MemoryStream(BufferSize);
|
||||
|
||||
var pk = body;
|
||||
while (true)
|
||||
{
|
||||
// 分析一个片段,如果该片段数据不足,则需要多次读取
|
||||
var data = pk.GetSpan();
|
||||
if (!ParseChunk(data, out var offset, out var len)) break;
|
||||
|
||||
// 最后一个片段的长度为0
|
||||
if (len <= 0) break;
|
||||
|
||||
// chunk是否完整
|
||||
var memory = pk.GetMemory();
|
||||
if (offset + len <= memory.Length)
|
||||
{
|
||||
// 完整数据,截取需要的部分
|
||||
memory = memory.Slice(offset, len);
|
||||
ms.Write(memory);
|
||||
|
||||
// 更新pk,可能还有粘包数据。每一帧数据后面有\r\n
|
||||
var next = offset + len + 2;
|
||||
if (next < pk.Length)
|
||||
pk = pk.Slice(next, -1, true);
|
||||
else
|
||||
{
|
||||
pk.TryDispose();
|
||||
pk = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 写入片段数据,数据不足
|
||||
memory = memory[offset..];
|
||||
ms.Write(memory);
|
||||
|
||||
pk.TryDispose();
|
||||
pk = null;
|
||||
|
||||
// 如果该片段数据不足,则需要多次读取
|
||||
var remain = len - memory.Length;
|
||||
while (remain > 0)
|
||||
{
|
||||
var pk2 = await SendDataAsync(null, null).ConfigureAwait(false);
|
||||
memory = pk2.GetMemory();
|
||||
|
||||
// 结尾的间断符号(如换行或00)。这里有可能一个数据包里面同时返回多个分片
|
||||
if (remain <= memory.Length)
|
||||
{
|
||||
ms.Write(memory[..remain]);
|
||||
|
||||
// 如果还有剩余,作为下一个chunk
|
||||
if (remain + 2 < memory.Length)
|
||||
pk = pk2.Slice(remain + 2, -1, true);
|
||||
else
|
||||
pk2.TryDispose();
|
||||
|
||||
remain = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
ms.Write(memory);
|
||||
remain -= memory.Length;
|
||||
|
||||
pk2.TryDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 还有粘包数据,继续分析
|
||||
if (pk?.Length > 0) continue;
|
||||
|
||||
// 读取新的数据片段,如果不存在则跳出
|
||||
pk = await SendDataAsync(null, null).ConfigureAwait(false);
|
||||
if (pk == null || pk.Length == 0) break;
|
||||
}
|
||||
|
||||
ms.Position = 0;
|
||||
return new ArrayPacket(ms);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
private static readonly Byte[] NewLine = [(Byte)'\r', (Byte)'\n'];
|
||||
private Boolean ParseChunk(Span<Byte> data, out Int32 offset, out Int32 octets)
|
||||
{
|
||||
// chunk编码
|
||||
// 1 ba \r\n xxxx \r\n 0 \r\n\r\n
|
||||
|
||||
offset = 0;
|
||||
octets = 0;
|
||||
var p = data.IndexOf(NewLine);
|
||||
if (p <= 0) return false;
|
||||
|
||||
// 第一段长度
|
||||
#if NET8_0_OR_GREATER
|
||||
octets = Int32.Parse(data[..p], NumberStyles.HexNumber);
|
||||
#else
|
||||
var str = data[..p].ToStr();
|
||||
octets = Int32.Parse(str, NumberStyles.HexNumber);
|
||||
#endif
|
||||
|
||||
offset = p + 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 主要方法
|
||||
/// <summary>异步获取。连接池操作</summary>
|
||||
/// <param name="url">地址</param>
|
||||
/// <returns></returns>
|
||||
public async Task<String?> GetStringAsync(String url)
|
||||
{
|
||||
var request = new HttpRequest
|
||||
{
|
||||
RequestUri = new Uri(url),
|
||||
};
|
||||
|
||||
using var rs = (await SendAsync(request).ConfigureAwait(false));
|
||||
return rs?.Body?.ToStr();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 远程调用
|
||||
/// <summary>异步调用,等待返回结果</summary>
|
||||
/// <typeparam name="TResult">返回类型</typeparam>
|
||||
/// <param name="method">Get/Post</param>
|
||||
/// <param name="action">服务操作</param>
|
||||
/// <param name="args">参数</param>
|
||||
/// <returns></returns>
|
||||
public async Task<TResult?> InvokeAsync<TResult>(String method, String action, Object? args = null)
|
||||
{
|
||||
var baseAddress = BaseAddress ?? throw new ArgumentNullException(nameof(BaseAddress));
|
||||
var request = BuildRequest(baseAddress, method, action, args);
|
||||
|
||||
using var rs = await SendAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (rs == null || rs.Body == null || rs.Body.Length == 0) return default;
|
||||
|
||||
return ProcessResponse<TResult>(rs.Body);
|
||||
}
|
||||
|
||||
private HttpRequest BuildRequest(Uri baseAddress, String method, String action, Object? args)
|
||||
{
|
||||
var req = new HttpRequest
|
||||
{
|
||||
Method = method.ToUpper(),
|
||||
RequestUri = new Uri(baseAddress, action),
|
||||
KeepAlive = KeepAlive,
|
||||
};
|
||||
|
||||
if (args == null) return req;
|
||||
|
||||
var ps = args.ToDictionary();
|
||||
if (method.EqualIgnoreCase("Post"))
|
||||
req.Body = (ArrayPacket)JsonHost.Write(ps).GetBytes();
|
||||
else
|
||||
{
|
||||
var sb = Pool.StringBuilder.Get();
|
||||
sb.Append(action);
|
||||
sb.Append('?');
|
||||
|
||||
var first = true;
|
||||
foreach (var item in ps)
|
||||
{
|
||||
if (!first) sb.Append('&');
|
||||
first = false;
|
||||
|
||||
var v = item.Value is DateTime dt ? dt.ToFullString() : (item.Value + "");
|
||||
sb.AppendFormat("{0}={1}", item.Key, HttpUtility.UrlEncode(v));
|
||||
}
|
||||
|
||||
req.RequestUri = new Uri(baseAddress, sb.Return(true));
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
private TResult? ProcessResponse<TResult>(IPacket rs)
|
||||
{
|
||||
var str = rs.ToStr();
|
||||
if (typeof(TResult).IsBaseType()) return str.ChangeType<TResult>();
|
||||
|
||||
// 反序列化
|
||||
var obj = JsonHost.Parse(str);
|
||||
if (obj is TResult result) return result;
|
||||
|
||||
var dic = obj as IDictionary<String, Object?>;
|
||||
if (dic == null || !dic.TryGetValue("data", out var data)) throw new InvalidDataException("Unrecognized response data");
|
||||
|
||||
if (dic.TryGetValue("result", out var result2))
|
||||
{
|
||||
if (result2 is Boolean res && !res) throw new InvalidOperationException($"remote error: {data}");
|
||||
}
|
||||
else if (dic.TryGetValue("code", out var code))
|
||||
{
|
||||
if (code is Int32 cd && cd != 0) throw new ApiException(cd, data + "");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidDataException("Unrecognized response data");
|
||||
}
|
||||
|
||||
if (data == null) return default;
|
||||
|
||||
return JsonHost.Convert<TResult>(data);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 日志
|
||||
/// <summary>日志</summary>
|
||||
public ILog Log { get; set; } = Logger.Null;
|
||||
|
||||
/// <summary>写日志</summary>
|
||||
/// <param name="format"></param>
|
||||
/// <param name="args"></param>
|
||||
public void WriteLog(String format, params Object?[] args) => Log?.Info(format, args);
|
||||
#endregion
|
||||
}
|
||||
227
src/Admin/ThingsGateway.NewLife.X/Http/TokenHttpFilter.cs
Normal file
227
src/Admin/ThingsGateway.NewLife.X/Http/TokenHttpFilter.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
using ThingsGateway.NewLife.Log;
|
||||
using ThingsGateway.NewLife.Remoting;
|
||||
using ThingsGateway.NewLife.Security;
|
||||
using ThingsGateway.NewLife.Serialization;
|
||||
using ThingsGateway.NewLife.Web;
|
||||
|
||||
#if !NET45
|
||||
using TaskEx = System.Threading.Tasks.Task;
|
||||
#endif
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>Http令牌过滤器,请求前加上令牌,请求后拦截401/403</summary>
|
||||
public class TokenHttpFilter : IHttpFilter
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>用户</summary>
|
||||
public String? UserName { get; set; }
|
||||
|
||||
/// <summary>密钥</summary>
|
||||
public String? Password { get; set; }
|
||||
|
||||
/// <summary>客户端唯一标识。一般是IP@进程</summary>
|
||||
public String? ClientId { get; set; }
|
||||
|
||||
/// <summary>安全密钥。keyName$keyValue</summary>
|
||||
/// <remarks>
|
||||
/// 公钥,用于RSA加密用户密码,在通信链路上保护用户密码安全,可以写死在代码里面。
|
||||
/// 密钥前面可以增加keyName,形成keyName$keyValue,用于向服务端指示所使用的密钥标识,方便未来更换密钥。
|
||||
/// </remarks>
|
||||
public String? SecurityKey { get; set; }
|
||||
|
||||
/// <summary>申请令牌动作名,默认 OAuth/Token</summary>
|
||||
public String Action { get; set; } = "OAuth/Token";
|
||||
|
||||
/// <summary>令牌信息</summary>
|
||||
public TokenModel? Token { get; set; }
|
||||
|
||||
/// <summary>令牌有效期</summary>
|
||||
public DateTime Expire { get; set; }
|
||||
|
||||
private DateTime _refresh;
|
||||
|
||||
/// <summary>清空令牌的错误码。默认401和403</summary>
|
||||
public IList<Int32> ErrorCodes { get; set; } = new List<Int32> { ApiCode.Unauthorized, ApiCode.Forbidden };
|
||||
#endregion
|
||||
|
||||
/// <summary>实例化令牌过滤器</summary>
|
||||
public TokenHttpFilter() => ValidClientId();
|
||||
|
||||
private void ValidClientId()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 刚启动时可能还没有拿到本地IP
|
||||
var id = ClientId;
|
||||
if (id.IsNullOrEmpty() || id?.Length > 0 && id[0] == '@')
|
||||
ClientId = $"{NetHelper.MyIP()}@{ProcessHelper.GetProcessId()}";
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>请求前</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="request">请求消息</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task OnRequest(HttpClient client, HttpRequestMessage request, Object? state, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request.Headers.Authorization != null) return;
|
||||
|
||||
var uri = request.RequestUri;
|
||||
var path = client.BaseAddress == null ? uri?.AbsoluteUri : uri?.OriginalString;
|
||||
if (path.StartsWithIgnoreCase(Action.EnsureStart("/"))) return;
|
||||
|
||||
// 申请令牌。没有令牌,或者令牌已过期
|
||||
var now = DateTime.Now;
|
||||
var token = Token;
|
||||
if (token == null || Expire < now)
|
||||
{
|
||||
token = await SendAuth(client, cancellationToken).ConfigureAwait(false);
|
||||
if (token != null)
|
||||
{
|
||||
Token = token;
|
||||
|
||||
// 过期时间和刷新令牌的时间
|
||||
Expire = now.AddSeconds(token.ExpireIn);
|
||||
_refresh = now.AddSeconds(token.ExpireIn / 2);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新令牌。要求已有令牌,且未过期,且达到了刷新时间
|
||||
if (token != null && Expire > now && _refresh < now)
|
||||
{
|
||||
try
|
||||
{
|
||||
token = await SendRefresh(client, cancellationToken).ConfigureAwait(false);
|
||||
if (token != null)
|
||||
{
|
||||
Token = token;
|
||||
|
||||
// 过期时间和刷新令牌的时间
|
||||
Expire = now.AddSeconds(token.ExpireIn);
|
||||
_refresh = now.AddSeconds(token.ExpireIn / 2);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.WriteLine("刷新令牌异常 {0}", token?.ToJson());
|
||||
XTrace.WriteException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用令牌。要求已有令牌,且未过期
|
||||
if (token != null && Expire > now)
|
||||
{
|
||||
var type = token.TokenType;
|
||||
if (type.IsNullOrEmpty() || type.EqualIgnoreCase("Token", "JWT")) type = "Bearer";
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue(type, token.AccessToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>发起密码认证请求</summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<TokenModel?> SendAuth(HttpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
if (UserName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(UserName));
|
||||
//if (Password.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Password));
|
||||
|
||||
ValidClientId();
|
||||
|
||||
var pass = EncodePassword(UserName, Password);
|
||||
return await client.PostAsync<TokenModel>(Action, new
|
||||
{
|
||||
grant_type = "password",
|
||||
username = UserName,
|
||||
password = pass,
|
||||
clientId = ClientId,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>发起刷新令牌请求</summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<TokenModel?> SendRefresh(HttpClient client, CancellationToken cancellationToken)
|
||||
{
|
||||
ValidClientId();
|
||||
|
||||
if (Token == null) throw new ArgumentNullException(nameof(Token));
|
||||
|
||||
return await client.PostAsync<TokenModel>(Action, new
|
||||
{
|
||||
grant_type = "refresh_token",
|
||||
refresh_token = Token.RefreshToken,
|
||||
clientId = ClientId,
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>编码密码,在传输中保护安全,一般使用RSA加密</summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
protected virtual String? EncodePassword(String username, String? password)
|
||||
{
|
||||
if (password.IsNullOrEmpty()) return password;
|
||||
|
||||
var key = SecurityKey;
|
||||
if (!key.IsNullOrEmpty())
|
||||
{
|
||||
var name = string.Empty;
|
||||
var p = key.IndexOf('$');
|
||||
if (p >= 0)
|
||||
{
|
||||
name = key[..p];
|
||||
key = key[(p + 1)..];
|
||||
}
|
||||
|
||||
// RSA公钥加密
|
||||
var pass = RSAHelper.Encrypt(password.GetBytes(), key).ToBase64();
|
||||
password = $"$rsa${name}${pass}";
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
/// <summary>获取响应后</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="response">响应消息</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task OnResponse(HttpClient client, HttpResponseMessage response, Object? state, CancellationToken cancellationToken)
|
||||
{
|
||||
var code = (Int32)response.StatusCode;
|
||||
if (ErrorCodes.Contains(code))
|
||||
{
|
||||
// 马上过期
|
||||
Expire = DateTime.MinValue;
|
||||
}
|
||||
|
||||
return TaskEx.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>发生错误时</summary>
|
||||
/// <param name="client">客户端</param>
|
||||
/// <param name="exception">异常</param>
|
||||
/// <param name="state">状态数据</param>
|
||||
/// <param name="cancellationToken">取消通知</param>
|
||||
/// <returns></returns>
|
||||
public virtual Task OnError(HttpClient client, Exception exception, Object? state, CancellationToken cancellationToken)
|
||||
{
|
||||
// 识别ApiException
|
||||
if (exception is ApiException ae && ErrorCodes.Contains(ae.Code))
|
||||
{
|
||||
// 马上过期
|
||||
Expire = DateTime.MinValue;
|
||||
}
|
||||
|
||||
return TaskEx.CompletedTask;
|
||||
}
|
||||
}
|
||||
196
src/Admin/ThingsGateway.NewLife.X/Http/WebSocket.cs
Normal file
196
src/Admin/ThingsGateway.NewLife.X/Http/WebSocket.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Net;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>WebSocket消息处理</summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="message"></param>
|
||||
public delegate void WebSocketDelegate(WebSocket socket, WebSocketMessage message);
|
||||
|
||||
/// <summary>WebSocket会话管理</summary>
|
||||
public class WebSocket
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>是否还在连接</summary>
|
||||
public Boolean Connected { get; set; }
|
||||
|
||||
/// <summary>消息处理器</summary>
|
||||
public WebSocketDelegate? Handler { get; set; }
|
||||
|
||||
/// <summary>Http上下文</summary>
|
||||
public IHttpContext? Context { get; set; }
|
||||
|
||||
/// <summary>版本</summary>
|
||||
public String? Version { get; set; }
|
||||
|
||||
/// <summary>协议。如mqtt</summary>
|
||||
public String? Protocol { get; set; }
|
||||
|
||||
/// <summary>活跃时间</summary>
|
||||
public DateTime ActiveTime { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>WebSocket 握手</summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static WebSocket? Handshake(IHttpContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
if (!request.Headers.TryGetValue("Sec-WebSocket-Key", out var key) || key.IsNullOrEmpty()) return null;
|
||||
|
||||
var manager = new WebSocket();
|
||||
manager.ProcessRequest(context);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
/// <summary>处理 WebSocket 握手</summary>
|
||||
/// <param name="context"></param>
|
||||
public Boolean ProcessRequest(IHttpContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
if (!request.Headers.TryGetValue("Sec-WebSocket-Key", out var key) || key.IsNullOrEmpty()) return false;
|
||||
#if NET6_0_OR_GREATER
|
||||
var buf = SHA1.HashData((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetBytes());
|
||||
#else
|
||||
var buf = SHA1.Create().ComputeHash((key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").GetBytes());
|
||||
#endif
|
||||
key = buf.ToBase64();
|
||||
|
||||
var response = context.Response;
|
||||
response.StatusCode = HttpStatusCode.SwitchingProtocols;
|
||||
response.Headers["Upgrade"] = "websocket";
|
||||
response.Headers["Connection"] = "Upgrade";
|
||||
response.Headers["Sec-WebSocket-Accept"] = key;
|
||||
|
||||
if (context is DefaultHttpContext dhc) dhc.WebSocket = this;
|
||||
|
||||
if (!Protocol.IsNullOrEmpty())
|
||||
response.Headers["Sec-WebSocket-Protocol"] = Protocol;
|
||||
if (!Version.IsNullOrEmpty())
|
||||
response.Headers["Sec-WebSocket-Version"] = Version;
|
||||
//if (request.Headers.TryGetValue("Sec-WebSocket-Version", out var ver)) Version = ver;
|
||||
|
||||
Context = context;
|
||||
Connected = true;
|
||||
ActiveTime = DateTime.Now;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>处理WebSocket数据包,不支持超大数据帧(默认8k)</summary>
|
||||
/// <param name="pk"></param>
|
||||
public void Process(IPacket pk)
|
||||
{
|
||||
using var message = new WebSocketMessage();
|
||||
if (message.Read(pk))
|
||||
{
|
||||
ActiveTime = DateTime.Now;
|
||||
|
||||
Handler?.Invoke(this, message);
|
||||
|
||||
// 释放内存
|
||||
message.Payload?.TryDispose();
|
||||
|
||||
var session = Context?.Connection;
|
||||
var socket = Context?.Socket;
|
||||
if (session == null && socket == null) return;
|
||||
|
||||
switch (message.Type)
|
||||
{
|
||||
case WebSocketMessageType.Close:
|
||||
{
|
||||
Close(1000, "Finished");
|
||||
session.TryDispose();
|
||||
socket.TryDispose();
|
||||
Connected = false;
|
||||
}
|
||||
break;
|
||||
case WebSocketMessageType.Ping:
|
||||
{
|
||||
var msg = new WebSocketMessage
|
||||
{
|
||||
Type = WebSocketMessageType.Pong,
|
||||
Payload = (ArrayPacket)$"Pong {DateTime.UtcNow.ToFullString()}",
|
||||
};
|
||||
Send(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Send(WebSocketMessage msg)
|
||||
{
|
||||
var session = Context?.Connection;
|
||||
var socket = Context?.Socket;
|
||||
if (session == null && socket == null) throw new ObjectDisposedException(nameof(Context));
|
||||
|
||||
var data = msg.ToPacket();
|
||||
if (session != null)
|
||||
session.Send(data);
|
||||
else
|
||||
socket?.Send(data);
|
||||
data.TryDispose();
|
||||
}
|
||||
|
||||
/// <summary>发送消息</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
public void Send(IPacket data, WebSocketMessageType type)
|
||||
{
|
||||
var msg = new WebSocketMessage { Type = type, Payload = data };
|
||||
Send(msg);
|
||||
}
|
||||
|
||||
/// <summary>发送消息</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
public void Send(Byte[] data, WebSocketMessageType type)
|
||||
{
|
||||
var msg = new WebSocketMessage { Type = type, Payload = (ArrayPacket)data };
|
||||
Send(msg);
|
||||
}
|
||||
|
||||
/// <summary>发送文本消息</summary>
|
||||
/// <param name="message"></param>
|
||||
public void Send(String message) => Send(message.GetBytes(), WebSocketMessageType.Text);
|
||||
|
||||
/// <summary>向所有连接发送消息</summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="predicate"></param>
|
||||
public void SendAll(IPacket data, WebSocketMessageType type, Func<INetSession, Boolean>? predicate = null)
|
||||
{
|
||||
var session = (Context?.Connection) ?? throw new ObjectDisposedException(nameof(Context));
|
||||
var msg = new WebSocketMessage { Type = type, Payload = data };
|
||||
var data2 = msg.ToPacket();
|
||||
session.Host.SendAllAsync(data2, predicate).Wait(30_000);
|
||||
data.TryDispose();
|
||||
}
|
||||
|
||||
/// <summary>想所有连接发送文本消息</summary>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="predicate"></param>
|
||||
public void SendAll(String message, Func<INetSession, Boolean>? predicate = null) => SendAll((ArrayPacket)message.GetBytes(), WebSocketMessageType.Text, predicate);
|
||||
|
||||
/// <summary>发送关闭连接</summary>
|
||||
/// <param name="closeStatus"></param>
|
||||
/// <param name="statusDescription"></param>
|
||||
public void Close(Int32 closeStatus, String statusDescription)
|
||||
{
|
||||
var msg = new WebSocketMessage
|
||||
{
|
||||
Type = WebSocketMessageType.Close,
|
||||
CloseStatus = closeStatus,
|
||||
StatusDescription = statusDescription
|
||||
};
|
||||
Send(msg);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
51
src/Admin/ThingsGateway.NewLife.X/Http/WebSocketHandler.cs
Normal file
51
src/Admin/ThingsGateway.NewLife.X/Http/WebSocketHandler.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using ThingsGateway.NewLife.Data;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>WebSocket处理器</summary>
|
||||
public class WebSocketHandler : IHttpHandler
|
||||
{
|
||||
/// <summary>处理请求</summary>
|
||||
/// <param name="context"></param>
|
||||
public virtual void ProcessRequest(IHttpContext context)
|
||||
{
|
||||
var ws = context.WebSocket;
|
||||
if (ws != null) ws.Handler = ProcessMessage;
|
||||
|
||||
WriteLog("WebSocket连接 {0}", context.Connection?.Remote);
|
||||
}
|
||||
|
||||
/// <summary>处理消息</summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="message"></param>
|
||||
public virtual void ProcessMessage(WebSocket socket, WebSocketMessage message)
|
||||
{
|
||||
var remote = (socket.Context?.Connection?.Remote) ?? throw new ObjectDisposedException(nameof(socket.Context));
|
||||
|
||||
//var remote = socket.Context.Connection.Remote;
|
||||
var msg = message.Payload?.ToStr();
|
||||
if (msg == null) return;
|
||||
|
||||
switch (message.Type)
|
||||
{
|
||||
case WebSocketMessageType.Text:
|
||||
WriteLog("WebSocket收到[{0}] {1}", message.Type, msg);
|
||||
// 群发所有客户端
|
||||
socket.SendAll($"[{remote}]说,{msg}");
|
||||
break;
|
||||
case WebSocketMessageType.Close:
|
||||
WriteLog("WebSocket关闭[{0}] [{1}] {2}", remote, message.CloseStatus, message.StatusDescription);
|
||||
break;
|
||||
case WebSocketMessageType.Ping:
|
||||
case WebSocketMessageType.Pong:
|
||||
WriteLog("WebSocket心跳[{0}] {1}", message.Type, msg);
|
||||
break;
|
||||
default:
|
||||
WriteLog("WebSocket收到[{0}] {1}", message.Type, msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteLog(String format, params Object?[] args) => XTrace.WriteLine(format, args);
|
||||
}
|
||||
230
src/Admin/ThingsGateway.NewLife.X/Http/WebSocketMessage.cs
Normal file
230
src/Admin/ThingsGateway.NewLife.X/Http/WebSocketMessage.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Buffers;
|
||||
using ThingsGateway.NewLife.Data;
|
||||
|
||||
namespace ThingsGateway.NewLife.Http;
|
||||
|
||||
/// <summary>WebSocket消息类型</summary>
|
||||
public enum WebSocketMessageType
|
||||
{
|
||||
/// <summary>附加数据</summary>
|
||||
Data = 0,
|
||||
|
||||
/// <summary>文本数据</summary>
|
||||
Text = 1,
|
||||
|
||||
/// <summary>二进制数据</summary>
|
||||
Binary = 2,
|
||||
|
||||
/// <summary>连接关闭</summary>
|
||||
Close = 8,
|
||||
|
||||
/// <summary>心跳</summary>
|
||||
Ping = 9,
|
||||
|
||||
/// <summary>心跳响应</summary>
|
||||
Pong = 10,
|
||||
}
|
||||
|
||||
/// <summary>WebSocket消息</summary>
|
||||
public class WebSocketMessage : IDisposable
|
||||
{
|
||||
#region 属性
|
||||
/// <summary>消息是否结束</summary>
|
||||
public Boolean Fin { get; set; }
|
||||
|
||||
/// <summary>消息类型</summary>
|
||||
public WebSocketMessageType Type { get; set; }
|
||||
|
||||
/// <summary>加密数据的掩码</summary>
|
||||
public Byte[]? MaskKey { get; set; }
|
||||
|
||||
/// <summary>负载数据</summary>
|
||||
public IPacket? Payload { get; set; }
|
||||
|
||||
/// <summary>关闭状态。仅用于Close消息</summary>
|
||||
public Int32 CloseStatus { get; set; }
|
||||
|
||||
/// <summary>关闭状态描述。仅用于Close消息</summary>
|
||||
public String? StatusDescription { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 构造
|
||||
/// <summary>销毁。回收数据包到内存池</summary>
|
||||
public void Dispose() => Payload.TryDispose();
|
||||
#endregion
|
||||
|
||||
#region 方法
|
||||
/// <summary>读取消息</summary>
|
||||
/// <param name="pk"></param>
|
||||
/// <returns></returns>
|
||||
public Boolean Read(IPacket pk)
|
||||
{
|
||||
if (pk.Length < 2) return false;
|
||||
|
||||
var reader = new SpanReader(pk) { IsLittleEndian = false };
|
||||
var b = reader.ReadByte();
|
||||
|
||||
Type = (WebSocketMessageType)(b & 0x7F);
|
||||
|
||||
// 仅处理一个包
|
||||
Fin = (b & 0x80) == 0x80;
|
||||
if (!Fin) return false;
|
||||
|
||||
var b2 = reader.ReadByte();
|
||||
|
||||
var mask = (b2 & 0x80) == 0x80;
|
||||
|
||||
/*
|
||||
* 数据长度
|
||||
* len < 126 单字节表示长度
|
||||
* len = 126 后续2字节表示长度,大端
|
||||
* len = 127 后续8字节表示长度
|
||||
*/
|
||||
var len = (Int64)(b2 & 0x7F);
|
||||
if (len == 126)
|
||||
len = reader.ReadUInt16();
|
||||
else if (len == 127)
|
||||
len = reader.ReadInt64();
|
||||
|
||||
// 如果mask,剩下的就是数据,避免拷贝,提升性能
|
||||
if (!mask)
|
||||
{
|
||||
Payload = reader.ReadPacket((Int32)len);
|
||||
}
|
||||
else
|
||||
{
|
||||
var masks = new Byte[4];
|
||||
if (mask) reader.Read(masks);
|
||||
MaskKey = masks;
|
||||
|
||||
if (mask)
|
||||
{
|
||||
// 直接在数据缓冲区修改,避免拷贝
|
||||
Payload = reader.ReadPacket((Int32)len);
|
||||
var data = Payload.GetSpan();
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
data[i] = (Byte)(data[i] ^ masks[i % 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊处理关闭消息
|
||||
if (Type == WebSocketMessageType.Close && Payload != null)
|
||||
{
|
||||
var data = Payload.GetSpan();
|
||||
CloseStatus = BinaryPrimitives.ReadUInt16BigEndian(data[..2]);
|
||||
StatusDescription = data[2..].ToStr();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>把消息转为封包</summary>
|
||||
/// <returns></returns>
|
||||
public virtual IPacket ToPacket()
|
||||
{
|
||||
var body = Payload;
|
||||
var len = body == null ? 0 : body.Total;
|
||||
var masks = MaskKey;
|
||||
|
||||
// 特殊处理关闭消息
|
||||
if (Type == WebSocketMessageType.Close)
|
||||
{
|
||||
len = 2;
|
||||
if (!StatusDescription.IsNullOrEmpty()) len += Encoding.UTF8.GetByteCount(StatusDescription);
|
||||
}
|
||||
|
||||
var size = len switch
|
||||
{
|
||||
< 126 => 1 + 1,
|
||||
< 0xFFFF => 1 + 1 + 2,
|
||||
_ => 1 + 1 + 8,
|
||||
};
|
||||
if (masks != null) size += masks.Length;
|
||||
if (Type == WebSocketMessageType.Close) size += len;
|
||||
|
||||
var rs = body.ExpandHeader(size);
|
||||
var writer = new SpanWriter(rs) { IsLittleEndian = false };
|
||||
|
||||
writer.WriteByte((Byte)(0x80 | (Byte)Type));
|
||||
|
||||
/*
|
||||
* 数据长度
|
||||
* len < 126 单字节表示长度
|
||||
* len = 126 后续2字节表示长度,大端
|
||||
* len = 127 后续8字节表示长度
|
||||
*/
|
||||
|
||||
if (masks == null)
|
||||
{
|
||||
if (len < 126)
|
||||
{
|
||||
writer.WriteByte((Byte)len);
|
||||
}
|
||||
else if (len < 0xFFFF)
|
||||
{
|
||||
writer.WriteByte(126);
|
||||
writer.Write((Int16)len);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteByte(127);
|
||||
writer.Write((Int64)len);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (len < 126)
|
||||
{
|
||||
writer.WriteByte((Byte)(len | 0x80));
|
||||
}
|
||||
else if (len < 0xFFFF)
|
||||
{
|
||||
writer.WriteByte(126 | 0x80);
|
||||
writer.Write((Int16)len);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteByte(127 | 0x80);
|
||||
writer.Write((Int64)len);
|
||||
}
|
||||
|
||||
writer.Write(masks);
|
||||
|
||||
// 掩码混淆数据。直接在数据缓冲区修改,避免拷贝
|
||||
if (body != null)
|
||||
{
|
||||
var data = body.GetSpan();
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
data[i] = (Byte)(data[i] ^ masks[i % 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (body?.Length > 0)
|
||||
{
|
||||
// 注意body可能是链式数据包
|
||||
//writer.Write(body.GetSpan());
|
||||
|
||||
// 扩展得到的数据包,直接写入了头部,尾部数据不用拷贝也无需切片
|
||||
return rs;
|
||||
|
||||
//return rs.Slice(0, writer.Position).Append(body);
|
||||
}
|
||||
else if (Type == WebSocketMessageType.Close)
|
||||
{
|
||||
writer.Write((Int16)CloseStatus);
|
||||
if (!StatusDescription.IsNullOrEmpty()) writer.Write(StatusDescription, -1);
|
||||
|
||||
rs.Next = null;
|
||||
}
|
||||
|
||||
return rs.Slice(0, writer.Position, true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user