前言
本文主要介绍关于 Netty 中大名鼎鼎的半包问题和粘包问题,并提供相关的解决方案和代码演示。
一、问题引入
1.1 粘包现象
服务端程序
java
package com.xiaolei.netty.haflAndAllQuestion;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/*
@Author xiaolei
@Email lei.xu@op-energy.com
@Description
@Date 2023/8/23 13:07
**/
public class NettyServer {
public static void main(String[] args) {
// 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
ServerBootstrap server = new ServerBootstrap();
try {
// 将前面创建的两个EventLoopGroup绑定在server上
server.group(boss,worker)
// 指定服务端的通道为Nio类型
.channel(NioServerSocketChannel.class)
// 为到来的客户端Socket添加处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 这个只会执行一次(主要是用于添加更多的处理器)
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加一个字符解码处理器:对客户端的数据解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
// 添加一个入站处理器,对收到的数据进行处理
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("读取到的数据是:"+msg);
super.channelRead(ctx, msg);
}
});
}
});
// 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
ChannelFuture cf = server.bind("127.0.0.1",8888).sync();
// 关闭服务端的方法(之后不会在这里关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅停止之前创建的两个Group
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
发送的 demo
ini
public class SocketDemo {
public static void main(String[] args) {
try {
//发送到8888端口
Socket socket = new Socket("127.0.0.1", 8888);
//输出流
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
for (int i = 0; i < 100; i++) {
printWriter.write("hello:"+i);
printWriter.flush();
}
//关闭资源
printWriter.close();
outputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在 demo 中,发送的结果如下,可以看到预期的 100 条数据,并未按理想的每条接收和发送,而是每次读取的数据中,存在很多条的消息都粘包在一起,这个就是 Netty 中著名的粘包问题。
粘包问题,并非是 Netty 本身造成的,而是 TCP 造成的。
1.2 半包现象引入
半包,指的是一条数据包被分割成了多条发送,即发送的数据不完整。
为了演示半包问题,服务端修改一下接收缓冲区,其它代码不变
serverBootstrap.option(ChannelOption.SO_RCVBUF, 10);
这段代码影响的底层接收缓冲区(即滑动窗口)大小,仅决定了 netty 读取的最小单位,netty 实际每次读取的一般是它的整数倍。
可以看到,出现的半包问题如上,接收的数据不完整。
1.3 现象分析
从两个角度看粘包和半包。
1、收发角度:一个发送可能被多次接收(半包),多个发送可能被一次接收(粘包)
2、传输角度:一个发送可能占用多个传输包(半包),多个发送可能公用一个传输包(粘包)
1.3.1 粘包现象
在上面的演示代码中,多条 hello 数据在服务端接收的时候,接收的时候都粘在一起。
根本原因是,TCP 协议是面向连接的,可靠的,基于字节流的传输层协议,是一种流式协议,消息无边界。所谓流式协议,就是没有边界的一串二进制数据,TCP 作为传输层并不了解上层业务的逻辑边界。它是根据自己对缓冲区的大小,默默的搬运这块的消息进行发送,所以造成上面的两种现象。
原因 1 :发送方每次写入数据 < 套接字缓冲区大小。
服务端每次接收的 ByteBuf 的数据是 1024 个字节,当消息小于这个时候,后面的消息就有可能在一起被服务端消费接收。
原因 2 :滑动窗口:接收方读取套接字缓冲区数据不够及时
消息接收不及时,那么就有可能造成后面的消息放在一起发送。假设发送方 256 bytes 表示一个完整报文,但是由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
- 拓展:滑动窗口
- TCP 以一个段(segment)为单位,每发送一个段就需要进行一次确认应答(ack)处理,但如果这么做,缺点是包的往返时间越长性能就越差
- 为了解决此问题,引入了窗口概念,窗口大小即决定了无需等待应答而可以继续发送的数据最大值
-
窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用
- 图中深色的部分即要发送的数据,高亮的部分即窗口
- 窗口内的数据才允许被发送,当应答未到达前,窗口必须停止滑动
- 如果 1001~2000 这个段的数据 ack 回来了,窗口就可以向前滑动
- 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收
原因 3:Nagle 算法(默认开启)
TCP 默认使用的算法,用于处理小报文段的发送问题,当一个数据包小于 MSS 的时候,该算法会把类似的数据包归纳成一个分组进行发送。
-
拓展 1:Nagle 算法
-
即使发送一个字节,也需要加入 tcp 头和 ip 头,也就是总字节数会使用 41 bytes,非常不经济。因此为了提高网络利用率,tcp 希望尽可能发送足够大的数据,这就是 Nagle 算法产生的缘由
-
该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送
- 如果 SO_SNDBUF 的数据达到 MSS,则需要发送
- 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭
- 如果 TCP_NODELAY = true,则需要发送
- 已发送的数据都收到 ack 时,则需要发送
- 上述条件不满足,但发生超时(一般为 200ms)则需要发送
- 除上述情况,延迟发送
-
拓展 2:MSS 限制
-
链路层对一次能够发送的最大数据有限制,这个限制称之为 MTU(maximum transmission unit),不同的链路设备的 MTU 值也有所不同,例如
-
以太网的 MTU 是 1500
-
FDDI(光纤分布式数据接口)的 MTU 是 4352
-
本地回环地址的 MTU 是 65535 - 本地测试不走网卡
-
MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数
-
ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
-
TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
-
MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS
1.3.2 半包现象
在上面的演示环境中,发送的消息被分割成多端消息发送。
原因 1:发送方写入数据 > 套接字缓冲区大小
接收方 ByteBuf 小于实际发送数据量。
原因 2:滑动窗口
假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
原因 3:MSS 限制
当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
二、解决方案
目的:解决该问题,就是找出消息的边界。
1、短链接,发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点是效率太低。(不推荐)
2、每条消息采用固定长度,缺点是浪费空间(不推荐)
3、每条消息采用分隔符,例如 \n , 缺点是需要转义(推荐)
4、每条消息分为 head 和 body ,head 中包含 body 的长度。(推荐)
Netty 提供了三种封装帧的处理方式。
解码 | 编码 | |
---|---|---|
固定长度 | FixedLengthFrameDecoder | 简单 |
行帧分隔符 | LineBasedFrameDecoder | 客户端发送消息需要在每条消息末尾加上 \n 或者 \r\n 代表结束标志 |
分隔符 | DelimiterBasedFrameDecoder | 客户端发送消息需要在每条消息末尾加上指定分隔符 |
固定长度字段内容存个长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
那什么叫编码器呢?编码器其实是一个出站处理器,负责处理出站数据,其次编码器则负责入站数据处理。将入站的数据进行编码或格式转换。业务处理后的结果(出站数据),需要从某个 Java POJO 对象,编码成最终的 ByteBuf 二进制数据,然后通过底层 Java 通道发送到对端。
2.1 短连接演示
修改发送端的例子的代码
ini
public class SocketDemo {
public static void main(String[] args) {
try {
//发送到8888端口
for (int i = 0; i < 50; i++) {
Socket socket = new Socket("127.0.0.1", 8888);
//输出流
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.write("123456789");
printWriter.flush();
//关闭资源
printWriter.close();
outputStream.close();
socket.close();
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端每次发完数据后,就关闭 socket 连接,服务端接收的数据正常。
将 TCP 连接改成短连接,一个请求一个连接,就制造出了消息边界:建立连接和释放连接中间的消息即为一条完整的消息。
这种方法非常简单,也不需要我们在服务端修改什么代码,但是缺点也很大!
由于每次断开连接,建立连接,都会涉及到 TCP 的三次握手和四次挥手,每个消息都会涉及到这些操作,十分浪费性能,因此,并不推荐。
2.2 固定长度演示
让所有的数据包长度固定,服务端加入下面代码
scss
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
发送端代码 :
ini
public class SocketDemo {
public static void main(String[] args) {
try {
//发送到8888端口
Socket socket = new Socket("127.0.0.1", 8999);
//输出流
OutputStream outputStream = socket.getOutputStream();
for (int i = 0; i < 50; i++) {
byte[] data = "12345".getBytes();
byte[] bytes = new byte[10];
// 将data数组中的元素复制到bytes数组中
System.arraycopy(data, 0, bytes, 0, data.length);
// 补齐剩余的元素为 *
Arrays.fill(bytes, data.length, bytes.length, (byte) '*');
System.out.println(Arrays.toString(bytes));
outputStream.write(bytes);
outputStream.flush();
}
//关闭资源
outputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
java
package com.xiaolei.netty.haflAndAllQuestion;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/*
@Author xiaolei
@Email lei.xu@op-energy.com
@Description
@Date 2023/8/23 13:07
**/
public class NettyServer {
public static void main(String[] args) {
// 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
ServerBootstrap server = new ServerBootstrap();
// server.option(ChannelOption.SO_RCVBUF, 10);
try {
// 将前面创建的两个EventLoopGroup绑定在server上
server.group(boss,worker)
// 指定服务端的通道为Nio类型
.channel(NioServerSocketChannel.class)
// 为到来的客户端Socket添加处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 这个只会执行一次(主要是用于添加更多的处理器)
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加一个入站处理器,对收到的数据进行处
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 添加一个字符解码处理器:对客户端的数据解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收的消息是:"+msg);
super.channelRead(ctx, msg);
}
});
}
});
// 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
ChannelFuture cf = server.bind("127.0.0.1",8999).sync();
// 关闭服务端的方法(之后不会在这里关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅停止之前创建的两个Group
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
接收端日志
在这个例子中,服务端会以 10 个字节为单位,对数据包进行处理,但是,需要客户端进行数据补全,比如上面的方法复制中,如果数据包长度不足 10,则以 * 号作为补充。
这种方式也存在很大问题,只适用于简单的场合,在固定场景的消息发送中使用。
-
如果数据长度不足,以*来代替,就会占用网络资源开销。
-
如果数据长度过长,还是会存在半包问题。
2.3 行帧解码器
上面说的固定长度解码器,只适合消息定长的场景,而使用行帧解码器,也是一种解决方案。
需要给服务端添加 LineBasedFrameDecoder(1024) 行解码器。
- 服务端代码
java
package com.xiaolei.netty.haflAndAllQuestion;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.nio.charset.Charset;
/*
@Author xiaolei
@Email lei.xu@op-energy.com
@Description
@Date 2023/8/23 13:07
**/
public class NettyServer {
public static void main(String[] args) {
// 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
ServerBootstrap server = new ServerBootstrap();
// server.option(ChannelOption.SO_RCVBUF, 10);
try {
// 将前面创建的两个EventLoopGroup绑定在server上
server.group(boss,worker)
// 指定服务端的通道为Nio类型
.channel(NioServerSocketChannel.class)
// 为到来的客户端Socket添加处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 这个只会执行一次(主要是用于添加更多的处理器)
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加一个入站处理器,对收到的数据进行处
// 方式 1 : 固定长度解决方案
// ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 方式 2 : 行帧解码器
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 添加一个字符解码处理器:对客户端的数据解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收的消息是:"+msg);
super.channelRead(ctx, msg);
}
});
}
});
// 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
ChannelFuture cf = server.bind("127.0.0.1",8999).sync();
// 关闭服务端的方法(之后不会在这里关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅停止之前创建的两个Group
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
- 客户端代码
java
package com.xiaolei.netty.haflAndAllQuestion;
import io.netty.buffer.ByteBuf;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/*
@Author xiaolei
@Email lei.xu@op-energy.com
@Description
@Date 2023/10/30 15:07
**/
public class SocketDemo {
public static void main(String[] args) {
try {
//发送到8888端口
Socket socket = new Socket("127.0.0.1", 8999);
//输出流
OutputStream outputStream = socket.getOutputStream();
for (int i = 0; i < 50; i++) {
byte[] data = "12345".getBytes();
byte[] bytes = new byte[10];
// 将data数组中的元素复制到bytes数组中
System.arraycopy(data, 0, bytes, 0, data.length);
// 补齐剩余的元素为 *
Arrays.fill(bytes, data.length, bytes.length, (byte) '*');
System.out.println(Arrays.toString(bytes));
outputStream.write(bytes);
outputStream.flush();
}
//关闭资源
outputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
消息的接收结果:
在这个案例中,给服务端添加了 LineBasedFrameDecoder(1024) 行解码器后,同时在客户端发送消息的末尾加上 \n 或者 \r\n 换行符,就可以按照接线划分,1024 代表最大字节,总不能一直往后识别找有没有 \n 吧,当加上 1024 后,如果长度超过 1024,就把这些数据看作是一个完整的数据包进行发送。
2.4 固定分隔符演示
上面是固定使用 \n 和 \r\n 进行按行分割数据报文。
而 Netty 也提供了固定分隔符的方式,使用自定义分隔符的解码器,随意自定义解码器。
加上编解码器:
scss
new DelimiterBasedFrameDecoder(1024,delimiter)
客户端:
ini
public class SocketDemo {
public static void main(String[] args) {
try {
//发送到8888端口
Socket socket = new Socket("127.0.0.1", 8999);
//输出流
OutputStream outputStream = socket.getOutputStream();
for (int i = 0; i < 50; i++) {
String msg ="12345"+i+"*";
byte[] data = msg.getBytes();
outputStream.write(data);
outputStream.flush();
}
//关闭资源
outputStream.close();
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端:
java
public class NettyServer {
public static void main(String[] args) {
// 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
ByteBuf delimiter = ByteBufAllocator.DEFAULT.buffer(1);
delimiter.writeByte('*');
ServerBootstrap server = new ServerBootstrap();
// server.option(ChannelOption.SO_RCVBUF, 10);
try {
// 将前面创建的两个EventLoopGroup绑定在server上
server.group(boss,worker)
// 指定服务端的通道为Nio类型
.channel(NioServerSocketChannel.class)
// 为到来的客户端Socket添加处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 这个只会执行一次(主要是用于添加更多的处理器)
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加一个入站处理器,对收到的数据进行处
// 方式 1 : 固定长度解决方案
// ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 方式 2 : 行帧解码器
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 方式 3: 自定义分隔符
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
// 添加一个字符解码处理器:对客户端的数据解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收的消息是:"+msg);
super.channelRead(ctx, msg);
}
});
}
});
// 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
ChannelFuture cf = server.bind("127.0.0.1",8999).sync();
// 关闭服务端的方法(之后不会在这里关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅停止之前创建的两个Group
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
服务端接收的数据如下
使用这种编解码器,可以自由的定义分隔符,在很多场景下也可以使用。
但是缺点也有,它需要对读进来的每个字节都要判断下,是否是结尾标志,也很影响性能,其次,当数据超过最大长度限制,也不可避免的出现半包问题。
2.5 LTC 解码器预设长度演示
在发送消息之前,先约定长字节表示接下来的数据长度。
scss
// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));
LTC 中文简称基于长度的域编码器,需要理解下这五个参数信息。
找到源码的位置。
- maxFrameLength : 帧的最大长度,如果帧的长度大于此值,则会抛出 TooLongFrameException
- lengthFieldOffset: 长度字段的偏移,正常情况下,读取的数据从偏移为 0 的地方开始读取,如果有需要可以从其他偏移量处读取
- lengthFieldLength:长度域占用的字节数。
- lengthAdjustment: 长度调整,在长度域与 内容 content 域中间是否需要填充其他字节数
- initialBytesToStrip: 解码后跳过的字节数,先将数据去掉 N 个字节后,再开始读取数据。
java
/**
* Creates a new instance.
*
* @param maxFrameLength
* the maximum length of the frame. If the length of the frame is
* greater than this value, {@link TooLongFrameException} will be
* thrown.
* @param lengthFieldOffset
* the offset of the length field
* @param lengthFieldLength
* the length of the length field
* @param lengthAdjustment
* the compensation value to add to the value of the length field
* @param initialBytesToStrip
* the number of first bytes to strip out from the decoded frame
*/
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip) {
this(
maxFrameLength,
lengthFieldOffset, lengthFieldLength, lengthAdjustment,
initialBytesToStrip, true);
}
演示 demo,在服务端添加下面这句
scss
// 最大长度,长度偏移,长度占用字节,长度调整,剥离字节数
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4)); // 超过1024字节后就停止读取,长度4个字节
客户端代码:客户端采用 Socket 发送原生的 ByteBuffer 数据
ini
public class SocketDemo {
public static void main(String[] args) {
try {
// 创建一个SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8999));
String msg ="12345";
byte[] data = msg.getBytes();
int dataLength = data.length;
// 创建一个容量为10的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// 向ByteBuffer中写入数据
buffer.putInt(dataLength);
buffer.put(data);
buffer.flip(); // 读写指针指倒缓冲区的第一个位置
// 循环发送数据
for (int i = 0; i < 50; i++) {
buffer.rewind(); // 将position重置为0,以便重新发送数据
socketChannel.write(buffer);
}
//关闭资源
socketChannel.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务端代码
java
public class NettyServer {
public static void main(String[] args) {
// 创建两个EventLoopGroup,boss:处理连接事件,worker处理I/O事件
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
// 创建一个ServerBootstrap服务端(同之前的ServerSocket类似)
ByteBuf delimiter = ByteBufAllocator.DEFAULT.buffer(1);
delimiter.writeByte('*');
ServerBootstrap server = new ServerBootstrap();
// server.option(ChannelOption.SO_RCVBUF, 10);
try {
// 将前面创建的两个EventLoopGroup绑定在server上
server.group(boss,worker)
// 指定服务端的通道为Nio类型
.channel(NioServerSocketChannel.class)
// 为到来的客户端Socket添加处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 这个只会执行一次(主要是用于添加更多的处理器)
@Override
protected void initChannel(NioSocketChannel ch) {
// 添加一个入站处理器,对收到的数据进行处
// 方式 1 : 固定长度解决方案
// ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
// 方式 2 : 行帧解码器
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// 方式 3: 自定义分隔符
// ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));
// 方式 4: LTC
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0,
4, 0, 4));
// 添加一个字符解码处理器:对客户端的数据解码
ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收的消息是:"+msg);
super.channelRead(ctx, msg);
}
});
}
});
// 为当前服务端绑定IP与端口地址(sync是同步阻塞至连接成功为止)
ChannelFuture cf = server.bind("127.0.0.1",8999).sync();
// 关闭服务端的方法(之后不会在这里关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 优雅停止之前创建的两个Group
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
服务端接收的数据:
结合我们数据,可以发现
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0,4, 0, 4))
- 第一个 4 是代表 int 的字节长度
- 第二个 4 是代表跳过 int 的字节长度,读取数据
这种解码器使用效果比前面的几种都好上不少,它不用逐个字节进行判断。不过在具体的使用场景中,选择合适的编解码器是我们应该做的。
三、二次编解码
Netty 的核心组件 Decoder(编码器)和 Encoder 是非常重要的,为什么呢?因为在 Netty 中进行数据传输使用的是 ByteBuf 二进制数据,我们需要对其进行 Decoder 转换成 Java 的 POJO 对象。同时,发送给对端的时候,需要把 Java 的 POJO 对象转换成最终的 ByteBuf 二进制数据,而这是通过 Encoder 完成的。
3.1 二次编码器核心概念
Netty codec : 二次编解码。
我们把解决半包粘包问题的常用三种解码器叫一次解码器,其作用是将原始数据流
(可能会出现粘包和半包的数据流)转换为用户数据(ByteBuf 中存储),但仍然是字节数据,所以我们需要二次解码器将字节数组转换为 Java 对象,或者将一种格式转换为另一种格式,方便上层应用程序使用。
- 一次解码器继承自:ByteToMessageDecoder:用于将字节转为消息,需要检测缓冲区是否有足够的字节
- 二次解码器继承自:MessageToMessageDecoder:从一种消息格式转化为其他消息格式,例如 UserVO -> UserEntity,将一种pojo对象解码成另外一个 pojo 对象。
但他们的本质都是继承 ChannelInboundHandlerAdapter。
常用的二次编解码方式:
-
Json 序列化:不推荐使用,占用空间大
-
Marshaling:比 java 序列化稍好
-
XML:可读性好,但是占用空间大
-
JSON:可读性也好,空间较小
-
MessagePack:占用空间比 Json 小,可读性不如 Json,但也还行
-
Protobuf:性能高,体积小,但是可读性差
-
hessian:跨语言,高效的二进制序列化协议,整体性能和 protobuf 差不多
还有 ByteToMessageCodec 编解码器,它是一个抽象类,从功能上来说,继承它就等同于 ByteToMessageDecoder 解码器和 MessageToByteEncoder 解码器这两个基类。
同时实现它的编码和解码方法。
eg:
java
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageCodec;
import java.util.List;
public class IntArrayCodec extends ByteToMessageCodec<int[]> {
// 定义每个整数占用的字节数(这里假设是4字节,即int类型)
private static final int INT_SIZE = 4;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 检查是否有足够的字节来解码一个整数
while (in.readableBytes() >= INT_SIZE) {
// 读取一个整数并添加到输出列表中
int value = in.readInt();
// 由于我们需要解码为int数组,这里可以稍作修改,累积整数直到遇到某个分隔符或达到某个条件
// 但为了简单起见,我们假设每个整数都是一个单独的消息(这在实际中是不常见的)
// 因此,我们每次只解码一个整数并将其作为长度为1的数组添加
int[] intArray = new int[]{value};
out.add(intArray);
}
}
@Override
protected void encode(ChannelHandlerContext ctx, int[] msg, ByteBuf out) throws Exception {
// 遍历整数数组并将每个整数写入输出缓冲区
for (int value : msg) {
out.writeInt(value);
}
}
}
3.2 自定义解码器-ByteToMessageDecoder
ByteToMessageDecoder是一个非常重要的解码器基类,它是一个抽象类,实现了解码的基础逻辑和流程。ByteToMessageDecoder 继承自 ChannelInboundHandlerAdapter 适配器,是一个入站处理器,实现了从 ByteBuf 到 Java POJO 对象的解码功能。
处理流程大致如下:首先,它会将上一站传过来的输入到 Bytebuf 中的数据进行解码,解码出一个 List对象列表;然后,迭代 List 列表,逐个将 Java POJO 对象传入下一站 Inbound 入站处理器。
暂时无法在飞书文档外展示此内容
ByteToMessageDecoder 是一个抽象类,不能以实例化方式创建对象,也就是说,我们得创建出解码器来继承抽象类 ByteToMessageDecoder 实现具体的转换逻辑。
注意:一般情况下,解码器都是位于入站的第一位,方便后续业务处理
1、定义解码器
scala
public class ByteToIntergerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf,
List<Object> list) throws Exception {
// 因为 int 类型的字节大小是 4 个字节
while (byteBuf.readableBytes() >=4 ){
Integer i = byteBuf.readInt();
list.add(i);
// 这个参数是我们通过解码器解码完成后,将解码后的数据传递给 inbound
// 通道中的下一个 handler 继续进行业务的处理
}
}
}
2、定义业务handler
scala
public class IntegerPorcessHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 此时的 msg 不是 buftebuf了,因为已经解码了。
// 解码器中的,第三个参数 list<Object>
System.out.println("业务数据处理:"+(Integer)msg);
}
}
3、测试类
java
public class Byte2IntegerDecoderTest {
public static void main(String[] args) {
test();
}
public static void test(){
ChannelInitializer channelInitializer = new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new ByteToIntergerDecoder());
channel.pipeline().addLast(new IntegerPorcessHandler());
}
};
// 嵌入式通道,无序启动 netty服务端和客户端
EmbeddedChannel channel = new EmbeddedChannel(channelInitializer);
for (int i = 0; i < 10; i++) {
ByteBuf buffer = Unpooled.buffer();
buffer.writeInt(i);
channel.writeInbound(buffer);
}
}
}
这段实例的逻辑大致是,在decoder 方法中,通过 ByteBuf 的 readInt 实例方法,从输入缓冲区读取到整数,其作用是将二进制数据解码成一个一个的整数。因为整数是4个字节为一个单位,然后将解析出来的数据放入到List 中,完成后,会把得到的整数都传给下一个入站处理器。
3.3 自定义解码器-二次解码器
MessageToMessageDecoder 属于二次解码器,需要继承一这个解码器基类,在继承它的时候,需要明确它的泛型实参,确定入站的消息Java POJO。
MessageToMessageDecoder 同样使用了模板模式,也有一个 decoder 抽象方法,其具体解码的逻辑需要子类去实现,下面通过实现一个整数 Integer 到字符串 String 转换的解码器,演示下。
程序结构如下:
typescript
@Data
public class UserVO {
private Integer age;
private String name;
@Override
public String toString() {
return "UserVO{" +
"age=" + age +
", name='" + name + ''' +
'}';
}
}
修改第二个InterToUserDecoder 放第二个入口处理
java
ChannelInitializer channelInitializer = new ChannelInitializer() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new ByteToIntergerDecoder());
channel.pipeline().addLast(new IntegerToUserDecoder());
channel.pipeline().addLast(new IntegerPorcessHandler());
}
};
自定义解码器
scala
public class IntegerToUserDecoder extends MessageToMessageDecoder<Integer> {
@Override
protected void decode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
UserVO userVO = new UserVO();
userVO.setAge(msg);
userVO.setName(UUID.randomUUID().toString());
out.add(userVO);
}
}
输出结果:
3.4 自定义编码器
在 Netty 的业务处理中,业务处理的结果往往是某个 Java POJO 对象,需要编码成最终的 ByteBuf 二进制类型,通过流水线写入到对端通道,这就需要 Encoder 了。
编码器,是一个Outbound 出站处理器,负责处理出站数据。
编码器首先会把上一站的输入数据进行编码,然后传递到下一个站的出站处理器。
我们可以使用自己的编码器,继承 MessageToByteEncoder基类,实现 encode 方法。
其功能是将 Java 整数编码成二进制 ByteBuf 数据包。
scala
public static void test2() {
// 创建解码器
class IntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= 4) {
out.add(in.readInt());
}
}
}
// 创建编码器
class IntegerEncoder extends MessageToByteEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
out.writeInt(msg);
}
}
// 设置 EmbeddedChannel 的 pipeline
EmbeddedChannel channel = new EmbeddedChannel(new ChannelInitializer<EmbeddedChannel>() {
@Override
protected void initChannel(EmbeddedChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IntegerDecoder()); // 添加解码器
pipeline.addLast(new IntegerEncoder()); // 添加编码器(虽然在这个测试中不会用到)
}
});
// 测试解码器:向入站方向写入 ByteBuf
for (int i = 0; i < 10; i++) {
ByteBuf buffer = Unpooled.buffer();
buffer.writeInt(i);
channel.writeInbound(buffer); // 解码器会处理这个 ByteBuf
}
channel.flushInbound(); // 确保所有入站数据都被处理
// 测试编码器(可选):向出站方向写入 Integer
for (int i = 0; i < 10; i++) {
channel.writeAndFlush(i); // 编码器会处理这个 Integer
}
channel.flushOutbound(); // 确保所有出站数据都被处理
// 读取编码后的 ByteBuf(可选)
ByteBuf encodedBuf;
while ((encodedBuf = (ByteBuf) channel.readOutbound()) != null) {
// 在这个例子中,我们不会直接读取 encodedBuf 的内容,
// 因为我们已经知道编码器只是简单地将整数转换为 ByteBuf。
// 但为了完整性,这里展示了如何读取 ByteBuf。
encodedBuf.markReaderIndex(); // 标记当前读索引位置
int encodedInt = encodedBuf.readInt(); // 读取整数(应该与写入的整数相同)
System.out.println("Encoded integer (from ByteBuf): " + encodedInt);
encodedBuf.release(); // 释放 ByteBuf
}
}
3.3 自定义协议开发
clientHandler
scala
public class ClientHandler extends SimpleChannelInboundHandler<Message>{
@Override
protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
System.out.println("Received message: " + msg.getContent());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 在连接建立后发送消息
Message message = new Message(5, "Hello");
ctx.writeAndFlush(message);
}
}
message
arduino
public class Message {
private int length;
private String content;
public Message(int length, String content) {
this.length = length;
this.content = content;
}
public int getLength() {
return length;
}
public String getContent() {
return content;
}
}
mesageDecoder
scala
public class MessageDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 检查是否有足够的字节可读
if (in.readableBytes() < 4) {
return;
}
// 读取消息长度
int length = in.readInt();
// 检查是否有足够的字节可读
if (in.readableBytes() < length) {
in.resetReaderIndex();
return;
}
// 读取消息内容
byte[] contentBytes = new byte[length];
in.readBytes(contentBytes);
String content = new String(contentBytes);
// 创建Message对象并添加到输出列表
Message message = new Message(length, content);
out.add(message);
}
}
messageEncoder
scala
public class MessageEncoder extends MessageToByteEncoder<Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message message, ByteBuf out) throws Exception {
// 将消息长度写入字节流
out.writeInt(message.getLength());
// 将消息内容写入字节流
out.writeBytes(message.getContent().getBytes());
}
}
nettyclient
java
public class NettyClient {
private static final String HOST = "127.0.0.1";
private static final int PORT = 8999;
public static void main(String[] args) throws InterruptedException {
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();
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new ClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect(HOST, PORT).sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
nettyserver
java
public class NettyServer {
private static final int PORT = 8999;
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
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();
pipeline.addLast(new MessageDecoder());
pipeline.addLast(new MessageEncoder());
pipeline.addLast(new ServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(PORT).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}