Netty
概念:
是一个异步的、基于事件驱动的网络引用框架,用以高速开发高性能、高可靠性的网络IO程序,其主要针对在TCP协议下,面向Clients端的高并发引用或者Peer-to-Peer场景下的大量数据持续传输的应用,Netty本质是NIO框架适用于服务器通讯相关的多应用场景
I/O模型
BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,可通过线程池机制实现多客户连接服务器
NIO:同步非阻塞线程,服务器实现模式为一个线程处理请求多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
AIO:异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化类程序编写,有效的额请求才启动线程,其特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
NIO三大核心组件:Channel(通道),Buffer(缓冲区),Selector(选择器)。
NIO与BIO 的比较:
- BIO以流的形式处理数据,而NIO以块的形式处理数据,块I/O的效率比流I/O高很多
- BIO是阻塞的,NIO则是非阻塞的
- BIO基于字节流和字符流进行操作,而BIO基于通道Channel和Buffer进行操作,数据总是从通道读取到缓冲区,或从缓冲器写入通道,selector用于监听多个通道的事件因此使用单线程就可以监听多个客户端通道
组件关系图
说明:
- 每个channel都会对应一个Buffer
- Selector对应一个线程,一个线程对应多个channel(连接)
- 程序切换到那个channel是由事件(Event)决定的
- Selector会根据不同的事件,在各个通道上切换
- Buffer就是一个内存块,底层就是一个数组
- 数据的读取写入是通过Buffer,NIO的Buffer可以读也可以写但是需要filp方法切换
- channel是双向,可以返回底层操作系统的情况。
BIO节点流和处理流注意事项:
- 读写顺序要一致
- 要求序列化或反序列化的对象实现Serializable
- 序列化的类中建议添加SerialVersionUID,为了提高版本的兼容性
- 序列化对象时默认将里面的所有属性都进行序列化。但除了static和transient修饰的成员
- 序列化对象时,要求里面的属性的类型也需要实现序列化接口
- 序列化具备可继承性,也就是如果某类已经实现的序列化,则它的所有子类也已经默认实现了序列化
转换流: 作用:解决乱码问题 inputStreamReader outputStreamWriter
缓冲区Buffer
缓冲区本质上是一个可读数据的内存块,可以理解为一个容器对象(含数组)该对象提供一组方法,可以更轻松的使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据必须经由Buffer
Buffer定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息:
- Capacity:容量,即可以容纳的最大数据量,在缓冲区创建时被设定并且不能改变
- Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
- Position:位置,下一个要读或写的元素索引,每次读写缓冲区数据时都会改变该值,为下次读写做准备
- Mark 标记
通道(Channel)
- 通道中可以同时进行读写,而流只能读或则只能写
- 通道可以实现异步读写数据
- 通道可以从缓冲读数据,也可以写数据到缓冲
public class FileChannel1 {
public static void main(String[] args) throws IOException {
//创建文件输出流对象
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
//创建输入流对象
FileInputStream fileInputStream = new FileInputStream("1.txt");
//获取文件输出通道
FileChannel fileOutputStreamChannelhannel = fileOutputStream.getChannel();
//创建buf
ByteBuffer allocate = ByteBuffer.allocate(1024);
//获取输入通道
FileChannel fileInputStreamChannel = fileInputStream.getChannel();
//读取文件
while (true){
allocate.clear();//此处一定要置位,buf参数恢复为默认值
int read = fileInputStreamChannel.read(allocate);
if (read==-1){
break;
}
allocate.flip();
//将数据写入文件
fileOutputStreamChannelhannel.write(allocate);
}
fileInputStream.close();
fileOutputStream.close();
}
}
Buffer与Channel的注意事项
- ByteBuffer支持类型化的put和get,put放入的是什么类型数据,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常
- 可以架构一个普通的Buffer转换为只读Buffer
- NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件有NIO完成
- NIO还支持通过多个Buffer(buffer数组)完成读写操作,即Scattering和Gatering
Selector(选择器)
- Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对事件进行相应的处理,这样就可以用一个单线程去管理去管理多个通道,也就是管理多个连接和请求
- java的NIO用非阻塞的IO方式,可以用一个线程,处理多个的客户端连接,就会使用Selector
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
selector相关方法:
selector.select() //阻塞
selector.select(1000) //阻塞1000ms,在1000ms后返回
selector.wakeup() //唤醒selector
selector.selectNow() //不阻塞,立马返回
NIO非阻塞网络相关的(Selector SelectionKey serverSocketChannel SocketChannel)关系图
说明:
- 当客户端连接时会通过serverSocketChannel得到SocketChannel
- Selector进行监听select方法,返回有事件发生的通道的个数
- 将sockerChannel注册到Selector上,register(Selector sel ,int ops)一个selector可以注册多个SocketChannel
- 注册后返回一个SelectionKey,会和该Selector关联
- 进一步得到各个SelectionKey(有事件发生)
- 通过SelectionKey反向获取SocketChannel,方法channel()
- 可以通过得到的channel完成业务处理
服务端代码
//服务端代码
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel -->ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个selector对象
Selector selector = Selector.open();
//绑定一个端口 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把serverSocketChannel注册到selector关心事件为OP_ACCPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//循环监听客户端连接
while (true){
if (selector.select(1000)==0){//没有事件发生
System.out.println("服务器等待了1s,无连接");
continue;
}
//返回大于0,就获取到相关的selectionKey集合
//如果返回大于0,表示已经获取到关注的事件
// selector.selectedKeys()返回关注事件的集合
//通过selectionKeys反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//使用迭代器遍历
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
//获取到各个事件的SelectionKey
SelectionKey key = iterator.next();
//根据key对应的通道发生的事件做相应处理
if (key.isAcceptable()){
//如果是op_accept有新的客户端连接
//该客户端生成一个SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功");
socketChannel.configureBlocking(false);
//将socketchannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个Buffer
socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()){//发生OP_READ事件,通过key,反向获取到对应channel
SocketChannel channel = (SocketChannel) key.channel();
// channel.configureBlocking(false);
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
channel.read(buffer);
System.out.println("客户端"+new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey,防止重复操作
iterator.remove();
}
}
}
}
客户端代码
public class NIOClient {
public static void main(String[] args) throws IOException {
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
//设置服务器端的IP和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8000);
//连接到服务器
if(!socketChannel.connect(inetSocketAddress)){
while (!socketChannel.finishConnect()){
System.out.println("此处执行其他时间");
}
}
//连接成功发送事件
String str="hello,java";
//将字符数组包裹进缓冲区,不需要指定字节大小
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8));
socketChannel.write(buffer);
//发送数据,将buffer数据写入channel
//使代码执行挺在此处
System.in.read();
}
}
SelectionKey
- 表示Selector和网络通道的注册关系
- int OP_ACCEPT:有新的网络连接可以accept 值为16
- int OP_CONNECT:代表连接已经建立,值为8
- int OP_READ:代表读操作,值为1
- int OP_WRITE:代表写操作,值为4
serverSocketChannel
- 在服务器监听新的客户端Socket连接
NIO与零拷贝
mmap优化(内存映射优化)
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,这样在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数
sendFile优化:
数据根本不经过用户态,直接从内核缓存区进入到Socket Buffer同时,由于和用户态完全无关,就减少了一次上下文切换
**零拷贝:**值从操作系统角度看,内核缓冲区之间没有数据是重复的(只有 kernel buffer有一份数据),零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享已经无CPU校验和计算
mmap和sendFile的区别:
- mmap适合小数据读写,sendFile适合大文件传输
- mmap需要4次上下问切换,3次数据拷贝;sendFile需要3次上下文切换,最少两次数据拷贝
- sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须经过内核拷贝到Socket缓冲区)
Netty概述
**概述:**Netty是由JBOSS提供的一个java开源框架,Netty提供异步的,基于事件驱动的网络应用程序框架,可以快速开发高性能,高可靠性的网络IO程序
基于传统阻塞I/O服务模型的缺点的解决方案
- 基于I/O复用模型:对个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池的复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务
注
- reactor模式通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
- 服务器端程序处理传入的多个请求,并将他们同步分派到相应的处理线程中,因此也叫Dispatcher模式
- Reactor模式使用IO 复用监听事件,收到事件后,分发给某个线程(进程),这就是网络服务器高并发的关键
Reactor核心组成
- Reactor:在一个单独线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应
- Handlers:处理程序执行I/O事件要完成的实际事件,Reactor通过调度适当的处理程序来相应I/O事件,处理程序执行非阻塞操作
Netty模型
netty基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型有多个Reactor
netty详细模型
注:
- Netty抽象出两种线程池BossGroup专门负责接收客户端的连接,workGroup专门负责网络的读写
- BossGroup和WorkGroup类型都是NioEventLoopGroup
- NioEventLoopGroup相当于一个事件循环组,这个组中包含有多个事件循环,每一个事件循环都是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
- NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop
- 每个Boss NioEventLoop循环执行的步骤有
- 轮询accept事件
- 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某worker NIOEventLoop上的selector
- 处理任务对列的任务,即runAllTasks
- 每个Worker NIOEventLoop循环执行的步骤
- 轮询read,write事件
- 处理I/O事件,即read,write事件,在对应NioSocketChannel处理
- 出路任务对列的任务,即runAllTasks
- 每个Worker NIOEventLoop处理业务时,会使用pipeLine(管道),pipeLine中包含了channel,即通过pipeLine可以获取到对应的通道,管道中维护类很多的处理器
taskQueue
使用场景:
- 用户程序自定义的普通任务
- 用户自定义定时任务
- 非当前Reactor线程调用Channel的各种方法
异步模型
- 当一个异步过程调用发生后,调用者不能立即得到结果,实际处理这个调用的组件在完成后,通过状态,通知和回调来通知调用者
- Netty中的I/O操作是异步的包括Bind、write、connect等操作会简单的返回一个ChannelFuture
- 调用者并不能立即获得结果而是通过Future-Listener机制用户可以方便的主动获取或者通过通知机制获得IO操作结果
- Netty的异步模型是建立在future和callback之上的,CallBack是回调
- future思想:假设一个方法fun,计算过程很耗时,可以在调用fun时,立马返回一个funture,后续可以通过Future去监控方法fun的处理过程(即Future-Listener机制)
Future:表示异步执行的结果,可以通过它提供的方法来检索执行是否完成,ChannelFuture中,我们可以添加监听器,当监听的事件发生时,就会通知到监听器
Future-Listener机制
当Future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作
常见操作
- 通过isDone来判断当前操作是否完成
- isSuccess方法来判断已完成的当前操作是否成功
- getCase来获取已完成的当前操作方法失败的原因
- isCancelled方法来判断已完成的当前操作是否被取消
- addListener来注册监听器,当操作已完成,将会通知指定的监听器;如果Future对象已完成,则通知指定的监听器
注册监听
ChannelFuture sync = bootstrap.bind(6666).sync();
sync.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (sync.isSuccess()){
System.out.println("注册监听成功");
}else {
System.out.println("error");
}
}
});
netty核心
PipeLine和ChannelPipeLine
ChannelPipeLine是一个重点:
- ChannelPipeLine是一个Handler的集合,它负责处理和拦截Inboud或者outbound的事件和操作,相当于一个贯穿Netty的链(ChannelPipeLine是保存ChannelHandler的List用于处理或拦截Channel的入站事件和出站操作)
- ChannelPipeLine实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互
pipeline组件
- 一个Channel包含了一个ChannelPipeLine,而ChannelPipeLine中又维护了一个由ChannelHanderContext组成的双向链表,并且每个ChannelHanderContext中又关联着一个ChannelHandler
- 入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最后一个出站的handler,两种类型的handler互不干扰
EventLoop组件
ChannelHandlerContext作用:
- 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
- ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息方便对ChannelHandler进行调用
ChannelOption参数:
ChannelOption.SO_BACKLOG:对应TCP/IP协议listen函数中的backlog参数,用阿里初始化服务器可连接对列大小,服务端客户处理客户端连接是顺序处理的,所以同一时间中了个处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在对列中等待处理,backlog参数指定了对列的大小
ChannelOption.SO_KEEPALIVE:保持连接活动状态
EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核CPU资源,一般会有多个EventLoop同时工作,每个EventLoop维护着一个Selector实例
EventLoop提供next接口,可以从组里面按照一定规则获取其中一个EventLoop来处理任务,Netty服务器端编程中们一般需要提供两EventLoopGroup 例如:BossEventLoopGroup和WorkerEventLoopGroup
注: netty的buff中不需要使用flip进行反转,原因是其底层维护了一个readerIndex和writerIndex
心跳检测机制:
//该方法为netty提供的空闲状态处理器
//参数分别表示为: long readerIdleTime,多长时间没读发送心跳检测包,检测是否连接
// long writerIdleTime,:多长时间没写发送心跳检测包检测是否连接
// long allIdleTime,多长时间即没有读也没有写
//该事件被触发后就会传递给管道的下一个handler去处理,通过调用下一个handler的userEventTiggered,在该方法中去处理IdleStateEvent (读空闲,写空闲,读写空闲)
pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateHandler){
//将evt向下转型为IdleStateEvent
IdleStateEvent event = (IdleStateEvent) evt;
String eventType=null;
switch (event.state()){
case READER_IDLE:
eventType="读空闲";
break;
case WRITER_IDLE:
eventType="写空闲";
break;
case ALL_IDLE:
eventType="读写空闲";
break;
}
System.out.println("事件超时");
}}
ProtoBuf
概念:全称Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化(序列化)适合做数据存储或RPC(远程过程调用)数据交换格式 支持跨语言,跨平台,以message的方式来管理数据
netty入站出站机制
channelHandler充当了处理入站和出站数据的应用程序逻辑的容器,例如:实现ChannelInboundHandler接口或(ChannelInboundHandlerAdapter)就可以接受入站事件和数据,这些数据会被业务逻辑处理,当要给客户端发送响应是也可以从ChannelInboundHandler冲刷数据,业务逻辑通常写在一个或多个ChannelInboundHandler中,ChannelOutboundHandler原理一样只是用来处理出站数据
ChannelPipeLine提供了ChannelHandler链的容器。
编码解码器
- 当Netty发送或者接受一个消息的时候,就将会发生一次数据转换,入站消息会被解码:从字节码转换为另一种格式;如果是出站消息,他会被编码成字节
- Netty提供一系列适用的编解码器,他们都实现了ChannelInboundHandler或者ChannelOuboundHandler接口,在这些类中channelRead方法已经被重写,以入站为例对于每个从入站Channel读取的消息,这个方法会被调用,随后他将调用由解码器提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeLine中的下一个ChannelInboundHandler
TCP粘包和拆包
TCP是面向连接,面向流的,提供高可靠性服务,收发两端都要有成对的socket发送端,为了将多个发给接收端的包,更有效的发给对方,使用了Nagle算法,将多次间隔较小且数据量小的数据合并成一个大的数据块然后进行封包,提高效率,但接收端难于分辨出完整的数据包,因为面向流的通信是无消息保护边界的,由于TCP无消息保护边界,需要在接收端处理消息边界问题,也就是粘包和拆包问题
解决方案: 使用自定义协议+编解码器 [当解决服务器端每次读取数据长度的问题,就不会出现服务器多读或少读数据的问题从而避免TCP粘包拆包]
nboundHandler或者ChannelOuboundHandler接口,在这些类中channelRead方法已经被重写,以入站为例对于每个从入站Channel读取的消息,这个方法会被调用,随后他将调用由解码器提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeLine中的下一个ChannelInboundHandler