diff --git a/README.md b/README.md index dddca1b..68c5990 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ # JTNewEnergyDotNetty -JTNewEnergyDotNetty + +基于DotNetty封装的JTNEDotNetty通用消息业务处理 + +[了解JT808协议进这边](https://github.com/SmallChi/JT808) + +[了解JT809协议进这边](https://github.com/SmallChi/JT809) + +[了解JTNE协议进这边](https://github.com/SmallChi/JTNewEnergy) + +[了解JTNE协议处理tcp粘包/拆包](https://github.com/SmallChi/JTNewEnergyDotNetty/blob/master/doc/jtne_tcp.md) + +[](https://github.com/SmallChi/JTNewEnergyDotNetty/blob/master/LICENSE) + +## 新网关的优势: + +1. 跨平台 +2. 借助 .NET Core模块化的思想 +3. 单机同时一万辆车在线不是梦(真有一万辆车那都很吃香了<( ̄3 ̄)> <( ̄3 ̄)> <( ̄3 ̄)> ) +4. 简单易上手 + +## 设计模型 + + \ No newline at end of file diff --git a/doc/img/design_model.png b/doc/img/design_model.png new file mode 100644 index 0000000..46cf40c Binary files /dev/null and b/doc/img/design_model.png differ diff --git a/doc/img/tcp_test1.png b/doc/img/tcp_test1.png new file mode 100644 index 0000000..260376e Binary files /dev/null and b/doc/img/tcp_test1.png differ diff --git a/doc/img/tcp_test2.png b/doc/img/tcp_test2.png new file mode 100644 index 0000000..fc3e5b3 Binary files /dev/null and b/doc/img/tcp_test2.png differ diff --git a/doc/jtne_tcp.md b/doc/jtne_tcp.md new file mode 100644 index 0000000..5017299 --- /dev/null +++ b/doc/jtne_tcp.md @@ -0,0 +1,80 @@ +# 了解JTNE协议处理tcp粘包/拆包 + +## 使用DotNetty的LengthFieldBasedFrameDecoder定长解码器 + +参数说明: + +maxFrameLength:解码的帧的最大长度 +lengthFieldOffset:长度字段的偏差(长度属性的起始位(偏移位),包中存放有整个大数据包长度的字节,这段字节的其实位置) +lengthFieldLength:长度字段占的字节数(即存放整个大数据包长度的字节所占的长度) +lengthAdjustmen:添加到长度字段的补偿值(长度调节值,在总长被定义为包含包头长度时,修正信息长度)。 +initialBytesToStrip:从解码帧中第一次去除的字节数(跳过的字节数,根据需要我们跳过lengthFieldLength个字节,以便接收端直接接受到不含“长度属性”的内容) +failFast :为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常 + +### JTNE协议处理 + +22 JTNEPackage数据体长度 +2 JTNEPackage数据体长度占两个字节 +1 JTNEPackage校验位 + +``` netty + +channel.Pipeline.AddLast("jtneTcpDecoder", new LengthFieldBasedFrameDecoder(int.MaxValue, 22, 2, 1, 0)); +``` + +## 使用SuperSocket的FixedHeaderReceiveFilter固定头部解码器 + +``` supersocket + + public class JTNEReceiveFilter: FixedHeaderReceiveFilter<JTNERequestInfo> +{ + // 24 JTNEPackage头部固定长度 + public NEReceiveFilter() + : base(24) + { + + } + + protected override int GetBodyLengthFromHeader(byte[] header, int offset, int length) + { + // +1 JTNEPackage数据长度加上一个字节的校验位 + return header.toIntH2L(22 + offset, 2) + 1; + } + + protected override JTNERequestInfo ResolveRequestInfo(ArraySegment<byte> header, byte[] bodyBuffer, int offset, int length) + { + var reInfo = new JTNERequestInfo(); + reInfo.OriginalPackage = new byte[header.Count + length]; + Array.Copy(header.Array, 0, reInfo.OriginalPackage, 0, header.Count); + Array.Copy(bodyBuffer, offset, reInfo.OriginalPackage, header.Count, length); + return reInfo; + } +} + +``` + +## 使用DotNetty测试粘包 + +1.一条实时位置上报的测试数据 + +``` data +2323020131323334353637383900000000000000000100D001040507003A00001A0A00640063030602007B02030202010201004100370300EC00640203020042023605085800650308AE006F0C9600030102030D1B221A0A560D086502040100CB006605010031AD030012D1CB061115007B0709000832124211320607110000159D03000003E8000003E9000003EA03000007D0000007D1000007D20300000BB800000BB900000BBA0300000FA000000FA100000FA20802010002007B0037006F03006F00DE014D03000504D2004200DE0301BC022B029A0902010004010203040200040506070867 +``` + +2.启动JTNE服务端程序 + +3.使用tcp客户端工具 + +创建两个tcp客户端,如图所示: + + + +4.客户端分别发送消息 + +每个客户端分别连续发送1w个包进行测试,如图所示: + +服务端接收并解析到包的数量等于客户端所发送的数量,那么处理就算正确。 + + + +5.解决了服务端协议的解码问题对于上层业务处理来说就是搬砖的是了 \ No newline at end of file diff --git a/src/JTNE.DotNetty.Core/Codecs/JTNETcpDecoder.cs b/src/JTNE.DotNetty.Core/Codecs/JTNETcpDecoder.cs new file mode 100644 index 0000000..23fc788 --- /dev/null +++ b/src/JTNE.DotNetty.Core/Codecs/JTNETcpDecoder.cs @@ -0,0 +1,18 @@ +using DotNetty.Buffers; +using DotNetty.Codecs; +using System.Collections.Generic; +using JTNE.Protocol; +using DotNetty.Transport.Channels; + +namespace JTNE.DotNetty.Core.Codecs +{ + public class JTNETcpDecoder : ByteToMessageDecoder + { + protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output) + { + byte[] buffer = new byte[input.Capacity]; + input.ReadBytes(buffer, 0, input.Capacity); + output.Add(buffer); + } + } +} diff --git a/src/JTNE.DotNetty.Core/Configurations/JTNEClientConfiguration.cs b/src/JTNE.DotNetty.Core/Configurations/JTNEClientConfiguration.cs new file mode 100644 index 0000000..2bae3d9 --- /dev/null +++ b/src/JTNE.DotNetty.Core/Configurations/JTNEClientConfiguration.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace JTNE.DotNetty.Core.Configurations +{ + public class JTNEClientConfiguration + { + public string Host { get; set; } + + public int Port { get; set; } + + private EndPoint endPoint; + + public EndPoint EndPoint + { + get + { + if (endPoint == null) + { + if (IPAddress.TryParse(Host, out IPAddress ip)) + { + endPoint = new IPEndPoint(ip, Port); + } + } + return endPoint; + } + } + } +} diff --git a/src/JTNE.DotNetty.Core/Configurations/JTNEConfiguration.cs b/src/JTNE.DotNetty.Core/Configurations/JTNEConfiguration.cs new file mode 100644 index 0000000..83b76e6 --- /dev/null +++ b/src/JTNE.DotNetty.Core/Configurations/JTNEConfiguration.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JTNE.DotNetty.Core.Configurations +{ + public class JTNEConfiguration + { + public int TcpPort { get; set; } = 909; + + public int UdpPort { get; set; } = 919; + + public int QuietPeriodSeconds { get; set; } = 1; + + public TimeSpan QuietPeriodTimeSpan => TimeSpan.FromSeconds(QuietPeriodSeconds); + + public int ShutdownTimeoutSeconds { get; set; } = 3; + + public TimeSpan ShutdownTimeoutTimeSpan => TimeSpan.FromSeconds(ShutdownTimeoutSeconds); + + public int SoBacklog { get; set; } = 8192; + + public int EventLoopCount { get; set; } = Environment.ProcessorCount; + + public int ReaderIdleTimeSeconds { get; set; } = 3600; + + public int WriterIdleTimeSeconds { get; set; } = 3600; + + public int AllIdleTimeSeconds { get; set; } = 3600; + + public int UdpSlidingExpirationTimeSeconds { get; set; } = 5*60; + + /// <summary> + /// WebApi服务 + /// 默认929端口 + /// </summary> + public int WebApiPort { get; set; } = 929; + + /// <summary> + /// 转发远程地址 (可选项)知道转发的地址有利于提升性能 + /// </summary> + public List<JTNEClientConfiguration> ForwardingRemoteAddress { get; set; } + } +} diff --git a/src/JTNE.DotNetty.Core/JTNE.DotNetty.Core.csproj b/src/JTNE.DotNetty.Core/JTNE.DotNetty.Core.csproj new file mode 100644 index 0000000..318b36e --- /dev/null +++ b/src/JTNE.DotNetty.Core/JTNE.DotNetty.Core.csproj @@ -0,0 +1,32 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>7.1</LangVersion> + <Copyright>Copyright 2018.</Copyright> + <Authors>SmallChi</Authors> + <PackageId>JTNE.DotNetty.Core</PackageId> + <Product>JTNE.DotNetty.Core</Product> + <Description>基于DotNetty实现的JTNEDotNetty的核心库</Description> + <PackageReleaseNotes>基于DotNetty实现的JTNEDotNetty的核心库</PackageReleaseNotes> + <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> + <RepositoryUrl>https://github.com/SmallChi/JTNewEnergyDotNetty</RepositoryUrl> + <PackageProjectUrl>https://github.com/SmallChi/JTNewEnergyDotNetty</PackageProjectUrl> + <PackageLicenseUrl>https://github.com/SmallChi/JTNewEnergyDotNetty/blob/master/LICENSE</PackageLicenseUrl> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <Version>1.0.0</Version> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="DotNetty.Handlers" Version="0.6.0" /> + <PackageReference Include="DotNetty.Transport.Libuv" Version="0.6.0" /> + <PackageReference Include="DotNetty.Codecs" Version="0.6.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.2.0" /> + <PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\JTNE.Protocol\src\JTNE.Protocol\JTNE.Protocol.csproj" /> + </ItemGroup> +</Project> diff --git a/src/JTNE.DotNetty.Core/JTNECoreDotnettyExtensions.cs b/src/JTNE.DotNetty.Core/JTNECoreDotnettyExtensions.cs new file mode 100644 index 0000000..b3af47f --- /dev/null +++ b/src/JTNE.DotNetty.Core/JTNECoreDotnettyExtensions.cs @@ -0,0 +1,42 @@ +using JTNE.DotNetty.Core.Codecs; +using JTNE.DotNetty.Core.Configurations; +using JTNE.DotNetty.Core.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Newtonsoft.Json; +using System; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("JTNE.DotNetty.Core.Test")] +[assembly: InternalsVisibleTo("JTNE.DotNetty.Tcp.Test")] +[assembly: InternalsVisibleTo("JTNE.DotNetty.Udp.Test")] +[assembly: InternalsVisibleTo("JTNE.DotNetty.WebApi.Test")] +namespace JTNE.DotNetty.Core +{ + public static class JTNECoreDotnettyExtensions + { + static JTNECoreDotnettyExtensions() + { + JsonConvert.DefaultSettings = new Func<JsonSerializerSettings>(() => + { + Newtonsoft.Json.JsonSerializerSettings settings = new Newtonsoft.Json.JsonSerializerSettings(); + //日期类型默认格式化处理 + settings.DateFormatHandling = Newtonsoft.Json.DateFormatHandling.MicrosoftDateFormat; + settings.DateFormatString = "yyyy-MM-dd HH:mm:ss"; + //空值处理 + settings.NullValueHandling = NullValueHandling.Ignore; + settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + return settings; + }); + } + + public static IServiceCollection AddJTNECore(this IServiceCollection serviceDescriptors, IConfiguration configuration, Newtonsoft.Json.JsonSerializerSettings settings=null) + { + serviceDescriptors.Configure<JTNEClientConfiguration>(configuration.GetSection("JTNEConfiguration")); + serviceDescriptors.AddSingleton<JTNETcpAtomicCounterService>(); + serviceDescriptors.AddScoped<JTNETcpDecoder>(); + return serviceDescriptors; + } + } +} \ No newline at end of file diff --git a/src/JTNE.DotNetty.Core/Metadata/JTNEAtomicCounter.cs b/src/JTNE.DotNetty.Core/Metadata/JTNEAtomicCounter.cs new file mode 100644 index 0000000..c5140de --- /dev/null +++ b/src/JTNE.DotNetty.Core/Metadata/JTNEAtomicCounter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; + +namespace JTNE.DotNetty.Core.Metadata +{ + /// <summary> + /// + /// <see cref="Grpc.Core.Internal"/> + /// </summary> + public class JTNEAtomicCounter + { + long counter = 0; + + public JTNEAtomicCounter(long initialCount = 0) + { + this.counter = initialCount; + } + + public void Reset() + { + Interlocked.Exchange(ref counter, 0); + } + + public long Increment() + { + return Interlocked.Increment(ref counter); + } + + public long Add(long len) + { + return Interlocked.Add(ref counter,len); + } + + public long Decrement() + { + return Interlocked.Decrement(ref counter); + } + + public long Count + { + get + { + return Interlocked.Read(ref counter); + } + } + } +} diff --git a/src/JTNE.DotNetty.Core/Services/JTNETcpAtomicCounterService.cs b/src/JTNE.DotNetty.Core/Services/JTNETcpAtomicCounterService.cs new file mode 100644 index 0000000..28bfe27 --- /dev/null +++ b/src/JTNE.DotNetty.Core/Services/JTNETcpAtomicCounterService.cs @@ -0,0 +1,51 @@ +using JTNE.DotNetty.Core.Metadata; + +namespace JTNE.DotNetty.Core.Services +{ + /// <summary> + /// Tcp计数包服务 + /// </summary> + public class JTNETcpAtomicCounterService + { + private readonly JTNEAtomicCounter MsgSuccessCounter = new JTNEAtomicCounter(); + + private readonly JTNEAtomicCounter MsgFailCounter = new JTNEAtomicCounter(); + + public JTNETcpAtomicCounterService() + { + + } + + public void Reset() + { + MsgSuccessCounter.Reset(); + MsgFailCounter.Reset(); + } + + public long MsgSuccessIncrement() + { + return MsgSuccessCounter.Increment(); + } + + public long MsgSuccessCount + { + get + { + return MsgSuccessCounter.Count; + } + } + + public long MsgFailIncrement() + { + return MsgFailCounter.Increment(); + } + + public long MsgFailCount + { + get + { + return MsgFailCounter.Count; + } + } + } +} diff --git a/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpConnectionHandler.cs b/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpConnectionHandler.cs new file mode 100644 index 0000000..b79db56 --- /dev/null +++ b/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpConnectionHandler.cs @@ -0,0 +1,93 @@ +using DotNetty.Handlers.Timeout; +using DotNetty.Transport.Channels; +using JTNE.DotNetty.Core; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace JTNE.DotNetty.Tcp.Handlers +{ + /// <summary> + /// JTNE服务通道处理程序 + /// </summary> + internal class JTNETcpConnectionHandler : ChannelHandlerAdapter + { + private readonly ILogger<JTNETcpConnectionHandler> logger; + + + public JTNETcpConnectionHandler( + ILoggerFactory loggerFactory) + { + logger = loggerFactory.CreateLogger<JTNETcpConnectionHandler>(); + } + + /// <summary> + /// 通道激活 + /// </summary> + /// <param name="context"></param> + public override void ChannelActive(IChannelHandlerContext context) + { + string channelId = context.Channel.Id.AsShortText(); + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($"<<<{ channelId } Successful client connection to server."); + base.ChannelActive(context); + } + + /// <summary> + /// 设备主动断开 + /// </summary> + /// <param name="context"></param> + public override void ChannelInactive(IChannelHandlerContext context) + { + string channelId = context.Channel.Id.AsShortText(); + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($">>>{ channelId } The client disconnects from the server."); + + base.ChannelInactive(context); + } + + /// <summary> + /// 服务器主动断开 + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + public override Task CloseAsync(IChannelHandlerContext context) + { + string channelId = context.Channel.Id.AsShortText(); + if (logger.IsEnabled(LogLevel.Debug)) + logger.LogDebug($"<<<{ channelId } The server disconnects from the client."); + + return base.CloseAsync(context); + } + + public override void ChannelReadComplete(IChannelHandlerContext context)=> context.Flush(); + + /// <summary> + /// 超时策略 + /// </summary> + /// <param name="context"></param> + /// <param name="evt"></param> + public override void UserEventTriggered(IChannelHandlerContext context, object evt) + { + IdleStateEvent idleStateEvent = evt as IdleStateEvent; + if (idleStateEvent != null) + { + if(idleStateEvent.State== IdleState.ReaderIdle) + { + string channelId = context.Channel.Id.AsShortText(); + logger.LogInformation($"{idleStateEvent.State.ToString()}>>>{channelId}"); + context.CloseAsync(); + } + } + base.UserEventTriggered(context, evt); + } + + public override void ExceptionCaught(IChannelHandlerContext context, Exception exception) + { + string channelId = context.Channel.Id.AsShortText(); + logger.LogError(exception,$"{channelId} {exception.Message}" ); + context.CloseAsync(); + } + } +} + diff --git a/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpServerHandler.cs b/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpServerHandler.cs new file mode 100644 index 0000000..12f0768 --- /dev/null +++ b/src/JTNE.DotNetty.Tcp/Handlers/JTNETcpServerHandler.cs @@ -0,0 +1,61 @@ +using DotNetty.Buffers; +using DotNetty.Transport.Channels; +using System; +using Microsoft.Extensions.Logging; +using JTNE.Protocol; +using JTNE.DotNetty.Core.Services; +using Newtonsoft.Json; + +namespace JTNE.DotNetty.Tcp.Handlers +{ + /// <summary> + /// JTNE服务端处理程序 + /// </summary> + internal class JTNETcpServerHandler : SimpleChannelInboundHandler<byte[]> + { + private readonly ILogger<JTNETcpServerHandler> logger; + private readonly JTNETcpAtomicCounterService jTNETcpAtomicCounterService; + + public JTNETcpServerHandler( + ILoggerFactory loggerFactory, + JTNETcpAtomicCounterService jTNETcpAtomicCounterService + ) + { + logger = loggerFactory.CreateLogger<JTNETcpServerHandler>(); + this.jTNETcpAtomicCounterService = jTNETcpAtomicCounterService; + } + + + protected override void ChannelRead0(IChannelHandlerContext ctx, byte[] msg) + { + try + { + JTNEPackage jtNePackage = JTNESerializer.Deserialize(msg); + jTNETcpAtomicCounterService.MsgSuccessIncrement(); + if (logger.IsEnabled(LogLevel.Debug)) + { + //logger.LogDebug(JsonConvert.SerializeObject(jtNePackage)); + logger.LogDebug("accept package success count<<<" + jTNETcpAtomicCounterService.MsgSuccessCount.ToString()); + } + } + catch (JTNE.Protocol.Exceptions.JTNEException ex) + { + jTNETcpAtomicCounterService.MsgFailIncrement(); + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("accept package fail count<<<" + jTNETcpAtomicCounterService.MsgFailCount.ToString()); + logger.LogError(ex, "accept msg<<<" + ByteBufferUtil.HexDump(msg)); + } + } + catch (Exception ex) + { + jTNETcpAtomicCounterService.MsgFailIncrement(); + if (logger.IsEnabled(LogLevel.Error)) + { + logger.LogError("accept package fail count<<<" + jTNETcpAtomicCounterService.MsgFailCount.ToString()); + logger.LogError(ex, "accept msg<<<" + ByteBufferUtil.HexDump(msg)); + } + } + } + } +} diff --git a/src/JTNE.DotNetty.Tcp/JTNE.DotNetty.Tcp.csproj b/src/JTNE.DotNetty.Tcp/JTNE.DotNetty.Tcp.csproj new file mode 100644 index 0000000..895dd79 --- /dev/null +++ b/src/JTNE.DotNetty.Tcp/JTNE.DotNetty.Tcp.csproj @@ -0,0 +1,32 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>7.1</LangVersion> + <Copyright>Copyright 2018.</Copyright> + <Authors>SmallChi</Authors> + <PackageId>JTNE.DotNetty.Tcp</PackageId> + <Product>JTNE.DotNetty.Tcp</Product> + <Description>基于DotNetty实现的JTNEDotNetty的Tcp服务</Description> + <PackageReleaseNotes>基于DotNetty实现的JTNEDotNetty的Tcp服务</PackageReleaseNotes> + <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> + <RepositoryUrl>https://github.com/SmallChi/JTNewEnergyDotNetty</RepositoryUrl> + <PackageProjectUrl>https://github.com/SmallChi/JTNewEnergyDotNetty</PackageProjectUrl> + <PackageLicenseUrl>https://github.com/SmallChi/JTNewEnergyDotNetty/blob/master/LICENSE</PackageLicenseUrl> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <Version>1.0.0</Version> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="DotNetty.Buffers" Version="0.6.0" /> + <PackageReference Include="DotNetty.Codecs" Version="0.6.0" /> + <PackageReference Include="DotNetty.Handlers" Version="0.6.0" /> + <PackageReference Include="DotNetty.Transport.Libuv" Version="0.6.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="2.2.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\JTNE.DotNetty.Core\JTNE.DotNetty.Core.csproj" /> + </ItemGroup> +</Project> diff --git a/src/JTNE.DotNetty.Tcp/JTNETcpDotnettyExtensions.cs b/src/JTNE.DotNetty.Tcp/JTNETcpDotnettyExtensions.cs new file mode 100644 index 0000000..ddc6421 --- /dev/null +++ b/src/JTNE.DotNetty.Tcp/JTNETcpDotnettyExtensions.cs @@ -0,0 +1,26 @@ +using JTNE.DotNetty.Core.Codecs; +using JTNE.DotNetty.Tcp.Handlers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Newtonsoft.Json; +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("JTNE.DotNetty.Tcp.Test")] + +namespace JTNE.DotNetty.Tcp +{ + public static class JTNETcpDotnettyExtensions + { + public static IServiceCollection AddJTNETcpHost(this IServiceCollection serviceDescriptors) + { + serviceDescriptors.TryAddScoped<JTNETcpConnectionHandler>(); + serviceDescriptors.TryAddScoped<JTNETcpDecoder>(); + serviceDescriptors.TryAddScoped<JTNETcpServerHandler>(); + serviceDescriptors.AddHostedService<JTNETcpServerHost>(); + return serviceDescriptors; + } + } +} \ No newline at end of file diff --git a/src/JTNE.DotNetty.Tcp/JTNETcpServerHost.cs b/src/JTNE.DotNetty.Tcp/JTNETcpServerHost.cs new file mode 100644 index 0000000..9a6e18b --- /dev/null +++ b/src/JTNE.DotNetty.Tcp/JTNETcpServerHost.cs @@ -0,0 +1,103 @@ +using DotNetty.Buffers; +using DotNetty.Codecs; +using DotNetty.Handlers.Timeout; +using DotNetty.Transport.Bootstrapping; +using DotNetty.Transport.Channels; +using DotNetty.Transport.Libuv; +using JTNE.DotNetty.Core.Codecs; +using JTNE.DotNetty.Core.Configurations; +using JTNE.DotNetty.Tcp.Handlers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace JTNE.DotNetty.Tcp +{ + /// <summary> + /// JTNE Tcp网关服务 + /// </summary> + internal class JTNETcpServerHost : IHostedService + { + private readonly IServiceProvider serviceProvider; + private readonly ILogger<JTNETcpServerHost> logger; + private DispatcherEventLoopGroup bossGroup; + private WorkerEventLoopGroup workerGroup; + private IChannel bootstrapChannel; + private IByteBufferAllocator serverBufferAllocator; + private readonly JTNEConfiguration configuration; + + public JTNETcpServerHost( + IServiceProvider provider, + ILoggerFactory loggerFactory, + IOptions<JTNEConfiguration> jTNEConfigurationAccessor) + { + serviceProvider = provider; + configuration = jTNEConfigurationAccessor.Value; + logger = loggerFactory.CreateLogger<JTNETcpServerHost>(); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + bossGroup = new DispatcherEventLoopGroup(); + workerGroup = new WorkerEventLoopGroup(bossGroup, configuration.EventLoopCount); + serverBufferAllocator = new PooledByteBufferAllocator(); + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.Group(bossGroup, workerGroup); + bootstrap.Channel<TcpServerChannel>(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) + || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + bootstrap + .Option(ChannelOption.SoReuseport, true) + .ChildOption(ChannelOption.SoReuseaddr, true); + } + bootstrap + .Option(ChannelOption.SoBacklog, configuration.SoBacklog) + .ChildOption(ChannelOption.Allocator, serverBufferAllocator) + .ChildHandler(new ActionChannelInitializer<IChannel>(channel => + { + IChannelPipeline pipeline = channel.Pipeline; + using (var scope = serviceProvider.CreateScope()) + { + channel.Pipeline.AddLast("systemIdleState", new IdleStateHandler( + configuration.ReaderIdleTimeSeconds, + configuration.WriterIdleTimeSeconds, + configuration.AllIdleTimeSeconds)); + channel.Pipeline.AddLast("jtneTcpConnection", scope.ServiceProvider.GetRequiredService<JTNETcpConnectionHandler>()); + //LengthFieldBasedFrameDecoder 定长解码器 + //参数说明: + //maxFrameLength:解码的帧的最大长度 + //lengthFieldOffset:长度字段的偏差(长度属性的起始位(偏移位),包中存放有整个大数据包长度的字节,这段字节的其实位置) + //lengthFieldLength:长度字段占的字节数(即存放整个大数据包长度的字节所占的长度) + //lengthAdjustmen:添加到长度字段的补偿值(长度调节值,在总长被定义为包含包头长度时,修正信息长度)。 + //initialBytesToStrip:从解码帧中第一次去除的字节数(跳过的字节数,根据需要我们跳过lengthFieldLength个字节,以便接收端直接接受到不含“长度属性”的内容) + //failFast :为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常 + //22 JTNEPackage数据体长度 + //2 JTNEPackage数据体长度占两个字节 + //1 JTNEPackage校验位 + channel.Pipeline.AddLast("jtneTcpDecoder", new LengthFieldBasedFrameDecoder(int.MaxValue, 22, 2, 1, 0)); + channel.Pipeline.AddLast("jtneTcpBuffer", scope.ServiceProvider.GetRequiredService<JTNETcpDecoder>()); + channel.Pipeline.AddLast("jtneTcpServerHandler", scope.ServiceProvider.GetRequiredService<JTNETcpServerHandler>()); + } + })); + logger.LogInformation($"JTNE TCP Server start at {IPAddress.Any}:{configuration.TcpPort}."); + return bootstrap.BindAsync(configuration.TcpPort) + .ContinueWith(i => bootstrapChannel = i.Result); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await bootstrapChannel.CloseAsync(); + var quietPeriod = configuration.QuietPeriodTimeSpan; + var shutdownTimeout = configuration.ShutdownTimeoutTimeSpan; + await workerGroup.ShutdownGracefullyAsync(quietPeriod, shutdownTimeout); + await bossGroup.ShutdownGracefullyAsync(quietPeriod, shutdownTimeout); + } + } +} diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/JTNE.DotNetty.Hosting.csproj b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/JTNE.DotNetty.Hosting.csproj new file mode 100644 index 0000000..ab95e4f --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/JTNE.DotNetty.Hosting.csproj @@ -0,0 +1,23 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>netcoreapp2.2</TargetFramework> + <LangVersion>7.1</LangVersion> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" /> + <ProjectReference Include="..\..\JTNE.DotNetty.Tcp\JTNE.DotNetty.Tcp.csproj" /> + </ItemGroup> + + <ItemGroup> + <None Update="appsettings.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + +</Project> diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/Program.cs b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/Program.cs new file mode 100644 index 0000000..77b7a51 --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/Program.cs @@ -0,0 +1,39 @@ +using JTNE.DotNetty.Core; +using JTNE.DotNetty.Tcp; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; + +namespace JTNE.DotNetty.Hosting +{ + class Program + { + static async Task Main(string[] args) + { + //232301FE313233343536373839000000000000000001002A130116173738000131323334353637383939383736353433323130300304313233343435363739383730FD + //2323020131323334353637383900000000000000000100D001040507003A00001A0A00640063030602007B02030202010201004100370300EC00640203020042023605085800650308AE006F0C9600030102030D1B221A0A560D086502040100CB006605010031AD030012D1CB061115007B0709000832124211320607110000159D03000003E8000003E9000003EA03000007D0000007D1000007D20300000BB800000BB900000BBA0300000FA000000FA100000FA20802010002007B0037006F03006F00DE014D03000504D2004200DE0301BC022B029A0902010004010203040200040506070867 + var serverHostBuilder = new HostBuilder() + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(AppDomain.CurrentDomain.BaseDirectory); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + }) + .ConfigureLogging((context, logging) => + { + logging.AddConsole(); + logging.SetMinimumLevel(LogLevel.Trace); + }) + .ConfigureServices((hostContext, services) => + { + services.AddSingleton<ILoggerFactory, LoggerFactory>(); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + services.AddJTNECore(hostContext.Configuration) + .AddJTNETcpHost(); + }); + await serverHostBuilder.RunConsoleAsync(); + } + } +} diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/appsettings.json b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/appsettings.json new file mode 100644 index 0000000..0eda78b --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Hosting/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Trace" + } + }, + "Console": { + "LogLevel": { + "Default": "Trace" + } + } + }, + "JTNEConfiguration": { + + } +} diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/JTNE.DotNetty.Tcp.Test.csproj b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/JTNE.DotNetty.Tcp.Test.csproj new file mode 100644 index 0000000..cb007e2 --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/JTNE.DotNetty.Tcp.Test.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp2.2</TargetFramework> + + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> + <PackageReference Include="xunit" Version="2.4.0" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\JTNE.DotNetty.Tcp\JTNE.DotNetty.Tcp.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/TestBase.cs b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/TestBase.cs new file mode 100644 index 0000000..c758da6 --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/TestBase.cs @@ -0,0 +1,36 @@ +using JTNE.DotNetty.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JTNE.DotNetty.Tcp.Test +{ + public class TestBase + { + public static IServiceProvider ServiceProvider; + + static TestBase() + { + var serverHostBuilder = new HostBuilder() + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(AppDomain.CurrentDomain.BaseDirectory); + config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); + }) + .ConfigureServices((hostContext, services) => + { + services.AddSingleton<ILoggerFactory, LoggerFactory>(); + services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + services.AddJTNECore(hostContext.Configuration) + .AddJTNETcpHost(); + }); + var build = serverHostBuilder.Build(); + build.Start(); + ServiceProvider = build.Services; + } + } +} diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/UnitTest1.cs b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/UnitTest1.cs new file mode 100644 index 0000000..ff9a066 --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/UnitTest1.cs @@ -0,0 +1,14 @@ +using System; +using Xunit; + +namespace JTNE.DotNetty.Tcp.Test +{ + public class UnitTest1 + { + [Fact] + public void Test1() + { + + } + } +} diff --git a/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/appsettings.json b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/appsettings.json new file mode 100644 index 0000000..0eda78b --- /dev/null +++ b/src/JTNE.DotNetty.Tests/JTNE.DotNetty.Tcp.Test/appsettings.json @@ -0,0 +1,18 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Trace" + } + }, + "Console": { + "LogLevel": { + "Default": "Trace" + } + } + }, + "JTNEConfiguration": { + + } +} diff --git a/src/JTNE.DotNetty.sln b/src/JTNE.DotNetty.sln new file mode 100644 index 0000000..050cefb --- /dev/null +++ b/src/JTNE.DotNetty.sln @@ -0,0 +1,58 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.329 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{62F3C213-D06A-4AD2-9806-6380E3412D36}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JTNE.Protocol", "JTNE.Protocol\src\JTNE.Protocol\JTNE.Protocol.csproj", "{B807AE69-CF8E-4AF7-8446-3C55DBA36915}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JTNE.DotNetty.Core", "JTNE.DotNetty.Core\JTNE.DotNetty.Core.csproj", "{045A407E-8D6B-4EDE-87F9-AD3E2531E978}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JTNE.DotNetty.Tcp", "JTNE.DotNetty.Tcp\JTNE.DotNetty.Tcp.csproj", "{2AD8F740-5468-4BF7-B37F-D98F061B9939}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8241ED6E-2BF2-4378-AC97-466A1CA6032C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JTNE.DotNetty.Tcp.Test", "JTNE.DotNetty.Tests\JTNE.DotNetty.Tcp.Test\JTNE.DotNetty.Tcp.Test.csproj", "{298C5650-2DAC-40E2-9309-12A8651E0347}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JTNE.DotNetty.Hosting", "JTNE.DotNetty.Tests\JTNE.DotNetty.Hosting\JTNE.DotNetty.Hosting.csproj", "{5BA5FC40-8A66-439F-B5C6-209E754488D9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B807AE69-CF8E-4AF7-8446-3C55DBA36915}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B807AE69-CF8E-4AF7-8446-3C55DBA36915}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B807AE69-CF8E-4AF7-8446-3C55DBA36915}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B807AE69-CF8E-4AF7-8446-3C55DBA36915}.Release|Any CPU.Build.0 = Release|Any CPU + {045A407E-8D6B-4EDE-87F9-AD3E2531E978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {045A407E-8D6B-4EDE-87F9-AD3E2531E978}.Debug|Any CPU.Build.0 = Debug|Any CPU + {045A407E-8D6B-4EDE-87F9-AD3E2531E978}.Release|Any CPU.ActiveCfg = Release|Any CPU + {045A407E-8D6B-4EDE-87F9-AD3E2531E978}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD8F740-5468-4BF7-B37F-D98F061B9939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD8F740-5468-4BF7-B37F-D98F061B9939}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD8F740-5468-4BF7-B37F-D98F061B9939}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD8F740-5468-4BF7-B37F-D98F061B9939}.Release|Any CPU.Build.0 = Release|Any CPU + {298C5650-2DAC-40E2-9309-12A8651E0347}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {298C5650-2DAC-40E2-9309-12A8651E0347}.Debug|Any CPU.Build.0 = Debug|Any CPU + {298C5650-2DAC-40E2-9309-12A8651E0347}.Release|Any CPU.ActiveCfg = Release|Any CPU + {298C5650-2DAC-40E2-9309-12A8651E0347}.Release|Any CPU.Build.0 = Release|Any CPU + {5BA5FC40-8A66-439F-B5C6-209E754488D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BA5FC40-8A66-439F-B5C6-209E754488D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BA5FC40-8A66-439F-B5C6-209E754488D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BA5FC40-8A66-439F-B5C6-209E754488D9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B807AE69-CF8E-4AF7-8446-3C55DBA36915} = {62F3C213-D06A-4AD2-9806-6380E3412D36} + {298C5650-2DAC-40E2-9309-12A8651E0347} = {8241ED6E-2BF2-4378-AC97-466A1CA6032C} + {5BA5FC40-8A66-439F-B5C6-209E754488D9} = {8241ED6E-2BF2-4378-AC97-466A1CA6032C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA9480D2-2C5C-4E82-9D6B-4D98C420E6A1} + EndGlobalSection +EndGlobal diff --git a/src/JTNE.Protocol b/src/JTNE.Protocol index 342b349..b799819 160000 --- a/src/JTNE.Protocol +++ b/src/JTNE.Protocol @@ -1 +1 @@ -Subproject commit 342b3492c22132c8be8984a531ac96594e01247c +Subproject commit b799819135d795f979eac484683e294f8a94a780