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, 03);
    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繁杂,使用麻烦;
需要掌握selectorchannelbuffer等;
需要熟悉java多线程编程,因为nio编程涉及reactor模式,需要熟悉多线程和网络编程。
开发工作量和难度大,需要处理断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
NIO bugepoll会导致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(多个,轮询的方式处理)轮询readwrite事件,顺序一一调用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(bossGroupworkerGroup)将group设置为bossGroupchildGroup设置为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(groupthis.filterName(namehandler), handler)对象关联pipeline和其他handler
    this.config().group().register(channel);方法注册
AbstractBootstrap.doBind0(regFuturechannellocalAddresspromise)方法
    channel.eventLoop().execute调用线程池执行this.javaChannel().bind(localAddressthis.config.getBacklog());
        Net.bind(this.fdvar4.getAddress(), var4.getPort());native方法绑定端口
    多线程执行run()方法循环调用select(wakenUp.getAndSet(false))、processSelectedKeys()和runAllTasks()方法

请求流程:

1.请求到达后,processSelectedKey方法触发,调用doReadMessages方法

SocketUtils.accept(javaChannel())方法调用serverSocketChannel.accept()方法获取SocketChannel
buf.add(new NioSocketChannel(thisch))方法封装为NioSocketChannel,添加到buf
遍历buf,执行pipeline.fireChannelRead(readBuf.get(i));
    最后通过childGroup.registry(child)方法将channel注册到childGroup中的一个EventLoop
        registry会先next随机取一个EventLoopregistry注册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方法

流程:

SingleThreadEventExecutorexecute方法会
    调用startThread方法,进而调用SingleThreadEventExecutor.this.run()方法。run方法正是循环select判断。
        select方法默认等待一秒钟,如果有定时任务,则定时剩余时间+0.5秒。
        当触发条件满足的时候,返回,执行processSelectedKeys方法,对selectKey处理,然后再按照ioRatio的比例执行runAllTasks
    addTask方法调用taskQueue.offer(task),将任务放入任务队列。

异步线程池源码:

概述:

ctx.channel.eventloop().execute提交的任务默认会占用当前EventLoop线程,影响其他任务,比如select循环。
可以考虑handlercontext加入自定义的线程池。

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得到结果。
posted @ 2022-04-02 11:31  心平万物顺  阅读(165)  评论(0编辑  收藏  举报