1.概述
2.Hello World
2.1 目标
开发一个简单的服务器和客户端
-
客户端向服务器发送hello ,world
-
服务仅接受,不反回
加入依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
2.2 服务器端
//1. 启动器,负责组装netty组件,启动服务器
new ServerBootstrap()
//2. BoosEventLoopGroup workEventLoopGroup(selectod,thread) ,group 组
.group(new NioEventLoopGroup())
//3.选择服务器的 ServerSocketChannel 实现
.channel(NioServerSocketChannel.class) // OIO BIO
//4. boss 负责处理连接worker(child) 负责处理读写,决定了worker(child) 能执行哪些操作(handler)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
// 5 channel 代表和客户端进行数据读写的通道 Initializer 初始化,负责添加别的handler
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 6 添加具体的handler
ch.pipeline().addLast(new StringDecoder()); // 将bytebuf 转为字符串
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){ //自定义handler
// 读事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//打印上一步转换好的字符串
System.out.println(msg);
}
});
}
//7 绑定监听端口
}).bind(8088);
2.3 客户端
//1 启动类
new Bootstrap()
// 2. 添加EventLoopGroup
.group(new NioEventLoopGroup())
//3. 选择客户端channel类
.channel(NioSocketChannel.class)
//4. 添加处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override //在连接监建立后 调用
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1",8088)
.sync()
//向服务器 发送消息
.channel().writeAndFlush("hello world");
3.组件
3.1 EventLoop
事件循环对象
EventLoop 本质是一个单线程执行器(同时维护了一个selector),里面有run方法处理Channel上源源不断的io事件
它的继承关系比较复杂
- 一条线是继承自j.u.c.ScheduledExecutorServer 因此包含了线程池中所有方法
- 另一条线是继承自netty自己的OrderEventExecutor,
- 提供了boolean inEventLoop(Thread thread)方法判断一个线程是否属于EventLoopGroup
- 提供了parent方法来看看自己属于哪个EventLoopGroup
EventLoopGroup 是一组EventLoop,Channel 一般会调用EventLoopGroup 的register方法来绑定其中一个EventLoop,后续这个Channel上的io事件都由此EventLoop来处理(保证了io事件处理时的线程安全)
- 继承自Netty自己的EventLoopGroup
- 实现了Iterable 接口提供遍历EventLoop 能力
- 另有next方法获取集中下一个EventLoop
以一个简单实例为例:
// 1. 创建事件循环组
EventLoopGroup group = new NioEventLoopGroup(2); // IO 事件,普通任务, 定时任务
// EventLoopGroup defaultGroup = new DefaultEventLoopGroup(); //普通任务,定时任务
// 2. 获取下一个事件循环
System.out.println(group.next());
// 3 执行普通任务
group.next().submit(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("ok");
});
//4 执行定时任务
group.next().scheduleAtFixedRate(() ->{
log.debug("ok");
},0,1, TimeUnit.SECONDS);
log.debug("main");
3.2 Channel
channel的主要作用
-
close()可以用来关闭channel
-
closeFuture()用来处理channel的关闭
- sync方法作用是同步等待channel关闭
- 而addListener方法是异步等待channel关闭
-
pipeline()方法添加处理器
-
write()方法将数据写入
-
writeAndFlush()方法将数据写入并刷出
这是刚才客户端代码
//1 启动类
Channel channel = new Bootstrap()
// 2. 添加EventLoopGroup
.group(new NioEventLoopGroup())
//3. 选择客户端channel类
.channel(NioSocketChannel.class)
//4. 添加处理器
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override //在连接监建立后 调用
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("127.0.0.1", 8088)
.sync().channel();
3.3 Future & Promise
在异步处理时,经常要用到这两个接口
首先要说明netty中的Future与jdk中future同名,但是是两个接口,netty的Future继承自jdk的Future,而promise 又对netty Future进行了扩展
- Jdk Future只能同步等待任务结束(或成功、或失败)才能得到结果
- netty Future 可以同步等待任务结束得到结果,也可以异步方式得到结果,但是都要等到任务结束
- netty Promise 不仅有Future 的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
| 功能/名称 | jdk Future | netty Future | Promise |
|---|---|---|---|
| cancel | 取消任务 | ||
| isCancel | 任务是否取消 | ||
| isDone | 任务是否完成 | ||
| get | 任务是否完成,不能区分成功失败 | ||
| getNow | 获取任务结果,阻塞等待 | 获取任务结果,非阻塞,还未产生结果时,返回null | |
| await | 等待任务结束,如果任务失败,不会抛异常,而是通过isSuccess判断 | ||
| sync | 等待任务结束,如果任务失败,抛出异常 | ||
| isSuccess | 判断任务是否成功 | ||
| cause | |||
| addLintener | |||
| setSuccess | 设置成功 | ||
| setFailure | 设置失败 |
jdkFuture 的使用
//1、 线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//2、 提交任务
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("执行计算");
Thread.sleep(1000);
return 50;
}
});
// 3、主线程通过future来获取结果
log.debug("等待结果" );
log.debug("结果是{}",future.get());
NettyFuture 的使用
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
Future<Integer> future = nioEventLoopGroup.submit(() -> {
log.debug("准备执行");
return 70;
});
// log.debug("等待执行结果");
// log.debug("结果是{}",future.get());
// future.get();
future.addListener(future1 -> log.debug("结果是{}", future1.getNow()));
PromiseFuture 的使用
EventLoop eventLoop = new NioEventLoopGroup().next();
DefaultPromise<Object> promise = new DefaultPromise<>(eventLoop);
new Thread(()->{
log.debug("开始执行计算...");
try {
int i = 1/0; //执行结果出错,抛出异常
Thread.sleep(1000);
promise.setSuccess(80);
} catch (Exception e) {
e.printStackTrace();
promise.setFailure(e); // promise出错异常消息
}
}).start();
log.debug("准备执行");
log.debug("结果是{}",promise.get()); // 获取结果
3.4 Handler & Pipeline
ChannleHandler用来处理Channel上的各种事件,分为入站、出站两种。所有ChannelHandler被连成一串,就是Pipeline
- 入站处理器通常是ChannelInboundHandlerAdapter的子类,主要用来读取客户端数据,写回结果
- 出站处理器通常是ChannelOutboundHandlerAdapter的子类,主要对写会结果进行加工
打个比喻,每个Channel是一个产品的加工车间,Pipeline是车间中的流水线,ChannelHandler就是流水线上的各道工序,ByteBuf是原材料,经过很多工序的加工:先经过一道道入站工序,在经过一道道出站工序最终变成产品
服务端
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 1. 通过channel 拿到pipeline
ChannelPipeline pipeline = ch.pipeline();
// 2. 添加处理器 head -> h1 -> - h2 -> tail
pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("1");
ByteBuf byteBuf = (ByteBuf) msg;
String s = byteBuf.toString(CharsetUtil.UTF_8);
super.channelRead(ctx, s); //向下传递,h2 能获取到s的值
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("2");
log.debug("h1向下传递的值是:{}",msg.toString());
// 如果入站不触发写操作,出站不会执行
ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server..".getBytes()));
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h3",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("3");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4");
super.write(ctx, msg, promise);
}
});
}
}
).bind(8088);
// 执行结果
/***
* 09:59:55.494 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 1
* 09:59:55.496 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 4
* 09:59:55.496 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 3
* 09:59:55.498 [nioEventLoopGroup-2-2] DEBUG pipeline.TestPipeLine - 2
*
*
*/
EmbeddedChannel 模拟入站出站操作
ChannelInboundHandlerAdapter h1 = new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("1");
super.channelRead(ctx, msg);
}
};
ChannelInboundHandlerAdapter h2 = new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("2");
super.channelRead(ctx, msg);
}
};
ChannelOutboundHandlerAdapter h3 = new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("3");
super.write(ctx, msg, promise);
}
};
ChannelOutboundHandlerAdapter h4 = new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
log.debug("4");
super.write(ctx, msg, promise);
}
};
EmbeddedChannel channel = new EmbeddedChannel(h1,h2,h3,h4);
//模拟入站操作
// channel.writeInbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("hello".getBytes())); // h1 h2
//模拟出站操作
channel.writeOutbound(ByteBufAllocator.DEFAULT.buffer().writeBytes("world".getBytes())); //h4 h3
3.5 ByteBuf
是对字节数据的封装
1)创建
// ByteBuf 是可扩充的字节数 默认是:256
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
log.info(byteBuf);
上面代码创建了一个默认ByteBuf(池化基于直接内存的ByteBuf),初始容量是10
输出
容量是:PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 10)
其中log方法参考如下
private static void log(ByteBuf buffer){
int length = buffer.readableBytes();
int rows = length / 16 + (length % 15 == 0 ? 0:1) + 4;
StringBuilder buf = new StringBuilder(rows * 80 * 2)
.append("read index:").append(buffer.readerIndex())
.append("write index:").append(buffer.writerIndex())
.append("capacity:").append(buffer.capacity())
.append(NEWLINE);
appendPrettyHexDump(buf,buffer);
System.out.println(buf.toString());
}
// ByteBuf 是可扩充的字节数 默认是:256
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
log(byteBuf);
StringBuilder sb = new StringBuilder();
for(int i=0;i<300;i++){
sb.append("a");
}
byteBuf.writeBytes(sb.toString().getBytes());
log(byteBuf);
2 ) 直接内存 vs 堆内存
可以使用下面的代码来创建池化基于堆的ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
也可以使用下面的代码来创建池化基于直接内存的ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);
- 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池功能一起用
- 直接内存堆GC压力小,因为这部分内存不受JVM垃圾回收的管理,但也要主要及时主动释放
3)池化 vs 非池化
池化的最大意义在于可以重用ByteBuf,有点有
- 没有池化, 则每次都地创建新的ByteBuf实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加GC压力
- 有了池化,则可以重用翅中ByteBuf 实例,并且采用了与jemalloc类似的内存分配算法提升分配效率。
- 高并发时,池化功能更节约内存,减少内存溢出的可能
池化功能是否开启,可以通过下面的系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
- 4.1 以后,非Android平台默认启用池化实现,Android 平台启用非池化实现
- 4.1 之前,池化功能还不成熟,默认是非池化实现
4) 组成
ByteBuf 由四部分组成
最开始读写指针都在0位置
5)写入
方法列表,省略一些不重要方法
| 方法签名 | 含义 | 备注 |
|---|---|---|
| writeBoolean(boolean value) | 写入boolean值 | 用一字节01||00 代表true | false |
| writeByte(int value) | 写入byte值 | |
| writeShort(int value) | 写入short值 | |
| writeInt(int value) | 写入int值 | Big Endian,即0x250,写入后00 00 02 50 |
| writeIntLE(int value) | 写入int值 | Little Endian,即0x250,写入后50 02 00 00 |
| writerBytes(ByteBuffer src) | 写入nio的ByteBuffer | |
| int writeCharSequence(CharSequence sequence,Charset charset) | 写入字符串 |
注意
- 这些方法的未指明返回值的,其返回值都是ByteBuf,意味着可以链式调用
- 网络传输,默认习惯是Big Endian
先写入4个字节
buffer.writeBytes(new byte[]{1,2,3,4});
log(buffer);
结果是:
read index:0 write index:4 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 |.... |
+--------+-------------------------------------------------+----------------+
在写入一个int整数,也是4个字节
buffer.writeInt(5);
log(buffer);
结果是
read index:0 write index:8 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 |........ |
+--------+-------------------------------------------------+----------------+
还有一类方法是set开头的一系列方法,也可以写入数据,但不会改变写指针位置
6)扩容
在写入一个int整数时,容量不够了(初始容量是10),这时会引发扩容
buffer.writeInt(6);
log(buffer);
扩容规则是
- 如何写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为12,则扩容后capacity是16
- 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为513,则扩容后capacity是2^10=1024(2^9-512已经不够了)
- 扩容不能超过max capacity 会报错
结果是
read index:0 write index:12 capacity:64
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05 00 00 00 06 |............ |
+--------+-------------------------------------------------+----------------+
7 ) 读取
例如读了4次,每次一个字节
System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
System.out.println(byteBuf.readByte());
log(byteBuf);
读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分
1
2
3
4
read index:4 write index:12 capacity:64
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06 |........ |
+--------+-------------------------------------------------+----------------+
如果需要重复读取int整数5,怎么办?
可以在read前先做个标记mark
buffer.markReaderIndex();
System.out.println(buffer.readInt());
log(buffer);
结果
5
read index:8 write index:12 capacity:64
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 06 |.... |
+--------+-------------------------------------------------+----------------+
这时要重复读取的话,重置到标记位置reset
byteBuf.resetReaderIndex();
log(byteBuf);
这时
read index:4 write index:12 capacity:64
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 05 00 00 00 06 |........ |
+--------+-------------------------------------------------+----------------+
还有种办法是才用get开头的一系列方法,这些方法不会改变read index
8 ) retain & release
由于Netty中有堆内存的ByteBuf 实现,堆外内存最好是手动来释放,而不是等GC垃圾回收
- UnpooledHeapByteBuf 使用jvm内存,只需等GC回收内存即可
- UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来收回内存
- PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存
回收内存的源码实现,请关注下面方法的不同实现
protected abstract void deallocate()
Netty这里才用了引用计数法来控制回收内存,每个ByteBuf都实现了ReferenceCounted接口
- 每个ByteBuf 对象的初始计数为1
- 调用release方法计数减1,如果计数为0,ByteBuf 内存被回收
- 调用retain 方法计数加1,表示调用者没用完之前,其它handler即使调用了release也不会造成回收
- 当计数为0时,底层内存会被回收,这时即使ByteBuf 对象还在,其各个方法均无法正常使用
9 ) Slice
【零拷贝】的体现之一,对原始ByteBuf 进行切片或多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,切片后的ByteBuf 维护独立的read,write 指针
例,原始 ByteBuf 进行一些初始操作
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
byteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i'});
BufferUtil.log(byteBuf);
// 在切片过程中,没有发生数据复制
ByteBuf f1 = byteBuf.slice(0, 5);
f1.retain();
ByteBuf f2 = byteBuf.slice(5, 4);
f2.retain();
byteBuf.release();
BufferUtil.log(f1);
BufferUtil.log(f2);
System.out.println("================");
f1.setByte(0,'b');
f1.release();
f2.release();
BufferUtil.log(byteBuf);
BufferUtil.log(f1);
输出
read index:0 write index:9 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 66 67 68 69 |abcdefghi |
+--------+-------------------------------------------------+----------------+
read index:0 write index:5 capacity:5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 65 |abcde |
+--------+-------------------------------------------------+----------------+
read index:0 write index:4 capacity:4
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 66 67 68 69 |fghi |
+--------+-------------------------------------------------+----------------+
================
read index:0 write index:9 capacity:10
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 63 64 65 66 67 68 69 |bbcdefghi |
+--------+-------------------------------------------------+----------------+
read index:0 write index:5 capacity:5
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 63 64 65 |bbcde |
+--------+-------------------------------------------------+----------------+
Process finished with exit code 0
10 ) duplicate
【零拷贝】的体现之一,就好比截取了原始ByteBuf 所有内容,并且没有max capacity 限制,也是与原始ByteBuf 使用同一块底层内存,只是读写指针独立的
11) copy
会将底层内存数据进行深拷贝,因此无论读写,都与原始ByteBuf 无关
13) Unpooled
Unpooled是一个工具类,类如其名,提供了非池化的ByteBuf创建、组合、复制等操作
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1,2,3,4,5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6,7,8,9,10});
//当包装ByteBuf 个数超过一个时,底层使用了 CompositeByteBuf
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1,buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));
ByteBuf 优势
- 池化-可以重用池中ByteBuf实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要像ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用,使用更流畅
- 很多地方体现零拷贝,例如Slice、duplicate 、 CompositeByteBuf
4.双向通信
4.1 练习
实现一个 echo server
编写server
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioServerSocketChannel>() {
@Override
protected void initChannel(NioServerSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = msg instanceof ByteBuf ? (ByteBuf)msg : null;
System.out.println(buffer.toString(Charset.defaultCharset()));
// super.channelRead(ctx, msg);
// 思考需要释放buffer吗
}
});
}
}).bind(8088);
怎么回应了? 只需要为channelRead 添加逻辑
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// ....
//建议使用ctx.alloc() 创建ByteBuf
ByteBuf response = ctx.alloc().buffer(20);
response.writeBytes("hello word".getBytes());
ctx.writeAndFlush(response);
//思考:需要释放 response 吗
}
编写client,这次使用了channelActive 事件, 它会在连接建立后触发
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//建议使用ctx.alloc() 创建bytebuf
ByteBuf buffer = ctx.alloc().buffer(10);
//首次建立连接,发送hello 信息
buffer.writeBytes("hello".getBytes());
ctx.writeAndFlush(buffer);
super.channelActive(ctx);
// 思考: 需要释放buffer吗
}
});
}
}).connect("127.0.0.1",8088);
客户端接收,与服务器接收代码类似,在客户端ChannelInboundHandlerAdapter中加入 channelRead事件
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buffer = msg instanceof ByteBuf ? (ByteBuf)msg : null;
System.out.println(buffer.toString(Charset.defaultCharset()));
// super.channelRead(ctx, msg);
// 思考需要释放buffer吗
}
三. Netty 进阶
1.粘(nian)包与半包
1.1 粘包现象
只要使用tcp 协议 ,都会出现半包 粘(nian)包问题 ,udp 不会出现
服务端代码
void start(){
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boss,work);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.debug("connected {}" ,ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.error("server error",e);
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
}
客户端代码
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// 会在连接channle 建立成功后,会触发active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i =0 ;i < 10;i++ ){
ByteBuf buffer = ctx.alloc().buffer(16);
buffer.writeBytes(new byte[] {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.writeAndFlush(buffer);
}
// super.channelActive(ctx);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8088).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
// e.printStackTrace();
logger.debug("client sesrver",e);
}finally {
worker.shutdownGracefully();
}
结果是: 发生粘包 , 一次发送16字节,结果一次收到160个字节
DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x2cade680, L:/127.0.0.1:8088 - R:/127.0.0.1:55461] RECEIVED: 160B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000030| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000040| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000050| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000060| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000070| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000080| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000090| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
+--------+-------------------------------------------------+----------------+
11:31:41.603 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 160, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
半包
//服务器加上 接收缓冲区
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
结果是:
11:37:28.364 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 36B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000010| 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|
|00000020| 00 01 02 03 |.... |
+--------+-------------------------------------------------+----------------+
11:37:28.364 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 36, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.365 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b |........ |
+--------+-------------------------------------------------+----------------+
11:37:28.365 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 1024) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0c 0d 0e 0f 00 01 02 03 04 05 06 07 08 09 0a 0b |................|
|00000010| 0c 0d 0e 0f 00 01 02 03 04 05 06 07 08 09 0a 0b |................|
|00000020| 0c 0d 0e 0f 00 01 02 03 |........ |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 512) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 40B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000010| 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 01 02 03 |................|
|00000020| 04 05 06 07 08 09 0a 0b |........ |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 40, cap: 512) that reached at the tail of the pipeline. Please check your pipeline configuration.
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.handler.logging.LoggingHandler - [id: 0x450ecff6, L:/127.0.0.1:8088 - R:/127.0.0.1:55636] RECEIVED: 4B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 0c 0d 0e 0f |.... |
+--------+-------------------------------------------------+----------------+
11:37:28.366 [nioEventLoopGroup-3-1] DEBUG io.netty.channel.DefaultChannelPipeline - Discarded inbound message PooledUnsafeDirectByteBuf(ridx: 0, widx: 4, cap: 496) that reached at the tail of the pipeline. Please check your pipeline configuration.
1.2
1.3 现象分析
粘包
- 现象,发送abc def,接受 abcdef
- 原因
- 应用层: 接收方ByteBuf 设置太大(Netty 默认1024)
- 滑动窗口:假设发送方256bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这256bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
- Nagle算法: 会造成粘包
半包
- 现象, 发送abcdef,接受abc edf
- 原因
- 应用层:接收方Bytebuf 小于实际发送数据量
- 滑动窗口: 假设接收方的窗口只剩了128bytes,发送方的报文大小是256 bytes, 这时放不下了,只能先发送前128 bytes, 等待ack后法能发送剩余部分,这就造成半包
- MSS 限制: 当发送的数据超过MSS 限制后,会将数据切分发送, 就会造成半包
本质是因为tcp是流失协议,消息无边界
粘包半包_解决短连接
解决 定长-解码器 LineBasedFrameDecoder
粘包半包-解决 -行解码器
服务端
void start(){
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup work = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
//调整系统的接受缓冲器(滑动窗口)
// serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
//调整 netty 的接收缓冲区(bytebuf)
serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss,work);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024)); //加入行解码器
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// 会在连接 channel 建立成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.debug("connected {}" ,ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
logger.error("server error",e);
}finally {
boss.shutdownGracefully();
work.shutdownGracefully();
}
}
客户端
//行解码器
public static StringBuilder makeString(char c,int len){
StringBuilder sb = new StringBuilder(len+2);
for(int i = 0; i < len; i++ ){
sb.append(c);
}
sb.append("\n");
return sb;
}
private static void send() {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
// 会在连接channle 建立成功后,会触发active 事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
char c = '0';
ByteBuf buf = ctx.alloc().buffer();
Random r = new Random();
for(int i = 0 ;i < 10 ;i++ ){
StringBuilder sb = makeString(c, r.nextInt(256) + 1);
c++;
buf.writeBytes(sb.toString().getBytes());
}
ctx.writeAndFlush(buf);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8088).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
// e.printStackTrace();
// logger.debug("client sesrver",e);
}finally {
worker.shutdownGracefully();
}
}
粘包-半包-解决LTC解码器 LengthFieldBasedFrameDecoder www.bilibili.com/video/BV1py…
lengthFieldOffset - 长度字段偏移量
lengthFieldLength - 长度字段长度
lenegthAdjustment - 长度字段为基准,还有几个字节是内容
initialBytesToStrip - 从头剥离几个字节
测试字节消息
main(){
EmbeddedChannel channel = new EmbeddedChannel(new LengthFieldBasedFrameDecoder(
1024,0,4,1,4),
new LoggingHandler(LogLevel.DEBUG));
// 4 个字节的内容长度, 实际内容
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
send(buffer, "Hello, world!");
send(buffer, "Hi!");
channel.writeInbound(buffer);
}
private static void send(ByteBuf buffer, String content) {
byte[] bytes = content.getBytes(); //实际内容
int length = bytes.length; // 实际内容长度
buffer.writeInt(length);
buffer.writeByte(1); //
buffer.writeBytes(bytes);
}
2. 协议设计与解析
使用netty测试http协议
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(boos,worker);
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() { //泛型,处理HttpRequest 请求
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) throws Exception {
// 获取请求
log.debug("获取请求 {}",msg.uri());
// 返回响应
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
byte[] bytes = "<h1>Hello world</h1>".getBytes();
response.headers().setInt(CONTENT_LENGTH,bytes.length);
response.content().writeBytes(bytes);
//写回响应
ctx.writeAndFlush(response);
}
});
/* ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.debug("{}",msg.getClass());
// super.channelRead(ctx, msg);
if(msg instanceof HttpRequest){ //请求行,请求头
}else if(msg instanceof HttpContent){ //请求体
}
}
});*/
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8088).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boos.shutdownGracefully();
worker.shutdownGracefully();
}
2.1 自定义协议要素
- 魔数,用来在第一时间判定是否是无效数据包
- 版本号,用来支持协议的升级
- 序列算法, 消息正文到底才用哪用序列化反序列化方法,可以由此扩展, 例如: json ,protobuf、hession、jdk
- 指令类型, 是登陆、注册、单聊、群聊.....跟业务相关
- 请求序号, 为了双工通信,提供异步能力
- 正文长度
- 消息正文
解码
已观看
https://www.bilibili.com/video/BV1py4y1E7oA?p=142