目录
27.ChannelHandler总结
27.1 一些概念
27.2 到底有几个handler?真的只有你想的那样吗?
27.3 channel.writeAndFlush 和 ctx.writeAndFlush的区别
27.4 ByteBuf的创建和销毁
27.5 Channel的生命周期方法
27.5.1 handlerAdded
27.5.2 channelRegistered
27.5.3 channelActive
27.5.4 channelRead
27.5.5 channelReadComplete
27.5.6 channelInactive
27.5.7 channelUnregistered
27.5.8 handlerRemoved
27.5.9 总结整个生命周期
27.6 异常的处理
27.7 Handler相关
27.7.1 IdleStateHandler(空闲检查)
27.7.1.1 空闲检查作用
27.1.1.2 心跳的两种模式
27.1.1.3 服务端再度容错
27.7.2 WebSocketServerProtocolHandler
27.7.2.1 netty如何支持websocket
27.7.2.2 双工推送
27.7.3 Sharable(可以共享的handler)
27.7.3.1 问题
27.7.3.2 解决问题
27.7.3.3 能被共用的handler
27.7.3.4 代码验证
27.7.3.5 总结
27.7.3.6 补充
27.8 规范化开发
前面说了一大堆关于编解码的概念,其实不管怎么说编码器。本质上就是一个个的handler。更多的来说,他就是一个ChannelHandler。下面我们来总结关于一波ChannelHandler的东西
ChannelHandler的作用
当channel网络连接建立之后,通过ChannelHandler进行IO相关的操作,也就是拿到IO过来的数据,然后进行我们的业务处理,既然已经是获取数据进行业务处理了,可见其重要性不言而喻。
pipeline的功能作用
netty设计ChannelHandler调用的时候,面对多个ChannelHandler其实就是按照pipeline来组织调用的,也就是pipeline是多个ChannelHandler处理器的组织者。
Pipeline中Handler的执行流程
我们可以初步的得到一个关于Pipeline的结构图,在这个流水线中,众多的handler被组织在头handler和尾handler之间执行。其中可能有netty自己的原生的handler,也可能是我们自定义实现的一些handler(自定义编码器就是自定义的Handler)
这些handler按照方向可以划分成两类。输入handler和输出handler。是一种责任链设计模式,在底层是一种双向链表来组织的数据结构。
主要负责两件事,一个是输入处理(入栈操作),输出处理(出栈操作),于是按照用途来区分,我们的结构图就变为这样了:
当客户端与服务端建立连接后,客户端首先给服务端发送数据,此时数据被封装成ByteBuf类型的数据。
服务端接收到数据后,使用的是head->handler1->handler2->tail这样的处理链路,这个处理是在Inbound处理器里面的
服务端写出数据时,使用的是tail->handler4->handler3这样的链路,这个处理是在outound处理里面的。
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 19:05
* @Version 1.0
*/
public class MyNettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder()) ;
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
});
Iterator> iterator = pipeline.iterator();
while (iterator.hasNext()) {
Map.Entry next = iterator.next();
System.out.println("#########" + next.getKey() + ":" + next.getValue().getClass()) ;
}
}
});
serverBootstrap.bind(8000);
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 19:12
* @Version 1.0
*/
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("leomessi");
group.shutdownGracefully();
}
}
服务端控制台输出结果:
在上面这段代码中我们手动添加了两个handler,一个是StringDecoder,一个是ChannelInboundHandlerAdapter,算上内部的head和tail,我们认为此时pipeline流水线应该一共是四个Handler,这个很自然。
测试的时候,我们直接启动服务端程序,但是控制台没有任何关于handler的打印输出。
因为此时还没有客户端连接过来,所以并没有触发initChannel这一初始化方法,所以验证了initChannel这一方法在服务端启动的时候并不会执行,而是在客户端连接过来的时候才开始执行,连接才意味着Channel管道的建立,以及后续一系列的IO事件的进行。
当客户端发起连接后,并且写出数据给服务端,我们看到如下输出:
这是三个Handler,再加上内部不输出的head和tail这俩Handler,一共就是五个Handler处理器。可见,并非像我们猜测的那样:有四个Handler。那么多出来的那一个Handler是哪个?其实是serverBootstrap.childHandler(new ChannelInitializer
它也被维护在pipeline处理器里面,所以这个图应该变成如下所示:
这里需要再一次做声明,Pipeline管道这个东西,其实就是属于Channel的,更进一步来说,它是属于SocketChannel的,而每一个SocketChannel都是一个客户端的连接,我们每一个客户端连接都会有自己的一个pipeline,也就是每一个客户端连接都需要执行一遍流水线里面所有的handler处理器,这个前面我们说过,而且想想也是符合业务设计的,不然你混到一起就乱了。
于是结构图进一步可以变为这样:
但是当我们服务端代码变为这样的时候,我们有多个ChannelInboundHandlerAdapter,那么此时多个ChannelInboundHandlerAdapter里面的ctx还是一样的吗?是同一个ctx上下文吗?
但是这里有一个tip:
一个对象没有实现toString的时候,直接输出对象其实是它的类全限定名#hashcode
但是实现了toString后,只能看到他的toString了。
这里我们输出ctx,ctx其实就是实现了toString,所以我们输出hashcode来对比
测试1:启动两个client,查看同一个channelInboundHandlerAdapter对应的ctx是否相同,答案是不同
测试2:启动一个客户端client,但是是不同的ChannelInboundHandlerAdapter,那么ctx也是不同的
总结:
测试一下:发现ctx确实不一样
其实思考一下想想,ctx肯定是不一样的呀,你都改变转化到另外一个pipeline中了,上下文咋可能一样,你都变了,上下文必然不同。
并且ChannelOutboundHandlerAdapter是同理的
channel.writeAndFlush():
因为SocketChannel是管理整个连接的,它是整个SocketChannel,也就是整个pipeline流水线,所以它是从整个pipeline的最后一个outHandler开始往前走,依次找到所有的outboundHandler,依次执行。
ctx.writeAndFlush():
ctx只是当前handler的上下文,它是从当前这个handler开始走你的流水线,往前找到所有的outboundHandler,依次执行。这不就对应上了前面所说的:每一个Inbound/outboundHandler对应的ctx是不一样的。
之前我们使用的创建方式就是直接分配:
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(8);
但是在netty中的pipeline中,如果你要创建,不建议你这样使用,应该采用如下这种方式:
ByteBuf byteBuf = ctx.alloc().buffer();
因为此时这样创建出来的ByteBuf是使用上下文对象创建出来的,它的内容是和Handler也就是Pipeline绑定的,可以在handler结束或者pipeline释放之后就会被释放掉,不用你操作,在netty的体系中自动被释放,减少安全问题。ByteBuf使用完后一定要release,使用分配的时候要使用计数器分配,方便释放。
tip:
前者是直接操作系统内核级内存分配,难管理。
后者是pipeline Handler级别的内存分配,当pipeline销毁后,对应ByteBuf的内存空间也跟着释放掉,相对好管理。
也可以称之为ChannelHandler中的回调方法----->其实就是诸如像ChannelInboundHandlerAdapter中的ChannelRead方法一样的各种回调方法。来看一组代码:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:00
* @Version 1.0
*/
public class MyNettyServer2 {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx, msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
}
});
}
});
}
}
我们看到上面在写入ChannelInboundHandlerAdapter的时候,其实复写了它的一组四个方法,这四个方法其实都是回调方法,当我们的SocketChannel在处理一系列事件行为的时候都会触发对应的函数,这里的函数都是SocketChannel的,这也是一句废话,你整个pipeline都是SocketChannel,函数肯定也是SocketChannel的呀。
下面我们依次看一下每个函数,在看这些函数之前,我们修改一下程序,在每一个函数内部打印一句话:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:00
* @Version 1.0
*/
public class MyNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered invoke...");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("channelActive invoke...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("channelRead invoke...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("channelReadComplete invoke...");
}
});
}
});
serverBootstrap.bind(8000);
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:09
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("leomessi");
group.shutdownGracefully();
}
}
tip:
从客户端启动到发数据,到服务端接收数据,回调函数依次被执行,顺序分别是:ChannelRegistered,ChannelActive,ChannelRead,ChannelReadComplete。首先说明一下:这些事件都是和SocketChannel有关的,也就是和数据IO,并不涉及客户端的连接,因为客户端连接时ServerSocketChannel处理做的。SocketChannel只负责IO相关的操作,不负责accepte()建立连接的操作
启动服务端,客户端,输出如下:
handlerAdded是父类方法
initChannel方法是在一开始执行下面所有事件之前执行,执行过程中要添加handler,会触发handlerAdded事件。
当由ServerSocketChannel完成连接之后,此时创建了SocketChannel,这个SocketChannel要和Reactor模型中的worker线程绑定,不绑定你怎么会有线程来处理后续的数据收发呢?这个绑定动作其实就是绑定worker线程。当SC绑定线程成功后,意味着SC这一Channel通道绑定成功了,绑定成功后,netty会回调该方法。tip:只不过在netty中,不管是worker还是boss,其实都是EventLoop。【不是Channel处理你的数据,而是Channel对应的线程来处理你的数据,无论是SSC还是SC。】
这也同时带来一个新问题:
会不会handler创建了,也就是SSC完成了accept连接后,但是在SC和worker线程绑定的时候没有绑定分配上
在多线程的场景下,一切皆有可能,因为在netty中worker线程其实是EventLoopGroup这一线程池中的一个EventLoop线程。EventLoopGroup内部封装了很多EventLoop线程,但是总归是有数量的,可能由于你客户端过于多了,超出了EventLoopGroup封装的EventLoop线程个数,此时就可能分配不上,一旦分配不上,那么SC此时就无法绑定分配线程,所以此时ChannelRegister就不会回调。
所以:不是说客户端-服务端连接成功后,就一定能回调channelRegsiter方法。
如果该方法不回调,意味着SC一直没有和worker线程绑定成功,那么就不能进行后面的通信操作,那么就阻塞等待了。
当channel的准备工作完成了,所有的pipeline上面的handler都添加完成了,此时回调这个函数。channel准备就绪,就可以开始收发数据了。其实是initChannel方法调用成功了,handler都添加了。channelActive方法调用就代表着initChannel成功了。
package com.messi.netty_core_02.netty12;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:00
* @Version 1.0
*/
public class MyNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered invoke...");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("channelActive invoke...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("channelRead invoke...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("channelReadComplete invoke...");
}
});
}
});
serverBootstrap.bind(8000);
}
}
package com.messi.netty_core_02.netty12;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:09
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//在所有的handler都添加完之后触发该回调方法,代表客户端-服务端完全准备注册完成了
//下一步就可以进行发数据了
ctx.channel().writeAndFlush("hello");
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
// channel.writeAndFlush("leomessi");
group.shutdownGracefully();
}
}
//这个地方我们的客户端没有在连接connect之后阻塞等待发送数据,而是在连接之后,触发了Active事件
//此时双方链接完毕就能发送数据了,这个时机就被放到了这里
这里总结一点:就是在客户端和服务端发起连接,三次握手之后,双方都会触发自己的active事件,Active事件在整个连接周期,只会触发一次。
在完成tcp三次握手之后,netty会在客户端pipeline中触发active事件的,服务端也是如此,应用过程中,可以使用这个事件回调来发起数据,客户端和服务端都可以利用这个事件向对方发数据。
对于服务端:
就是双方的active事件都是在三次握手之后,就触发了,对应的channelRegistered事件是socketChannel注册到worker线程成功后并且触发在active事件之前,也就是说服务端的channelRegistered其实也是三次握手过程中去回调的
对于客户端:
其他的都差不多,只是客户端的channelRegistered方法是在三次握手之前执行的,就是它发起的时候没等到服务端ack就触发了该channelRegistered回调方法。你可以思考一下,如果不在connect连接之前注册到epoll上,客户端如何知道connect就绪了呢?
接收数据的操作,每次数据发过来时都会回调该方法
读操作结束完成后,回调该方法。在该方法内,我们可以做一些资源的释放,最终在这里结束读取数据的周期
当连接被断开的时候,调用该方法,其实就是tcp连接进行close了,应用层会回调该方法
当连接断开后 并且 当worker线程处理完对应的逻辑操作,把该线程归还给线程池后,此时会回调该方法:channelUnregistered
服务端断开连接:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:00
* @Version 1.0
*/
public class MyNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered invoke...");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("channelActive invoke...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("channelRead invoke...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("channelReadComplete invoke...");
//关闭Channel
ctx.channel().close();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelUnregistered invoke...");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("channelInactive invoke...") ;
}
});
}
});
serverBootstrap.bind(8000);
}
}
//在channelReadComplete中关闭连接,就会依次调用channelInative和channelUnregistered
//首先代表连接断开,然后归还线程给线程池。你也可以在客户端连接完了后直接断开,也会触发这个流程。
客户端断开连接
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:00
* @Version 1.0
*/
public class MyNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelRegistered invoke...");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("channelActive invoke...");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("channelRead invoke...");
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
log.info("channelReadComplete invoke...");
//关闭Channel
// ctx.channel().close();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
log.info("channelUnregistered invoke...");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("channelInactive invoke...") ;
}
});
}
});
serverBootstrap.bind(8000);
}
}
package com.messi.netty_core_02.netty12;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/21 21:09
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//在所有的handler都添加完之后触发该回调方法,代表客户端-服务端完全准备注册完成了
//下一步就可以进行发数据了
ctx.channel().writeAndFlush("hello 0819");
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
// channel.writeAndFlush("leomessi");
// group.shutdownGracefully();
//客户端进行关闭SocketChannel连接
channel.close();
}
}
输出:
客户端:
服务端:
看到其他的差不多,只是channelReadComplete事件被触发了两次,因为客户端关闭的时候也会触发一个数据,类似于一个信号,只是不需要对端去读该信号数据,它只是一个信号,所以不需要在read中触发读,但是会做channelReadComplete方法的回调进而去做channelInactive和channelUnregistered方法的回调。当然如果你的客户端被kill暴力杀死了,也是可以触发这两个回调方法的,都一样,netty还是厉害,断开了也能感知到。
但是也有一些情况感知不到,比如说:网络波动【网线断了,物理级别的断开】,则就感知不到了,但是可以做心跳保活机制。【后面细致总结】
这个和上面几个不一样,这个也是父类方法,它只是基础了。
它的作用时间就是当handler被从pipeline移除的时候,就会触发,就是当最后pipeline结束的时候就是在这里移除的时候触发。
以上这些回调就是对应着channel的生命周期,在什么时候做什么事,我们就能在这些回调事件中知道他所处的状态,对他进行监听,监控到底处于什么情况了。 这就是Netty为我们做的封装,让我们在应用层去操控底层操作系统级别的网络各个时期。
所以我们可以总结一下这个事件的脉络了。步骤如下:
1.Netty调用initChannel,然后pipeline加入一系列handler,加入完成后触发handlerAdded方法
2.EventLoop会分配给对应的SocketChannel,此时会触发channelRegistered方法注册到Selector【Selector底层是epoll/poll/select】
3.client和Server连接建立后,此时处于双方可以使用,此时触发channelActive方法
4.进行读操作,此时触发channelRead方法
5.读操作完成后,此时回调channelReadComplete方法
6.tcp链接断开(也可能是被动断开),此时会触发回调channelInactive方法
7.当最后的任务执行完时(最后的任务其实就是指断开连接这一任务哈哈哈),对应的EventLoop线程已经完成了全部的任务,此时就会归还回收给EventLoopGroup线程池。归还完线程后回调channelUnRegistered方法
如果期间异常触发,会触发exceptionCaught方法
正因为做了这一系列细粒度的回调方法,才可以让我们用户对CS连接,收发数据的过程进行非常细致化的掌控:
Netty是一个非阻塞I/O客户端-服务器框架,主要用于开发Java网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。[3]Netty包括了反应器编程模式的实现。
异步事件驱动的网络应用程序框架:
Netty的事件驱动不仅仅局限于read或write这种IO读写事件,而是对读或写事件做了更加细粒度的分离,设置了一系列的事件,并且提供了事件回调机制。这样可以让用户更加少的进行编码,并且允许用户更加细粒度的控制事件的生命周期过程。
exceptionCaught事件
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 15:07
* @Version 1.0
*/
public class MyNettyClient {
private static final Logger log = LoggerFactory.getLogger(MyNettyClient.class);
public static void main(String[] args) throws InterruptedException {
//初始化一个客户端启动器
Bootstrap bootstrap = new Bootstrap();
//客户端也要启动一个连接通道,它是做IO和连接的,所以就是SocketChannel即可
bootstrap.channel(NioSocketChannel.class);
//我们实现一个NioEventLoopGroup,是一个线程池
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("接收到服务端的数据为:{}",(String) msg);
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("hello 0819");
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 15:08
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服务端接收到的数据为:{}",msg);
super.channelRead(ctx,msg) ;
}
});
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
super.channelRead(ctx,msg);
//模拟异常抛出,实际开发时可以做成自定义异常 以code,msg来定义
throw new RuntimeException("空指针异常");
}
});
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("服务端接收到的数据为:{}",msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//
if ("空指针异常".equals(cause.getMessage())) {
log.info("cause.getCause(): " + cause.getCause());
//把异常信息写出给客户端
ctx.channel().writeAndFlush(cause.getMessage()) ;
}
}
});
}
});
serverBootstrap.bind(8000);
}
}
客户端输出:
服务端输出:
分析现象:
exceptionCaught:
1.exceptionCaught可以处理handler中的异常,所有抛出的异常都能在这里进行处理,不用每个handler都处理一下
2.exceptionCaught可以跨ChannelInboundHandlerAdapter处理别的handlerAdapter的异常。我们只需要重写一个exceptionCaught方法即可,不需要每一个handlerAdapter都去重写一个exceptionCaught
之前接触了这些handler,都在下面图里面:
此外还有一些普通的handler用来支持各种功能,下面会总结一些。
这是一个用来做空闲检查的Handler处理器
我们说当客户端和服务端在进行网络通信的时候,可能会做一些业务数据的处理,或者因为网络的问题,存在一些延迟或者耗时的长操作。
在这个期间可能存在以下三种情况:【注意:由于我们主要是针对于服务端的开发,所以这里所有空闲状态都是相对于服务端来说的】
1.读空闲:客户端可能长时间没有向服务端写数据,此时站在服务端的角度来说,服务端就处于读数据的空闲,也就是读空闲
2.写空闲:服务端可能长时间没有向客户端响应数据(写数据),此时站在服务端的角度而言,服务端就处于写数据的空闲,也就是写空闲
3.读写空闲,双方静默,都没有操作。站在服务端的角度而言,既没有读入也没有写出,处于读写空闲状态,空闲指的就是没有通信。
IdleStateHandler就可以进行空闲监控,他就是站在服务端角度来说的,所以它的监控其实也是对服务端而言的
来看下面一段读写空闲检查的代码操作:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 16:19
* @Version 1.0
*/
public class MyNettyServer2 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringDecoder());
/**
* 添加空闲监听Handler,指定四个参数:
* 参数1:读空闲触发时间,认为超过 3秒没有监听到读事件的触发 就是读空闲
* 参数2:写空闲触发时间,认为超过 5秒没有监听到写事件的触发 就是写空闲
* 参数3:读写空闲触发时间,认为超过 7秒没有监听到读或写事件的触发 就是读写空闲
* 参数4:时间单位,设置时间单位为秒
*/
pipeline.addLast(defaultEventLoopGroup,new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
//空闲事件监听触发
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.READER_IDLE) {
log.info("连接{} 发生读空闲",ctx.channel());
} else if (idleStateEvent.state() == IdleState.WRITER_IDLE) {
log.info("连接{} 发生写空闲",ctx.channel());
} else if (idleStateEvent.state() == IdleState.ALL_IDLE){
log.info("连接{} 发生读写空闲",ctx.channel());
}
}
});
}
});
serverBootstrap.bind(8000);
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 16:19
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException{
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("hello 0819");
}
}
先启动服务端,后启动客户端,记住一定要启动客户端,客户端去连接服务端,只有客户端连接上了服务端,服务端方才能触发initChannel方法,进而才能有后续的操作
服务端:
客户端:
客户端是先进行注册Register,然后才会监测到连接成功
我们看到确实输出了我们预期的信息,而且看一下时间间隔也基本符合我们的设置
心跳和空闲检查在网络通信中是两个相关但不完全相同的概念,它们通常被用于保持连接的稳定和可靠。
心跳是一种周期性发送的小型数据包,用于确认通信双方的连接仍然活跃。通常,心跳消息由发送方定期发送给接收方,接收方收到心跳消息后会发送响应以表示连接的存活。心跳消息的主要目的是检测连接是否仍然有效,以防止连接超时或意外断开。
空闲检查是一种用于检测连接是否处于空闲状态的机制。通过监测在一段时间内是否有数据传输或其他活动,可以判断连接是否处于空闲状态。如果连接在一段时间内没有任何活动,可能表示连接已经闲置或出现问题。在这种情况下,可以采取相应的操作,例如关闭空闲连接或发送空闲状态的通知。
虽然心跳和空闲检查都与连接的稳定性和可靠性有关,但它们的作用和目的略有不同。心跳主要用于确认连接的存活性,而空闲检查主要用于检测连接的空闲状态。然而,它们可以相互结合使用来确保连接的稳定和可靠,例如通过在心跳消息中包含空闲状态的信息或在空闲检查中发送心跳消息来同时检测连接的存活和空闲状态。
编码如下:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 16:19
* @Version 1.0
*/
public class MyNettyServer3 {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer2.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringDecoder());
/**
* 添加空闲监听Handler,指定四个参数:
* 参数1:读空闲触发时间。但是这里设置为0秒,说明不监控读空闲
* 参数2:写空闲触发时间,但是这里设置为0秒,说明不监控写空闲
* 参数3:读写空闲触发时间,认为超过 7秒 就是读写空闲
* 参数4:时间单位,设置时间单位为秒
*/
pipeline.addLast(defaultEventLoopGroup,new IdleStateHandler(0,0,7, TimeUnit.SECONDS));
pipeline.addLast(defaultEventLoopGroup,new ChannelInboundHandlerAdapter(){
//空闲事件监听触发
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.ALL_IDLE) {
//如果7秒没发生读写事件,那么触发读写空闲
log.info("连接{} 发生读写空闲",ctx.channel());
ctx.channel().close();
}
}
});
}
});
serverBootstrap.bind(8000);
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 16:19
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException{
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("hello 0819");
}
}
服务端:
客户端:
分析一下服务端的控制台输出:
1.当7秒内服务端配置的IdleStateHandler这一Handler还没有监控到读或写事件时,会触发读写空闲,并且设置状态为IdleState.ALL_IDLE。
2.当监控到空闲状态后,会回调userEventTriggered方法,在该方法中你可以对监控到的读写空闲状态,进行善后处理,比如:保存当前客户端连接所对应的业务数据到数据库,然后断开该客户端连接所对应的SocketChannel通道。
具体的操作在上述代码就是:ctx.channel().close(),首先通过ctx上下文对象获取到当前客户端连接所对应的SocketChannel对象,然后执行close方法关闭该channel。
当然也需要一个过程:CLOSE表示进行触发客户端连接的关闭---->INACTIVE表示完成该客户端连接的关闭---->UNREGISTERED表示服务端给该断开连接的客户端分配的线程给回收完成了
后续:
根据上述代码的测试可以观察到客户端确实已经断开了,输出了channelInactive方法的回调,断开了客户端连接后,这个连接就不能再作用了,并且该客户端连接所对应分配的EventLoop线程资源都归还给线程池了。
但是这个空闲时间间隔不能太短,可能客户端或服务端执行了一段很长很复杂的业务逻辑导致无法立刻回应读或写,再或者网络就抖动了一下,马上又恢复好了,此时你就需要考虑你的业务考量,这里你需要按照你的实际业务控制这个空闲时间长度,但是也不能太长了。
实际上上面设置的每隔7秒检查一次,逻辑上就是一种心跳的思路,其他的思路像可能是一段时间内通信几次发现都没有认为断开,我们这里这种就是一个范围,直接认为超时断开。
1.时间间隔不要太小,避免误伤
2.监控到空闲阙值,可以断开连接,也可以就加一些业务处理,兜底之类的善后操作
3.可能存在一种问题,服务端发现空闲然后断开了,但是人家本来就是卡了,不是真正的断开了,或者就是网络抖动导致断开了一下,此时客户端是无辜的,所以说如果你的阙值间隔设置的太小,就会在这种情况下误伤客户端。误伤肯定是避免不了的,凡事都不是绝对的,所以我们就需要考虑在被误伤后要做一些容错的处理,比如说:当客户端被误伤断开后,此时服务端断开SocketChannel-客户端连接,客户端也会回调到channelInactive方法,客户端发现了自己被断开。我们可以在channelInactive方法中做一些判断,如果判断除客户端是被误伤断开的,那么我们就需要重试与服务端重新连接一下,但是重试一次可能会失败,所以我们需要多次重试。对于多次重试,我们可以起一个定时任务周期性的重试几次,这样重试的可靠性又变得更高了。
如果你使用定时任务处理,就可以使用netty的定时任务,也就是时间轮,即HashWheelTimer这个类。为什么没有使用jdk原生的Timer呢?
无论是jdk原生的Timer还是Netty封装的HashWheelTimer,其实本质上都是一种数据结构,数据结构就是为了提升数据查询和检索的效率。
其实并不是说jdk原生的Timer一定比HashWheelTimer全方位的差。而是你需要综合考究,你别忘了Netty以及后续业务架构的要求。高性能异步通信框架,高性能高可用。
jdk原生的Timer:是基于二叉树这一数据结构,对于每一个定时任务都会有一个启动时间,我们使用二叉树按照启动时间的先后进行排序这非常多的定时任务。
HashWheelTimer:是基于Hash时间轮的。其实它就是基于jdk原生的Timer去优化,去做的,但是它并不像jdk的Timer一样基于二叉树去排序的非常精确,而是划分了很多个分区,每一个分区都会分配一个时间间隔,在这一时间间隔内可以启动起来的所有定时任务都会放在这一分区内。然后这一分区内的定时任务的执行先后顺序按照放入的先后顺序来执行。
其实答案已经出来了,之所以我们不使用jdk原生的Timer,就是因为在高并发的场景下,可能会有很多定时任务,我们或许不需要那么的精确,没必要说真一个个按照顺序去排列起来,这样太耗费性能了。我们做HashWheelTimer就是为了节省精确排序定时任务和搜索该执行哪一个定时任务所做的性能优化。但是做的让步就是:同一分区内的定时任务执行的先后顺序可能不精确,HashWheelTimer底层肯定会对这一部分的让步导致的不精确会进行一些优化和改进。但是这个让步是必要的,是必须的。
除此之外,HashWheelTimer相比jdk原生的Timer肯定还做了许多其他的优化,具体到看源码的时候再去总结
我们再总结一下心跳和空闲检查Handler之间的联系:
IdleStatementHandler:作用为空闲检查,常用于服务端监控的一个Handler,按照参数设置的间隔去监控读空闲,写空闲以及读写空闲(空闲指的就是两端之间没有数据通信)
这个Handler所做的空闲检查,对应的开发场景为:心跳机制的建立
eg:
netty为我们提供的空闲检查Handler,我们可以利用这个Handler去实现自己的心跳机制
客户端与服务端之间如何进行通信检查?如何知道对端是活着的?
心跳机制。心跳其实就是发送一个小数据包。如果对端存活,则xxxx。如果对端不存活,则xxxx。根据心跳监测结果去触发不同的逻辑。
# 心跳机制的两种模式:
1. 纯净的IdleStateHandler:是使用参数设置的间隔空闲时间,如果超过,那么就认为心跳失败。
2. 纯净的IdleStateHandler+次数计时策略。
当我们需要发现失败的时候,在一断时间内连续几次失败就认为断开。但是这种几次一定要做次数计时。因为多次之间要是间隔过长,其实没啥用意义了,你隔一个小时来了第二次,你觉得这是超时吗,这都废了。所以需要处理事件过来两次或者多次之间的间隔,超过一个间隔就认为失败了。不能无限等下一次记次数。
我们说避免误伤客户端,那么我们就可以做一个容错,我们配置一个变量计数器,当客户端和服务端之间发生四次读写空闲后才发生断开连接的操作,但是我们这个没有设计计时。代码如下:
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 18:16
* @Version 1.0
*/
public class MyNettyHeartBeatServer {
private static final Logger log1 = LoggerFactory.getLogger(MyNettyHeartBeatServer.class);
private static final Logger log = LoggerFactory.getLogger(MyNettyHeartBeatServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
//设置读写空闲次数计数器,它是一个原子类
//每一个客户端连接会独享一份SocketChannel,所以每一个客户端都会独享一份count计数器类
//所以这里不会有线程并发安全问题。而且每次事件过来都是触发回调的,不会再执行initChannel,所以不会每次都初始化count为0
AtomicInteger count = new AtomicInteger() ;
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new IdleStateHandler(0,0,7, TimeUnit.SECONDS));
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt ;
if (idleStateEvent.state() == IdleState.ALL_IDLE) {
log.info("连接{}发生读写空闲",ctx.channel());
int addAndGet = count.addAndGet(1);
if (addAndGet == 4) {
log.info("当连接{}发生读写空闲四次后,会断开对应客户端连接,避免服务端资源被无效的占用",ctx.channel());
ctx.channel().close();
}
}
}
});
serverBootstrap.bind(8000);
}
});
}
}
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/22 16:19
* @Version 1.0
*/
public class MyNettyClient2 {
public static void main(String[] args) throws InterruptedException{
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush("hello 0819");
}
}
成功:
1.增加了netty处理websocket的能力
什么是websocket,它是干什么的?
websocket是一种协议,和你实现的语言无关,你可以使用任何支持的语言去实现,但是它的特点是什么呢?
1.websocket是一种全双工的协议,在传统的http协议中,只能是客户端给服务端来发请求,做不到双向通信。但是websocket可以做到双工通信。
2.websocket是基于长连接的,http1.0的时候,是发送完请求就断开了,是无状态的短连接。但是websocket要支持双工就需要长连接,为什么需要长连接?因为只有长连接才能保证长时间的连接状态,不然你服务端向客户端推送数据时,二者断开了怎么办?对吧。
必读文章:
HTTP1.0、HTTP1.1 和 HTTP2.0 的区别 - 掘金
------》http1.0协议
在基于以上这些东西的介绍下,我们要知道一点就是它要做这种双工的长连接的通信操作,那么就不能是无状态的,你不能发完就断开了,这样肯定不行,为什么?上面也说过了,就是因为需要保证服务端向客户端推送数据时,二者之间是连接状态。也就是需要记住连接会话的状态。这就是用户的会话追踪,其实使用的就是cookie或session(session也是基于cookie去做的)。如果不保存状态,短连接之后就会结束连接,浪费资源,每一次连接都只能发送一次,然后就断开。所以每新发送一次就需要建立一个新连接,而每一次连接的创建都需要三次握手等等,三次握手的过程中也不止建立连接这一功能,包括缓冲区的初始化等等,断开连接做的性能消耗也同样很大。这种开销太大了,所以要考虑长连接,避免开销,能够多次通信收发数据在一次连接。这就是http1.0协议。
-----》http1.1协议
也就是http1.1解决的问题。http1.1核心的理念就是有限的长连接,不是一直连着,而是有时间限制,这个限制由请求头中的keep-alive来决定的连接时间长度。尽量保持连接,从而减少连接时三次握手断开时四次挥手的性能消耗,默认情况下是双方连接达到60秒无通信交互时才断开连接
但是基于http1.1协议,服务端不能推送数据给客户端。当你服务器端发生变化时,不能主动告诉客户端,客户端无感知。所以当使用http1.1协议时则必须做到如下:客户端要不断的轮询去请求服务端,如果服务端的有新的数据,那么会响应返回给客户端。所谓轮询请求其实就是一个定时任务。但是这样存在很大的弊端:如果服务端数据一直没有更新,那样岂不是你轮询请求这么多次都是白请求的。我们目的不就是获取服务端想要发送给客户端的新数据吗?所以可以让服务端在具有新数据后主动进行推送给客户端吗?所以无论是http2.0还是websocket协议都解决了这个问题。
-------》http2.0或websocket协议
所以http2.0或websocket出场,如何解决上述问题的?很简单,当服务端有啥变化时,服务端就会主动推给客户端。所以想要做到服务端能够主动推数据给客户端,这样就降低了前面不断轮询导致无效的请求性能消耗。使用webSocket协议,实际上就是建立了一个C-S长连接,当服务端具有新数据时,可以推送给客户端,当然也不是啥新数据都推送的,这里就需要你在服务端做一些自定义的设置了。当然这都是站在应用层的角度,我们必须建立一个长连接才能保证C-S连接存活然后S端推送数据给C端,但是在传输层TCP角度的话,就无需管啥长连接,直接发就行了。
------》
本次主要介绍的是netty支持的websocket协议,websocket和http协议的关系是什么?
websocket是http协议上层的一个协议,websocket底层还是http协议,它是http协议的一种升级,增强。
那么netty是如何支持websocket的呢?
其实就是引入了一个Handler,叫做WebSocketServerProtocolHandler,它在http协议之上增加了通信机制,做到了真正的长连接,而不是有限的长连接。
只需要一个WebSocketServerProtocolHandler的处理器
package com.messi.netty_core_02.netty14;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.DefaultEventLoopGroup;
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.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LoggingHandler;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 10:56
* @Version 1.0
*/
public class MyNettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup();
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
//websocket是基于http协议的,所以它还是要走http协议的数据编解码,传给它的websocket处理
pipeline.addLast(new HttpServerCodec());
//对上面的http协议接收到的头体分离的包,做聚合成一个FullRequest,不然webSocket不能处理
pipeline.addLast(new HttpObjectAggregator(1024));
/**
* 进行websocket的handler处理,它处理完后的数据的类型为TextWebSocketFrame
*
* 识别到的协议格式为:ws://ip:port/url,所以WebSocketServerProtocolHandler的参数其实就是这个url
* 这样才能走到它要对应处理的地方
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/leomessi"));
//添加自定义的Handler,用来处理WebSocketServerProtocolHandler处理完生成的TextWebSocketFrame类型的数据
pipeline.addLast(new MyWebSocketHandler());
}
});
serverBootstrap.bind(8000);
}
}
package com.messi.netty_core_02.netty14;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 10:57
* @Version 1.0
*/
public class MyWebSocketHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(MyWebSocketHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("接收到websocket的数据为:{}",msg.text());
//双工机制,可以继续推给客户端
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务端接收到你的数据"+msg.text()+"了,给你响应一个hello webSocket"));
}
}
Suns
启动服务端
启动客户端:此时启动服务端,然后启动客户端这个levi.html,在idea中只要用浏览器打开就行了,idea会给你创建tomcat的。
测试
此时我们还能继续发送,在左边输入框还能输入。这就是长连接。不会断开的。
但是我们来看服务端的日志输出:但是我们先不写请求,只是简单的打开刷一下页面,先不写数据进
去,看下日志。
服务端日志:
你不发数据就能看到这个升级效果了,当然你发也一样,数据都能收到。
而你此时关闭服务端断开,就能收到服务端的推送的关闭消息,所以能证明他可以双工通信
我们来进一步验证一下双工的概念。此时我们再自定义的handler里面启动一个定时器,定时给客户端推
送数据。
package com.messi.netty_core_02.netty14;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 10:57
* @Version 1.0
*/
public class MyWebSocketHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(MyWebSocketHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("接收到websocket的数据为:{}",msg.text());
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//双工机制,可以继续推给客户端
ctx.channel().writeAndFlush(new TextWebSocketFrame("服务端接收到你的数据"+msg.text()+"了,给你响应一个hello webSocket"));
}
},4000,4000);//最开始延迟4秒后再发送,并且每间隔4秒发送一次
}
}
因为内部维护了refCnt计数器,每当text()读取一次,当读取完时,该值变为0。那么再读取时,由于refCnt为0,所以抛出异常
package com.messi.netty_core_02.netty14;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 10:57
* @Version 1.0
*/
public class MyWebSocketHandler extends SimpleChannelInboundHandler {
private static final Logger log = LoggerFactory.getLogger(MyWebSocketHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
log.info("接收到websocket的数据为:{}",msg.text());
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//双工机制,可以继续推给客户端
// ctx.channel().writeAndFlush(new TextWebSocketFrame("服务端接收到你的数据"+msg.text()+"了,给你响应一个hello webSocket"));
ctx.channel().writeAndFlush(new TextWebSocketFrame(new Date().toString() + ":给你响应一个hello webSocket"));
}
},4000,4000);//最开始延迟4秒后再发送,并且每间隔4秒发送一次
}
}
启动服务端
启动客户端
测试
其实也可以在哪些回调事件里面回推数据,看你那个时机了。还可以把长连接保存下来,随时随地使
用。
需要做一句说明,长连接和短连接是业务角度区分的,你即便是TCP用完就断开,那也是短连接。http设计的协议开始就是连接就断开,即便他底层是tcp,他连接完了也是立马断开tcp连接,这是他的概念。
你也可以基于tcp实现别的协议,不断开,就是长连接了,比如后面的http2,websocket等等。
补充:也可以使用EventLoopGroup线程池,具体测试自行测试
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 17:19
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LoggingHandler());
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Long) {
Long data = (Long) msg ;
log.info("接收到客户端的数据为:{}",data);
}
}
});
}
});
serverBootstrap.bind(8000);
}
}
分析问题:
这段代码之前都写烂了,但是存在一个很大的问题,我们之前说过,pipeline和SC是绑定在一起的。
每一个客户端连接过来,SSC会给该客户端连接对应分配一个SC,每一个SC都会有自己的一套pipeline流水线【流水线是由多个处理器Handler组成的】,然后执行pipeline里面的逻辑代码。但是在高并发多客户端连接请求的情况下,由于每一个客户端都会独享一份pipeline流水线,所以每一个客户端都会new一个LoggingHandler,都会new一个StringDecoder,每一个客户端都这样new,内存压力极大。所以我们要学会复用这些handler,于是代码被修改成如下这样:
package com.messi.netty_core_02.netty15;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 17:19
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup(1),new NioEventLoopGroup(8));
//全局的日志打印处理器
LoggingHandler loggingHandler = new LoggingHandler();
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//使用全局的日志打印处理器
pipeline.addLast(loggingHandler);
pipeline.addLast(new StringDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Long) {
Long data = (Long) msg ;
log.info("接收到客户端的数据为:{}",data);
}
}
});
}
});
serverBootstrap.bind(8000);
}
}
我们再这里使用全局的日志打印处理器,这样你每个客户端连接过来都用的是同一个。
为什么这样能保证不反复new创建LoggingHandler?
因为客户端连接过来不会重启main了,NioServerSocketChannel会给每一个客户端分配好连接。不同的客户端过来会反复执行serverBootstrap.childHandler,这样会给每一个客户端独享一份pipeline流水线的操作。
这样我们就完成了第一步解决这个问题。
全局提取handler
上面我们的代码就是采用提取了handler,这个反复导致我们这个handler作为共享变量从而被多线程(多个客户端)使用,这样就带来了一个问题就是:线程安全问题。
所以问题就是什么样的handler能被共用,什么样的handler有线程安全问题则不能共用呢?所有的对象包括handler都分为两类:
1.有状态的,涉及数据状态,不能共用,因为有线程安全问题
一个对象或者类有可写的成员变量,这叫做有状态,因为涉及到数据并发写,可能会有并发安全问题。final则表示不可写。
如果这个类属于堆位置,那就是共享的,可写,就有线程安全问题。对应的没有成员变量就是无状态的。
或者你的成员变量是无状态的(final类型),那么你也是无状态的。
对于有状态的数据,你也可以通过加锁或线程绑定(ThreadLocal)来实现解决线程安全的问题
2.无状态的,没状态,谁类都一样,可以共用,没有线程安全问题
无状态的数据是位于栈空间中的,像方法参数,方法中的局部变量,都是位于栈空间的,每一个线程都会独享一份栈空间,所以是无状态的。
补充:
在开发的过程中,为了保证线程安全,要么把有状态的变量(成员变量)设置成无状态的(final类型)或加锁或保存到ThreadLocal中。
但是你这种做法实际上还是在找不痛快。直接把变量设置成无状态的不就行了吗?哈哈哈对吧。
于是在分析了关于能不能共享的问题后
我们可以知道:ByteToMessageDecoder这个类以及子类是不能被共用的。因为你可能处理客户端A的一次消息数据话没有处理完,客户端B就过来了,那么此时会去处理客户端B的消息数据,那么就会造成客户端B的消息数据追加到客户端A未处理完的消息数据后面,就造成数据污染问题。所以LineBasedFrameDecoder都不可以被共享。即便是你自己自定义实现的编解码器也不能被共享
但是作为编码器的MessageToByteEncoder是可以被共享并且加@Shareable注解的,因为作为编码器,你肯定在客户端代码中进行生效的,客户端就是一个一个的,各自独立的程序应用自己的编码器。各自部署一份代码,所以不会有线程安全问题,所以可以共享。
ByteToMessageDecoder:
如何保证不能加@ Shareable共享的?
MessageToByteEncoder:
我们前面说的LoggingHandler是可以的,因为它打上了@ Shareable注解,表示你可以共享。它是线程安全的。以后你开发的时候,能共用的就加@ Shareable注解,不能共用的就不可以加@ Shareable
能加的你就可以把new创建对象的操作进行提取出来共享使用,不能的就还可以像以前那样老老实实的new创建对象并且传递给pipeline.addLast里面。
当然,加@ Shareable也可以new,不加的也可以new。
话又说回来了,假如说你实现了自己的编解码器,继承了ByteToMessageDecoder的子类,你还是加了@ Shareable注解,那么就违背了原则【该类以及子类已经明确说明不能共享】,结果会怎么样呢?会抛出异常,会在构造函数中做校验。如下:
【继承ByteToMessageDecoder的子类初始化创建对象时,默认会调用父类ByteToMessageDecoder的构造方法,然后会调用ensureNotSharable方法,在该方法中会做异常校验】
然后MessageToMessageDecoder和MessageToMessageEncoder这个体系它是没有说不能加@ Sharable注解的要求,所以它是可以共享的,因为该类是属于偏上层一些,因为它不做封帧操作,所以可以共享。
比如:因为netty认为MessageToMessageDecoder这种不做封帧,我们写代码的时候封帧要在它前面去做,所以到这里已经是完整的Message消息了,所以不会出现像ByteToMessageDecoder的数据混乱追加的问题【ByteToMessageDecoder封装了封帧的操作】,所以就可以共用。
# 验证目的:
1. MessageToByteEncoder编码器可以共享,可以加@Sharable注解。
2. MessageToMessageEncoder编码器可以共享,可以加@Sharable注解。
3. MessageToByteDecoder解码器不能共享,加@Sharable注解报错。
4. MessageToMessageDecoder解码器可以共享,可以加@Sharable注解。
验证目的1和3
package com.messi.netty_core_02.netty15.sharable;
import java.io.Serializable;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:50
* @Version 1.0
*/
//消息类
public class Message implements Serializable {
private String userName;
private String password;
public Message() {
}
public Message(String userName, String password) {
this.userName = userName;
this.password = password;
}
@Override
public String toString() {
return "Message{" +
"userName='" + userName + '\'' +
", password='" + password + '\'' +
'}';
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.messi.netty_core_02.netty15.sharable;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:51
* @Version 1.0
*/
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException {
//@Sharable共用 不会报错
final LoggingHandler loggingHandler = new LoggingHandler();
//@Sharable共用 不会报错
final MyMessage2ByteEncoder myMessage2ByteEncoder = new MyMessage2ByteEncoder();
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(loggingHandler);
pipeline.addLast(myMessage2ByteEncoder);
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush(new Message("leo","123456"));
group.shutdownGracefully();
}
}
package com.messi.netty_core_02.netty15.sharable;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:54
* @Version 1.0
*/
@ChannelHandler.Sharable
public class MyMessage2ByteEncoder extends MessageToByteEncoder {
private static final Logger log = LoggerFactory.getLogger(MyMessage2ByteEncoder.class);
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
log.info("MyMessage2ByteEncoder invoke...") ;
//1.魔数 8字节
out.writeBytes("leomessi".getBytes());
//2.版本 1字节
out.writeByte(1);
//3.序列化方式 1字节。数据值为1代表json 为2代表protobuf 为3代表hessian
out.writeByte(1);
//4.指令功能值 1字节
out.writeByte(1);
//5.正文长度 4字节
ObjectMapper objectMapper = new ObjectMapper() ;
String jsonContent = objectMapper.writeValueAsString(msg);
out.writeInt(jsonContent.length());
//6.正文,直接写char序列
out.writeCharSequence(jsonContent, Charset.defaultCharset());
}
}
package com.messi.netty_core_02.netty15.sharable;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:55
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
final LoggingHandler loggingHandler = new LoggingHandler();
final MyByte2MessageDecoder myByte2MessageDecoder = new MyByte2MessageDecoder();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/**
* maxFrameLength:数据包最大长度为1024字节
* lengthFieldOffset:数据长度字段前面有多少字节的偏移?
* lengthAdjustment:数据长度字段与真实数据体之间有多少距离?
* initialBytesToStrip:最终服务端输出的数据去除前面多少字节的长度?
* 具体见:Netty应用04-Netty这一笔记
*/
//封帧解码器不可以共享 避免数据污染
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,11,4,0,0));
pipeline.addLast(loggingHandler);
pipeline.addLast(myByte2MessageDecoder);
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
log.info("服务端接收到客户端的数据为:{}",message) ;
}
});
}
});
serverBootstrap.bind(8000);
}
}
package com.messi.netty_core_02.netty15.sharable;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.List;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 20:04
* @Version 1.0
*/
@ChannelHandler.Sharable
public class MyByte2MessageDecoder extends ByteToMessageDecoder {
private static final Logger log = LoggerFactory.getLogger(MyByte2MessageDecoder.class);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
刚启动服务端,就直接报错:
验证出ByteToMessageDecoder及其子类不可以使用@ Sharable进行共享
package com.messi.netty_core_02.netty15.sharable;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:55
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
//可以加@Sharable 实现共享
final LoggingHandler loggingHandler = new LoggingHandler();
//不可以加@Sharable 不可以实现共享
// final MyByte2MessageDecoder myByte2MessageDecoder = new MyByte2MessageDecoder();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/**
* maxFrameLength:数据包最大长度为1024字节
* lengthFieldOffset:数据长度字段前面有多少字节的偏移?
* lengthAdjustment:数据长度字段与真实数据体之间有多少距离?
* initialBytesToStrip:最终服务端输出的数据去除前面多少字节的长度?
* 具体见:Netty应用04-Netty这一笔记
*/
//封帧解码器不可以共享 避免数据污染
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,11,4,0,0));
pipeline.addLast(loggingHandler);
pipeline.addLast(new MyByte2MessageDecoder());
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
log.info("服务端接收到客户端的数据为:{}",message) ;
}
});
}
});
serverBootstrap.bind(8000);
}
}
启动服务端正常,但是当客户端连接上发送数据后,服务端还是报错了!
package com.messi.netty_core_02.netty15.sharable;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.List;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 20:04
* @Version 1.0
*/
//@ChannelHandler.Sharable
public class MyByte2MessageDecoder extends ByteToMessageDecoder {
private static final Logger log = LoggerFactory.getLogger(MyByte2MessageDecoder.class);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
启动服务端
启动客户端
一切正常。所以说明:ByteToMessageDecoder及其子类不可以加@ Sharable注解,不可以实现共享
验证目的2和4:
我们现在知道了ByteToMessage这个体系的共享问题了。现在我们来看MessageToMessageDecoder和
Encoder的问题。 我们说他们是可以共享的,所以我们需要重新定义这个类型的自定义编解码器。
package com.messi.netty_core_02.netty15.sharable;
import java.io.Serializable;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:50
* @Version 1.0
*/
//消息类
public class Message implements Serializable {
private String userName;
private String password;
public Message() {
}
public Message(String userName, String password) {
this.userName = userName;
this.password = password;
}
@Override
public String toString() {
return "Message{" +
"userName='" + userName + '\'' +
", password='" + password + '\'' +
'}';
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.messi.netty_core_02.netty15.sharable;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import java.net.InetSocketAddress;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:51
* @Version 1.0
*/
public class MyNettyClient {
public static void main(String[] args) throws InterruptedException {
//@Sharable共用 不会报错
final LoggingHandler loggingHandler = new LoggingHandler();
//@Sharable共用 不会报错
final MyMessage2ByteEncoder myMessage2ByteEncoder = new MyMessage2ByteEncoder();
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(loggingHandler);
pipeline.addLast(myMessage2ByteEncoder);
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
channel.writeAndFlush(new Message("leo","123456"));
group.shutdownGracefully();
}
}
package com.messi.netty_core_02.netty15.sharable;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.MessageToMessageEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.List;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:54
* @Version 1.0
*/
@ChannelHandler.Sharable
public class MyMessage2ByteEncoder extends MessageToMessageEncoder {
private static final Logger log = LoggerFactory.getLogger(MyMessage2ByteEncoder.class);
@Override
protected void encode(ChannelHandlerContext ctx, Message msg, List
package com.messi.netty_core_02.netty15.sharable;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/30 19:55
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
//可以加@Sharable 实现共享
final LoggingHandler loggingHandler = new LoggingHandler();
//可以加@Sharable 不可以实现共享
final MyByte2MessageDecoder myByte2MessageDecoder = new MyByte2MessageDecoder();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(new NioEventLoopGroup());
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
/**
* maxFrameLength:数据包最大长度为1024字节
* lengthFieldOffset:数据长度字段前面有多少字节的偏移?
* lengthAdjustment:数据长度字段与真实数据体之间有多少距离?
* initialBytesToStrip:最终服务端输出的数据去除前面多少字节的长度?
* 具体见:Netty应用04-Netty这一笔记
*/
//封帧解码器不可以共享 避免数据污染
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024,11,4,0,0));
pipeline.addLast(loggingHandler);
pipeline.addLast(myByte2MessageDecoder);
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Message message = (Message) msg;
log.info("服务端接收到客户端的数据为:{}",message) ;
}
});
}
});
serverBootstrap.bind(8000);
}
}
package com.messi.netty_core_02.netty11;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.util.List;
@ChannelHandler.Sharable
public class MyByte2MessageDecoder extends MessageToMessageDecoder {
private static final Logger log = LoggerFactory.getLogger(MyByte2MessageDecoder.class);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
启动服务端
启动客户端
最后输出正常,所以我们说不管是客户端的MessageToMessageEncoder编码器,还是服务端的
MessageToMessageDecoder解码器,都是可以共享的,当然了,MessageToMessage不做封帧,所以我们在服务端MessageToMessage前面的handler加了LengthFieldBasedFrameDecoder处理封帧,这个LengthFieldBasedFrameDecoder你就不能提取了,因为他是ByteToMessageDecoder,不能共享。
于是我们的2,4也得以证明。
尽量设计不包含可写成员变量的handler,这样就不用考虑这个共享问题了,实在不行就要考虑共享问题了
TCP连接本质上就是一个长连接,但是基于TCP的应用层协议可以做一些自定义,结果站在应用层角度,就出现了短连接,有限的长连接,长连接之分。但是站在传输层TCP角度,都是长连接。
http1.0是处于应用层,是短连接,一次响应完成后就主动关闭tcp连接
http1.1是处于应用层,是有限的长连接,连接的时长是取决于keepalive这个请求头所设置的时间长度的,当keepalive时间长度内双方没有进行通信,那么断开连接。但是http1.1无法实现服务端推送数据给客户端,为什么?因为http1.1是有限的长连接,说不定过了一会后连接断开,所以无法保证服务端推送数据给客户端时连接存活,所以无法实现推送。但是基于http1.1可以模拟出服务端推送功能,如何模拟呢?其实就是客户端轮询请求服务端的方式,客户端不断的请求服务端,如果服务端有新数据的产生,那么就响应客户端。如果没有,则无响应或形式上响应一下。但是这种方式存在弊端:如果服务端一直没有新数据产生,无效请求太多。
webSocket是处于应用层,是真正的长连接,具备服务端推送数据给客户端的功能。
-----》
【Netty内置的Handler还有很多,比如SSL加密相关的Handler等】
-----》
自定义开发Handler的过程中,我们要把Handler处理成无状态的(没有成员变量),这样就可以使用@ Sharable注解进行共享开发
下面是一个客户端和服务端的模板
package com.messi.netty_core_02.规范化开发;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/31 14:18
* @Version 1.0
*/
/**
* 客户端何时优雅的关闭断开?
* 1.发生异常时
* 2.监听到对应的事件回调,需要退出的时候,这个需要看你业务需求了
* 3.正常退出
*
* 客户端何时发送数据,channelActive建立连接后发送
*/
public class MyNettyClient {
private static final Logger log = LoggerFactory.getLogger(MyNettyClient.class);
public static void main(String[] args) {
NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
//多个客户端进行共享同一份LoggingHandler或StringEncoder,因为这两个类都是@Sharable可共享的
final LoggingHandler loggingHandler = new LoggingHandler();
final StringEncoder stringEncoder = new StringEncoder();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(nioEventLoopGroup);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//共享使用
pipeline.addLast(loggingHandler);
pipeline.addLast(stringEncoder);
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//Active事件被触发,表示连接完全建立,可以开始发送数据啦
ctx.writeAndFlush("leomessi");
}
});
}
});
Channel channel = bootstrap.connect(new InetSocketAddress(8000)).sync().channel();
//监控Channel管道的关闭。这句代码并不会关闭channel,只是阻塞在这里,异步监控channel是不是关闭的
//closeFuture是异步的,sync这里是同步等待关闭。
//那么你说在哪关闭channel?---》
// 1.发生异常时 2.监听回调方法,根据相关的业务场景进行关闭channel管道。比方说:空闲心跳检查时,空闲时间过久会触发回调,然后会关闭channel通道
// 3.正常退出
//线程走到这句代码后会一直阻塞,直到channel关闭触发了,此时才会接着往下走,执行后续的finally语句块
channel.closeFuture().sync();
} catch (InterruptedException e) {
//异常可以抛给上一级,也可以在这里打印输出,根据需求而定
log.info("客户端异常捕获....{}", e);
} finally {
nioEventLoopGroup.shutdownGracefully();
}
}
}
package com.messi.netty_core_02.规范化开发;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LoggingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description TODO
* @Author etcEriksen
* @Date 2023/12/31 14:18
* @Version 1.0
*/
public class MyNettyServer {
private static final Logger log = LoggerFactory.getLogger(MyNettyServer.class);
public static void main(String[] args) {
//提出共用
final LoggingHandler loggingHandler = new LoggingHandler();
//一主多从的Reactor模型的使用
//创建boss的eventLoopGroup,用来处理accept事件,线程数自定义设置为1个
NioEventLoopGroup bossEventLoopGroup = new NioEventLoopGroup(1);
//创建worker的eventLoopGroup,用来处理IO事件(read或write),线程数交给netty动态设置
NioEventLoopGroup workerEventLoopGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
//Reactor模型
serverBootstrap.group(bossEventLoopGroup,workerEventLoopGroup);
serverBootstrap.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//多个客户端共享同一Handler
pipeline.addLast(loggingHandler);
}
});
//bind就在这里监听绑定,也是异步的
Channel channel = serverBootstrap.bind(8000).sync().channel();
//我们在这里阻塞等待,监听到关闭就往下走到finally,执行finally块中优雅的关闭
channel.closeFuture().sync();
} catch (InterruptedException e) {
//异常可以抛给上一级,也可以在这里打印输出,根据需求而定
log.info("服务端异常捕获{}", e.getMessage());
} finally {
bossEventLoopGroup.shutdownGracefully();
workerEventLoopGroup.shutdownGracefully();
}
}
}