.NET Core3.1 Dotnetty实战第三章
一、概要
本章主要内容就是讲解如何在dotnetty的框架中进行网络通讯以及编解码对象、数据包分包拆包的相关知识点。
后续会专门开一篇避坑的文章,主要会描述在使用dotnetty的框架时会遇到的哪些问题帮助各位开发者在使用过程当中出现问题,会不断的收集问题不断的更新肯定是附带问题的解决方案的。
希望有兴趣的小伙伴可以提供相关的“坑”一起更新一起解决困难,让dotnetty的框架更容易使用。
二、简介
1.什么是编码、解码
2.解码器Decoder讲解
3.编码器Encoder讲解
4.编解码器类Codec讲解
5.网络传输TCP粘包拆包
6.核心模块缓冲ByteBuffer
7.实战环节
8.Dotnetty所用到的设计模式
三、详细内容
1.什么是编码、解码
前面说的:高性能RPC框架的3个要素:IO模型、数据协议、线程模型
最开始接触的编码码:序列化/反序列化(就是编解码)、url编码、base64编解码
业界里面也有其他编码框架: google的 protobuf(PB)、Facebook的Trift、json等
DotNetty里面的编解码:
解码器:负责处理入站 InboundHandler”数据
编码器:负责出站 OutboundHandler” 数据
DotNetty里面提供默认的编解码器,也支持自定义编解码器
Encoder:编码器
Decoder:解码器
Codec:编解码器
2.解码器Decoder讲解
Decoder对应的就是ChannelInboundHandler,主要就是字节数组转换为消息对象
主要是两个方法 decode decodeLast
抽象解码器
- ByteToMessageDecoder用于将字节转为消息,需要检查缓冲区是否有足够的字节
- ReplayingDecoder继承ByteToMessageDecoder,不需要检查缓冲区是否有足够的字节,但是ReplayingDecoder速度略满于ByteToMessageDecoder,不是所有的ByteBuf都支持。
- 选择:项目复杂性高则使用ReplayingDecoder,否则使用 ByteToMessageDecoder
- MessageToMessageDecoder用于从一种消息解码为另外一种消息(例如POJO到POJO)
解码器具体的实现,用的比较多的是(更多是为了解决TCP底层的粘包和拆包问题)
- DelimiterBasedFrameDecoder: 指定消息分隔符的解码器
- LineBasedFrameDecoder: 以换行符为结束标志的解码器
- FixedLengthFrameDecoder:固定长度解码器
- LengthFieldBasedFrameDecoder:message = header+body, 基于长度解码的通用解码器
- StringDecoder:文本解码器,将接收到的对象转化为字符串,一般会与上面的进行配合,然后在后面添加业务handle
3.编码器Encoder讲解
Encoder对应的就是ChannelOutboundHandler,消息对象转换为字节数组
Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对等的
- MessageToByteEncoder消息转为字节数组,调用write方法,会先判断当前编码器是否支持需要发送的消息类
型,如果不支持,则透传;
- MessageToMessageEncoder用于从一种消息编码为另外一种消息
4.编解码器类Codec讲解
组合解码器和编码器,以此提供对于字节和消息都相同的操作
优点:成对出现,编解码都是在一个类里面完成
缺点:耦合在一起,拓展性不佳
Codec:组合编解码
1)ByteToMessageCodec
2)MessageToMessageCodec
decoder:解码
1)ByteToMessageDecoder
2)MessageToMessageDecoder
encoder:编码
1)ByteToMessageEncoder
2)MessageToMessageEncoder
5.网络传输TCP粘包拆包
- TCP拆包: 一个完整的包可能会被TCP拆分为多个包进行发送
- TCP粘包: 把多个小的包封装成一个大的数据包发送, client发送的若干数据包 Server接收时粘成一包发送方和接收方都可能出现这个原因
- 发送方的原因:TCP默认会使用Nagle算法
- 接收方的原因: TCP接收到数据放置缓存中,应用程序从缓存中读取
- UDP: 是没有粘包和拆包的问题,有边界协议
应用层解决半包读写的办法:
1.设置定长消息 (10字符)
123456789 123456789 123456789 123456789
2.设置消息的边界 ( | | 切割)
123456789||123456789||123456789||
3.使用带消息头的协议,消息头存储消息开始标识及消息的长度信息
DelimiterBasedFrameDecoder: 指定消息分隔符的解码器 LineBasedFrameDecoder: 以换行符为结束标志的解码器
FixedLengthFrameDecoder:固定长度解码器 LengthFieldBasedFrameDecoder:message = header+body, 基于长
度解码的通用解码器。
使用解码器LineBasedFrameDecoder解决半包读写
1)LineBaseFrameDecoder 以换行符为结束标志的解码器 ,构造函数里面的数字表示最长遍历的帧数
2)StringDecoder解码器将对象转成字符串。
- 自定义分隔符解决TCP读写
MaxLength:表示一行最大的长度,如果超过这个长度依然没有检测自定义分隔符,将会抛出
TooLongFrameException
FailFast:如果为true,则超出maxLength后立即抛出TooLongFrameException,不进行继续解码.如果为
False,则等到完整的消息被解码后,再抛出TooLongFrameException异常
StripDelimiter:解码后的消息是否去除掉分隔符
Delimiters:分隔符,ByteBuf类型
- 自定义长度半包读写器LengthFieldBasedFrameDecoder
MaxFrameLength 数据包的最大长度
LengthFieldOffset 长度字段的偏移位,长度字段开始的地方,意思是跳过指定长度个字节之后的才是消息体字段
LengthFieldLength 长度字段占的字节数, 帧数据长度的字段本身的长度
LengthAdjustment
一般 Header + Body,添加到长度字段的补偿值,如果为负数,开发人员认为这个 Header的长度字段是整个消息
包的长度,则Netty应该减去对应的数字
InitialBytesToStrip 从解码帧中第一次去除的字节数, 获取完一个完整的数据包之后,忽略前面的指定位数的长度字节,
应用解码器拿到的就是不带长度域的数据包
6.核心模块缓冲ByteBuffer
ByteBuf:传递字节数据的容器
ByteBuf的创建方法
1)ByteBufAllocator
池化( PooledByteBufAllocator提高性能并且最大程度减少内存碎片
非池化UnpooledByteBufAllocator: 每次返回新的实例
2)Unpooled: 提供静态方法创建未池化的ByteBuf,可以创建堆内存和直接内存缓冲区
ByteBuf使用模式
堆缓存区HEAP BUFFER:
优点:存储在的堆空间中,可以快速的分配和释放
缺点:每次使用前会拷贝到直接缓存区(也叫堆外内存)
直接缓存区DIRECR BUFFER:
优点:存储在堆外内存上,堆外分配的直接内存,不会占用堆空间
缺点:内存的分配和释放,比在堆缓冲区更复杂
复合缓冲区COMPOSITE BUFFER:
可以创建多个不同的ByteBuf,然后放在一起,但是只是一个视图
选择:大量IO数据读写,用“直接缓存区”; 业务消息编解码用“堆缓存区”
四、实战环节
实战环节使用的编解码器是
- ByteToMessageDecoder
- MessageToByteEncoder
数据包结构定义(https://www.cnblogs.com/justzhuzhu/p/12129328.html)之前已经在其他文章里写过了,所以这里直接开始编解码的操作。
解码
- /// <summary>
- /// Decoder Packet
- /// </summary>
- public class DecoderHandler : ByteToMessageDecoder
- {
- private readonly PacketParser packetParser = new PacketParser();
- protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
- {
- var outputBufferList = new List<byte[]>();
- var resultByte = new byte[input.ReadableBytes];
- input.ReadBytes(resultByte);
- packetParser.TryParsing(ref resultByte,ref outputBufferList);
- output.AddRange(outputBufferList);
- input.Clear();
- }
- }
编码
- /// <summary>
- /// Encoder Packet
- /// </summary>
- public class EncoderHandler : MessageToByteEncoder<RpcResponse<Gimind.Infrastructure.Common.Packet.IMessage>>
- {
- protected override void Encode(IChannelHandlerContext context, RpcResponse<Gimind.Infrastructure.Common.Packet.IMessage> message, IByteBuffer output)
- {
- var arry = SerializePacket(message.Length, message.Header, message.Body);
- output.WriteBytes(arry);
- }
- public byte[] SerializeHeader(RespHeader header)
- {
- header.Checkbit = Header.Checkbit;
- var headerArry = new byte[];
- try
- {
- BytesWriter.Write(header.Checkbit, ref headerArry, ); //
- BytesWriter.Write(header.RequestId, ref headerArry, ); //
- BytesWriter.Write(header.Code, ref headerArry, ); //
- BytesWriter.Write(header.IsEncrypt, ref headerArry, );//
- BytesWriter.Write(header.CommandId, ref headerArry, );//
- BytesWriter.Write(header.Ext1, ref headerArry, ); //
- }
- catch (Exception ex)
- {
- NLogger.Error("SerializeHeader",ex.Message,ex);
- }
- return headerArry;
- }
- private byte[] SerializePacket(int length,RespHeader header,IMessage body)
- {
- try
- {
- var Header = SerializeHeader(header);
- length += Header.Length;
- byte[] Body = null;
- var protobytes = SerializerUtilitys.Serialize(body);
- if (protobytes != null)
- {
- Body = protobytes;
- length += Body.Length;
- }
- var packageArry = new byte[ + length];
- BytesWriter.Write(length, ref packageArry, );
- BytesWriter.Write(Header, ref packageArry, );
- if (body != null)
- {
- BytesWriter.Write(Body, ref packageArry, + RespHeader.Length);
- }
- return packageArry;
- }
- catch (Exception ex)
- {
- NLogger.Error("SerializeHeader", ex.Message, ex);
- }
- return null;
- }
- }
分包拆包逻辑
- public class PacketParser
- {
- private readonly List<byte[]> _bufferList = new List<byte[]>();
- public void TryParsing(ref byte[] inBytes, ref List<byte[]> outBytes)
- {
- try
- {
- _bufferList.Add(inBytes);
- var tempBuffer = new byte[_bufferList.Sum(item => item.Length)];
- var size = ;
- foreach (var item in _bufferList)
- {
- item.CopyTo(tempBuffer, size);
- size += item.Length;
- }
- if (tempBuffer.Length < ) return;
- var packetLen = BytesReader.ReadInt32(ref tempBuffer, );
- if (tempBuffer.Length < ( + packetLen))
- {
- return;
- }
- if (tempBuffer.Length == ( + packetLen))
- {
- _bufferList.Clear();
- outBytes.Add(tempBuffer);
- }
- if (tempBuffer.Length > ( + packetLen))
- {
- var left = new byte[ + packetLen];
- Array.Copy(tempBuffer, , left, , left.Length);
- var right = new byte[tempBuffer.Length - left.Length];
- Array.Copy(tempBuffer, left.Length, right, , right.Length);
- _bufferList.Clear();
- outBytes.Add(left);
- TryParsing(ref right, ref outBytes);
- }
- }
- catch (Exception ex)
- {
- NLogger.Error("PacketParser Error", ex.Message, ex);
- }
- }
- }
Protobuffer
- using ProtoBuf;
- using System;
- using System.IO;
- namespace Protobuffer.Utilities
- {
- public class SerializerUtilitys
- {
- /// <summary>
- /// 序列化
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="serializeObj">序列化对象</param>
- /// <returns></returns>
- public static byte[] Serialize<T>(T serializeObj)
- {
- try
- {
- using (var stream = new MemoryStream())
- {
- ProtoBuf.Serializer.Serialize<T>(stream, serializeObj);
- var result = new byte[stream.Length];
- stream.Position = 0L;
- stream.Read(result, , result.Length);
- return result;
- }
- }
- catch (Exception e)
- {
- return null;
- }
- }
- /// <summary>
- /// 反序列化
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="bytes">二进制对象数组</param>
- /// <returns></returns>
- public static T DeSerialize<T>(byte[] bytes)
- {
- try
- {
- using (var stream = new MemoryStream())
- {
- stream.Write(bytes, , bytes.Length);
- stream.Position = 0L;
- return ProtoBuf.Serializer.Deserialize<T>(stream);
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(e);
- return default(T);
- }
- }
- }
- }
如果你看到这里,可能会有意外的收获。在DotNetty里面的应用里用到如下设计模式:
- Builder构造器模式:ServerBootstap
- 责任链设计模式:pipeline的事件传播
- 工厂模式: 创建Channel
- 适配器模式:HandlerAdapter
- 推荐书籍:《大话设计模式》《Head First设计模式》《CLR VIA C#》《大型网站技术架构 核心原理与案例分析》《.net 框架设计》《.net 性能优化》《编写高性能的.net代码》
希望大家多多支持。不胜感激。
- E-Mail:zhuzhen723723@outlook.com
- QQ: 580749909(个人群)
- Blog: https://www.cnblogs.com/justzhuzhu/
- Git: https://github.com/JusterZhu
- 微信公众号
.NET Core3.1 Dotnetty实战第三章的更多相关文章
- Rxjava2实战--第三章 创建操作符
Rxjava2实战--第三章 创建操作符 Rxjava的创建操作符 操作符 用途 just() 将一个或多个对象转换成发射这个或者这些对象的一个Observable from() 将一个Iterabl ...
- .NET Core3.1 Dotnetty实战第二章
一.概要 在上一篇文章讲到Dotnetty的基本认识,本文这次会讲解dotnetty非常核心的模块是属于比较硬核的干货了,然后继续往下讲解如何根据自己的需求或者自己的喜好去配置Dotnetty而不是生 ...
- .NET Core3.1 Dotnetty实战第一章
一.概要 本系列文章主要讲述由微软Azure团队研发的.net的版本的netty,Dotnetty.所有的开发都将基于.net core 3.1版本进行开发. Dotnetty是什么,原本Netty是 ...
- .NET ORM框架HiSql实战-第三章-使用自定义编号生成【申请编号】
一.引言 上一篇.NET ORM框架HiSql实战-第二章-使用Hisql实现菜单管理(增删改查) 中菜单编号采用的是雪花ID,生成的编号无法自定义.比如本系统的一个申请业务,需要按前缀+日期+流水号 ...
- 学习笔记-[Maven实战]-第三章:Maven使用入门(1)
说明:[Maven实战]一书还介绍了怎么样手工创建Maven工程,学习这本书是为了能尽快在工作中使用,就忽略了手工建工程的部分 如果想了解这部分的内容,可以自己看看书 开始: 1.新建一个maven工 ...
- .NET Core IdentityServer4实战 第三章-使用EntityFramework Core进行持久化配置
内容:本文带大家使用IdentityServer4进行使用使用EntityFramework Core进行配置和操作数据 作者:zara(张子浩) 欢迎分享,但需在文章鲜明处留下原文地址. 前两章内容 ...
- DirectX12 3D 游戏开发与实战第三章内容
变换 学习目标 理解如何使用矩阵表示线性变换和仿射变换 学习对几何体进行缩放.旋转和平移的坐标变换 根据矩阵之间的乘法运算性质,将多个变换矩阵合并为一个单独的净变换矩阵 找寻不同坐标系之间的坐标转换方 ...
- .NET Core3.1 Dotnetty实战系列视频
一.概要 由于在.net的环境当中对dotnetty相关资料相对较少,所以这里主要分享一个dotnetty使用教程希望能帮助到正在使用这套框架的开发者们.虽然这套框架已微软官方已经不在维护,但是这套框 ...
- 学习笔记-[Maven实战]-第三章:Maven使用入门(3)
这里说一下在建测试工程里遇到的问题 1.第一次建工程,junit依赖始终没有成功,最后删除现在工程,新建了一个工程就好了 2.使用junit4的问题.工程默认的依赖是junit3.8.1,我改成了4. ...
随机推荐
- PHP wordwrap() 函数
实例 按照指定长度对字符串进行折行处理: <?php高佣联盟 www.cgewang.com$str = "An example of a long word is: Supercal ...
- 京东架构师:日均 5 亿查询量的ElasticSearch架构如何设计?
作者:张sir 来源:京东技术(id:jingdongjishu) 1. 背景 京东到家订单中心系统业务中,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调用量都非常大,造成了订单数 ...
- springMVC 与 html RESTful 解决方案
若前端为html 而非jsp 且 拦截如下 <servlet-mapping> <servlet-name>springMVC</servlet-name> &l ...
- Unity 笔记
摄像机 Main Camera 跟随主角移动,不看 UI 剧情摄像机 当进入剧情时,可以关闭 main camera,启用剧情摄像机,不看 UI UI 摄像机 看 UI Unity编辑器常用的sett ...
- Bystack跨链技术源码解读
Bystack是由比原链团队提出的一主多侧链架构的BaaS平台.其将区块链应用分为三层架构:底层账本层,侧链扩展层,业务适配层.底层账本层为Layer1,即为目前比较成熟的采用POW共识的Bytom公 ...
- XCTF-WEB-新手练习区(5-8)笔记
5:disabled_button X老师今天上课讲了前端知识,然后给了大家一个不能按的按钮,小宁惊奇地发现这个按钮按不下去,到底怎么才能按下去呢? 删除disable="" 字段 ...
- fetch封装
import {fetch as fetchPro} from "whatwg-fetch" import qs from "qs" const get=(ur ...
- C++游戏(大型PC端枪战游戏)服务器架构
实习期间深入参与到某大型pc端枪战游戏的后端开发中,此游戏由著名游戏工作室编写,代码可读性极高,自由时间对游戏后台代码进行了深入研究,在满足自身工作需要的同时对游戏后台的架构也有了理解,记录在此,以便 ...
- C#LeetCode刷题之#257-二叉树的所有路径(Binary Tree Paths)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4082 访问. 给定一个二叉树,返回所有从根节点到叶子节点的路径. ...
- C#LeetCode刷题-树
树篇 # 题名 刷题 通过率 难度 94 二叉树的中序遍历 61.6% 中等 95 不同的二叉搜索树 II 43.4% 中等 96 不同的二叉搜索树 51.6% 中等 98 验证二叉搜索树 ...