netty框架入门
Java IO模型:
BIO:
概述:
同步并阻塞。
java.io包。
编程流程:
1.服务器端启动一个ServerSocket。
2.client每来一个请求,服务器端需要对每个client建立一个线程。
如果没有空闲线程,需要等待或拒绝。
缺点:
并发大时需要大量线程。
连接建立后,read等操作阻塞。
NIO:
概述:
非阻塞,多路复用。JDK4开始。
主要位于java.nio包及子包下,并且对原java.io包中的很多类进行改写。
思想:
等待可以读取或者等待写入完毕的时候,线程可以执行其他任务。
核心部分:
Channel通道:
概述:
一个channel对应一个buffer
写入读取的时候先将数据放入缓冲区
Channel接口:
概述:
常用channel类有FileChannel文件读写、DatagramChannel(UDP)、ServerSocketChannel和SocketChannel(TCP)
FileChannel:
常用方法:
read从通道读取数据并放入缓冲区
write(ByteBuffer src)把缓冲区的数据写到通道中
transferFrom从目标通道中复制数据到当前通道
transferTo从当前通道复制给目标通道,不用用户态拷贝。
写入示例:
FileOutputStream fileOutputStream = new FileOutputStream("./read.txt");
FileChannel channel = fileOutputStream.getChannel(); // 获取channel
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello".getBytes());
byteBuffer.flip(); // 读写切换
channel.write(byteBuffer);
fileOutputStream.close();
读取示例:
File file = new File("./read.txt");
FileInputStream fileInputStream = new FileInputStream(file);
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
channel.read(byteBuffer);
fileInputStream.close();
System.out.println(new String(byteBuffer.array()));
ServerSocketChannel:
概述:
主要负责接收socket连接
常用方法:
open()
bind()
configureBlocking(boolean block)
accept()
register(Selector sel, int ops)
SocketChannel:
概述:
主要用于读写
常用方法:
open()
configureBlocking(boolean block)
connect(SocketAddress remote)
finishConnect() 判断是否连接完成
write()
read()
register(Selector sel, int ops, Object attr)
close()
Buffer缓冲区:
概述:
内存块,底层是一个数组,内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Buffer类:
概述:
主要子类有ByteBuffer、ShortBuffer等7个基本数据类型
属性:
capacity容量,创建时指定,不能修改
limit可以修改,方便读写切换
postition位置,每次读写时都会改变
mark标记
常用方法:
position(int i) 设置position
limit(int i) 设置limit
flip() 读变为写的时候,limit=position,position变为0
asReadOnlyBuffer() 变为只读buffer
MappedByteBuffer:
可以直接在堆外内存中读取文件进行修改,而不用操作系统拷贝一次。
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 3);
mappedByteBuffer.put(0, (byte) 'H');
分散和聚集:
概述:
分散:写入buffer的时候,可以依次写入buffer数组,
聚集:从buffer读取的时候,可以依次读取buffer数组。
示例:
socketChannel.read(byteBuffers);
socketChannel.write(byteBuffers);
Selector选择器:
概述:
监听多个注册的channel的读取或者写入buffer完毕的Event。
只有连接真正有读写事件发生的时候,才会进行读写,大大减小了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
Selector类:
常用方法:
open()
select(long timeout) 监听所有注册的通道,当其中有IO操作就绪时,将对应的selectionKey加入到内部集合并返回
selectKeys() 得到所有的selectionKey(找到channel)
selectNow()
架构:
当client连接时,会通过ServerSocketChannel得到SocketChannel。
将SocketChannel注册registry到Selector上,可以指定监听事件OP_READ|OP_WRITE|OP_CONNECT|OP_ACCEPT,返回selectionKey
selector通过select方法进行监听,通过selectKeys反向拿到所有的channel。
SelectionKey类:
常用方法:
selector()
channel()
attachment() 得到与之关联的共享数据
interestOps() 设置或改变监听事件
isAcceptable()
isReadable()
isWritable()
与BIO的比较:
BIO以流的方式处理数据,而NIO以块的方式处理数据,块IO的效率高
BIO基于字节流和字符流进行操作(单向),NIO基于channel和buffer操作(双向,flip切换)。selector监听channel的事件。
AIO:
概述:
异步非阻塞,引入了异步通道的概念,采用了Proactor模式,简化了持续程序编写,有效的请求才启动线程。
使用于连接数目多且连接比较长的应用,比如相册服务器,JDK7开始支持。
Jdk7引入Asynchronous IO,即AIO。
常用两种模式:Reactor(NIO)和Proactor模式(AIO)。
AIO引入异步通道的概念,采用了Proactor模式,先由操作系统完成后才通知服务端程序启动线程去处理。
零拷贝:
概述:
主要分为mmap内存映射和sendFile
传统IO:
read操作先进行DMA直接内存access,拷贝到内核态。
然后CPU拷贝到用户态。
用户态拷贝到内核中的socket buffer。
socket buffer通过DMA拷贝到protocol engine。
一共5次拷贝,4次上下文切换。
mmap:
通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据,减少了内核到用户态的拷贝。
仍然存在4次上下文的切换,3次拷贝。
sendFile:
linux2.1版本提供,数据不经过用户态,直接从内核缓冲区通过CPU拷贝进入socket buffer,与用户态无关,减少了一次上下文切换。
一共3次拷贝,3次状态变化。
linux2.4版本,避免了从内核缓冲区拷贝到Socket buffer的操作,直接拷贝到协议栈,从而去掉了CPU拷贝。
其实存在一次CPU拷贝,从kernel到socket buffer,只是拷贝的信息很少。比如length,offset。
一共2次拷贝,2次状态变化。
真正的零拷贝:中间不存在kernel到socket的拷贝,kernel直接到protocol engine。
netty:
背景:
原生NIO存在的问题:
NIO类库和api繁杂,使用麻烦;
需要掌握selector、channel、buffer等;
需要熟悉java多线程编程,因为nio编程涉及reactor模式,需要熟悉多线程和网络编程。
开发工作量和难度大,需要处理断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
NIO bug,epoll会导致selector空轮询,直到1.7版本仍然存在。
介绍:
JBOSS开源的JAVA框架
一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序
架构:
TCP/IP -> JAVA io网络 -> NIO -> Netty框架
应用场景:
RPC框架(hadoop组件AVRO)的基础通信组件
高性能通信场景
主要针对在TCP协议下,面向Clients端的高并发应用,或者peer-to-peer场景下的大量数据持续传输的应用
特点:
设计优雅
使用方便
高性能、吞吐量
安全
社区活跃
线程模型:
传统阻塞IO服务模型
每个请求一个线程
解决需要基于IO复用模型
Reactor模式
基本设计思想:
IO复用结合线程池
概念:
Reactor:一个单独的线程运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。
Handlers:处理程序执行IO事件要完成的实际事件。
分类:
根据Reactor数量和处理资源池线程的数量不同,有3种典型的实现。
单Reactor单线程
优点:
模型简单,没有多线程的并发问题
缺点:
并发时,只能串行执行业务逻辑。
单Reactor多线程
工作流程:
1.Reactor对象通过select监控客户端请求事件,收到事件后dispatch分发
2.如果是建立连接请求,由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件
如果不是连接请求,由reactor分发调用连接对应的handler来处理
3.handler只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务
4.worker线程池分配线程执行业务逻辑,并将结果返回给handler
5.handler收到响应后,通过send将结果返回给client
主从Reactor多线程(netty使用)
流程:
1.Reactor主线程MainReactor对象通过select监听事件
2.如果是accpt请求,Acceptor处理连接事件。当Acceptor处理完后,MainReactor将连接分配给多个SubReactor。
3.subReactor监听连接队列,并创建handler处理各种事件。
netty线程模型:
概念:
BossGroup:专门负责接收客户端的连接NioEventLoopGroup
WorkerGroup:专门负责网络的读写NioEventLoopGroup
NioEventLoopGroup:事件循环组,含有多个事件循环,每一个事件循环都是NioEventLoop
NioEventLoop:表示一个不断循环的执行处理任务的单个线程,其中的selector负责监听事件,taskQueue任务队列,多个方法先后调用会串行执行。
NioChannel:网络通信组件,绑定到唯一的NioEventLoop。常见NioSocketChannel异步TCP socket连接、NioServerSocketChannel、NioDatagramChannel异步udp、NioSctpChannel等
ChannelHandler:负责处理具体的事件,内置有HttpServerCodec等
PipeLine:与NioChannel一一绑定,维护了一个ChannelHandlerContext双向链表(入站和出站遍历方向相反),每个ChannelHandlerContext有一个ChannelHandler,用于处理channel的入站和出站操作。
ChannelHandlerContext:包装了handler相关的上下文,方便pipeline操作handler。
TaskQueue:可以执行ChannelHandler通过ctx.channel().eventLoop().execute/schedule提交的任务
ServerBootstrap:服务端启动引导类
Bootstrap:客户端启动引导类
流程:
1.BossGroup(多个NioEventLoop,默认核数*2)轮询accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个workerGroup的selector。
2.workerGroup的NIOEventLoop(多个,轮询的方式处理)轮询read和write事件,顺序一一调用pipeline的多个handler在对应的NioSocketChannel处理io事件。
Task类型:
1.用户自定义的普通任务
ctx.channel().eventLoop().execute
多次提交会串行执行,因为eventLoop是一个线程。
2.用户自定义定时任务
ctx.channel().eventLoop().schedule(()->{
ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client3", CharsetUtil.UTF_8));
}, 5, TimeUnit.SECONDS);
3.非当前Reactor线程调用Channel的各种方法
比如消息推送,根据用户的标识找到对应的Channel引用,然后推送任务(write消息)到对方的eventloop上。
Bootstrap常用方法:
group()服务端设置两个EventLoop,客户端设置一个
channel()设置channel的实现类
option()给ServerChannel添加配置
childOption()给接收到的channel添加配置
childHandler()给workerGroup设置业务处理类
handler()给bossGroup设置连接处理类
bind()设置端口
connect()连接
channelHandler类:
实现类:
ChannelHandler
ChannelOutBoundHandler出站io事件 ChannelInBoundHandler入站io事件
ChannelOutBoundHandlerAdapter ChannelInBoundHandlerAdapter
常用方法:
channelActive通道就绪
channelRead通道读取数据
channelReadComplete读取完毕
handlerAdded连接建立
pipeline:
常用方法:
addFirst添加到链表开始
addLast添加到链表最后
ChannelHandlerContext:
概述:
保存Channel相关的所有上下文信息(比如pipeline和channel),同时关联一个ChannelHandler对象
常用方法:
close()
flush()刷新
writeAndFlush()将数据写到ChannelPipeline中
EventLoopGroup:
概述:
一组EventLoop的抽象,每个EventLoop维护一个selector。
常用方法:
next()从组里按照一定规则获取其中一个EventLoop来处理任务
shutdownGracefully断开连接,关闭线程。
NioEventLoopGroup:
常用的异步实现类
Unpooled类:
概述:
netty专门用来操作缓冲区的工具类
常用方法:
Unpooled.buffer(int cap) 返回ByteBuf实例
Unpooled.copiedBuffer(CharSequence string, Charset charset)
ByteBuf类:
概述:
维护了readerIndex、writerIndex、capacity、array数组。
readerIndex和writerIndex之间的数组可读,writerIndex和capacity之间的数组可写。
不需要flip反转。
常用方法:
writeByte(int var1)
getByte(int var1)
readByte() readerIndex会增加
readerIndex()
writerIndex()
readableBytes()可读长度
异步模型:
概述:
当一个异步的过程调用发出后,调用者不能立刻得到结果,而是通过Future-listen机制,用户可以方便的主动获取或者通过通知机制获取结果。
netty的异步模型建立在future和callback之上,调用run的时候立刻返回一个future,后续可以通过future监控run的状态。
Future-listen机制:
概述:
当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成的操作。
常见操作:
isDone是否完成
isSuccess判断操作是否成功
getCause获取操作失败的原因
isCancelled操作是否被取消
addListener当isDone完成后,通知指定的监听器。如果future对象已完成,则通知指定的监听器。
心跳检测:
概述:
netty提供IdleStateHandler,针对指定时间内没有读、写、读写就会发送一个心跳检测包检测是否连接。
当IdleStateHandler触发后,可以添加自定义handler的userEventTriggerd方法处理心跳检测的各种情况。
示例:
if (evt instanceof 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(ctx.channel().remoteAddress() + eventType);
}
websocket长连接:
背景:
http协议无状态,默认请求完后断开连接。
概述:
基于websocket可以实现长连接,客户端和服务器可以相互发送消息
示例:
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// http协议的编码和解码器
pipeline.addLast(new HttpServerCodec());
// 块方式写
pipeline.addLast(new ChunkedWriteHandler());
// 聚合多个段发送一次
pipeline.addLast(new HttpObjectAggregator(8192));
// 使用websocket协议,websocket发送的方式是帧
pipeline.addLast(new WebSocketServerProtocolHandler("/"));
pipeline.addLast(new Handler());
}
});
编码和解码:
概述:
网络传输的是字节,需要根据协议来编码(发送)和解码(接收)数据。
codec由decoder解码器和encoder编码器组成
编码器:
StringEncoder
ObjectEncoder对java对象
MessageToByteEncoder
ZipEncoder自动压缩
解码器:
StringDecoder
ObjectDecoder
ByteToMessageDecoder底层抽象类
ReplayingDecoder继承yteToMessageDecoder,不用再判断readableBytes
HttpObjectDecoder一个http协议的解码器
LengthFiledBaseFrameDecoder指定长度来标识整包消息,这样可以自动处理粘包和半包消息。
解码原理:
底层是ByteToMessageDecoder类,比如ToIntegerDecoder实现类,会尝试根据类型的byte位数逐个读取字节转为对应类型,放入List中。
然后再将List<Integer>交给下一个Handler处理。
Protobuf:
背景:
内置编解码器底层使用Java序列化技术,而Java序列化技术本身效率不高,无法跨语言、体积大、性能低
概述:
google开源的结构化数据存储格式,适合做数据存储或RPC。
编码器:
ProtobufEncoder
解码器:
ProtobufDecoder
使用流程:
1.引入依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
2.编写proto文件
3.通过protoc命令编译
protoc --java_out=. Student.proto
4.pipeline添加指定的编解码器
pipeline.addLast(new ProtobufEncoder());
pipeline.addLast(new ProtobufDecoder(StudentModel.Student.getDefaultInstance()));
自定义编解码器:
编码器,如果接收来自业务的数据不是对应的类型,acceptOutboundMessage方法判断false,不会被调用,而是直接发送字节。
public class LongToByteDecoder extends MessageToByteEncoder<Long> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Long o, ByteBuf byteBuf) throws Exception {
System.out.println("long to byte");
byteBuf.writeLong(o);
}
}
解码器的方法会被调用多次直到无新字节,输出的list如果有多个,会先后顺序触发下游的handler
public class MyDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception {
if (byteBuf.readableBytes() >= 8) {
list.add(byteBuf.readLong());
}
}
}
入站和出站:
概述:
pipeline管理了ChannelHandler链
入站会调用ChannelInBoundHandler类别(inbound字段),出站会调用ChannelOutBoundHandler类别(outbound字段)
ChannelDuplexHandler出入站
粘包和拆包:
粘包:
TCP协议的nagle算法会合并小数据块,封成一个包发送。服务端一次收到两个包。
拆包:
数据一次发送不完,分多次发送。
自定义协议(消息长度/分隔符)+ 编解码器:
概述:
利用自定义encoder序列化数据发送,先发长度,再发body。
自定义decoder接收,先读取长度,再按照长度读取字节。
如果拆包,等待下一次读取事件触发再读取。
如果粘包,按照长度读取没问题。
示例:
解码:
int len = in.readInt();
byte[] bytes = new byte[len];
in.readBytes(bytes);
编码:
byteBuf.writeInt(myProtocol.getLen());
byteBuf.writeBytes(myProtocol.getContent());
源码:
启动流程:
1.new NioEventLoopGroup()调用MultithreadEventExecutorGroup方法实例化
this.children = new EventExecutor[nThreads]初始化数组
this.children[i] = this.newChild((Executor)executor, args)初始化每个元素,类型为NioEventLoop
e.terminationFuture().addListener(terminationListener);为每个EventLoop添加监听器
2.bootstrap
.group(bossGroup, workerGroup)将group设置为bossGroup,childGroup设置为workerGroup。用于后期引导。
.channel(NioServerSocketChannel.class)通过反射创建ChannelFactory,用于后续创建channel实例。
.handler(new LoggingHandler())将handler字段设置为传入的实例。属于ServerSocketChannel
.childHandler()将childHandler字段设置为传入的实例。属于每个SocketChannel
.option(ChannelOption.SO_BACKLOG, 128)将配置放入LinkedHashMap
3.bootstrap.bind(7000)方法调用this.doBind(localAddress)方法
initAndRegister方法通过this.channelFactory.newChannel();方法创建channel。
init(Channel channel)方法初始化channel
setChannelOptions设置属性
addLast方法添加this.newContext(group, this.filterName(name, handler), handler)对象关联pipeline和其他handler。
this.config().group().register(channel);方法注册
AbstractBootstrap.doBind0(regFuture, channel, localAddress, promise)方法
channel.eventLoop().execute调用线程池执行this.javaChannel().bind(localAddress, this.config.getBacklog());
Net.bind(this.fd, var4.getAddress(), var4.getPort());native方法绑定端口
多线程执行run()方法循环调用select(wakenUp.getAndSet(false))、processSelectedKeys()和runAllTasks()方法
请求流程:
1.请求到达后,processSelectedKey方法触发,调用doReadMessages方法
SocketUtils.accept(javaChannel())方法调用serverSocketChannel.accept()方法获取SocketChannel
buf.add(new NioSocketChannel(this, ch))方法封装为NioSocketChannel,添加到buf
遍历buf,执行pipeline.fireChannelRead(readBuf.get(i));
最后通过childGroup.registry(child)方法将channel注册到childGroup中的一个EventLoop上
registry会先next随机取一个EventLoop,registry注册READ事件
2.调用fireChannelRead方法进入循环,select判断事件
pipeline处理流程:
fireChannelRead方法
调用invokeChannelRead(findContextInbound(), msg)方法处理读,其中findContextInbound方法会next遍历,直到找到inbound=true的ctx,调用该handler对应的ChannelRead方法。
ChannelPipeline、ChannelHandler、ChannelHandlerContext:
创建流程:
1.SocketChannel创建的时候,创建pipeline(head和tail context初始化)。
2.调用addLast方法,添加的时候,先创建handler传入,再生成Context封装handler。
心跳服务源码:
概述:
IdleStateHandler、ReadTimeoutHandler、WriteTimeoutHandler三个Handler,继承
流程:
添加handler的时候,handlerAdded触发,执行initialize方法,schedule对应事件的定时任务,比如ReaderIdleTimeoutTask(继承AbstractIdleTask、Runnable)
每当指定delay到达后,判断上一次事件对应的时间与现在的间隔,重新schedule,如果不符合,则fireUserEventTriggered传递对应的idle事件到自定义的处理handler。
写空闲会额外判断出站缓慢的情况。
ReadTimeoutHandler继承IdleStateHandler,当触发读空闲事件的时候,触发ctx.fireExceptionCaught,传入ReadTimeoutException,并关闭socket。
WriteTimeoutHandler通过定时任务判断promise是否写完,超时会抛出异常。
NioEventLoop源码:
概述:
子类EventLoop实现ScheduledExecutorService接口,提供schedule方法
实现ExecutorService等接口
继承SingleThreadEventExecutor,单个线程的addTask、runAllTasks方法
流程:
SingleThreadEventExecutor的execute方法会
调用startThread方法,进而调用SingleThreadEventExecutor.this.run()方法。run方法正是循环select判断。
select方法默认等待一秒钟,如果有定时任务,则定时剩余时间+0.5秒。
当触发条件满足的时候,返回,执行processSelectedKeys方法,对selectKey处理,然后再按照ioRatio的比例执行runAllTasks。
addTask方法调用taskQueue.offer(task),将任务放入任务队列。
异步线程池源码:
概述:
ctx.channel.eventloop().execute提交的任务默认会占用当前EventLoop线程,影响其他任务,比如select循环。
可以考虑handler或context加入自定义的线程池。
handler方案:
Handler类加入线程池静态成员变量
private EventExecutorGroup eventExecutors = new DefaultEventExecutorGroup(16);
eventExecutors.submit(耗时任务)
调用ctx.write的时候会切换回IO线程。(判断线程id,如果不是的话,封装task提交到mpsc队列,耗时相比更久)
优点:
自由控制哪些代码用指定线程池执行。
context方案:
private EventExecutorGroup eventExecutors = new DefaultEventExecutorGroup(16);
pipeline.addLast(eventExecutors, new ServerHandler()); pipeline添加的时候指定handler执行的线程池。否则使用IO线程。
read/write方法判断executor.inEventLoop()当前线程是否IO线程。
缺点:
整个handler方法都用指定线程池,不够灵活。
实现RPC:
概述:
常见阿里Dubbo、GRPC
自定义:
约定接口和协议,远程调用
设计说明:
1.创建一个接口,定义抽象方法,用于消费者和提供者之间的约定。
2.创建一个提供者,该类需要通过netty监听消费者的请求,并按照约定返回。
3.创建一个消费者,调用指定的方法,内部通过netty请求提供者获取结果。
流程:
1.client调用方法。
2.rpc封装方法和参数
3.编码,发送到服务端
4.server rpc收到消息后,解码
5.server rpc根据解码内容调用具体的方法。
6.本地服务返回结果。
7.server rpc将结果编码,发送给client。
8.client rpc将结果解码。
9.client得到结果。