介绍:

  • 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码
  • codec(编解码器) 的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成业务数据

示意图:

Netty自身提供的一些编解码器:

  1. Netty 提供的编码器

    StringEncoder,对字符串数据进行编码

    ObjectEncoder,对 Java 对象进行编码

    ...
  2. Netty 提供的解码器

    StringDecoder, 对字符串数据进行解码

    ObjectDecoder,对 Java 对象进行解码

Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,底层使用的仍是 Java 序列化技术 , 而Java 序列化技术本身效率就不高,存在如下问题

  • 无法跨语言
  • 序列化后的体积太大,是二进制编码的 5 倍多。
  • 序列化性能太低

可以使用Google 的Protobuf 技术解决上述问题, 这个后面介绍 先说一下 编解码器的调用机制

用StringEncoder(编码) 和 StringDecoder(解码) 为例:

public class StringEncoder extends MessageToMessageEncoder<CharSequence>
public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter

查看源码得知 StringEncoder 间接继承了 ChannelOutboundHandlerAdapter类,所以 他实际上是一个outboundHandler(出站),很好理解,信息往外发送 当时是编码了,以此推断StringDecoder 是一个inboundHandler(入站),读取数据时解码.

编码解码器都是以Handler 处理器注册在pipeLine的调用链中 在读取 发送数据 时进行编解码

自定义编码解码器

服务端:

注册自定义的编码解码器

public class MyServer {
public static void main(String[] args) throws Exception{ EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>{
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline(); //入站的handler进行解码 MyByteToLongDecoder
pipeline.addLast(new MyByteToLongDecoder());
// pipeline.addLast(new MyByteToLongDecoder2());
//出站的handler进行编码
pipeline.addLast(new MyLongToByteEncoder());
//自定义的handler 处理业务逻辑
pipeline.addLast(new MyServerHandler());
System.out.println("xx");
}
}); //自定义一个初始化类 ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync(); }finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} }
}

自定义的 解码器 继承 ByteToMessageDecoder 会在自定义Handler类之前执行

在解码器 进行数据解码时,需要判断 缓存区(ByteBuf)的数据是否足够 ,否则接收到的结果会期望结果可能不一致(** 继承ReplayingDecoder<S> 则不需要进行判断 这个类自动检测 数据是否符合 泛型S **):

读取通道中的数据 并调用 该Handler的 decode方法, ByteBuf则为读取到的数据,进行自定义的解码,并添加到List中, 方法结束后 会遍历List中的数据 多次执行下一个Handler

