从开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。
Netty 定义了两个重要的 ChannelHandler 子接口:
- ChannelInboundHandler:处理入站数据以及各种状态变化;
- ChannelOutboundHandler:处理出站数据并且允许拦截所有的操作。
- channelRegistered:当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
- channelUnregistered:当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
- channelActive:当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
- channelInactive:当 Channel 离开活动状态并且不再连接它的远程节点时被调用
- channelReadComplete:当 Channel 上的一个读操作完成时被调用
- channelRead:当从 Channel 读取数据时被调用ChannelWritabilityChanged: 当 Channel 的可写状态发生改变时被调用。可以通过调用 Channel 的 isWritable()方法来检测 Channel 的可写性。与可写性相关的阈值可以通过 Channel.config().setWriteHighWaterMark()和 Channel.config().setWriteLowWaterMark()方法来设置
- userEventTriggered:当 ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。
这些方法将会在数据被接收时或者与其对应的 Channel 状态发生改变时被调用。这些方法也与 Channel 的生命周期密切相关。
1.2 ChannelOutboundHandler 接口方法- bind(ChannelHandlerContext,SocketAddress,ChannelPromise):当请求将 Channel 绑定到本地地址时被调用
- connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise):当请求将 Channel 连接到远程节点时被调用
- disconnect(ChannelHandlerContext,ChannelPromise):当请求将 Channel 从远程节点断开时被调用
- close(ChannelHandlerContext,ChannelPromise):当请求关闭 Channel 时被调用
- deregister(ChannelHandlerContext,ChannelPromise):当请求将 Channel 从它的 EventLoop 注销时被调用
- read(ChannelHandlerContext):当请求从 Channel 读取更多的数据时被调用
- flush(ChannelHandlerContext):当请求通过 Channel 将入队数据冲刷到远程节点时被调用
- write(ChannelHandlerContext,Object,ChannelPromise):当请求通过 Channel 将数据写到远程节点时被调用
我们可以使用 ChannelInboundHandlerAdapter 和 ChannelOutboundHandlerAdapter 类作为自己的 ChannelHandler 的父类。因为它们提供了定义在对应接口中的所有方法的默认实现。所以,我们在开发过程中,只需要关注实现我们感兴趣的事件即可。
不管是读和写,Buffer 用完后都必须进行释放,否则可能会造成内存泄露。 Netty为我们提供的各种预定义 Handler 实现,都实现了数据的正确处理,所以我们自行在编写业务 Handler 时,也需要注意这一点:要么继续传递,要么自行释放。
SimpleChannelInboundHandler
是 ChannelInboundHandlerAdapter的子类,做了一些资源管理封装,这个实现会在数据被 channelRead0()方法
消费之后自动释放数据。使用它也是不多的选择。
网络编程里,“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。因此,Netty 提供了 两种类型的启动器引导:
- Bootstrap:用于客户端启动,
- ServerBootstrap:用于服务器启动。
Bootstrap和ServerBootStrap是 Netty提供的一个创建客户端和服务端启动器的工厂类,使用这个工厂类非常便利地创建启动类,大大地减少了开发的难度。
看一下它们类图:
可以看出都是继承于 AbstractBootStrap抽象类,所以大致上的配置方法都相同。
Bootstrap和ServerBootstrap的区别:
- 区别一 ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
- 区别二 一个客户端只需要一个 EventLoopGroup,但是一个 ServerBootstrap 则需要两个(也可以是同一个实例)EventLoopGroup。
一般来说,使用 Bootstrap/ServerBootstrap创建启动器的步骤可分为以下几步: 一般代码如下:下面就从这几个方法了解 Bootstrap。
private void start() throws InterruptedException {
//创建两个线程组 boosGroup、workerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//创建服务端的启动对象,设置参数
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
//设置服务端通道实现类型
.channel(NioServerSocketChannel.class)
//设置线程队列得到连接个数
.option(ChannelOption.SO_BACKLOG, 128)
//设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
//使用本地地址,绑定端口号
.localAddress(new InetSocketAddress(port))
//使用匿名内部类的形式初始化通道对象
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//给pipeline管道设置处理器
socketChannel.pipeline().addLast(new MyServerHandler());
}
});
System.out.println("MyServer 服务端已经准备就绪...");
// 异步绑定,启动服务端, sysc()会阻塞到完成
ChannelFuture channelFuture = bootstrap.bind().sync();
System.out.println("服务器启动完成");
//阻塞当前线程,对关闭通道进行监听(直到服务器 channel被关闭)
channelFuture.channel().closeFuture().sync();
} finally {
//释放掉所有的资源,包括创建的线程
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
1、group()
因为服务端要使用两个线程组,一般创建线程组直接使用以下 new就完事了。
- bossGroup:用于监听客户端连接,专门负责与客户端创建连接,并把连接注册到workerGroup的Selector中。
- workerGroup:用于处理每一个连接发生的读写事件。
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
既然是线程组,那线程数默认是多少呢?
通过源码可以看到,如果不传,默认的线程数是 cpu核数的两倍。 假设想自定义线程数,可以使用有参构造器:
//设置bossGroup线程数为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//设置workerGroup线程数为16
EventLoopGroup workerGroup = new NioEventLoopGroup(16);
2、channel()
这个方法用于设置通道类型,当建立连接后,会根据这个设置创建对应的 Channel实例。
通道类型有以下:
- NioSocketChannel: 异步非阻塞的客户端 TCP Socket 连接。
NioServerSocketChannel
: 异步非阻塞的服务器端 TCP Socket 连接。因为是异步非阻塞的。所以一般首选- OioSocketChannel: 同步阻塞的客户端 TCP Socket 连接。
- OioServerSocketChannel: 同步阻塞的服务器端 TCP Socket 连接。
- NioSctpChannel: 异步的客户端 Sctp(Stream Control Transmission Protocol,流控制传输协议)连接。
- NioSctpServerChannel: 异步的 Sctp 服务器端连接。
option()与childOption()的区别:
- option():设置的是服务端参数,用于接收进来的连接,也就是 boosGroup线程。
- childOption(),是提供给父管道接收到的连接,也就是 workerGroup线程。
ChannelOption类的各种属性在套接字选项中都有对应。所以我们直接指定它的属性即可。
常用属性如下:
- ChannelOption.SO_BACKLOG SO_BACKLOG (Socket参数),服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128
- ChannelOption.SO_REUSEADDR ChanneOption.SO_REUSEADDR 对应于套接字选项中的 SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口。
- ChannelOption.SO_KEEPALIVE SO_KEEPALIVE (Socket参数),连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。
- ChannelOption.SO_RCVBUF SO_RCVBUF(Socket参数),TCP数据接收缓冲区大小。
- ChannelOption.SO_LINGER ChannelOption.SO_LINGER 参数对应于套接字选项中的 SO_LINGER,Linux 内核默认的处理方式是当用户调用 close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用 SO_LINGER 可以阻塞 close()的调用时间,直到数据完全发送
- ChannelOption.TCP_NODELAY TCP_NODELAY (TCP参数),立即发送数据,默认值为Ture。
localAddress() 方法:提供用于服务端或者客户端绑定服务器地址和端口号(SocketAddress)。
localAddress() 与 bind()方法在绑定 地址和端口号时是类似的,两者选一个绑定即可。
5、childHandler(重点)在 Bootstrap中 childHandler()方法需要初始化通道,实例化一个 ChannelInitializer,这时候需要重写 initChannel()初始化通道的方法,装配流水线就是在这个地方进行。代码如下:
//使用匿名内部类的形式初始化通道对象
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//给pipeline管道设置处理器
socketChannel.pipeline().addLast(new MyServerHandler());
}
});
ChannelPipeline是 Netty处理请求的责任链,ChannelHandler则是具体处理请求的处理器。
我们的重点就是创建一个 ChannelHandler,然后实现业务Handler的处理。
处理器Handler主要分为两种:一般我们会继承这两个。
- ChannelInboundHandlerAdapter(入站处理器):入站指的是数据从底层java NIO Channel到Netty的Channel。
- ChannelOutboundHandler(出站处理器):出站指的是通过Netty的Channel来操作底层的java NIO Channel。
SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter的子类,做了一些封装,一般我们也会使用。
6、bind()bind()方法:提供用于服务端或者客户端绑定服务器地址和端口号(SocketAddress),默认是异步启动。如果加上 sync()方法则是同步。
// 异步绑定,启动服务端, sysc()会阻塞到完成
ChannelFuture channelFuture = bootstrap.bind().sync();
bind() 有五个同名的重载方法,作用都是用于绑定地址和端口号,并启动服务。
//释放掉所有的资源,包括创建的线程
bossGroup.shutdownGracefully().sync();;
workerGroup.shutdownGracefully();
优雅地关闭所有的 child Channel。关闭之后,释放掉底层的资源。如果加上 sync()方法则是同步。
参考文章:
- 超详细Netty入门,看这篇就够了!:https://blog.csdn.net/yunqiinsight/article/details/107953180
– 求知若饥,虚心若愚。