# Netty自定义协议
使用Netty可以帮助我们很快的自定义属于自己的协议。说明如何自定义协议之前先来看看Netty是如何解决TCP的粘包和半包问题的。
## 一、粘包和半包
- 粘包:客户端发送的多次数据被服务端一次性接受,例如客户端分别发送“abc”、“efd”、“hijk”;可能被服务器接收的时候一次性接收成“abcefdhijk”。
- 半包:客户端发送的数据可能会被服务端分多次接收,例如客户端发送“abcefdhijk”,但是服务端却接收成“abc”、“efd”、“hijk”。
粘包和半包的问题的本质是TCP协议采用流式传送,其传送是以字节为单位的,消息没有边界,TCP协议每次发送的大小和接收的大小都会送**滑动窗口**的限制;同时对于应用层读取传输层的数据,并不是每次传输层有数据了就一次性读取,而是有个缓冲区在起作用。
除此之外,粘包现象可能由Nagle算法造成,在发送端中,如果每次发送的字节数目太小,则会根据Nagle算法将字节缓存在缓冲区中,等待后面一次性发送。而半包现象可能是要发送的数据包多大,超过了数据链路层的MTU的限制,导致数据链路层不得不将发送的数据分成多个数据帧发送出去。
**Nagle 算法**
使用tcp协议,即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由。该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送,具体情况如下:
- 如果 SO\_SNDBUF 的数据达到 MSS(MTU - 头部),则需要发送。
- 如果 SO\_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭。
- 如果 TCP\_NODELAY = true,则需要发送。
- 已发送的数据都收到 ack 时,则需要发送。
- 上述条件不满足,但发生超时(一般为 200ms)则需要发送。
- 除上述情况,延迟发送。
### 解决粘包和半包问题的方案
1. 短链接:即发送一个数据包则创建一条tcp链接,这种方式效率很低。
2. 固定长度:每一条消息采用固定的长度,接收端根据协商好的固定长度一次性读取。
Netty中提供*FixedLengthFrameDecoder*类就是采用固定长度编解码器来解决粘包和半包的问题。
使用:
~~~
ch.pipeline.addLast(new FixedLengthFrameDecoder(8)); # 固定长度为8字节
~~~
缺点:
- 数据包的长度大小不好把握,长度固定太长则会造成浪费,长度固定太小则会对某些数据包不够使用。
3. 固定分割符:对每一条消息最后添加分割符、例如‘\n’,‘\r\n’。
Netty中提供了*LineBasedFrameDecoder*来使用‘\n’,或者‘\r\n’来作为分割符。如果要使用自定义的字符的话可以使用*DelimiterBasedFrameDecoder*。
使用:
~~~
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
~~~
服务端加入,默认以 \\n 或 \\r\\n 作为分隔符,如果超出指定长度仍未出现分隔符,则抛出异常。
缺点:处理字符数据比较合适,但如果内容本身包含了分隔符(字节数据常常会有此情况),那么就会解析错误。
4. 预设长度:每一条消息分为 head 和 body,head 中包含 body 的长度,**常用!**
Netty提供*LengthFieldBasedFrameDecoder*来支持预设长度的方式解决粘包和半包问题。
构造函数:
~~~
public LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip) {...}
~~~
- maxFrameLength:数据包的最大长度。
- lengthFieldOffset:长度字段的偏移值,即要从接收的数据包的哪个字节开始才算长度字段,一般都设置为0。
- lengthFieldLength:长度字段占多少个字节。
- lengthAdjustment:长度字段为基准,还有几个字节是内容。
- initialBytesToStrip:从头开始剥离多少个字节是内容。
例如:
~~~
// 表示最大长度为1024个字节,长度字段为第一个节点,并且占1个字节,内容从第一个字节后开始。
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
~~~
客户端:
~~~
public class HelloWorldClient {
static final Logger log = LoggerFactory.getLogger(HelloWorldClient.class);
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.debug("connetted...");
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.debug("sending...");
Random r = new Random();
char c = 'a';
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte length = (byte) (r.nextInt(16) + 1);
// 先写入长度
头部信息
buffer.writeByte(length);
// 再写入数据
body信息
for (int j = 1; j <= length; j++) {
buffer.writeByte((byte) c);
}
c++;
}
// 所以发送的数据为 length+content,这个length要和服务端的协商好
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("192.168.0.103", 9090).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.error("client error", e);
} finally {
worker.shutdownGracefully();
}
}
}
~~~
## 二、协议的设计与解析
其实所谓粘包和半包的问题的本质是如何基于TCP协议设计一个不定长的应用层协议。因为TCP中使基于流的方式传输的,消息是没有边界的。**所以协议的目的就是划定消息的边界,制定通信双方要遵守的通信规则。**
只要是按照协议的,双方就能够通信,例如redis中set方法的协议格式如下:
~~~
set name zhangsan
# 其协议的格式如下:
*3 \n
$3 \n
set \n
$4 \n
name \n
$8 \n
zhangsan \n
~~~
则在Netty中可以进行模拟:
~~~
private void set(ChannelHandlerContext ctx) {
ByteBuf buf = ctx.alloc().buffer();
buf.writeBytes("*3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("set".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("aaa".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("$3".getBytes());
buf.writeBytes(LINE);
buf.writeBytes("bbb".getBytes());
buf.writeBytes(LINE);
ctx.writeAndFlush(buf);
}
~~~
连接到对应的redis即可进行通信。
### 2.1 自定义协议要素
**头部head**:
1. 魔数:一般在数据包的前面几个字节,用来第一时间判断是否是无效数据包。
2. 版本号:可以支持协议的升级。
3. 序列化算法:消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk。
4. 指令类型:根据具体业务区分指令类型。
5. 请求序号:为了双工通信双方提供异步通信的能力。
6. 正文长度
**正文body**
消息正文,json、xml、对象流等。
- 第一章 Java基础
- ThreadLocal
- Java异常体系
- Java集合框架
- List接口及其实现类
- Queue接口及其实现类
- Set接口及其实现类
- Map接口及其实现类
- JDK1.8新特性
- Lambda表达式
- 常用函数式接口
- stream流
- 面试
- 第二章 Java虚拟机
- 第一节、运行时数据区
- 第二节、垃圾回收
- 第三节、类加载机制
- 第四节、类文件与字节码指令
- 第五节、语法糖
- 第六节、运行期优化
- 面试常见问题
- 第三章 并发编程
- 第一节、Java中的线程
- 第二节、Java中的锁
- 第三节、线程池
- 第四节、并发工具类
- AQS
- 第四章 网络编程
- WebSocket协议
- Netty
- Netty入门
- Netty-自定义协议
- 面试题
- IO
- 网络IO模型
- 第五章 操作系统
- IO
- 文件系统的相关概念
- Java几种文件读写方式性能对比
- Socket
- 内存管理
- 进程、线程、协程
- IO模型的演化过程
- 第六章 计算机网络
- 第七章 消息队列
- RabbitMQ
- 第八章 开发框架
- Spring
- Spring事务
- Spring MVC
- Spring Boot
- Mybatis
- Mybatis-Plus
- Shiro
- 第九章 数据库
- Mysql
- Mysql中的索引
- Mysql中的锁
- 面试常见问题
- Mysql中的日志
- InnoDB存储引擎
- 事务
- Redis
- redis的数据类型
- redis数据结构
- Redis主从复制
- 哨兵模式
- 面试题
- Spring Boot整合Lettuce+Redisson实现布隆过滤器
- 集群
- Redis网络IO模型
- 第十章 设计模式
- 设计模式-七大原则
- 设计模式-单例模式
- 设计模式-备忘录模式
- 设计模式-原型模式
- 设计模式-责任链模式
- 设计模式-过滤模式
- 设计模式-观察者模式
- 设计模式-工厂方法模式
- 设计模式-抽象工厂模式
- 设计模式-代理模式
- 第十一章 后端开发常用工具、库
- Docker
- Docker安装Mysql
- 第十二章 中间件
- ZooKeeper