记录
写本系列文章的目的主要是记录调研的成果。
一、Netty是什么
Netty 是一款用于创建高性能网络应用程序的高级框架。
Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端
二、DotNetty是什么
DotNetty是微软的Azure团队仿造Netty编写的网络应用程序框架。
三、BIO和NIO
3.1 以上图就是代表BIO的流程
- 建立连接要阻塞线程,读取数据要阻塞线程
- 如果要管理多个客户端,就需要为每个客户端建立不同的线程
- 会有大量的线程在休眠状态,等待接收数据,资源浪费
- 每个线程都要占用系统资源
- 线程的切换很耗费系统资源
3.2 以上图就是NIO的流程
Netty 的 IO 聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起,减少了线程数量导致的资源占用,减少了线程切换导致的资源消耗。
四、使用步骤
1.引入库
版本:.netframework4.6 ,DotNetty 0.6.0
引入:DotNetty.Buffers, DotNetty.Codecs , Dotnetty.Codecs.Mqtt ,DotNetty.Common , DotNetty.Handlers ,DotNetty.Transport
2.服务端
服务端代码如下:
DiscardServerHandler dis= new DiscardServerHandler()
// 主工作线程组,设置为1个线程
var bossGroup = new MultithreadEventLoopGroup(1);
// 工作线程组,默认为内核数*2的线程数
var workerGroup = new MultithreadEventLoopGroup();//声明一个服务端Bootstrap,每个Netty服务端程序,都由ServerBootstrap控制,
//通过链式的方式组装需要的参数
var bootstrap = new ServerBootstrap();
bootstrap
.Group(bossGroup, workerGroup) // 设置主和工作线程组
.Channel<TcpServerSocketChannel>() // 设置通道模式为TcpSocket
.Option(ChannelOption.SoBacklog, 100) // 设置网络IO参数等,这里可以设置很多参数,当然你对网络调优和参数设置非常了解的话,你可以设置,或者就用默认参数吧
.Option(ChannelOption.SoKeepalive, true)//保持连接
//.Option(ChannelOption.RcvbufAllocator, new FixedRecvByteBufAllocator(4000))
.ChildHandler(new ActionChannelInitializer<ISocketChannel>(channel =>
{
byte[] messageBytes = Encoding.UTF8.GetBytes("^$end");
IByteBuffer initialMessage=Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
//工作线程连接器 是设置了一个管道,服务端主线程所有接收到的信息都会通过这个管道一层层往下传输
//同时所有出栈的消息 也要这个管道的所有处理器进行一步步处理
IChannelPipeline pipeline = channel.Pipeline;
pipeline.AddLast(new DelimiterBasedFrameDecoder(40000, initialMessage));//格式规则:每条数据必须以后缀名以^$end格式发送
pipeline.AddLast(new CommonServerDecoder());
pipeline.AddLast(new CommonServerEncoder());
pipeline.AddLast(new IdleStateHandler(0, 0, 180));
//业务handler ,这里是实际处理业务的Handler
pipeline.AddLast(dis);
}));
// bootstrap绑定到指定端口的行为 就是服务端启动服务,同样的Serverbootstrap可以bind到多个端口
IChannel boundChannel = await bootstrap.BindAsync(3399);
3.客户端
客户端示例如下:
private MultithreadEventLoopGroup group;
private IChannel clientChannel;
group = new MultithreadEventLoopGroup();
var bootstrap = new Bootstrap();
bootstrap
.Group(group)
.Channel<TcpSocketChannel>()
.Option(ChannelOption.TcpNodelay, true)
//.Option(ChannelOption.RcvbufAllocator, new FixedRecvByteBufAllocator(40000))
.Handler(new ActionChannelInitializer<ISocketChannel>(c =>
{
byte[] messageBytes = Encoding.UTF8.GetBytes("^$end");
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
NettyClientHand nettyClientHand = new NettyClientHand();
IChannelPipeline pipeline = c.Pipeline;
pipeline.AddLast(new DelimiterBasedFrameDecoder(40000, initialMessage));
pipeline.AddLast(new IdleStateHandler(0, 0, 180));
pipeline.AddLast(new ClientDecoder());
pipeline.AddLast(new ClientEncoder());
pipeline.AddLast(nettyClientHand);
}));
clientChannel = await bootstrap.ConnectAsync(new IPEndPoint(ip, port));
4.粘包和拆包
产生粘包和拆包问题的主要原因是在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
5.粘包和拆包解决方案
1.FixedLengthFrameDecoder :对于使用固定长度的粘包和拆包场景
2.LineBasedFrameDecoder与DelimiterBasedFrameDecoder: 这里LineBasedFrameDecoder的作用主要是通过换行符,即\n或者\r\n对数据进行处理;而DelimiterBasedFrameDecoder的作用则是通过用户指定的分隔符对数据进行粘包和拆包处理
3.LengthFieldBasedFrameDecoder与LengthFieldPrepender:LengthFieldBasedFrameDecoder与LengthFieldPrepender需要配合起来使用,其实本质上来讲,这两者一个是解码,一个是编码的关系。它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度。LengthFieldBasedFrameDecoder会按照参数指定的包长度偏移量数据对接收到的数据进行解码,从而得到目标消息体数据;而LengthFieldPrepender则会在响应的数据前面添加指定的字节数据,这个字节数据中保存了当前消息体的整体字节数据长度。
4.LengthFieldBasedFrameDecoder和LengthFieldPrepender:自定义编码器
以上使用第二种分隔符对数据进行粘包 和拆包处理。如果客户端发送消息后缀没有用分隔符,服务端就不会接收。
6.编码和解码
1.服务端CommonServerDecoder解码类代码如下
class CommonServerDecoder: ByteToMessageDecoder
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}
2.服务端CommonServerEncoder编码类代码如下
class CommonServerEncoder : MessageToByteEncoder<string>
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}
3.客户端ClientDecoder编码类代码如下
class ClientDecoder : ByteToMessageDecoder
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}
4.客户端ClientEncoder编码类代码如下
class ClientEncoder : MessageToByteEncoder<string>
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}
7. 业务代码
1.业务类(服务端DiscardServerHandler,客户端NettyClientHand)继承ChannelHandlerAdapter类
2.IsSharable 一个handler可以被多个通道共享
3.HandlerAdded和HandlerRemoved 上线和下线事件
4.ChannelActive和ChannelInactive 通道活动和不活动事件
5.ChannelRead 接收事件
public override void ChannelRead(IChannelHandlerContext context, object message)
{
//if (message is IByteBuffer buffer)
//{
// Console.WriteLine($"IByteBuffer方式,从服务端接收:{buffer.ToString(Encoding.UTF8)}");
//}
if (message is byte[] o)
{
Console.WriteLine($"byte方式,从服务端接收:{Encoding.UTF8.GetString(o)}");
}
}
6.UserEventTriggered心跳事件,在IdleStateHandler配置读和写是180秒。在180秒没有接收或者发送消息就会触发心跳机制。注意物理拔插网线不会走掉线事件需要用心跳机制处理。
public override void UserEventTriggered(IChannelHandlerContext context, object evt)
{
if (evt is IdleStateEvent)
{
IdleStateEvent e = evt as IdleStateEvent;
if (e.State == IdleState.AllIdle)
{
//读或者写
}
else if (e.State == IdleState.ReaderIdle)
{
//读
}
else if (e.State == IdleState.WriterIdle) {
//写
}
}
}
7.WriteAndFlushAsync发送
服务端发送
channelcontext = dis.channel;
channelcontext?.WriteAndFlushAsync(string.Format("{0}^$end",121345));
客户端发送
clientChannel.WriteAndFlushAsync(string.Format("{0}^$end",message));
8.CloseAsync关闭
服务端关闭
boundChannel.CloseAsync();
//释放工作组线程
await Task.WhenAll(bossGroup.ShutdownGracefullyAsync(),
workerGroup.ShutdownGracefullyAsync());
客户端关闭
await clientChannel.CloseAsync();
await group.ShutdownGracefullyAsync();
总结
以上就是DotNetty的搭建.