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()方法做了什么事情:
- 初始化,创建socketImpl()实现类
- 校验套接字
- 检测请求是否合法
- 异常处理
阻塞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
*/
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
*/
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
/**
* 读取的过程
* @param ctx
* @param cause
* @throws Exception
*/
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