Docs
- Netty官方文档:netty.io/4.1/api/ind…
- Practice Repository:github.com/bakazhou/JU…
Netty
一、概述
1、什么是Netty
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端
注意:netty的异步还是基于多路复用的,并没有实现真正意义上的异步IO
2、Netty的优势
如果使用传统NIO,其工作量大,bug 多
- 需要自己构建协议
- 解决 TCP 传输问题,如粘包、半包
- 因为bug的存在,epoll 空轮询导致 CPU 100%
Netty 对 API 进行增强,使之更易用,如
- FastThreadLocal => ThreadLocal
- ByteBuf => ByteBuffer
二、入门案例
1、服务器端代码
public class HelloServer {
public static void main(String[] args) {
// 1、启动器,负责装配netty组件,启动服务器
new ServerBootstrap()
// 2、创建 NioEventLoopGroup,可以简单理解为 线程池 + Selector
.group(new NioEventLoopGroup())
// 3、选择服务器的 ServerSocketChannel 实现
.channel(NioServerSocketChannel.class)
// 4、child 负责处理读写,该方法决定了 child 执行哪些操作
// ChannelInitializer 处理器(仅执行一次)
// 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
// 5、SocketChannel的处理器,使用StringDecoder解码,ByteBuf=>String
nioSocketChannel.pipeline().addLast(new StringDecoder());
// 6、SocketChannel的业务处理,使用上一个处理器的处理结果
nioSocketChannel.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println(s);
}
});
}
// 7、ServerSocketChannel绑定8080端口
}).bind(8080);
}
}
2、客户端代码
public class HelloClient {
public static void main(String[] args) throws InterruptedException {
new Bootstrap()
.group(new NioEventLoopGroup())
// 选择客户 Socket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现
.channel(NioSocketChannel.class)
// ChannelInitializer 处理器(仅执行一次)
// 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) throws Exception {
// 消息会经过通道 handler 处理,这里是将 String => ByteBuf 编码发出
channel.pipeline().addLast(new StringEncoder());
}
})
// 指定要连接的服务器和端口
.connect(new InetSocketAddress("localhost", 8080))
// Netty 中很多方法都是异步的,如 connect
// 这时需要使用 sync 方法等待 connect 建立连接完毕
.sync()
// 获取 channel 对象,它即为通道抽象,可以进行数据读写操作
.channel()
// 写入消息并清空缓冲区
.writeAndFlush("hello world");
}
}
3、运行流程
左:客户端 右:服务器端
Client发送数据,经过client自身的handler,将数据转为ByteBuf,server接收到数据后,通过server的handler将数据从byteBuf转为所需要的数据,并进行处理
组件解释
-
channel 可以理解为数据的通道
-
msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 中的各个 handler 加工,会变成其它类型对象,最后输出又变成 ByteBuf
-
handler 可以理解为数据的处理工序
-
工序有多道,合在一起就是 pipeline(传递途径) ,pipeline 负责发布事件(读、读取完成…)传播给每个 handler, handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)
- pipeline 中有多个 handler,处理时会依次调用其中的 handler
-
handler 分 Inbound 和 Outbound 两类
- Inbound 入站
- Outbound 出站
-
-
eventLoop 可以理解为处理数据的工人
- eventLoop 可以管理多个 channel 的 io 操作,并且一旦 eventLoop 负责了某个 channel,就会将其与channel进行绑定,以后该 channel 中的 io 操作都由该 eventLoop 负责
- eventLoop 既可以执行 io 操作,也可以进行任务处理,每个 eventLoop 有自己的任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务
- eventLoop 按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每个 handler 指定不同的 eventLoop
三、组件
1、EventLoop
事件循环对象 EventLoop
EventLoop 本质是一个单线程执行器(同时维护了一个 Selector),里面有 run 方法处理一个或多个 Channel 上源源不断的 io 事件
它的继承关系如下
-
继承自 j.u.c.ScheduledExecutorService 因此包含了线程池中所有的方法
-
继承自 netty 自己的 OrderedEventExecutor
- 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于此 EventLoop
- 提供了 EventLoopGroup parent() 方法来看看自己属于哪个 EventLoopGroup
事件循环组 EventLoopGroup
EventLoopGroup 是一组 EventLoop,Channel 一般会调用 EventLoopGroup 的 register 方法来绑定其中一个 EventLoop,后续这个 Channel 上的 io 事件都由此 EventLoop 来处理(保证了 io 事件处理时的线程安全)
-
继承自 netty 自己的 EventExecutorGroup
- 实现了 Iterable 接口提供遍历 EventLoop 的能力
- 另有 next 方法获取集合中下一个 EventLoop
处理普通与定时任务
public class TestEventLoop {
public static void main(String[] args) {
//1 创建事件循环组
// io事件,普通任务,定时任务
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(2);
//普通任务,定时任务
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
//2 获取事件循环对象
System.out.println(nioEventLoopGroup.next());
System.out.println(nioEventLoopGroup.next());
System.out.println(nioEventLoopGroup.next());
//3 执行普通任务
nioEventLoopGroup.next().execute(new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
});
//4 定时任务
nioEventLoopGroup.next().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(new Date());
}
},0,2, TimeUnit.SECONDS);
//5 关闭
nioEventLoopGroup.shutdownGracefully();
}
}
关闭 EventLoopGroup
优雅关闭 shutdownGracefully 方法。该方法会首先切换 EventLoopGroup 到关闭状态从而拒绝新的任务的加入,然后在任务队列的任务都处理完成后,停止线程的运行。从而确保整体应用是在正常有序的状态下退出的
处理IO任务
服务器代码
public class EventLoopServer {
public static void main(String[] args) {
// 只负责accept事件
NioEventLoopGroup boss = new NioEventLoopGroup();
//负责read,write事件
NioEventLoopGroup worker = new NioEventLoopGroup(2);
// 对eventloop进行指责划分 分为boss和worker
// 此处划分为accept事件和read事件
new ServerBootstrap().
group(boss,worker).
//NioServerSocketChannel只会和NioEventLoopGroup中的一个EventLoop绑定
channel(NioServerSocketChannel.class).
childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
// 为handler设置指定的NioEventGroup
ch.pipeline().addLast(worker,"handleRead",new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(worker.next());
ByteBuf buffer = (ByteBuf) msg;
System.out.println(buffer.toString(Charset.defaultCharset()));
}
});
}
}).
bind(8080);
}
}
客户端代码
public class EventLoopClient {
public static void main(String[] args) throws InterruptedException {
Channel client = new Bootstrap()
.group(new NioEventLoopGroup())
// 选择客户 Socket 实现类,NioSocketChannel 表示基于 NIO 的客户端实现
.channel(NioSocketChannel.class)
// ChannelInitializer 处理器(仅执行一次)
// 它的作用是待客户端SocketChannel建立连接后,执行initChannel以便添加更多的处理器
.handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
// 消息会经过通道 handler 处理,这里是将 String => ByteBuf 编码发出
ch.pipeline().addLast(new StringEncoder());
}
})
// 指定要连接的服务器和端口
.connect(new InetSocketAddress("localhost", 8080))
// Netty 中很多方法都是异步的,如 connect
// 这时需要使用 sync 方法等待 connect 建立连接完毕
.sync()
// 获取 channel 对象,它即为通道抽象,可以进行数据读写操作
.channel();
client.writeAndFlush("hello\n");
client.writeAndFlush("world\n");
}
}
分工
Bootstrap的group()方法可以传入两个EventLoopGroup参数,分别负责处理不同的事件
public class MyServer {
public static void main(String[] args) {
new ServerBootstrap()
// 两个Group,分别为Boss 负责Accept事件,Worker 负责读写事件
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
}
}
一个EventLoop可以负责多个Channel,且EventLoop一旦与Channel绑定,则一直负责处理该Channel中的事件
增加自定义EventLoopGroup
当有的任务需要较长的时间处理时,可以使用非NioEventLoopGroup,避免同一个NioEventLoop中的其他Channel在较长的时间内都无法得到处理
public class MyServer {
public static void main(String[] args) {
// 增加自定义的非NioEventLoopGroup
EventLoopGroup group = new DefaultEventLoopGroup();
new ServerBootstrap()
.group(new NioEventLoopGroup(1), new NioEventLoopGroup(2))
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 增加两个handler,第一个使用NioEventLoopGroup处理,第二个使用自定义EventLoopGroup处理
socketChannel.pipeline().addLast("nioHandler",new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8));
// 调用下一个handler
ctx.fireChannelRead(msg);
}
})
// 该handler绑定自定义的Group
.addLast(group, "myHandler", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(Thread.currentThread().getName() + " " + buf.toString(StandardCharsets.UTF_8));
}
});
}
})
.bind(8080);
}
}
切换的实现
不同的EventLoopGroup切换的实现原理如下
由上面的图可以看出,当handler中绑定的Group不同时,需要切换Group来执行不同的任务
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
// 获得下一个EventLoop, excutor 即为 EventLoopGroup
EventExecutor executor = next.executor();
// 如果下一个EventLoop 在当前的 EventLoopGroup中
if (executor.inEventLoop()) {
// 使用当前 EventLoopGroup 中的 EventLoop 来处理任务
next.invokeChannelRead(m);
} else {
// 否则让另一个 EventLoopGroup 中的 EventLoop 来创建任务并执行
executor.execute(new Runnable() {
public void run() {
next.invokeChannelRead(m);
}
});
}
}
- 如果两个 handler 绑定的是同一个EventLoopGroup,那么就直接调用
- 否则,把要调用的代码封装为一个任务对象,由下一个 handler 的 EventLoopGroup 来调用
2.Channel
主要方法
-
close() 可以用来关闭Channel
-
closeFuture() 用来处理 Channel 的关闭
- sync 方法作用是同步等待 Channel 关闭
- 而 addListener 方法是异步等待 Channel 关闭
-
pipeline() 方法用于添加处理器
-
write() 方法将数据写入
- 因为缓冲机制,数据被写入到 Channel 中以后,不会立即被发送
- 只有当缓冲满了或者调用了flush()方法后,才会将数据通过 Channel 发送出去
-
writeAndFlush() 方法将数据写入并立即发送(刷出)
ChannelFuture
带Future和Promise的类都是和异步方法配套使用的
public class ChannelClient {
public static void main(String[] args) throws InterruptedException {
ChannelFuture channelFuture = new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
}
})
//connect是异步非阻塞方法,在main函数中只是发起了调用,真正执行的是另一个nio线程,建立连接往往是需要消耗时间的,而如果不执行sync方法,就可能产生连接还没有建立成功,而主线程直接获取了channel
//,并进行了消息的发送
.connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel channel = channelFuture.channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true){
String input = scanner.nextLine();
if (input.equals("quit")){
channel.close();
break;
}
channel.writeAndFlush(input);
}
}).start();
//异步等待直到线程关闭
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.sync();
//线程已经关闭
System.out.println("thread is closed");
}
}
ChannelFuture连接问题
connect是异步非阻塞方法,在main函数中只是发起了调用,真正执行的是另一个nio线程,建立连接往往是需要消耗时间的,而如果不执行sync方法,就可能产生连接还没有建立成功,而主线程直接获取了channel,这个channel是没有建立连接的channel,所以消息并不能真正发送出去
ChannelFuture关闭问题
当我们要关闭channel时,可以调用channel.close()方法进行关闭。但是该方法也是一个异步方法。真正的关闭操作并不是在调用该方法的线程中执行的,而是在NIO线程中执行真正的关闭操作
如果我们想在channel真正关闭以后,执行一些额外的操作,可以选择以下两种方法来实现
-
通过channel.closeFuture()方法获得对应的ChannelFuture对象,然后调用sync()方法阻塞执行操作的线程,等待channel真正关闭后,再执行其他操作
// 获得closeFuture对象 ChannelFuture closeFuture = channel.closeFuture(); // 同步等待NIO线程执行完close操作 closeFuture.sync(); -
调用closeFuture.addListener方法,添加close的后续操作
closeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture channelFuture) throws Exception { // 等待channel关闭后才执行的操作 System.out.println("关闭之后执行一些额外操作..."); // 关闭EventLoopGroup group.shutdownGracefully(); } });
Handler -> Pipeline
ChannelHandler用来处理Channel上的各种事件,分为入栈,出栈两种,所有的ChannelHandler连成一串就组成了Pipeline流水线
- 入栈处理器通常是ChannelInBoundHandlerAdapter的子类,用来读取客户端数据,写回结果
- 出栈处理器通常是ChannelOutBoundHandlerAdapter的子类,主要对写回的结果进行加工
public class PipeLineServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 在socketChannel的pipeline中添加handler
// pipeline中handler是带有head与tail节点的双向链表,的实际结构为
// head <-> handler1 <-> ... <-> handler4 <->tail
// Inbound主要处理入栈操作,一般为读操作,发生入栈操作时会触发Inbound方法
// 入栈时,handler是从head向后调用的
socketChannel.pipeline().addLast("handler1" ,new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
String message = byteBuf.toString(Charset.defaultCharset())+" 1 ";
System.out.println("handler1 msg:"+message);
// 父类该方法内部会调用fireChannelRead
// 将数据传递给下一个handler
super.channelRead(ctx, message);
}
});
socketChannel.pipeline().addLast("handler2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String handle2 = msg.toString()+" 2 ";
System.out.println("handler2 msg:"+ handle2);
// 执行write操作,使得Outbound的方法能够得到调用
socketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("Server...".getBytes(StandardCharsets.UTF_8)));
super.channelRead(ctx, handle2);
}
});
// Outbound主要处理出栈操作,一般为写操作,发生出栈操作时会触发Outbound方法
// 出栈时,handler的调用是从tail向前调用的
socketChannel.pipeline().addLast("handler3" ,new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println(Thread.currentThread().getName() + " Outbound handler 1");
super.write(ctx, msg, promise);
}
});
socketChannel.pipeline().addLast("handler4" ,new ChannelOutboundHandlerAdapter(){
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println(Thread.currentThread().getName() + " Outbound handler 2");
super.write(ctx, msg, promise);
}
});
}
})
.bind(8080);
}
}
通过channel.pipeline().addLast(name, handler)添加handler时,可以handler取名字。这样可以调用pipeline的addAfter、addBefore等方法更灵活地向pipeline中添加handler
handler需要放入通道的pipeline中,才能根据放入顺序来使用handler
-
pipeline是结构是一个带有head与tail指针的双向链表,其中的节点为handler
- 要通过ctx.fireChannelRead(msg)等方法,将当前handler的处理结果传递给下一个handler
-
当有入栈(Inbound)操作时,会从head开始向后调用handler,直到handler不是处理Inbound操作为止
-
当有出栈(Outbound)操作时,会从tail开始向前调用handler,直到handler不是处理Outbound操作为止
具体结构如下
调用顺序如下
OutboundHandler
socketChannel.writeAndFlush()
当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从tail向前寻找OutboundHandler
ctx.writeAndFlush()
当handler中调用该方法进行写操作时,会触发Outbound操作,此时是从当前handler向前寻找OutboundHandler
总结
EventLoop定义了Netty的核心对象,用于处理IO事件,多线程模型、并发,EventLoop, channel, Thread 以及 EventLoopGroup 之间的关系如下图:
1、一个EventLoopGroup包含一个或者多个EventLoop;
2、一个EventLoop在它的生命周期内只和一个Thread绑定;
3、所有有EventLoop处理的I/O事件都将在它专有的Thread上被处理;
4、一个Channel在它的生命周期内只注册于一个EventLoop;
5、一个EventLoop可能会被分配给一个或多个Channel;
其实我们可以简单的把EventLoop及其相关的实现NioEventLoop、NioEventLoopGroup等理解为netty针对我们网络编程时创建的多线程进行了封装和优化,构建了自己的线程模型。
练习
练习一 双向通信
编写一个服务端和一个客户端,如果客户端给服务端发送ping,那么服务端会回复pong,同时客户端也要接收pong并打印
服务端代码
//实现一个双向通信,客户端发送ping,服务端回复pong
public class Server {
public static void main(String[] args) {
// acceptWorker用于处理accept事件
NioEventLoopGroup acceptWorker = new NioEventLoopGroup();
// readWriteWorker用于处理read和write事件
NioEventLoopGroup readWriteWorker = new NioEventLoopGroup();
ChannelFuture channelFuture = new ServerBootstrap().
//设置为NioEventLoopGroup
group(acceptWorker, readWriteWorker).
channel(NioServerSocketChannel.class).
childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
//readHandler 处理来自客户端的信息
socketChannel.pipeline().addLast(acceptWorker, "readHandler", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
String message = byteBuf.toString(Charset.defaultCharset());
System.out.println(message);
//如果客户端信息为ping
if ("ping".equals(message)){
//触发writeHandler
socketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("pong".getBytes()));
}
}
});
//writeHandler 向客户端返回信息
socketChannel.pipeline().addLast(readWriteWorker, "writeHandler", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
super.write(ctx, msg, promise);
}
});
}
}).
bind(8080);
}
}
客户端代码
public class Client {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup worker = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap().
group(worker).
channel(NioSocketChannel.class).
handler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) throws Exception {
//发送消息的处理器,对信息编码
ch.pipeline().addLast(new StringEncoder());
//处理服务端返回的数据
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
super.channelRead(ctx, msg);
}
});
}
}).connect(new InetSocketAddress("localhost", 8080));
channelFuture.sync();
Channel channel = channelFuture.channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true){
String input = scanner.nextLine();
if (input.equals("quit")){
channel.close();
break;
}
channel.writeAndFlush(input);
}
}).start();
//异步等待直到线程关闭
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.sync();
//线程已经关闭
System.out.println("thread is closed");
worker.shutdownGracefully();
}
}
练习二 粘包半包
通过netty的方式解决粘包和半包的问题
服务端代码
@Slf4j
public class Server {
void start() {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(1);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
//设置系统接收缓冲区大小,复显半包的问题
//serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
//serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(10));
//设置netty的缓冲区大小
//serverBootstrap.childOption(ChannelOption.RCVBUF_ALLOCATOR,new AdaptiveRecvByteBufAllocator(16,16,16));
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 连接建立时会执行该方法
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 连接断开时会执行该方法
super.channelInactive(ctx);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
channelFuture.sync();
// 关闭channel
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error");
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new Server().start();
}
}
客户端代码
public class Client {
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 {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
/*
分十次每次发送十个字节
期望服务端每次收到16个字节,一共收到十次
但是实际情况是服务端一次收到了160个字节
*/
for (int i = 0; i < 10; i++) {
ByteBuf byteBuf = ctx.alloc().buffer(16);
byteBuf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.writeAndFlush(byteBuf);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("client error");
} finally {
worker.shutdownGracefully();
}
}
}
粘包现象
客户端向服务端发送十次数据,一次16个字节,原本期望的情况是,服务端接收到十次,每次16字节,但是实际情况是服务端一次就接收到了160个字节,这就是粘包
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] REGISTERED
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] ACTIVE
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] READ: 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 |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:21:51 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xd5093bd0, L:/127.0.0.1:8080 - R:/127.0.0.1:57243] READ COMPLETE
半包现象
通过设置接收缓冲区的大小,从而限制接收方一次接收到的最大数据量,从而会对发送方的数据产生截断,这就是半包现象,只要使用TCP传输协议就一定会产生半包问题,而UDP不会
//设置接收缓冲区大小,复显半包的问题
//serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(3));
现象分析
粘包
-
现象
- 发送 abc def,接收 abcdef
-
原因
-
应用层
- 接收方 ByteBuf 设置太大(Netty 默认 1024)
-
传输层-网络层
- 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大(大于256 bytes),这 256 bytes 字节就会缓冲在接收方的滑动窗口中, 当滑动窗口中缓冲了多个报文就会粘包
- Nagle 算法:会造成粘包
-
半包
-
现象
- 发送 abcdef,接收 abc def
-
原因
-
应用层
- 接收方 ByteBuf 小于实际发送数据量
-
传输层-网络层
- 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时接收方窗口中无法容纳发送方的全部报文,发送方只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
-
数据链路层
- MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
-
解决方案一 短连接
客户端每次向服务器发送数据以后,就与服务器断开连接,此时的消息边界为连接建立到连接断开。这时便无需使用滑动窗口等技术来缓冲数据,则不会发生粘包现象。但如果一次性数据发送过多,接收方无法一次性容纳所有数据,还是会发生半包现象,所以短链接无法解决半包现象
短连接客户端代码
//通过短连接的方式解决粘包问题
public class ShortConnectClient {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10 ; i++) {
send();
//使发送变得有序
Thread.sleep(1000);
}
}
private static void send() {
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 {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf byteBuf = ctx.alloc().buffer(16);
byteBuf.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(byteBuf);
ctx.channel().close();
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("client error");
} finally {
worker.shutdownGracefully();
}
}
}
运行结果
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] REGISTERED
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] ACTIVE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ: 16B
+-------------------------------------------------+
| 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 |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ COMPLETE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 - R:/127.0.0.1:57699] READ COMPLETE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelInactive
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 ! R:/127.0.0.1:57699] INACTIVE
Sep 21, 2022 10:56:41 AM io.netty.handler.logging.LoggingHandler channelUnregistered
INFO: [id: 0x4f7f1e6b, L:/127.0.0.1:8080 ! R:/127.0.0.1:57699] UNREGISTERED
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] REGISTERED
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] ACTIVE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ: 16B
+-------------------------------------------------+
| 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 |................|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ COMPLETE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 - R:/127.0.0.1:57700] READ COMPLETE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelInactive
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57700] INACTIVE
Sep 21, 2022 10:56:42 AM io.netty.handler.logging.LoggingHandler channelUnregistered
INFO: [id: 0xb3ce3bc9, L:/127.0.0.1:8080 ! R:/127.0.0.1:57700] UNREGISTERED
...
解决方案二 定长解码器
客户端与服务器约定一个最大长度,保证客户端每次发送的数据长度都不会大于该长度。若发送数据长度不足则需要补齐至该长度
服务器接收数据时,将接收到的数据按照约定的最大长度进行拆分,即使发送过程中产生了粘包,也可以通过定长解码器将数据正确地进行拆分。服务端需要用到FixedLengthFrameDecoder对数据进行定长解码
定长解码服务端代码
@Slf4j
public class FixedLengthServer {
void start() {
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup(1);
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);;
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
//通过定长解码器规定最大消息长度为10
ch.pipeline().addLast(new FixedLengthFrameDecoder(16));
ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 连接建立时会执行该方法
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
// 连接断开时会执行该方法
super.channelInactive(ctx);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
channelFuture.sync();
// 关闭channel
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error");
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
public static void main(String[] args) {
new FixedLengthServer().start();
}
}
定长解码器客户端代码
public class FixedLengthClient {
public static void main(String[] args) throws InterruptedException {
send();
}
private static void send() {
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 {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 约定最大长度为16
final int maxLength = 16;
// 被发送的数据
char c = 'a';
// 向服务器发送10个报文
for (int i = 0; i < 10; i++) {
ByteBuf buffer = ctx.alloc().buffer(maxLength);
// 定长byte数组,未使用部分会以0进行填充
byte[] bytes = new byte[maxLength];
// 生成长度为0~15的数据
for (int j = 0; j < (int) (Math.random() * (maxLength - 1)); j++) {
bytes[j] = (byte) c;
}
buffer.writeBytes(bytes);
c++;
// 将数据发送给服务器
ctx.writeAndFlush(buffer);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("localhost", 8080).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("client error");
} finally {
worker.shutdownGracefully();
}
}
}
运行结果
INFO: [id: 0x856d9368, L:/127.0.0.1:8080 - R:/127.0.0.1:57793] READ: 16B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |a...............|
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 11:11:21 AM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0x856d9368, L:/127.0.0.1:8080 - R:/127.0.0.1:57793] READ: 16B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 62 62 62 00 00 00 00 00 00 00 00 00 00 00 00 |bbbb............|
+--------+-------------------------------------------------+----------------+
...
解决方案三 LTC长度字段解码器
在传送数据时可以在数据中添加一个用于表示有用数据长度的字段,在解码时读取出这个用于表明长度的字段,同时读取其他相关参数,即可知道最终需要的数据是什么样子的
LengthFieldBasedFrameDecoder解码器可以提供更为丰富的拆分方法,其构造方法有五个参数
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip)Copy
参数解析
-
maxFrameLength 数据最大长度
- 表示数据的最大长度(包括附加信息、长度标识等内容)
-
lengthFieldOffset 数据长度标识的起始偏移量
- 用于指明数据第几个字节开始是用于标识有用字节长度的,因为前面可能还有其他附加信息
-
lengthFieldLength 数据长度标识所占字节数(用于指明有用数据的长度)
- 数据中用于表示有用数据长度的标识所占的字节数
-
lengthAdjustment 长度表示与有用数据的偏移量
- 用于指明数据长度标识和有用数据之间的距离,因为两者之间还可能有附加信息
-
initialBytesToStrip 数据读取起点
- 读取起点,不读取 0 ~ initialBytesToStrip 之间的数据
参数图解
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0 (= do not strip header)
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+Copy
从0开始即为长度标识,长度标识长度为2个字节
0x000C 即为后面 HELLO, WORLD的长度
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 2 (= the length of the Length field)
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+Copy
从0开始即为长度标识,长度标识长度为2个字节,读取时从第二个字节开始读取(此处即跳过长度标识)
因为跳过了用于表示长度的2个字节,所以此处直接读取HELLO, WORLD
lengthFieldOffset = 2 (= the length of Header 1)
lengthFieldLength = 3
lengthAdjustment = 0
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+Copy
长度标识前面还有2个字节的其他内容(0xCAFE),第三个字节开始才是长度标识,长度表示长度为3个字节(0x00000C)
Header1中有附加信息,读取长度标识时需要跳过这些附加信息来获取长度
lengthFieldOffset = 0
lengthFieldLength = 3
lengthAdjustment = 2 (= the length of Header 1)
initialBytesToStrip = 0
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+Copy
从0开始即为长度标识,长度标识长度为3个字节,长度标识之后还有2个字节的其他内容(0xCAFE)
长度标识(0x00000C)表示的是从其后lengthAdjustment(2个字节)开始的数据的长度,即HELLO, WORLD,不包括0xCAFE
lengthFieldOffset = 1 (= the length of HDR1)
lengthFieldLength = 2
lengthAdjustment = 1 (= the length of HDR2)
initialBytesToStrip = 3 (= the length of HDR1 + LEN)
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+Copy
长度标识前面有1个字节的其他内容,后面也有1个字节的其他内容,读取时从长度标识之后3个字节处开始读取,即读取 0xFE HELLO, WORLD
客户端代码
@Slf4j
public class LengthFieldDecoder {
public static void main(String[] args) {
// 模拟服务器
// 使用EmbeddedChannel测试handler
EmbeddedChannel channel = new EmbeddedChannel(
/*
数据最大长度为1KB,长度标识前后各有1个字节的附加信息,长度标识长度为4个字节(int)
只获取其中的message信息 其他不需要
数据实际为
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| ca 00 00 00 05 fe 57 6f 72 6c 64 |......World |
+--------+-------------------------------------------------+----------------+
0的位置是长度前的信息,占位一个字节,所以lengthFieldOffset需要设置为1,从1的位置开始读取信息长度
1-4是长度信息,int类型占位四个字节,所以lengthFieldLength需要设置为4
5的位置为实际信息前的额外数据,所以lengthAdjustment需要设置为1,表明其后1位开始才是实际信息
6-a的位置是实际信息,如果想解码取出实际信息,initialBytesToStrip设置为6,因为前面的多余信息所占字节数为1+4+1=6
*/
new LengthFieldBasedFrameDecoder(1024, 1, 4, 1, 6),
new LoggingHandler(LogLevel.INFO)
);
// 模拟客户端,写入数据
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
send(buffer, "Helloooooooo");
channel.writeInbound(buffer);
send(buffer, "World");
channel.writeInbound(buffer);
}
private static void send(ByteBuf buf, String msg) {
// 得到数据的长度
int length = msg.length();
byte[] bytes = msg.getBytes(StandardCharsets.UTF_8);
// 将数据信息写入buf
// 写入长度标识前的其他信息 占一个字节
buf.writeByte(0xCA);
// 写入数据长度标识 一个int占四个字节
buf.writeInt(length);
// 写入长度标识后的其他信息 占一个字节
buf.writeByte(0xFE);
// 写入具体的数据
buf.writeBytes(bytes);
}
}
运行结果
INFO: [id: 0xembedded, L:embedded - R:embedded] READ: 12B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 6f 6f 6f 6f 6f 6f 6f |Helloooooooo |
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 2:00:15 PM io.netty.handler.logging.LoggingHandler channelReadComplete
INFO: [id: 0xembedded, L:embedded - R:embedded] READ COMPLETE
Sep 21, 2022 2:00:15 PM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xembedded, L:embedded - R:embedded] READ: 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 57 6f 72 6c 64 |World |
+--------+-------------------------------------------------+----------------+
问题1.系统缓冲区大小设置无效
原想通过以下代码,强行设置系统接收缓冲区的大小为10,从而复现半包的问题,但是实际发现并不可行,其原因是issue,由于不同os的差异,实际上这个参数未必会和设置的一样,最终缓冲区大小还是由os决定的,netty的默认大小是1024B。
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.option(ChannelOption.RCVBUF_ALLOCATOR,new FixedRecvByteBufAllocator(8));
练习三 协议设计与解析
Redis协议
如果我们要向Redis服务器发送一条set name TianLe Zhou的指令,需要遵守如下协议
// 代表该指令一共有3部分,每条指令之后都要添加回车与换行符
*3\r\n
// 第一个指令的长度是3
$3\r\n
// 第一个指令是set指令
set\r\n
// 下面的指令以此类推
$4\r\n
name\r\n
$11\r\n
TianLe Zhou\r\n
客户端代码
public class RedisClient {
/*
*3
$3
set
$4
name
$11
TianLe Zhou
*/
public static void main(String[] args) {
final byte[] FORMAT = "\r\n".getBytes();
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 {
ch.pipeline().addLast(worker,new LoggingHandler(LogLevel.INFO));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes("*3".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("$3".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("set".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("$4".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("name".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("$11".getBytes());
buffer.writeBytes(FORMAT);
buffer.writeBytes("TianLe Zhou".getBytes());
buffer.writeBytes(FORMAT);
// 发送命令给Redis执行
ctx.channel().writeAndFlush(buffer);
}
//获取Redis返回的结果
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println(buf.toString(Charset.defaultCharset()));
}
});
}
});
// 连接到redis
ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress("localhost", 6379)).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("client error");
} finally {
worker.shutdownGracefully();
}
}
}
运行结果
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] WRITE: 41B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 2a 33 0d 0a 24 33 0d 0a 73 65 74 0d 0a 24 34 0d |*3..$3..set..$4.|
|00000010| 0a 6e 61 6d 65 0d 0a 24 31 31 0d 0a 54 69 61 6e |.name..$11..Tian|
|00000020| 4c 65 20 5a 68 6f 75 0d 0a |Le Zhou.. |
+--------+-------------------------------------------------+----------------+
Sep 21, 2022 2:26:19 PM io.netty.handler.logging.LoggingHandler flush
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] FLUSH
Sep 21, 2022 2:26:19 PM io.netty.handler.logging.LoggingHandler channelRead
INFO: [id: 0xd679c3af, L:/127.0.0.1:59257 - R:localhost/127.0.0.1:6379] READ: 5B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 2b 4f 4b 0d 0a |+OK.. |
+--------+-------------------------------------------------+----------------+
自定义协议
组成要素
-
魔数:作为判定协议是否有效的依据,例如Java起始字节码是CAFEBABE
-
版本号:可以支持协议的升级
-
序列化算法:消息正文到底采用哪种序列化反序列化方式
- 如:json、protobuf、hessian、jdk
-
指令类型:与业务相关
-
请求序号:为了双工通信,提供异步能力
-
正文长度
-
消息正文:序列化,一般是json
自定义编解码协议实现
编解码器代码
//通过泛型制定编解码的对象
public class MessageCodec extends ByteToMessageCodec<Message> {
//出栈时进行编码
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
//魔数 BAKAZHOU 占八个字节
out.writeBytes("BAKAZHOU".getBytes());
//版本 1 占四个字节
out.writeInt(1);
//序列化算法 0代表Json 1代表jdk 占四个字节
out.writeInt(0);
//指令类型 int类型占四个字节
out.writeInt(msg.getMessageType());
//请求序号 占四个字节
out.writeInt(msg.getSequenceId());
//将消息正文站位bytes
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] msgBytes = bos.toByteArray();
//正文长度 占四个字节
out.writeInt(msgBytes.length);
//正文前一共有28个字节
//写入正文
out.writeBytes(msgBytes);
}
//入栈时进行解码
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//魔数 BAKAZHOU 8字节
String magicNum = in.readBytes(8).toString(Charset.defaultCharset());
//版本号 4字节
int version = in.readInt();
//序列化算法
int serializationAlgorithm = in.readInt();
int messageType = in.readInt();
//请求序号
int sequenceId = in.readInt();
//正文长度
int length = in.readInt();
//正文内容
byte[] msg = new byte[length];
in.readBytes(msg,0,length);
//判断序列化方式
switch (serializationAlgorithm){
case 0:
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(msg));
Message message = (Message) ois.readObject();
//传给下一个处理器使用
out.add(message);
break;
case 1:
break;
default:
break;
}
}
}
-
编码器与解码器方法源于父类ByteToMessageCodec,通过该类可以自定义编码器与解码器,泛型类型为被编码与被解码的类。此处使用了自定义类Message,代表消息
public class MessageCodec extends ByteToMessageCodec<Message>Copy -
编码器负责将附加信息与正文信息写入到ByteBuf中,其中附加信息总字节数最好为2n,不足需要补齐。正文内容如果为对象,需要通过序列化将其放入到ByteBuf中
-
解码器负责将ByteBuf中的信息取出,并放入List中,该List用于将信息传递给下一个handler
测试代码
@Slf4j
public class TestMessageCodec {
public static void main(String[] args) throws Exception {
MessageCodec messageCodec = new MessageCodec();
EmbeddedChannel channel = new EmbeddedChannel(
// 添加解码器,避免粘包半包问题
new LengthFieldBasedFrameDecoder(1024, 28, 4, 4, 0),
new LoggingHandler(LogLevel.INFO),
messageCodec);
//encode
LoginRequestMessage loginUser = new LoginRequestMessage("bakazhou", "123456");
channel.writeOutbound(loginUser);
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
System.out.println(byteBuf);
messageCodec.encode(null, loginUser, byteBuf);
channel.writeInbound(loginUser);
System.out.println(byteBuf);
}
}
运行结果
第一行0-7的位置即为魔数BAKAZHOU
第一行8-b为版本号
....
以此类推
INFO: [id: 0xembedded, L:embedded - R:embedded] WRITE: 285B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 42 41 4b 41 5a 48 4f 55 00 00 00 01 00 00 00 00 |BAKAZHOU........|
|00000010| 00 00 00 00 00 00 00 00 00 00 01 01 ac ed 00 05 |................|
|00000020| 73 72 00 41 63 6f 6d 2e 63 6e 2e 74 77 2e 67 72 |sr.Acom.cn.tw.gr|
|00000030| 61 64 75 61 74 65 2e 62 61 6b 61 7a 68 6f 75 2e |aduate.bakazhou.|
|00000040| 50 72 61 63 74 69 63 65 33 2e 6d 65 73 73 61 67 |Practice3.messag|
|00000050| 65 2e 4c 6f 67 69 6e 52 65 71 75 65 73 74 4d 65 |e.LoginRequestMe|
|00000060| 73 73 61 67 65 76 36 80 36 45 1a d9 d3 02 00 02 |ssagev6.6E......|
|00000070| 4c 00 08 70 61 73 73 77 6f 72 64 74 00 12 4c 6a |L..passwordt..Lj|
|00000080| 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b |ava/lang/String;|
|00000090| 4c 00 08 75 73 65 72 6e 61 6d 65 71 00 7e 00 01 |L..usernameq.~..|
|000000a0| 78 72 00 35 63 6f 6d 2e 63 6e 2e 74 77 2e 67 72 |xr.5com.cn.tw.gr|
|000000b0| 61 64 75 61 74 65 2e 62 61 6b 61 7a 68 6f 75 2e |aduate.bakazhou.|
|000000c0| 50 72 61 63 74 69 63 65 33 2e 6d 65 73 73 61 67 |Practice3.messag|
|000000d0| 65 2e 4d 65 73 73 61 67 65 d6 50 c5 58 ac 0f 63 |e.Message.P.X..c|
|000000e0| 63 02 00 02 49 00 0b 6d 65 73 73 61 67 65 54 79 |c...I..messageTy|
|000000f0| 70 65 49 00 0a 73 65 71 75 65 6e 63 65 49 64 78 |peI..sequenceIdx|
|00000100| 70 00 00 00 00 00 00 00 00 74 00 06 31 32 33 34 |p........t..1234|
|00000110| 35 36 74 00 08 62 61 6b 61 7a 68 6f 75 |56t..bakazhou |
+--------+-------------------------------------------------+----------------+
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
//成功进行了解码buf填充了289
PooledUnsafeDirectByteBuf(ridx: 0, widx: 289, cap: 512)
练习四 简易的IM通讯系统
任务说明
实现一个简单的IM通讯系统,包括用户的登录,用户间的消息收发,用户群组间的消息收发,以及群组相关的系列操作,例如创建群聊,加入群聊,退出群聊等
基本架构图
包结构
- client: 与客户端相关的文件
- command: 客户端的所有基本操作指令
- message: 所有类型的请求信息和返回信息
- protocol: 自定义编解码器
- server: 与服务端相关的文件
- handler: 服务端处理入栈请求的处理器
- session: 缓存用户与channel的关系,用户与群组之间的关系
代码实现
git repository practice3模块为具体实现代码