目录
Netty概述
Java BIO编程
- 创建ServerSocket,绑定端口,接收客户端连接请求,为每个连接都创建一个线程处理,没有数据交互时进行阻塞
Java NIO编程
- Non-blocking IO,同步非阻塞
- 创建ServerSocketChannel,绑定端口,设置非阻塞
- 创建Selector,向Selector注册ServerSocketChannel,监听事件(OP_ACCEPT,用于接收请求)
- Selector.select()进行阻塞等待连接
- 客户端连接后select()响应处理连接请求,接收请求后,将每个请求的channel注册到Selector,监听事件(OP_READ,用于读取数据)
- Selector有连接事件或者读数据事件时进行处理该事件(如果有多个事件到来分别处理,事件驱动)
- 三大核心部分:Channel,Buffer,Selector
- 一个channel对应一个buffer
- 多个channel可以注册到一个selector上
- 一个selector对应一个线程
- 是面向缓冲区或块编程,数据读取到缓冲区,使用缓冲区可以提供非阻塞的高伸缩网络
- 事件驱动
Buffer
缓存,基本类型除了boolean类型,都有对应的Buffer类
Channel
类似于流,但有以下区别:
- 通道可以同时进行读写,流只能读或者写
- 通道可以实现异步读写,流的读写是同步的
- 通道可以从缓冲区读数据,也可以写数据到缓冲区
常用channel
- FileChannel(文件读写)
- DatagramChannel(UDP数据读写)
- ServerSocketChannel、SocketChannel(TCP数据读写)
Selector
可以注册多个channel到selector(封装成SelectionKey),通过selector的select方法可以获取到有事件发生的channel,返回SelectionKey,进而获取到有事件发的channel
Netty
NIO问题:
- 类库及api繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO编程涉及Reactor模式,需要对多线程和网络编程非常熟悉,才能写出高质量的NIO程序
- 开发工作量和难度非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等
- NIO的bug:如臭名昭著的epoll bug,导致selector空轮询,最终导致cpu 100%,直到jdk1.7问题仍然存在
Netty是由JBOSS提供的一个Java开源框架,Netty提供异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠的网络IO程序。
Netty可以快速、简单的开发出一个网络应用,简化和流程化了NIO的开发过程。是目前最流行的NIO框架,在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,如elasticsearch、dubbo框架内部都使用了netty
线程模型
线程模型:传统阻塞IO服务模型、Reactor模型
根据reactor的数量和处理资源线程池的数量不同,有三种典型的实现:单Reactor单线程、单Reactor多线程、主从Reactor多线程(nginx、memcached、netty)
netty主要基于主从Reactor多线程模型做了一定的改进,主从Reactor有多个Reactor
Reactor:
- 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只要在一个阻塞对象等待,无序阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池复用线程资源:不必为每个连接创建线程,将连接完成后的业务处理任务分配给线程处理,一个线程可以处理多个连接的业务
netty线程模型:
- 有两个线程组,bossGroup、workGroup,前者用于处理连接,后者用于网络IO的读写
- NioEventLoop表示一个不断循环执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的socket网络通道,内部采用串行化设计,从消息的读取、解码、处理、编码、发送,都由该线程NioEventLoop负责
- NioEventLoopGroup下包含了多个NioEventLoop,每个NioEventLoop包含一个Selector、一个TaskQueue
- 每个NioEventLoop的Selector上可以注册多个NioChannel,每个NioChannel只会绑定到一个NioEventLoop上(它的Selector),每个NioChannel都会绑定一个自己的ChannelPipline
异步自定义任务
- 自定义普通任务:ctx.channle().eventLoop().execute()
- 定时任务:ctx.channle().eventLoop().schedule()
异步模型原理
ChannelFuture,IO操作的异步结果
Netty核心模块组件
- Bootstrap、ServerBootstrap
- Future、ChannelFuture
- Channel,涵盖了tcp及udp网络io及文件io
- NioSocketChannel,异步客户端tcp socket连接
- NioServerSocketChannel,异步服务端tcp socket连接
- NioDatagramChannel,异步udp连接
- NIOSctpChannel,异步客户端sctp连接
- NioSctpServerChannel,异步的sctp服务器端连接
- Selector
- netty基于Selector对象实现IO多路复用,通过Selector实现一个线程可以监听多个连接的channel事件
- 向一个Selector注册channel之后,Selector内部的机制就可以自动不断的查询这些注册的channel是否有已就绪的io事件(如可读、可写、连接完成等),这样就可以很简单的使用一个线程高效的管理多个channel
- ChannelHandler,是一个接口,处理IO事件,并将其转发到ChannelPipeline的下一个处理程序中
- Pipeline、ChannelPipeline,ChannelPipeline是Handler的一个集合,每个Handler对应一个ChannelHandlerContext(实际实现是DefaultChannelHandlerContext)
- Unpooled类,netty提供的一个专门用来操作缓冲区的工具类(netty的数据容器)
- ByteBuf,不需要flip,有读写指针
Unpooled.buffer(10)
,10字节大小的缓存Unpooled.copiedBuffer("hello", StandardCharsets.UTF_8)
,从字符串创建,实际容量大小可能比内容大
- ByteBuf,不需要flip,有读写指针
心跳机制
IdleStateHandler
WebSocket长连接
WebSocket是http协议的升级,会通过http返回101状态码进行升级协议
Protobuf
netty提供了一些编解码器:StringEncoder/StringDecoder、ObjectEncoder/ObjectDecoder(底层使用的仍然是java序列化技术,而java序列化本身效率就不高,无法跨语言、序列化后体积大,是二进制编码的5倍多,序列化性能低)
编写proto文件,使用插件生成java实体类,在handler中加入protobuf的编解码器及业务handler
Netty提供的编解码器
- ReplayingDecoder,扩展了ByteToMessage类,使用这个类就不用调用readableBytes()方法判断字节数了,参数S指定用户状态管理的类型,Void表示不需要状态管理
- 缺点:
- 并不是所有的ByteBuf都支持,如果调用了一个不被支持的方法,会抛出UOE
- 在某些情况下可能稍慢于ByteToMessageDecoder,比如当网络缓慢并且消息格式复杂时,消息会被拆分成多个碎片,速度会变慢
- 缺点:
- LineBasedFrameDecoder,使用行尾控制字符(\n或\r\n)
- DelimiterBasedFrameDecoder,使用自定义的特殊字符作为消息的分隔符
- HttpObjectDecoder,http数据的编解码器
- LengthFieldBasedFrameDecoder,通过指定长度来标识整包消息,这样就可以自动处理粘包和半包消息
TCP粘包和拆包
tcp是面向连接的、面向流的、提供高可靠服务。收发两端都要有一一成对的socket,发送端为了提高发送效率,使用了优化算法nagle算法,将多次间隔较小且数据量小的数据合成一个大的数据块 然后进行封包。虽然提高了效率,但是接收端就很难分辨出完整的数据包了,因为面向流的通信是无消息保护边界的。
由于tcp无消息保护边界,需要在接收端处理消息边界问题,也解释粘包、拆包问题。
使用自定义协议解决
如每次发送消息先发送一个int字节的数据长度,再发送该长度字节的数据
源码分析
启动源码
- NioEventLoopGroup的创建
- ServerBootstrap的创建
三大核心组件
- ChannelPipeline,ChannelHandler,ChannelHandlerContext
心跳handler
- IdleStateHandler,ReadTimeoutHandler,WriteTimeoutHandler,检测连接有效性
IdleStateHandler,当连接的空闲时间(读或写)太长时,将会触发一个IdleStateEvent事件,可以在Inbound中重写userEventTriggered方法处理该事件
ReadTimeoutHandler,在指定的时间内,如果没有读事件就会抛出异常,并自动关闭连接
WriteTimeoutHandler,一个写操作在指定的时间内没有完成时,就会抛出异常,并关闭连接
EventLoop
线程池
-
handler中加入线程池
ctx.channel().eventLoop().execute(),实际执行的也是当前handler的线程 -
context中添加线程池
在netty中做耗时的,不可预料的操作,比如数据库、网络请求、会严重影响netty对socket的处理速度。解决办法是将耗时任务添加到异步线程池中
- handler中加入线程池:在handler中创建一个DefaultEventExecutorGroup,被所有handler共享
- context中添加线程池:在创建server时,创建一个DefaultEventExecutorGroup,添加handler时指定该group,那么handler的处理会优先使用该group
- AbstractChannelHandlerContext的invokeChannelRead方法执行时,会判断executor是不是在当前线程,如果在当前线程会直接执行,如果不在当前线程(这个handler在之前提交的group中),会异步提交到group中执行
比较:
- 在handler中添加异步,更加自由,比如如果需要访问数据库就异步,不需要就不异步,异步会拖长接口响应时间,因为需要将任务放入task中,如果io时间很短,task很多,可能一个循环下来,都没时间执行整个task,导致响应时间过长。
- 在context中添加时netty的标准方式(加入到队列),这么做会将整个handler都交给设置的业务线程池,不管耗不耗时,都加入到队列,不够灵活
- 从灵活性考虑,第一种较好
自定义协议实现RPC
- 定义公共接口,消费者端使用代理获取服务代理,在进行rpc时连接服务端进行通信调用