netty介绍
netty是一个异步事件驱动NIO框架,由jboss提供的一个java开源框架,是业界最流行的NIO架构,整合了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的经验实现, 精心设计的框架,在多个大型商业项目中得到充分验证。
API使用简单
成熟、稳定
社区活跃 有很多种NIO框架,如mina
经过大规模行业(互联网、大数据、网络游戏、电信行业)
一、netty线程模型和reactor模式
设计模式--reactor模式(反应器设计模式),是一种基于事件驱动的设计模式,在事件驱动的应用中,将一个或多个客户的服务请求分离(demultiplex)和调度(dispatch)给应用程序。 在事件驱动的应用当中,同步的、有序的处理同时接收的多个服务请求。一般出现在高并发系统中,比如netty、redis等
优点:
响应快,不会因为单个同步而阻塞,虽然reactor本身依然是同步的。
编程相对简单,最大程度的避免了复杂的多线程以及同步问题,并且避免了多线程/进程之间的切换开销
可拓展性,可以方便的通过reactor实例个数充分利用CPU资源
缺点:
相比传统的简单模型,reactor增加了一定的复杂性,因而有一定的门槛,不利于调试
reactor模式需要系统底层的支持,比如java中的selector支持,操作系统中的select系统调用支持
reactor模式基于事件驱动,适合处理海量的IO事件,属于同步非阻塞IO(NIO)
二、reactor单线程模型(少用)
作为NIO服务端,接收客户端的TCP连接;作为NIO的客户端,像服务端发起TCP连接;
服务端读请求数据并响应;客户端读写并读取响应。
使用场景:
对于小业务合适,编码简单;对于高负载、大并发的应用场景不合适,一个NIO线程处理太多请求,则负载过高,并且可能响应变慢,导致大量的请求超时,万一线程挂了,则不可用。
三、reactor多线程模型
一个acceptor线程,一组NIO线程,一般是自带的线程池,包含一个任务队列和多个可用线程
使用场景:
可满足大多数场景,但是当acceptor需要负责操作的时候,比如认证等耗时操作,在高并发情况下也会有性能问题。
四、reactor主从线程模型
acceptor不是在一个线程,而是一组NIO线程;IO线程也是一组NIO线程,这样就是两个线程池去处理接入连接和处理IO
使用场景:
满足目前大部分场景的需求,也是netty使用的线程模型。
bossgroup和workgroup
五、EventLoop和EventLoopGroup线程模型
高性能rpc框架的三个要素:IO模型、数据协议(http/protobuf/Thrift)、线程模型
EventLoop好比一个线程,一个EventLoop可以服务多个Channel,一个Channel只有一个EventLoop。
通过创建多个EventLoop来优化资源,也就是EventLoopGroup。(类似线程池)
- EventLoopGroup 负责分配EventLoop到新创建的Channel,里面包含多个EventLoop
EventLoopGroup ->拥有多个EventLoop
EventLoop ->维护一个Selector(可以用单个线程来处理多个Channels)
selector学习资料:http://ifeve.com/selectors/
- EventLoopGroup 默认线程池数量:CPU核心数*2
六、netty启动引导类Bootstrap模块
服务器启动引导类ServerBootstrap
group:设置线程组模型,Reactor线程模型对比EventLoopGroup
单线程:只有一个线程在处理任务
//创建主线程--接收连接的池子 EventLoopGroup bossGroup = new NioEventLoopGroup(1); try { //创建服务器端的启动助手配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap //指定线程池--主线程和工作线程 .group(bossGroup)多线程:接收线程为一个主线程,工作线程为多个线程
//创建主线程--接收连接的池子 EventLoopGroup bossGroup = new NioEventLoopGroup(1); //创建工作线程--真正的工作线程,主线程调用工作线程里的 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //创建服务器端的启动助手配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap //指定线程池--主线程和工作线程 .group(bossGroup,workerGroup)主从线程:接收线程和工作线程都是一个组
//创建主线程--接收连接的池子 EventLoopGroup bossGroup = new NioEventLoopGroup(); //创建工作线程--真正的工作线程,主线程调用工作线程里的 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //创建服务器端的启动助手配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap //指定线程池--主线程和工作线程 .group(bossGroup,workerGroup)
channel:设置channel管道类型NioServerSocketChannel、OioServerSocketChannel(已废弃)
option:作用于每个新连接的channel,设置tcp连接中的一些参数。
- ChannelOption.SO_BACKLOG:存放已完成三次握手请求的等待队列最大长度;
linux服务器tcp连接底层知识:
syn queue:半连接队列,防洪水攻击,tcp_max_syn_backlog
accept queue:全连接队列,net.core.somaxconn
系统默认的somaxconn参数要足够大,如果backlog比somaxconn大,则会优先选择后者。
- ChannelOption.TCP_NODELAY:为了解决Nagle的算法问题,默认是false,要求高实时性,有数据时马上发送,就将该选项设置为true关闭Nagle算法;如果减少发送次数,就设置为false,会积累一定的大小后再发送。
childOption:作用于被accept以后的连接,设置连接的状态等
childHandler:指定通道工作内容,当该事件通道收到消息以后,如何处理。需要传入实现
ChannelInitializer<SocketChannel>接口的类。
七、channel模块
channel :客户端和服务端建立的一个连接通道
channelHandler:负责channel的逻辑处理
channelPipeLine:负责管理channelHandler的有序容器
一个channel包含一个channelPipeLine,所有的channelHandler都会顺序加入channelPipeLine当中,创建channel时会自动创建一个channelPipeLine, 每个channel都有一个管理它的pipeline,这个关联是永久性的
当channel状态出现变化,就会触发对应的事件。
状态(顺序也是channel的生命周期):
channelRegistered:channel注册到一个EventLoop
channelActive:变为活跃状态,即连接到了远程主机,可以接收和发送数据。
channelInactive:channel处于非活跃状态,没有连接到远程主机。
channelUnregistered:创建了一个channel,但是未注册到EventLoop里,也就是没有和Selector绑定
八、channelHandler和channelPipeline
handler里的方法:
handlerAdded:当channelHandler添加到channelPipeline调用。
handlerRemoved:当channelHandler从channelPipeline移除时调用。
exceptionCaught:执行抛出异常时调用。
channelHandler下主要接口:
- channelInboundHandler:处理数据和channel状态类型的改变。
适配器:channelInboundHandlerAdapter
常用:SimpleChannelInboundHandler
- channelOutboundHandler:处理输出数据
适配器 ChannelOutboundHandlerAdapter
ChannelPipeline:
类似工厂流水线一样,上面可以添加多个channelHandler,也可看成是一串ChannelHandler实例,拦截穿过Channel的输入输出event,ChannelPipeline实现了一种拦截器的高级形式, 使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权。
九、ChannelHandlerContext
ChannelHandlerContext是连接ChannelHandler和ChannelPopeline之间的桥梁。ChannelHandlerContext部分方法和channel和ChannelPopeline重合, 好比调用write方法。
channel、channelPipeline、channelHandlerContext都可以调用此方法,前两者都会在整个管道流里传播,而ChannelHandlerContext只会在后续的Handler里面传播
channelInboundHandler之间的传递,主要是通过调用ctx里的fireXXX()方法来实现下一个handler的调用,比如handler1里调用read方法,需要调用下一个handler的read, 需要添加代码:ctx.fireRead();
十、InboundHandler和OutboundHandler
一般在pipeline里添加的InboundHandler和OutboundHandler,执行的顺序是按照下面的规则执行。
InboundHandler是按顺序执行,OutboundHandler是倒序执行。如客户端如下:
ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new InboundHandler2());
ch.pipeline().addLast(new InboundHandler3());InboundHandler之间的的数据传递是通过ctx.fireChannelRead(msg)。
InboundHandler通过ctx.write(msg)传递到OutboundHandler。
ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然OutboundHandler不会执行
但是如果通过ctx.channel.write(msg)或ctx.pipeline.write(msg),情况会不一样,都会执行。
- Outbound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接收数据,先Outbound再Inbound,服务端则相反
十一、异步操作ChannelFuture
Netty中所有的I/O操作都是异步的,这就意味着任何I/O调用都会立即返回,而ChannelFuture会提供有关的I/O操作的结果或状态
ChannelFuture状态:
未完成:当I/O操作开始时,将创建一个新的对象,新的最初是未完成的,它即没有成功,也没有被取消,因为I/O操作尚未完成。
已完成:当I/O操作完成,不管是成功、失败还是取消,future的标记都是已完成的。
失败的时候也有具体的信息,例如失败原因,但是状态一定是已完成的状态。
注意:不要在I/O线程(Handler)内调用future对象的sync或者await方法,不能在channelHandler中调用sync和await。因为netty的底层就是一个异步的操作, 如果调用了会造成堵塞,甚至会死锁。
channelPromise:继承channelFuture,进一步拓展用于设置IO操作的结果。
十二、编码和解码
编解码:java序列化/反序列化
netty自己开发 编解码的原因(java自带序列化的缺点):
无法跨语言
序列化以后码流太大,也就是数据包太大
序列化和反序列化性能比较差
业界里面也有其它的编码框架:google的protobuf(PB)、facebook的Trift、Jboss的Marshaling、kyro等
netty里面的编解码:
解码器:负责处理入站(InboundHandler)的数据
解码器:负责处理出站(OutboundHandler)的数据
netty提供默认的编码器,也可以自定义。
Encoder:编码器
Decoder:解码器
Codec:编解码器,组合编码器和解码器,以此提供于字节和消息都相同的操作。优点:成对出现,编解码都是在一个类完成。 缺点:耦合在一起,拓展性不佳。
十三、Encoder编码器
Encoder对应的就是ChannelOutboundHandler,消息对象转换为字节数组
Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对等的。
MessageToByteEncoder:消息转为字节数组,调用write方法,会先判断当前编码器是否支持需要发送的消息类型,如果不支持,则透传。
MessageToMessageEncoder用于从一种消息编码到另一种消息(从一个POJO到另外的POJO)
十四、Decoder解码器
Decoder对应就是ChannelInboundHandler,主要就是字节数组转换为消息对象。主要两个方法:
decode:一般用这个
decodeLast:用于最后几个字节的处理也就是channel关闭的时候,产生的最后一个消息
自定义解码器,需要继承以下几个抽象解码器:
ByteToMessageDecoder:用于将字节转为消息,需要检查缓冲区是否有足够的字节。
public class EchoDecode extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out) throws Exception{ //字节的大小为4,需要判断字节是否大于等于一个字节数 if(in.readableBytes() >=4){ //添加到解码信息里 out.add(in.readInt()); } } }ReplayingDecoder:继承ByteToMessageDecoder。不需要检查缓冲区是有足够多的数据,速度略慢于ByteToMessageDecoder。
ByteToMessageDecoder和ReplayingDecoder是字节码解码,它们之间的选择:项目复杂性比较高则使用ReplayingDecoder,否则使用ByteToMessageDecoder
- MessageToMessageDecoder:MessageToMessageDecoder是字符解码,用于从一种消息解码到另一种消息(从一个POJO到另外的POJO)
netty自带的解码器,用的比较多的解码器(主要是为了解决TCP底层的粘包和拆包问题):
DelemiterBasedFrameDecoder:指定消息分隔符的解码器,如指定:xxx、&&&。会对消息进行分割,类似split
LineBasedFrameDecoder:以换行符为结束标志的解码器
FixedLengthFrameDecoder:固定长度的解码器
LengthFieldBasedFrameDecoder:message = header + body,基于长度解码的通用解码器
StringDecoder:文本解码器,将接收到的文本和消息转为字符串,一般会与上面的几种进行组合,然后再后面加业务的handler
十五、字节容器ByteBuf
netty的字节容器ByteBuf对比jdk原生的ByteBuffer:
JDK原生ByteBuffer:共用读写索引,每次读写操作都需要flip();扩容麻烦,而且扩容后容易造成浪费
Netty的ByteBuf:读写使用不同的索引,所以操作便捷;自动扩容,使用便捷等
ByteBuf(传递字节数据的容器)创建方法:
- ByteBufAllocator:
netty4.x以后默认使用池化:PooledByteBufAllocator,提升性能并且最大程度减少内存碎片
非池化:UnpooledByteBufAllocator,每次返回新的实例
- Unpooled:提供静态方法创建未池化的ByteBuf,可以直接创建堆内存和直接内存缓存区
ByteBuf使用模式:
- 堆缓存区HEAP BUFFER:
优点:存储在JVM的堆空间里,可以快速的分配和释放
缺点:每次使用前都会拷贝到直接缓存区(堆外内存)
- 直接缓存区DIRECR BUFFER:
优点:存储在堆外内存,堆外内存直接分配,不会占用堆空间
缺点:内存的分配和释放,比在堆缓冲区更复杂
- 复合缓冲区COMPOSITE BUFFER:
可以创建多个不同的ByteBuf,然后放在一起,但是只是一个视图
//创建复合缓冲区
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
//堆缓冲区
ByteBuf heapBuffer = Unpooled.buffer(16);
//堆外缓冲区
ByteBuf directBuffer = Unpooled.directBuffer(16);
//添加缓冲区
compositeByteBuf.addComponents(heapBuffer,directBuffer);16、linux设置网络最大连接数
linux默认局部文件句柄限制(fd:单个进程最大文件打开数)为:65535
修改局部的fd最大数:ulimit -n查看一个进程的最大打开文件数
修改:vi /etc/security/limits.conf
把 root soft nofile、root hard nofile、soft nofile、hard nofile都设成想要的最大连接数,修改完需要重启
修改全局的最大数(所有进程最大打开文件数):cat /proc/sys/fs/file-max查看命令
修改:vi /etc/sysctl.conf 增加一行:fs.file-max=100000 设置最大数
执行sysctl -p 立即生效
