1.Netty简介
Netty是由JBOSS提供的一个java开源框架。
Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
2.为什么使用Netty
虽然 JAVA NIO 框架提供了多路复用 IO 的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol Buffer、JSON 这些信息格式的封装,但是 Netty 框架提供了这些数据格式封装(基于责任链模式的编
码和解码功能);
2、NIO 的类库和 API 相当复杂,使用它来开发,需要非常熟练地掌握 Selector、ByteBuffer、ServerSocketChannel、SocketChannel 等,需要很多额外的编程技能来辅助使用 NIO,例如,因为 NIO 涉及了 Reactor 线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的 NIO 程序
3、要编写一个可靠的、易维护的、高性能的 NIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取,断连重连,半包读写,心跳等等,这些 Netty 框架都提供了响应的支持。
4、JAVA NIO 框架存在一个 poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能 block 意味着 CPU 的使用率会变成 100%(这是底层 JNI 的问题,上层要处理这个异常实际上也好办)。当然这个 bug 只有在 Linux内核上才能重现
3.Netty重要组件
3.1 Channel接口
在Java 的网络编程中,其基本的构造是类 Socket。Netty 的 Channel 接口所提供的 API,被用于所有的 I/O 操作。大大地降低了直接使用 Socket类的复杂性。此外,Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
channel生命周期
- ChannelUnregistered :Channel 已经被创建,但还未注册EventLoop
- ChannelRegistered :Channel 已经被注册到了 EventLoop
- ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
- ChannelInactive :Channel 没有连接到远程节点
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给 ChannelPipeline中的 ChannelHandler,其可以随后对它们做出响应。
channel重要方法
1.eventLoop: 返回分配给 Channel 的 EventLoop
2.pipeline: 返回分配给 Channel 的 ChannelPipeline
3.isActive: 如果 Channel 是活动的,则返回 true。活动的意义可能依赖于底层的传输。
例如,一个 Socket 传输一旦连接到了远程节点便是活动的,而一个 Datagram 传输一旦被打开便是活动的。
4.localAddress: 返回本地的 SokcetAddress
5,remoteAddress: 返回远程的 SocketAddress
6.write: 将数据写到远程节点。这个数据将被传递给 ChannelPipeline,并且排队直到它被冲刷
7.flush: 将之前已写的数据冲刷到底层传输,如一个 Socket
8.writeAndFlush: 一个简便的方法,等同于调用 write()并接着调用 flush()
3.2 EventLoop丶EventLoopGroup
EventLoop 定义了 Netty 的核心抽象,用于处理网络连接的生命周期中所发生的事件。EventLoop充当任务调度丶线程管理丶线程分配的重要对象。
3.2.1任务调度
查看EventLoop的继承关系可看出,EventLoop具有任务调度,充当线程池的作用,一个 EventLoop 将由一个永远都不会改变的 Thread 驱动.根据配置和可用核心的不同,可能会创建多个 EventLoop 实例用以优化资源的使用,且单个 EventLoop可指派用于服务多个 Channel(处理多个网络连接)。
Netty 的 EventLoop 在继承了ScheduledExecutorService,可调度一个任务以便稍后(延迟)执行或者周期性地执行。
比如,想要注册一个在客户端已经连接了 5 分钟之后触发的任务。一个常见的做法是,发送心跳消息到远程节点,检查连接是否还活着。如果没有响应,你便知道可以关闭该 Channel了。
3.2.2线程管理
在内部,提交任务,如果(当前)调用线程正是支撑 EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当 EventLoop 下次处理它的事件时,它会执行队列中的那些任务/事件。
3.2.3线程分配
服务于 Channel 的 I/O 和事件的 EventLoop 则包含在 EventLoopGroup 中。
IO多路复用:在当前的线程模型中,它们可能会被多个 Channel 所共享。这使得可以通过尽可能少量的 Thread 来支撑大量的 Channel,而不是每个 Channel 分配一个 Thread。
分配EventLoop:EventLoopGroup 负责为每个新创建的 Channel 分配一个 EventLoop。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的 EventLoop 可能会被分配给多个 Channel。
线程安全:一旦一个 Channel 被分配给一个 EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的 Thread)。请牢记这一点,因为它可以使你从担忧你的ChannelHandler 实现中的线程安全和同步问题中解脱出来。
注意:需要注意,EventLoop对 ThreadLocal的使用的影响。因为一个 EventLoop通常会用于支撑多个 Channel.
所以对于所有相关联的 Channel 来说若使用它来实现状态追踪则会有线程安全问题。但是在一些无状态的上下文中,它仍然可以被用于在多个 Channel 之间共享一些重度的或者代价昂贵的对象,甚至是事件。
3.3 ChannelFuture 接口
Netty中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在未来的某个时间点确定其结果的方法。因此Netty 提供了ChannelFuture 接口。
其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。
3.4 ChannelHandler 接口
3.4.1 ChannelHandler主要接口
对于应用程序开发人员的角度来看,Netty 的主要组件是 ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。
对于netty来说,主要流程就是消息通讯从进入入站处理器再到出去出站处理器,即接收消息再响应消息。
入站处理器:举例来说,ChannelInboundHandler是一个经常实现的子接口。这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。
主要方法如下:
- channelRegistered 当 Channel 已经注册到它的 EventLoop 并且能够处理 I/O 时被调用
- channelUnregistered 当 Channel 从它的 EventLoop 注销并且无法处理任何 I/O 时被调用
- channelActive 当 Channel 处于活动状态时被调用;Channel 已经连接/绑定并且已经就绪
- channelInactive 当 Channel 离开活动状态并且不再连接它的远程节点时被调用
- channelReadComplete 当 Channel 上的一个读操作完成时被调用
- channelRead 当从 Channel 读取数据时被调用
出站处理器:当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 直接冲刷数据然后输出到对端,即调用writeAndFlush或flush时则会经过出站处理器(常实现channelOutbountHandler子接口)
主要方法如下:
- 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 将数据写到远程节点时被调用。
ChannelHandler 的适配器有一些适配器类使用于自定义处理器,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以 Netty 提供了抽象基类 ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter。
3.4.2 ChannelHandler生命周期
接口 ChannelHandler 定义的生命周期操作,在ChannelHandler被添加到ChannelPipeline中或者被从 ChannelPipeline 中移除时会调用这些操作。这些方法中的每一个都接受一个ChannelHandlerContext 参数。
- handlerAdded: 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用
- handlerRemoved: 当从 ChannelPipeline 中移除 ChannelHandler 时被调用
- exceptionCaught: 当处理过程中在 ChannelPipeline 中有错误产生时被调用
3.5 ChannelPipeline 接口
与channel绑定:当 Channel 被创建时,它将会被自动地分配一个新的 ChannelPipeline。这项关联是永久性的;Channel 既不能附加另外一个 ChannelPipeline,也不能分离其当前的。
ChannelHandler容器: ChannelHandler在ChannelPipeline里工作,执行顺序是由它们被添加的顺序来决定的,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个 ChannelHandler。
入站运动:如果一个消息或者任何其他的入站事件被读取,那么它会从 ChannelPipeline 的头部开始流动,最终,数据将会到达 ChannelPipeline 的尾端,届时,所有处理就都结束了。
出站运动:数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从ChannelOutboundHandler 链的尾端开始流动,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,这里显示为 Socket。通常情况下,这将触发一个写操作。
顺序性:当两个类别的ChannelHandler都混合添加到同一个ChannelPipeline,虽然 ChannelInboundHandle 和 ChannelOutboundHandle 都扩展自 ChannelHandler,但是Netty 能区分 ChannelInboundHandler 实现和 ChannelOutboundHandler 实现,并确保数据只会在具有相同定向类型的两个 ChannelHandler 之间传递。
主要方法如下:
- addFirst、addBefore、addAfter、addLast将一个 ChannelHandler 添加到 ChannelPipeline 中
- remove 将一个 ChannelHandler 从 ChannelPipeline 中移除
- replace 将 ChannelPipeline 中的一个 ChannelHandler 替换为另一个 ChannelHandler
- get 通过类型或者名称返回 ChannelHandler
- context 返回和 ChannelHandler 绑定的 ChannelHandlerContext
- names 返回 ChannelPipeline 中所有 ChannelHandler 的名称
4.第一个简易netty程序
4.1 服务端
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
int port = 9999;
EchoServer echoServer = new EchoServer(port);
System.out.println("服务器即将启动");
echoServer.start();
System.out.println("服务器关闭");
}
public void start() throws InterruptedException {
final EchoServerHandler serverHandler = new EchoServerHandler();
/*线程组*/
EventLoopGroup group = new NioEventLoopGroup();
try {
/*服务端启动必须*/
ServerBootstrap b = new ServerBootstrap();
b.group(group)/*将线程组传入*/
.channel(NioServerSocketChannel.class)/*指定使用NIO进行网络传输*/
.localAddress(new InetSocketAddress(port))/*指定服务器监听端口*/
/*服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,
所以下面这段代码的作用就是为这个子channel增加handle*/
.childHandler(new ChannelInitializer() {
protected void initChannel(SocketChannel ch) throws Exception {
/*添加到该子channel的pipeline的尾部*/
ch.pipeline().addLast(serverHandler);
}
});
ChannelFuture f = b.bind().sync();/*异步绑定到服务器,sync()会阻塞直到完成*/
f.channel().closeFuture().sync();/*阻塞直到服务器的channel关闭*/
} finally {
group.shutdownGracefully().sync();/*优雅关闭线程组*/
}
}
}
@ChannelHandler.Sharable
/*不加这个注解那么在增加到childHandler时就必须new出来*/
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
/*客户端读到数据以后,就会执行*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf)msg;
System.out.println("Server accept"+in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
/*** 服务端读取完成网络数据后的处理*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
/*** 发生异常后的处理*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
4.2 客户端
public class EchoClient {
private final int port;
private final String host;
public EchoClient(int port, String host) {
this.port = port;
this.host = host;
}
public void start() throws InterruptedException {
/*线程组*/
EventLoopGroup group = new NioEventLoopGroup();
try{
/*客户端启动必备*/
Bootstrap b = new Bootstrap();
b.group(group)/*把线程组传入*/
/*指定使用NIO进行网络传输*/
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host,port))
.handler(new EchoClientHandle());
/*连接到远程节点,阻塞直到连接完成*/
ChannelFuture f = b.connect().sync();
/*阻塞程序,直到Channel发生了关闭*/
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient(9999,"127.0.0.1").start();
}
}
public class EchoClientHandle extends SimpleChannelInboundHandler {
/*客户端读到数据以后,就会执行*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg)
throws Exception {
System.out.println("client acccept:"+msg.toString(CharsetUtil.UTF_8));
}
/*连接建立以后*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer(
"Hello Netty",CharsetUtil.UTF_8));
//ctx.fireChannelActive();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
一个简易的netty程序就完成了,实现了通信功能。