//源码
for (int i = 0; i < numElements; i ++) {
ctx.fireChannelRead(msgs.getUnsafe(i));
}
public class MyByteToLongDecoder extends ByteToMessageDecoder {
/**
*
* @param ctx 上下文对象
* @param in 入站的 ByteBuf
* @param out List 集合,将解码后的数据传给下一个handler . 下一个Handler 接受的数据就为List中的数据
向下一个Handler执行的代码 如果List 数据为多个 则传递多次
* @throws Exception
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { //如果读取的数据 等于 16 则会 分两次执行下一个Handler
System.out.println("MyByteToLongDecoder 被调用");
//因为 long 8个字节, 需要判断有8个字节,才能读取一个long
if(in.readableBytes() >= 8) {
out.add(in.readLong());
}
}
}

自定义编码器 继承MessageToByteEncoder类 泛型为 处理的数据 如果发送的数据不为 Long 则不会调用该方法 :

public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
//编码方法
@Override
protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
System.out.println("MyLongToByteEncoder encode 被调用");
System.out.println("msg=" + msg);
out.writeLong(msg);
}
}

客户端:

public class MyClient {
public static void main(String[] args) throws Exception{ EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); //加入一个出站的handler 对数据进行一个编码
pipeline.addLast(new MyLongToByteEncoder());
//这时一个入站的解码器(入站handler )
pipeline.addLast(new MyByteToLongDecoder());
// pipeline.addLast(new MyByteToLongDecoder2());
//加入一个自定义的handler , 处理业务
pipeline.addLast(new MyClientHandler()); }
}); //自定义一个初始化类 ChannelFuture channelFuture = bootstrap.connect("localhost", 7000).sync(); channelFuture.channel().closeFuture().sync(); }finally {
group.shutdownGracefully();
}
}
}

客户端Handler 通道建立 发送信息 只有发送Long类型的数据才会调用编码器

public class MyClientHandler  extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception { System.out.println("服务器的ip=" + ctx.channel().remoteAddress());
System.out.println("收到服务器消息=" + msg); } //重写channelActive 发送数据 @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyClientHandler 发送数据");
ctx.writeAndFlush(123456L); //发送的是一个long // ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabcd",CharsetUtil.UTF_8)); }
}

Netty自带的解码器:

  • LineBasedFrameDecoder:这个类在Netty内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。

  • DelimiterBasedFrameDecoder:使用自定义的 特殊字符作为消息的分隔符。

  • HttpObjectDecoder:一个HTTP数据的解码器

  • LengthFieldBasedFrameDecoder:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。

Protobuf技术

上文中说到,在客户端和服务端传输数据的格式问题上 有着或多或少的问题,而Google Protobuf 就是解决这些问题的. 官方文档

工作机制:

使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述。然后通过 protoc.exe 编译器根据.proto 自动生成.java 文件.

原理分析:

通过一定的规范 构建你想要表达的业务含义,只要解码端能够支持这种规范,就能还原出对应的 该语言代码(跨语言),而它本身也对序列化过程进行优化,极尽所能的压榨每一寸空间和性能.如netty的 支持此规范的 编码器中.

各个语言的字段表达含义:

protobuf 使用示意图:

简单的小案例

实例代码:

syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名 //protobuf 使用message 管理数据 message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}

使用protobuf编译器解析这个文件,命令protoc.exe --java_out=. Student.proto

生成的java类伪代码:

class StudentPOJO{
class Student{
int id;
String name;
}
}

使用Netty 包装发送此对象

服务端 注册ProtobufDecoder 解码器:

public class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(); //8 try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
// .handler(null) // 该 handler对应 bossGroup , childHandler 对应 workerGroup
.childHandler(new ChannelInitializer<SocketChannel>() {//创建一个通道初始化对象(匿名对象)
//给pipeline 设置处理器
@Override
protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline();
//在pipeline加入ProtoBufDecoder
//指定对哪种对象进行解码
pipeline.addLast("decoder", new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
}); System.out.println(".....服务器 is ready..."); //绑定一个端口并且同步, 生成了一个 ChannelFuture 对象
//启动服务器(并绑定端口)
ChannelFuture cf = bootstrap.bind(6668).sync(); //给cf 注册监听器,监控我们关心的事件 cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (cf.isSuccess()) {
System.out.println("监听端口 6668 成功");
} else {
System.out.println("监听端口 6668 失败");
}
}
});
//对关闭通道进行监听
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
} } }

服务端Handler:

public class NettyServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {

    //读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, StudentPOJO.Student msg) throws Exception {
//读取从客户端发送的StudentPojo.Student
System.out.println("客户端发送的数据 id=" + msg.getId() + " 名字=" + msg.getName());
} //数据读取完毕
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

客户端 发送数据给服务端 添加 ProtoBufEncoder编码器:

public class NettyClient {
public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try {
//创建客户端启动对象
//注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
Bootstrap bootstrap = new Bootstrap(); //设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//在pipeline中加入 ProtoBufEncoder
pipeline.addLast("encoder", new ProtobufEncoder());
pipeline.addLast(new NettyClientHandler()); //加入自己的处理器
}
}); System.out.println("客户端 ok.."); //启动客户端去连接服务器端
//关于 ChannelFuture 要分析,涉及到netty的异步模型
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
//给关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally { group.shutdownGracefully(); }
}
}

客户端Handler 构建Student类的过程看代码:

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //当通道就绪就会触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { //发生一个Student 对象到服务器
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("智多星 吴用").build();
//Teacher , Member ,Message
ctx.writeAndFlush(student);
} //当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

这个案例有个很大问题 ,就是已经对发送和接受数据的类型定死,下面进行改造:

proto 定义一个包含 Student 或者 Worker的类 发送接受时 根据 枚举字段 获取:

syntax = "proto3";
option optimize_for = SPEED; // 加快解析
option java_package="com.atguigu.netty.codec2"; //指定生成到哪个包下
option java_outer_classname="MyDataInfo"; // 外部类名, 文件名 //protobuf 可以使用message 管理其他的message
message MyMessage { //定义一个枚举类型( 内部类 ))
enum DataType {
StudentType = 0; //在proto3 要求enum的编号从0开始
WorkerType = 1;
} //用data_type 来标识传的是哪一个枚举类型
DataType data_type = 1; //表示每次枚举类型最多只能出现其中的一个, 节省空间 ,这两个属性只能赋值一个 赋值的后一个会替换前一个
oneof dataBody {
Student student = 2;
Worker worker = 3;
} }
message Student {
int32 id = 1;//Student类的属性
string name = 2; //
}
message Worker {
string name=1;
int32 age=2;
}

java伪代码:

class MyDataInfo{
class MyMessage{
enum DataType{
StudentType,WorkerType
} DataType data_type; //Student 和Worker 只可存在一个 设置后一个会替换前一个
Student student;
Worker worker; }
class Student{
int id;
String name;
}
class Worker{
String name;
int age;
}
}

改造上述案例的 客户端发送信息的方法 channelActive (随机发送一个对象) :

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机的发送Student 或者 Workder 对象
int random = new Random().nextInt(3);
MyDataInfo.MyMessage myMessage = null; if(0 == random) { //发送Student 对象
myMessage = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType).setStudent(MyDataInfo.Student.newBuilder().setId(5).setName("玉麒麟 卢俊义").build()).build();
} else { // 发送一个Worker 对象
myMessage = MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType).setWorker(MyDataInfo.Worker.newBuilder().setAge(20).setName("老李").build()).build();
} ctx.writeAndFlush(myMessage);
}

改造服务端 添加的 解码器 指定的是包含 Student 和Worker的 MyMessage对象:

...
//指定对哪种对象进行解码(注册解码器)
pipeline.addLast("decoder", new ProtobufDecoder(MyDataInfo.MyMessage.getDefaultInstance()));
...

改造 服务端接受数据的Handler 指定接受数据类型的泛型也为MyMessage对象 ,并根据 DataType字段 获取相应的对象:

//处理Handler类 指定的对象为 MyMessage 类型
public class NettyServerHandler extends SimpleChannelInboundHandler<MyDataInfo.MyMessage> { //读取数据实际(这里我们可以读取客户端发送的消息)
/*
1. ChannelHandlerContext ctx:上下文对象, 含有 管道pipeline , 通道channel, 地址
2. Object msg: 就是客户端发送的数据 默认Object
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, MyDataInfo.MyMessage msg) throws Exception { //根据dataType 来显示不同的信息 MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
if(dataType == MyDataInfo.MyMessage.DataType.StudentType) { MyDataInfo.Student student = msg.getStudent();
System.out.println("学生id=" + student.getId() + " 学生名字=" + student.getName()); } else if(dataType == MyDataInfo.MyMessage.DataType.WorkerType) {
MyDataInfo.Worker worker = msg.getWorker();
System.out.println("工人的名字=" + worker.getName() + " 年龄=" + worker.getAge());
} else {
System.out.println("传输的类型不正确");
}
}

Netty笔记(5) - 编码解码机制 和 Protobuf技术的更多相关文章

  1. 理解netty对protocol buffers的编码解码

    一,netty+protocol buffers简要说明 Netty是业界最流行的NIO框架之一优点:1)API使用简单,开发门槛低:2)功能强大,预置了多种编解码功能,支持多种主流协议:3)定制能力 ...

  2. 网络编程 -- RPC实现原理 -- Netty -- 迭代版本V3 -- 编码解码

    网络编程 -- RPC实现原理 -- 目录 啦啦啦 V2——Netty -- pipeline.addLast(io.netty.handler.codec.MessageToMessageCodec ...

  3. 一个低级错误引发Netty编码解码中文异常

    前言 最近在调研Netty的使用,在编写编码解码模块的时候遇到了一个中文字符串编码和解码异常的情况,后来发现是笔者犯了个低级错误.这里做一个小小的回顾. 错误重现 在设计Netty的自定义协议的时候, ...

  4. 《PHP 实现 Base64 编码/解码》笔记

    前言 早在去年 11 月底就已经看过<PHP 实现 Base64 编码/解码>这篇文章了,由于当时所掌握的位运算知识过于薄弱,所以就算是看过几遍也是囫囵吞枣一般,不出几日便忘记了其滋味. ...

  5. Netty 笔记

    1.Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器和客户端. 2.早期Java API 使用的阻塞函数 // 创建一个新的ServerSocket, ...

  6. Netty源码死磕一(netty线程模型及EventLoop机制)

    引言 好久没有写博客了,近期准备把Netty源码啃一遍.在这之前本想直接看源码,但是看到后面发现其实效率不高, 有些概念还是有必要回头再细啃的,特别是其线程模型以及EventLoop的概念. 当然在开 ...

  7. Solr In Action 笔记(2) 之 评分机制(相似性计算)

    Solr In Action 笔记(2) 之评分机制(相似性计算) 1 简述 我们对搜索引擎进行查询时候,很少会有人进行翻页操作.这就要求我们对索引的内容提取具有高度的匹配性,这就搜索引擎文档的相似性 ...

  8. python摸爬滚打之day06----小数据池、编码解码

    1.小数据池 代码块:  一个模块, 一个函数, 一个类, 甚至每一个command命令都是一个代码块. 一个文件也是一个代码块.而不需要创建一个新的数据. 这样会节省更多的内存区域. 在cmd命令行 ...

  9. BOM / URL编码解码 / 浏览器存储

    BOM 浏览器对象模型 BOM(Browser Object Model) 是指浏览器对象模型,是用于描述这种对象与对象之间层次关系的模型,浏览器对象模型提供了独立于内容的.可以与浏览器窗口进行互动的 ...

  10. day8_文件操作及编码解码

    一.文件操作基本流程 计算机系统分为:计算机硬件,操作系统,应用程序三部分. 我们用python或其他语言编写的应用程序若想要把数据永久保存下来,必须要保存于硬盘中,这就涉及到应用程序要操作硬件,众所 ...

随机推荐

  1. Vue3中ref和toRef的区别

    1. ref是复制,视图会更新 如果利用ref将某一个对象中的某一个属性值变成响应式数据 我们修改响应式数据是不会影响原始数据的; 同时视图会跟新. ref就是复制 复制是不会影响原始数据的 < ...

  2. 洛谷P3101 题解

    输入格式 第 \(1\) 行,三个整数 \(m,n,t\). 第 \(2\) 到 \(m+1\) 行,\(m\) 个整数,表示海拔高度. 第 \(2+m\) 到 \(2m+1\) 行,\(m\) 个整 ...

  3. CANVAS ----- 鼠标移动画圆

    1.增加鼠标移动事件 $('#canvas').mousemove(function (e) { draw(event); }); 2.获取鼠标在canvas上的坐标 function getCanv ...

  4. OpenIM Open Source Instant Messaging Project Docker Compose Deployment Guide

    The deployment of OpenIM involves multiple components and supports various methods including source ...

  5. 手撕Vuex-实现getters方法

    经上一篇章介绍,完成了实现共享数据的功能,实现方式是在 Store 构造函数中将创建 Store 时将需要共享的数据添加到 Store 上面,这样将来我们就能通过 this.$store 拿到这个 S ...

  6. .NET 云原生架构师训练营(模块二 基础巩固 引入)--学习笔记

    2.1 引入 http协议 web server && web application framework .net 与 .net core asp .net core web api ...

  7. Linux-ln命令创建链接(软连接/硬链接)

    1.ln命令介绍 ln命令可以看作是 link 的缩写,其功能是创建文件间的链接,链接类型包括硬链接(hard link)和软链接(符号链接,symbolic link) 2.ln命令格式 ln 命令 ...

  8. 通过程序自动设置网卡的“internet共享”选项

    操作系统 : Windows 10_x64 [版本 10.0.19042.685] Windows下可以通过网卡共享进行上网,但是需要在网卡的属性里面进行设置,需要在视窗界面进行操作,不能实现自动化. ...

  9. Embedding 模型部署及效果评测

    写在前面 最近大模型发展迅速,与之对应的向量化需求也被带动起来了,由此社区也衍生出很多模型,本文选几款,简单做下评测. 前置概念 为方便读者,先简单介绍几个概念. 概念1:Vector Embeddi ...

  10. google三驾马车之一:Bigtable解读(英文版)

    本文重点关注了系统设计相关的内容,paper后半部分的具体应用此处没有过多涉及.从个人笔记修改而来,因此为英文版本. Bigtable: A Distributed Storage System fo ...