该栏目讲解NIO、Netty组件、Netty参数调优、粘包与半包解决方案、聊天室
Netty入门
1、简介
概述
:Netty 是一个异步的、基于事件驱动的网络应用框架特点
2、入门案例
public class ServerDemo {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel channel) {
channel.pipeline().addLast(new StringDecoder());
channel.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8888);
}
}
public class ClientDemo {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(SocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) {
channel.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8888)
.sync()
.channel()
.writeAndFlush("Hello Netty!");
}
}
Netty 组件
1、事件驱动
1.1 EventLoop
概述
:EventLoop 本质是一个单线程执行器(内部维护了一个 Selector),里面有 run 方法处理 Channel 上源源不断的 I/O 事件继承关系
- 继承了
ScheduledExecutorService
线程池类 - 继承了
OrderedEventExecutor
,说明每个 EventExecutor 是一个单独的线程,可以执行 Runnable 任务
1.2 EventLoopGroup
概述
:EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 I/O 事件都由此 EventLoop 来处理(保证了 I/O 事件处理时的线程安全)继承关系
:继承 EventExecutorGroup
public class EventLoopDemo {
public static void main(String[] args) {
DefaultEventLoopGroup group = new DefaultEventLoopGroup(2);
final EventLoop eventLoop = group.next();
System.out.println(eventLoop);
NioEventLoopGroup workers = new NioEventLoopGroup(2);
workers.execute(() -> {
System.out.println("normal task...");
});
workers.scheduleAtFixedRate(() -> {
System.out.println("running...");
}, 0, 2, TimeUnit.SECONDS);
workers.shutdownGracefully();
}
}
2、通道
2.1 Channel
概述
:连接了网络套接字或能够进行 I/O 操作的组件常用方法
close()
:关闭 channelcloseFuture()
:处理 channel 关闭后的事件pipeline()
:添加处理器write()
:数据写入writeAndFlush()
:数据写入并刷新
2.2 ChannelFuture
概述
:异步通知的 Channel 类常用方法
sync()
:同步等待连接建立完成channel()
:获取Channel对象
2.3 CloseFuture
概述
:用于 channel 关闭后的处理常用方法
sync()
:同步等待连接关闭完成addListener()
:异步关闭连接
public class ChannelDemo {
public static void main(String[] args) throws InterruptedException {
final NioEventLoopGroup group = new NioEventLoopGroup();
final ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
channel.pipeline().addLast(new StringEncoder());
}
})
.connect("localhost", 8888);
final Channel channel = channelFuture.sync().channel();
channel.close();
final ChannelFuture closeFuture = channel.closeFuture();
closeFuture.addListener((ChannelFutureListener) channelFuture1 -> {
System.out.println("处理关闭之后的操作");
group.shutdownGracefully();
});
}
}
3、Future & Promise
3.1 JDK Future
方法名称 | 功能描述 |
---|
cancel | 取消任务 |
isCanceled | 任务是否取消 |
isDone | 任务是否完成,不能区分成功失败 |
get | 获取任务结果,阻塞等待 |
3.2 Netty Future
概述
:可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束相关方法
方法名称 | 功能描述 |
---|
getNow | 获取任务结果,非阻塞,还未产生结果时返回null |
await | 等待任务结束,如果任务失败,不会抛异常,而是通过 isSuccess 判断 |
sync | 等待任务结束,如果任务失败,抛出异常 |
isSuccess | 判断任务是否成功 |
cause | 获取失败信息,非阻塞,如果没有失败,返回 null |
addLinstener | 添加回调,异步接收结果 |
3.3 Promise
概述
:不仅有 Netty Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器相关方法
方法名称 | 功能描述 |
---|
setSuccess | 设置成功结果 |
setFailure | 设置失败结果 |
3.4 案例
public class PromiseDemo {
public static void main(String[] args) throws Exception {
syncPromise();
asyncPromise();
failurePromise();
}
private static void syncPromise() throws Exception {
DefaultEventLoop eventLoop = new DefaultEventLoop();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
eventLoop.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
promise.setSuccess(10);
RuntimeException e = new RuntimeException("Error...");
promise.setFailure(e);
});
System.out.println("同步非阻塞结果:" + promise.getNow());
System.out.println("同步阻塞结果:" + promise.get());
}
private static void asyncPromise() {
DefaultEventLoop eventLoop = new DefaultEventLoop();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
eventLoop.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
promise.setSuccess(10);
});
promise.addListener(future -> System.out.println("异步结果:" + promise.getNow()));
}
private static void failurePromise() throws InterruptedException {
DefaultEventLoop eventLoop = new DefaultEventLoop();
DefaultPromise<Integer> promise = new DefaultPromise<>(eventLoop);
eventLoop.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
RuntimeException e = new RuntimeException("Error...");
promise.setFailure(e);
});
promise.await();
System.out.println(promise.isSuccess() ? promise.getNow() : promise.cause());
}
}
4、Handler & Pipline
4.1 ChannelHandler
概述
:用来处理 Channel 上的各种事件,分为入站、出站两种。类型
- 入站处理器:ChannelInboundHandlerAdapter 的子类,主要用来读取客户端数据
- 出站处理器:ChannelOutboundHandlerAdapter 的子类,主要对写回结果进行加工
执行顺序
:入站处理器是按照添加的顺序执行的,而出站处理器是按照添加的逆序执行的
4.2 ChannelPipeline
概述
:由 ChannelHandlerContext(包装了 ChannelHandler)组成的双向链表常用方法
context.fireChannelRead()
:调用下一个入站处理器context.write()
:从当前节点找上一个出站处理器context.channel().write()
:从尾部开始触发后续出站处理器

