一、概要

本章主要内容就是讲解如何在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里面的应用里用到如下设计模式:

  1. Builder构造器模式:ServerBootstap
  2. 责任链设计模式:pipeline的事件传播
  3. 工厂模式: 创建Channel
  4. 适配器模式:HandlerAdapter
  5. 推荐书籍:《大话设计模式》《Head First设计模式》《CLR VIA C#》《大型网站技术架构 核心原理与案例分析》《.net 框架设计》《.net 性能优化》《编写高性能的.net代码》

希望大家多多支持。不胜感激。

.NET Core3.1 Dotnetty实战第三章的更多相关文章

  1. Rxjava2实战--第三章 创建操作符

    Rxjava2实战--第三章 创建操作符 Rxjava的创建操作符 操作符 用途 just() 将一个或多个对象转换成发射这个或者这些对象的一个Observable from() 将一个Iterabl ...

  2. .NET Core3.1 Dotnetty实战第二章

    一.概要 在上一篇文章讲到Dotnetty的基本认识,本文这次会讲解dotnetty非常核心的模块是属于比较硬核的干货了,然后继续往下讲解如何根据自己的需求或者自己的喜好去配置Dotnetty而不是生 ...

  3. .NET Core3.1 Dotnetty实战第一章

    一.概要 本系列文章主要讲述由微软Azure团队研发的.net的版本的netty,Dotnetty.所有的开发都将基于.net core 3.1版本进行开发. Dotnetty是什么,原本Netty是 ...

  4. .NET ORM框架HiSql实战-第三章-使用自定义编号生成【申请编号】

    一.引言 上一篇.NET ORM框架HiSql实战-第二章-使用Hisql实现菜单管理(增删改查) 中菜单编号采用的是雪花ID,生成的编号无法自定义.比如本系统的一个申请业务,需要按前缀+日期+流水号 ...

  5. 学习笔记-[Maven实战]-第三章:Maven使用入门(1)

    说明:[Maven实战]一书还介绍了怎么样手工创建Maven工程,学习这本书是为了能尽快在工作中使用,就忽略了手工建工程的部分 如果想了解这部分的内容,可以自己看看书 开始: 1.新建一个maven工 ...

  6. .NET Core IdentityServer4实战 第三章-使用EntityFramework Core进行持久化配置

    内容:本文带大家使用IdentityServer4进行使用使用EntityFramework Core进行配置和操作数据 作者:zara(张子浩) 欢迎分享,但需在文章鲜明处留下原文地址. 前两章内容 ...

  7. DirectX12 3D 游戏开发与实战第三章内容

    变换 学习目标 理解如何使用矩阵表示线性变换和仿射变换 学习对几何体进行缩放.旋转和平移的坐标变换 根据矩阵之间的乘法运算性质,将多个变换矩阵合并为一个单独的净变换矩阵 找寻不同坐标系之间的坐标转换方 ...

  8. .NET Core3.1 Dotnetty实战系列视频

    一.概要 由于在.net的环境当中对dotnetty相关资料相对较少,所以这里主要分享一个dotnetty使用教程希望能帮助到正在使用这套框架的开发者们.虽然这套框架已微软官方已经不在维护,但是这套框 ...

  9. 学习笔记-[Maven实战]-第三章:Maven使用入门(3)

    这里说一下在建测试工程里遇到的问题 1.第一次建工程,junit依赖始终没有成功,最后删除现在工程,新建了一个工程就好了 2.使用junit4的问题.工程默认的依赖是junit3.8.1,我改成了4. ...

随机推荐

  1. PHP pclose() 函数

    定义和用法 pclose() 函数关闭由 popen() 打开的进程. 如果失败,该函数返回 FALSE. 语法 pclose(pipe) 参数 描述 pipe 必需.规定由 popen() 打开的进 ...

  2. MySQL中EXPLAIN命令详细解析

    很多情况下我们需要知道某条SQL语句的性能,都会通过EXPLAIN命令来查看查询优化器是如何执行的. 如何使用 使用EXPLAIN很简单,只需要在执行的SQL前面加上EXPLAIN即可 explain ...

  3. Python爬虫获取百度贴吧图片

    #!/usr/bin/python# -*- coding: UTF-8 -*-import urllibimport re文章来源:https://www.cnblogs.com/Axi8/p/57 ...

  4. mongodb 4.0副本集搭建

    近期有同学问mongodb副本集难不难部署,我的回答是不难,很快,几分钟搞定,比mysql MHA简单的不止一点半点. 那么到底如何部署呢?请看下文. 1.  准备工作 1.1 下载软件 选择版本并下 ...

  5. Java实现获取一个随机的两位数

    import java.util.Random; //获取一个随机的 两位数public class getrandomdouble { public static void main(String[ ...

  6. java流程控制语句switch

    switch 条件语句也是一种很常用的选择语句,它和if条件语句不同,它只能针对某个表达 式的值作出判断,从而决定程序执行哪一段代码. 格式: switch (表达式){ case 目标值1: 执行语 ...

  7. 高级搜索树-红黑树(RBTree)代码实现

    代码实现 代码参考了<数据结构(c++语言版)>--清华大学邓俊辉 "RBTree.h" #pragma once //#include"pch.h" ...

  8. git日常使用的常用命令总结

    git日常使用的常用命令总结 git 是什么? Git是目前世界上最先进的分布式版本控制系统(没有之一). Git(读音为/gɪt/.)是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常 ...

  9. 听说同学你搞不懂Java的LinkedHashMap,可笑

    先看再点赞,给自己一点思考的时间,微信搜索[沉默王二]关注这个有颜值却假装靠才华苟且的程序员.本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试题 ...

  10. 遗传算法框架-基于java jenetics库实现

    本篇并非介绍如何从0开始开发遗传算法框架,反而推荐各位使用已有的GA库jenetics来做遗传算法. GA算法的逻辑还是贴下: 好了,下面介绍的是基于jenetics开发的更贴近业务侧的框架,以及使用 ...