Netty实战学习
第一章 Netty-异步和事件驱动
Java 网络编程实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 1.创建服务端套接字并指定端口 ServerSocket serverSocket = new ServerSocket(portNumber); // 2.阻塞式接受客户端连接 Socket clientSocket = serverSocket.accept(); // 3.获取输入字符流 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // 4.获取输出字符流 PrintWriter out = new PrintWriter(clientSocket.getOutputStream(),true); String request,response; // 5.循环处理请求 while(request = in.nextLine() != null){ // 6.当请求为"Done"时,停止处理 if("Done".equals(request)){ break; } // 7.具体处理请求的逻辑 response = processRequest(request); // 8.将处理后的结果输出 out.println(response); } |
问题:
- 该方式为阻塞式,每次只能处理一个套接字如果采取多线程进行处理,则会出现大量线程处于休眠状态的资源浪费,并且由于资源的有限,上下文切换带来的消耗,也会导致能处理的并发量较差
Java NIO
Java非阻塞I/O实现的关键.他使用了事件通知API(select、poll、epoll)以确定在一组非阻塞套接字中有哪些已经就绪能够进行I/O相关的操作,以此实现一个单一的线程便可以处理多个并发的链接.
优点:
-
使用较少的线程便可以处理许多连接,因此减少了内存管理和上下文切换所带来的开销
-
当没有I/O操作需要处理的时候,线程也可被用于其他任务
异步和事件驱动
异步和可伸缩性之间的联系
- 非阻塞网络调用使得我们可以不必等待一个操作的完成.完全异步的I/O正是基于这个特性构建的,并且更近一步:异步方法会立即返回,并且在它完成时,会直接或者在稍后的某个时间点通知用户
- 选择器使得我们通过较少的线程便可监视许多连接上的事件
将这些元素结合在一起,与使用阻塞I/O来处理大量事件相比,使用非阻塞I/O来处理更快速、更经济.从网络编程的角度来看,这是我们构建理想系统的关键,也是Netty 的设计底蕴的关键.
Netty 的核心组件
- Channel
- 回调
- Future
- 事件和ChannelHandler
Channel
Channel 是 Java NIO的一个基本构造
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行的一个或者多个不同的I/O操作的程序组件) 的开发连接,如读操作和写操作
目前,可以把Channel 看做是传入(入站)或者传出(出站) 数据的载体.因此,它可以被打开或者被关闭,连接或者断开连接
回调
一个回调其实就是一个方法,一个指向已经被提供给另一个方法的方法的引用.这使得后者可以在适当的时候调用前者.回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一.
Netty在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interface ChannelHandler 的实现处理.
例:
1 2 3 4 5 6 7 | // 当一个新的链接已经建立时,channelActive(ChannelHandlerContext)将会被调用 Public ChannelHandler extends ChannelInboundHandlerAdapter{ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception{ System.out.println("Client" + ctx.channel().remoteAddress() + " connected"); } } |
Future
Future 提供了另一种在操作完成时通知应用程序的方式.**这个对象可以看做是一个异步操作的结果的占位符;**它将在未来的某个时刻完成,并提供对其结果的访问.
JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成,这是非常繁琐的,所以Netty提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用.
ChannelFuture 提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener 实例.监听器的回调方法 operationComplete(),将会在对应的操作完成时被调用,**然后监听器可以判断该操作时成功地完成了还是出错了.如果是后者,我们可以检索产生的Throwable.简言之,由ChannelFutureListener 提供的通知机制消除了手动检查对应的操作是否完成的必要
每个Netty 的出站I/O操作都将返回一个ChannelFuture;也就是说,它们都不会阻塞,正如我们前面所提到过的一样,Netty 完全是异步和事件驱动的
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | channel channel = ...; // Does not block //1.异步连接远程地址,不需要等待返回 ChannelFuture future = channel.connect(new InetSocketAddress("198.168.0.1",25)); //2.添加监听 future.addListener(new ChannelFutureListener(){ //3.监听事件后执行的回调方法 @Override public void operationComplete(ChannelFuture future) { // 4.执行成功执行的方法 if(future.isSuccess()){ ByteBuff buffer = unpooled.copiedBuffer("Hello",Charset.defaultCharset()); ChannelFuture wf = future.channel().writeAndFlush(buffer); .... }else{ //5.执行失败执行的方法 Throwable cause = future.cause(); cause.printStackTrace(); } } }); |
事件和ChannelHandler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态.这使得我们能够基于已经发生的事件来触发适当的动作.这些动作可能是:
- 记录日志
- 数据转化
- 流控制
- 应用程序的逻辑
Netty 是一个网络编程的框架,所以事件是按照它们与入站或者出站数据流的相关性进行分类的.可能由入站数据或者相关的状态更改而触发的事件包括:
- 连接已经被激活或者连接失效
- 数据读取
- 用户事件
- 错误事件
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
- 打开或者关闭到远程节点的链接
- 将数据写到或者冲刷到套接字
每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法.这是一个很好的将事件驱动范式直接转换为应用程序构件块的例子.
选择器、事件和EventLoop
Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所以本来将需要手动编写的派发代码.在内部,将会为每个 Channel 分配一个EventLoop,用以处理所有事件,包括:
- 注册感兴趣的事件
- 将事件派发给 ChannelHandler
- 安排进一步的动作
EventLoop 本身只由一个线程驱动,其处理了一个Channel 的所有I/O事件,并且该EventLoop 的整个生命周期内都不会改变.这个简单而强大的设计消除了你可能有的在来有兴趣的数据要处理的时候执行.
第二章 实现自己的Netty 应用程序
实现EchoServer
- 实现自己的EchoServerHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | @ChannelHandler.Sharable @Log public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { log.info(new Date() + "接受到消息:" + msg); String ackMsg = new Date() + "以接收到消息!"; ByteBuf buf = Unpooled.buffer(ackMsg.getBytes().length); buf.writeBytes(ackMsg.getBytes("GBK")); ctx.writeAndFlush(buf); super.channelRead(ctx, msg); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { SocketChannel socketChannel = (SocketChannel) ctx.channel(); log.info("链接报告:" + new Date() + "IP:" + socketChannel.localAddress().getHostString() + "port:" + socketChannel.localAddress().getPort()); log.info("链接断开:" + new Date()); String ackMsg = new Date() + "链接已断开"; ByteBuf buf = Unpooled.buffer(ackMsg.getBytes().length); buf.writeBytes(ackMsg.getBytes("GBK")); ctx.writeAndFlush(buf); super.channelInactive(ctx); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { SocketChannel socketChannel = (SocketChannel) ctx.channel(); log.info("链接报告:" + new Date() + "IP:" + socketChannel.localAddress().getHostString() + "port:" + socketChannel.localAddress().getPort()); log.info("链接建立成功!"); String ackMsg = new Date() + "链接已建立成功!"; ByteBuf buf = Unpooled.buffer(ackMsg.getBytes().length); buf.writeBytes(ackMsg.getBytes("GBK")); ctx.writeAndFlush(buf); super.channelActive(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.warning("the error message is "+ cause.getMessage()); cause.printStackTrace(); ctx.close(); super.exceptionCaught(ctx, cause); } } |
- 实现EchoServer 引导类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class EchoServer { private final int port; public EchoServer(int port){ this.port = port; } public static void main(String[] args) throws InterruptedException { new EchoServer(7397).start(); } private void start() throws InterruptedException { final EchoServerHandler echoServerHandler = new EchoServerHandler(); NioEventLoopGroup eventExecutors = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); try { b.group(eventExecutors) .channel(NioServerSocketChannel.class) .localAddress(new InetSocketAddress(port)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new LineBasedFrameDecoder(1024)); socketChannel.pipeline().addLast(new StringDecoder(Charset.forName("GBK"))); socketChannel.pipeline().addLast(echoServerHandler); } }); ChannelFuture f = b.bind().sync(); f.channel().closeFuture().sync(); }finally { eventExecutors.shutdownGracefully().sync(); } } } |
第三章 Netty 的组件和设计
Channel 接口
基本的I/O操作(bind()、connect()、read()、write()) 依赖于底层网络传输所提供的基本原语.Netty 的Channel 接口所提供的API,大大降低了直接使用Socket 类的复杂性.可以认为是对Socket 类的再封装
EventLoop 接口
EventLoop 定义了Netty 的核心抽象,用于处理连接的生命周期中所发生的事件.
- 一个EventLoopGroup 包含一个或者多个EventLoop
- 一个EventLoop 在他的生命周期内只和一个Thread绑定
- 所有由EventLoop 处理的I/O 事件都将在它专有的Thread上被处理
- 一个Channel 在它的生命周期内只注册一个EventLoop
- 一个EventLoop可能会被分配给一个或者多个Channel
**注意,在这种设计中,一个给定的channel 的I/O操作都是由相同的Thread执行的,所以实际上消除了对于同步的需要
ChannelFuture 接口
Netty 中的所有I/O操作都是异步的,因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法.为此Netty 提供了ChannelFuture 接口,其addListener() 方法注册了一个ChannelFutureListener,以便在某个操作完成时得到通知
第四章 ByteBuf
如何工作的(与ByteBuffer比较)
ByteBuf维护了两个不同的索引,一个用于读取,一个用于写入.当你从ByteBuf中读取时,它的readIndex 将会被递增已经被读取的字节数.同样的,当你写入ByteBuf时,它的writeIndex 也会被递增.
名为get、set操作不会改变索引位置,会通过相对索引直接读取字节,而read、write会推进索引
ByteBuf的使用模式
- 堆缓存区
最常用的ByteBuf 模式是将数据存储在 JVM 的堆空间中.能够提供快速分配和释放,但是会受到GC影响
- 直接缓存区
直接在内存分配,更快,但是需要调用系统分配和释放开销较大,并且在处理数据时不得不进行一次复制
- 复合缓存区将多个缓存区表示为单个合并缓冲区的虚拟表示
ByteBuf复制
- 副本(不共享数据,副本和原本之间数据独立,不会相互影响)
- 切片(共享数据,修改切片或原本,数据都会发生变化)
ByteBuf分配
池化技术、拷贝技术、非池化静态获取、引用计数
第五章 ChannelHandler 和ChannelPipeline
Channel的生命周期
状态 | 描述 |
---|---|
ChannelUnregistered | Channel已经被创建,但还未注册到EventLoop |
ChannelRegistered | Channel 已经被注册到了EvenLoop中 |
ChannelActive | Channel 处于活动状态.现在可以接受和发送数据 |
ChannelInactive | Channel 没有连接到远程节点 |
ChannelHandler 的生命周期
类型 | 描述 |
---|---|
handlerAdded | 当把 ChannelHandler 添加到 ChannelPipeline 中时被调用 |
handlerRemoved | 当从 ChannelPipeline 中移除 ChannelHandler时被调用 |
exceptionCaught | 当处理过程中在 ChannelPipeline 中有错误出现时被调用 |
- ChannelInboundHandler — 处理入站数据以及各种状态变化
- ChannelOutboundHandler — 处理出站数据并且允许拦截所有的操作
第六章 EventLoop 和线程模型
EventLoopGroup 负责为每个新创建的Channel 分配一个EventLoop.并且采用顺序循环的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel.
一旦一个Channel被分配给一个EventLoop,它将在他的整个生命周期中都使用这个EventLoop(以及相关联的Thread),这是保证线程安全的关键!
因为这种实现方式,所以对于一个EventLoop的多个channel来说他们的Threadlocal是相同的
第七章 引导(BootStrap)
服务器致力于使用一个父Channel 来接受来自客户端的连接,并创建子Channel以用于他们之间的通信(类似JDK 实现 NIO 服务器,会采取一个选择器处理连接事件,一个选择器处理读写异常事件);而客户端将最可能只需要一个单独的、没有父 Channel 的 Channel 来用于他们之间的通信
常用API:
BootStrap group(EventLoopGroup) 设置用于处理Channel 所有事件的EventLoopGroup
BootStrap option(SocketAddress) 设置ChannelOption,其将被应用到每个新创建的Channel 的 ChannelConfig.这些选项将会通过bind() 或者 connect() 方法设置到 Channel,不管哪个先被调用.
其他
- 数据入站处理、数据出站处理
- 消息资源释放、手动释放,简易自动释放
- ctx 与 channel 与 pipeline 与 handler之间的联系
- 如何保留ctx引用共享变量,线程安全问题,异常如何处理(没看懂)
B站课程学习
byteBuff与channel
- 学习了bytebuffer处理数据,编码解码,以及处理半包黏包问题,学习了channel与bytebuffer之间的关系.
- ByteBuffer内部结构,”读写模式”、”mark and reset”等常见API,如何分多次读取数据,已经channel的分片思想
- 文件拷贝的方式,零拷贝API,当文件过大,超过2G,如何利用零拷贝拷贝数据
半包粘包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class ByteBufferTest { public static void main(String[] args) { ByteBuffer source = ByteBuffer.allocate(32); source.put("Hello,word\nI'm zhangsan\nHo".getBytes()); split(source); source.put("w are you?\n".getBytes()); } public static void split(ByteBuffer source){ source.flip(); for (int i = 0; i < source.limit(); i++) { if(source.get(i) == '\n'){ int length = i + 1 - source.position(); ByteBuffer target = ByteBuffer.allocate(length); for (int j = 0; j < length; j++) { target.put(source.get()); } } } source.compact(); } } |
零拷贝大小超过2G
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class TestFileChannelTransferTo { public static void main(String[] args) { try ( FileChannel from = new FileInputStream("data.txt").getChannel(); FileChannel to = new FileOutputStream("copyData.txt").getChannel(); ) { long size = from.size(); for(long left = size;left > 0;){ System.out.println("position:" + (size - left) + "left:" + left); left -= from.transferTo((size - left),left,to); } } catch (IOException e) { e.printStackTrace(); } } } |
Files与Path
- 学习Path获取文件路径,创建文件等方法
- 学习files的walkfiletree遍历多级目录的方式(其中运用了观察者模式)
利用walkfiletree删除多级目录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class TestFilesWalkFileTree { public static void main(String[] args) throws IOException { //获取path的目录,并使用观察者进行遍历 Files.walkFileTree(Paths.get("E:\\work\\NettyServer\\src\\main"),new SimpleFileVisitor<Path>(){ // 进入目录中 @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return super.visitFile(file, attrs); } // 进入目录后 @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { Files.delete(dir); return super.postVisitDirectory(dir, exc); } }); } } |
阻塞和非阻塞的理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | ByteBuffer buffer = ByteBuffer.allocate(16); // 1.创建服务器 ServerSocketChannel ssc = ServerSocketChannel.open(); // 设置非阻塞,默认为阻塞 ssc.configureBlocking(false); // 2.绑定监听端口 ssc.bind(new InetSocketAddress(8080)); // 3.连接集合(因为可能会多次发送数据,一次无法完全读取,所以用集合记录) List<SocketChannel> channels = new ArrayList<>(); while(true){ // 4.accept 建立与客户端连接,SocketChannel用来和客户端进行通信(阻塞模式会一直等待连接建立,否则一直阻塞等待,非阻塞则会直接返回null) SocketChannel sc = ssc.accept(); if(sc != null){ // 设置下面的Read方法为非阻塞 sc.configureBlocking(false); channels.add(sc); } for(SocketChannel channel : channels){ int read = channel.read(buffer); if(read > 0){ buffer.flip(); prosession(); buffer.clear(); } } } |
- 阻塞方法会导致线程得到返回值之前一直阻塞不再向下运行,而非阻塞方法若没有得到返回值则会直接返回null并向下继续运行
- 并且这种非阻塞方式设计的时候导致的CPU一直转动,优化为selector选择器模式有了更好的理解
Selector 的学习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @Log public class TestSelectorServer { public static void main(String[] args) throws IOException { // 1.创建Selector,管理多个channel Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.configureBlocking(false); // 2.建立 selector 和 channel 的联系(注册) // SelectionKey 就是将来事件发生后,通过它可以知道事件和哪个channel的事件 SelectionKey sscKey = ssc.register(selector, 0, null); // key 只关注accept事件 sscKey.interestOps(SelectionKey.OP_ACCEPT); log.info("register key : " + sscKey); ssc.bind(new InetSocketAddress(8080)); List<SocketChannel> channels = new ArrayList<>(); while (true){ // 3. select方法,没有事件发生,线程阻塞,有事件,线程才会恢复运行 // select 在事件未处理时,不会阻塞 selector.select(); // 4.处理事件,是一个集合,里面包含了所有发送的事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectionKeys.iterator(); while (iter.hasNext()){ SelectionKey key = iter.next(); log.info("key : " + key); ServerSocketChannel channel =(ServerSocketChannel) key.channel(); SocketChannel sc = channel.accept(); log.info(" " + sc); } } } } |
更加深入理解selector,I/O多路复用模式,其对LT,ET有了初步认识
Selector底层有两个空间,一个空间存放注册事件的channel,另一个空间存储以就绪事件的key,如果key被处理了或者取消,则标记为已触发但不会删除事件,所以为了避免空指针问题,处理一个key之后要进行删除操作,这里也体现了JDK本身对于非阻塞I/O操作的复杂性
- 如何解决空指针问题(为什么要删除Key)
- 如何解决客户端断开问题(客户端断开也会触发读事件,但是由于链接已断开,读事件无法处理,所以会一直循环触发报错,这时候应该将该事件取消即反注册)
注意:如果是直接强制断开,则服务端会抛出异常,可以通过捕获异常并反注册关闭连接解决循环读事件,但是如果要是客户端调用shutdown方法关闭,则连接处于半关闭,read方法不会抛异常,但是可以通过读取到的ByteBuffer长度为-1进行判断以此关闭连接,反注册事件
消息边界处理
- ByteBuffer 容量不足
将ByteBuffer与channel关联,使得每个channel都有自己的ByteBuffer,当读取数据之后没有找到结束符,则会调用compact方法,此时position与limit相同,以此判断进行扩容,然后将扩容后的新buffer与channel进行关联
缺点:无法缩容,Netty更好的解决了这个问题
- 写入内容过多
当写入数据较大时,会分多次写buffer,这样可能会导致缓冲区写满,一直循环阻塞,所以通过关联可写事件,当缓冲区可写时才进行写数据,缓冲区满了可以处理其他事情
利用多线程优化
分两组选择器
- 单线程配一个选择器,专门处理 accept 事件
- 创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read 事件
从中已经可以看到 Netty EventLoopGroup 线程模型的影子
将Thread 和 selector 看做一个 EventLoop ,而一个 EventLoop 分配多个Channel,将多个EventLoop 看做一个 EventLoopGroup,就是Netty 的底层设计思想
组件
EventLoop
- 继承了ScheduledExecutorService,因此包含线程池中的所有方法
- 继承了 Netty 自己的 OrderedEventExecutor
- 提供了 boolean inEventLoop(Thread thread) 方法判断一个线程是否属于该EventLoop
- 提供了 parent 方法来看自己属于哪个 EventLoopGroup
EventLoopGroup
- 继承自 Netty 自己的EventExecutorGroup
- 实现了 Iterable 接口提供遍历 EventLoop 的能力
- 另有 next 方法获取集合中的下一个 EventLoop
EventLoopGroup 创建如果不传入线程,则会以默认线程创建
1 2 3 | protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); } |
默认线程计算逻辑
1 2 3 | // 1 或者 当前 CPU 核心数二倍的较大值 private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)); |