Netty——异步和事件驱动

  • JAVA网络编程
  • Netty简介
  • Netty核心组件

Netty是什么?

基于java NIO的进一步升级,也是真正意义上的异步非阻塞的模型,使用最小的资源开销来达到最大的性能

  • 支持多种通讯协议
  • 高度的抽象,隐藏底层实现细节
  • 简单易用
  • IO性能良好

核心思想:使用最小的开销来实现最高效率的能力

核心组件:

  • Channel
  • 回调
  • Future 设计模式
  • 事件和ChannelHandle

Channel

可以看做一个黑盒,载体,可以输入和输出

回调

​一个指向被提供给另一个方法的引用​

Future

核心组件之一

用于实现异步通知

​ interface java.util.concurrent.Future​​是JDK预置的接口的完成通知形式

由 ChannelFutureListener 提供的通知机制

每次的IO都会产生一个ChannelFutrue

事件和ChannelHandler

由于Netty是事件驱动的,将整个IO拆分为下面的事件

  • 连接事件
  • 数据读取
  • 用户事件
  • 错误事件
  • 关闭连接事件
  • 数据写入

谁在使用它?

  • Elastic Search
  • Kibana

为什么要学netty?

  • 高并发框架
  • 极高的IO性能
  • 对于NIO有进一步的扩展
  • 将关注点由底层转向业务实现
  • 大大的降低了网络编程,IO,多线程复杂结构

Netty的同类框架

  • hadoop
  • 大数据框架
  • elasticsearch
  • 为什么es性能那么强,底层就是netty
  • Mina
  • 如何学习?
  • 看书
  • 写案例
  • netty的历史
代理设置:
127.0.0.1
端口:
1080
具体设置:
windows10.microdone.cn

JAVA 网络编程

阻塞I/O样板代码

大致流程:

  • 创建一个ServerSocket,用来监听客户端请求
  • 调用**accept()**核心方法,该方法会产生阻塞,等待请求创建
  • 使用socket获取输出和输出流对象,一般使用包装buffer对应提高读写效率
  • 创建流对象
  • 读取数据,处理请求数据,得到处理后的响应信息,最后在进行处理

accept()方法做了什么事情:

  1. 初始化,创建socketImpl()实现类
  2. 校验套接字
  3. 检测请求是否合法
  4. 异常处理

阻塞I/O的优点:

  • 简单易懂
  • 对于并发量不是很大情况可以很好的处理

阻塞I/O的缺点:

  • 每一个请求就要建立一个线程,并且要一直等待,资源浪费
  • 开启的线程数取决于操作系统,
  • 上下文切换的代价昂贵

示范代码:

public static void main(String[] args) throws IOException {
// 1. 创建一个套接字
ServerSocket socket = new ServerSocket(8080);
// 创建一个请求对象
Socket accept = socket.accept();
// 获取输入流和输出流
BufferedReader in = new BufferedReader(new InputStreamReader(accept.getInputStream()));

PrintWriter out = new PrintWriter(accept.getOutputStream());

// 创建流对象
String request, response;
while ((request =in.readLine())!=null){
// 读取字节流数据
if(request.equals("Done")){
break;
}
// 调用服务器处理流数据的方法,处理请求流
response = processRequest(request);
out.println(response);
}
}

JAVA NIO

原理:调用本地套接字的非阻塞套接字

  • 使用​​setsockept()​​配置套接字,读写的时候没有数据可以返回
  • 使用事件通知的API注册非阻塞的套接字


非阻塞I/O是 jdk1.4引入的

这里有个很大的误区:

NIO是不是非阻塞的?

结论:错误

原因:

NIO是 1.4引入的,它不再新了,而且该API其实内部还是阻塞的,后面会讲到


​class java.nio.channels.Selector​​ 非阻塞IO的实现类

优点:

  • 较少的线程处理很多链接
  • 没有IO的时候可以执行其他操作

编写第一个Netty应用程序

环境准备

  • Apach maven
  • JDK1.7以上

开发工具

  • IntelinJ IDEA 2019版本

编写服务端

ChannelHandler - 接受客户端数据

  • ​channelRead()​
  • 对每个传入的消息都要调用;
  • ​channelReadComplete()​
  • 通过​​ChannelInboundHandler​​​ 最后一个对​​channel-Read()​​ 的调用是当前批量读取中的最后一条消息;
  • ​exceptionCaught() ​
  • 在读取操作期间,有异常抛出时会调用j