5、ByteBuf
-
概述
:是对字节的封装
-
结构

-
创建对象
- ByteBufAllocator.DEFAULT.buffer(size):创建ByteBuf
- ByteBufAllocator.DEFAULT.heapBuffer(size):创建池化基于堆的 ByteBuf
- ByteBufAllocator.DEFAULT.directBuffer(size):创建池化基于直接内存的 ByteBuf
- ByteBufAllocator.DEFAULT.compositeBuffer():创建零拷贝的 ByteBuf
- Unpooled.wrappedBuffer(byteBuf):使用Unpooled工具类创建零拷贝的 ByteBuf
-
常用方法
- 写入数据:
writeInt(int value)
- 读取数据:
readByte()
- 释放:
retain()
& release()
- 切片:
slice()
:对原始ByteBuf进行切片成多个 ByteBuf,还是使用原始 ByteBuf 的内存,切片后的ByteBuf 维护独立的 read,write 指针 - 复制:
duplicate()
:取了原始 ByteBuf 所有内容,并且没有 max capacity 的限制,也是与原始 ByteBuf 使用同一块底层内存,只是读写指针是独立的 - 复制:
copy()
:会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关
-
扩容规则
- 如何写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后capacity是 16
- 如果写入后数据大小超过 512,则选择下一个2 ^ n,例如写入后大小为 513,则扩容后 capacity 是2 ^ 10 = 1024(2 ^ 9=512 已经不够了)
- 扩容不能超过 max capacity 会报错
-
释放规则
- 采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted接口
- 每个 ByteBuf 对象的初始计数为 1
- 调用 release 方法计数减1,如果计数为 0,ByteBuf 内存被回收
- 调用 retain 方法计数加1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收
- 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用
-
池化
- 概述:池化的最大意义在于可以重用 ByteBuf,没有池化,则每次都得创建新的 ByteBuf 实例
- 开启池化功能:-Dio.netty.allocator.type={unpooled|pooled}
public class ByteBufDemo {
public static void main(String[] args) {
ByteBuf buffer1 = ByteBufAllocator.DEFAULT.buffer();
ByteBuf buffer2 = ByteBufAllocator.DEFAULT.heapBuffer();
ByteBuf buffer3 = ByteBufAllocator.DEFAULT.directBuffer();
ByteBuf buffer4 = Unpooled.wrappedBuffer(buffer1);
CompositeByteBuf compositeByteBuf = ByteBufAllocator.DEFAULT.compositeBuffer();
buffer2.writeBytes(new byte[]{1, 2, 4, 5, 6});
byte value1 = buffer2.readByte();
System.out.println(value1);
buffer3.retain();
buffer3.release();
final ByteBuf buffer6 = buffer4.slice(0, 5);
System.out.println(buffer6);
final ByteBuf buffer7 = buffer4.duplicate();
System.out.println(buffer7);
final ByteBuf buffer8 = buffer4.copy();
System.out.println(buffer8);
final CompositeByteBuf byteBuf = compositeByteBuf.addComponents(buffer1, buffer2);
System.out.println(byteBuf);
}
}
粘包与半包
1、粘包
现象
:发送abc def,接收abcdef原因
- 应用层:接收方 ByteBuf 设置太大(Netty 默认1024)
- 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
- Nagle 算法:会造成粘包
2、半包
现象
:发送 abcdef,接收 abc def原因
- 应用层:接收方 ByteBuf 小于实际发送数据量
- 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
- MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
3、解决方案
- 短链接:发一个包建立一次连接,这样连接建立到连接断开之间就是消息的边界,缺点效率太低
- 固定长度:每一条消息采用固定长度,缺点浪费空间
- 分隔符:每一条消息采用分隔符,例如 \n,缺点需要转义
- 预设长度:每一条消息分为head和body,head中包含body的长度
协议设计
1、简介
原因
:TCP/IP 中消息传输基于流的方式,没有边界。协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则要素
- 魔数,用来在第一时间判定是否是无效数据包
- 版本号,可以支持协议的升级
- 序列化算法,消息正文到底采用哪种序列化反序列化方式,可以由此扩展,例如:json、protobuf、hessian、jdk
- 指令类型,是登录、注册、单聊、群聊… 跟业务相关
- 请求序号,为了双工通信,提供异步能力
- 正文长度
- 消息正文
2、编解码器
@ChannelHandler.Sharable
@Slf4j
public class MessageCodec extends MessageToMessageCodec<ByteBuf, Message> {
@Override
protected void encode(ChannelHandlerContext ctx, Message message, List<Object> outs)
throws IOException {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{1, 2, 3, 4});
buffer.writeByte(1);
buffer.writeByte(1);
buffer.writeByte(message.getMessageType());
buffer.writeInt(message.getSequenceId());
buffer.writeByte(0xff);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(message);
final byte[] msgBytes = baos.toByteArray();
buffer.writeInt(msgBytes.length);
buffer.writeBytes(msgBytes);
outs.add(buffer);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> outs)
throws Exception {
int magicNum = buffer.readInt();
byte version = buffer.readByte();
byte serializeType = buffer.readByte();
int messageType = buffer.readByte();
int sequenceId = buffer.readInt();
buffer.readByte();
final int length = buffer.readInt();
byte[] msgBytes = new byte[length];
buffer.readBytes(msgBytes, 0, length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(msgBytes));
Message message = (Message) ois.readObject();
outs.add(message);
}
}
聊天室