方式:

继承​​CahnelInboundHandlerAdapter​​的方式

不捕获异常,会发生什么情况

每个​​Channel​​​ 拥有一个关联的​​ChannelPipeline​​​,如果没有实现,会放到​​Piepeline​​的尾端,但是,必须有一个实现了捕获异常的​​Caught()​​方法

  • 针对不同事件,调用handler
  • 使用责任链加上事件响应的方式处理

引导服务器启动

  • 业务逻辑增加了之后,需要启动这个​​Channel​​监听请求
  • 配置​​Channel​​,将入站信息通知给handler实例

创建传输的server

  • 建立我们自己实现的​​channelHandler​​ EchoServer对象
  • 创建​​EventLoopGroup​​,用来处理​​channel​
  • 创建​​boostrapServer​​,用于建立套接字连接
  • 设置​​group​
  • 使用​​NIOSocket​​ 套接字
  • 设置请求接受端口
  • 设置子channel的channelHandler
  • 初始化调用,设置新的channel -> 就是我们自我实现的​​EchoServer​
  • 设置​​future​​, 使用异步开启服务器并且阻塞线程等待结果
  • 获取​​future​​的​​closeFuture​​,同样阻塞等待所有任务结束之后发送通知
  • 最后,finally 方法关闭group,并且释放所有的资源


如果想要使用阻塞的IO要如何处理:

​OioServerSocketChannel ​​​和 ​​OioEventLoopGroup ​


示范代码:

EchoServer:我们实现的channelHandler

public class EchoServer extends ChannelInboundHandlerAdapter {

/**
* 读取事件
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.err.println("Server receive" + in.toString(CharsetUtil.UTF_8));

}

/**
* 最后一次读取完成之后,发送通知到channelHandler
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}

/**
* 读取的过程
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

EchoServerClinet:服务器客户端开启

public class EchoServerClient {

private static final int PORT = 8080;

public static void main(String[] args) throws InterruptedException {
if(args.length > 1){
System.err.println("server ");
}
final EchoServer echoServer = new EchoServer();
// 创建事件驱动器 EventLoopGroup
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try{
// 创建server bootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 1. 指定使用NIO传输的渠道
// 2. 指定端口
// 3. 添加一个echoseverhandler 到子channel的chanelPipeline
serverBootstrap.group(eventLoopGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(PORT))
.childHandler(new ChannelInitializer() {
// 很关键,需要使用pipeline加入事件来帮我们处理channel
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(echoServer);
}
});
// 阻塞当前线程,异步绑定服务器
ChannelFuture channelFuture = serverBootstrap.bind().sync();
// 获取当前channel的closeFuture,阻塞直到线程完成
channelFuture.channel().closeFuture().sync();

}finally {
// 关闭group 释放资源
eventLoopGroup.shutdownGracefully().sync();
}
}

}

编写客户端

客户端需要做的事情比较简单,只需要如下步骤:

  • 建立一个与socket的连接
  • 发送一条或者多条消息
  • 对于每一条消息,等待服务器返回处理的数据
  • 关闭连接

chanelHandler实现客户端

  • 继承​​SimpleChannelInboundHandler​
  • 实现方法​​channelRead0()​​从服务器收到消息之后,会调用这个方法
  • 重写​​channelActive()​​,被通知channel活跃的时候发送通知
  • 重写​​exceptionCaught()​​,处理异常


为什么客户端要使用​​SimpleChannelInboundHandler​​​而不是服务端的​​ChannelInboundHandlerAdapter​

SimpleChannelInboundHandler 在处理完传入消息之后,理论上一个请求就已经被处理完成,此时channel会该类默认会释放这条传入消息的内存引用

而​​ChannelInboundHandlerAdapter​​​处理完消息之后,此时​​channelRead()​​结果不一定返回,因为整个过程都是异步的

问题又来了,什么时候会释放channelRead()处理之后的消息资源呢

答:当channelComplete()​writeAndFlush()​​之后,该消息资源才会被真正释放


运行案例

  • 启动EchoServerClient
  • 启动EchoClientServer

总结

以上就是一个最为简单的netty Demo

后续的笔记将会深入到netty的几个关键点

  • Channel
  • CahnnelHandler
  • Futrue
  • BootstrapServer
  • EventLoop
  • EventLoopGroup