3-1-3 (中) 三种io模型与netty

一、 BIO、NIO、AIO

1.1 同步和异步

同步(synchronize)、异步(asychronize)是指应用程序和内核的交互而言的.

同步:
指用户进程触发IO操作等待或者轮训的方式查看IO操作是否就绪。

同步举例:
银行取钱,我自己去取钱,取钱的过程中等待.

异步:
当一个异步进程调用发出之后,调用者不会立刻得到结果。而是在调用发出之后,被调用者通过状态、通知来通知
调用者,或者通过回调函数来处理这个调用。
使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS,OS需要支持异步IO操作
举个例子:

异步举例:
我请朋友帮我取钱,他取到钱后返回给我. (委托给操作系统OS, OS需要支持IO异步API)

1.2 阻塞和非阻塞

阻塞和非阻塞是针对于进程访问数据的时候,根据IO操作的就绪状态来采取不同的方式.
简单点说就是一种读写操作方法的实现方式. 阻塞方式下读取和写入将一直等待, 而非阻塞方式下,读取和写入方法会理解返回一个状态值.

举个例子:

阻塞: ATM机排队取款,你只能等待排队取款(使用阻塞IO的时候,Java调用会一直阻塞到读写完成才返回。)

非阻塞:

柜台取款,取个号,然后坐在椅子上做其他事,等广播通知,没到你的号你就不能去,但你可以不断的问大堂经理排到了没有。(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)

例子

老张煮开水。 老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
1 老张把水壶放到火上,站立着等水开。(同步阻塞)
2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)
3 老张把响水壶放到火上,立等水开。(异步阻塞)
4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

二、三种IO模型

2.1、BIO模型

2.1.1、BIO模型剖析

同步阻塞IO。B代表blocking

服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

适用场景:Java1.4之前唯一的选择,简单易用但资源开销太高

在这里插入图片描述

在这里插入图片描述

2.1.2、BIO模型代码实现

服务端代码


package com.lagou;


import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class IOServer {
    
    

    public static void main(String[] args) throws IOException {
    
    

        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1",8081));

        while (true){
    
    

            Socket socket = serverSocket.accept();
            new Thread(()->{
    
    
                try {
    
    
                    byte[] bytes = new byte[1024];
                    InputStream inputStream = socket.getInputStream();
                    int length = inputStream.read(bytes);

                    System.out.println(new String(bytes,0,length));
                    socket.getOutputStream().write(bytes,0,length);
                    socket.getOutputStream().flush();

                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

客户端代码


package com.lagou;

import java.io.IOException;
import java.net.Socket;

public class IOClient {
    
    

    public static void main(String[] args) throws IOException {
    
    

        Socket socket = new Socket("127.0.0.1", 8081);
        socket.getOutputStream().write("hello".getBytes());
        socket.getOutputStream().flush();

        System.out.println("server send data ===");

        byte[] bytes = new byte[1024];
        int length = socket.getInputStream().read(bytes);
        System.out.println(new String(bytes,0,length));
        socket.close();
    }
}

执行结果:

2.2、NIO模式

2.2.1、NIO介绍

同步非阻塞IO (non-blocking IO / new io)是指JDK 1.4 及以上版本。
服务器实现模式为一个请求一个通道,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。

  • 通道(Channels)
    NIO 新引入的最重要的抽象是通道的概念。Channel 数据连接的通道。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中 .

  • 缓冲区(Buwers)
    通道channel可以向缓冲区Buwer中写数据,也可以像buwer中存数据。

  • 选择器(Selector)
    使用选择器,借助单一线程,就可对数量庞大的活动 I/O 通道实时监控和维护。
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.2.2、NIO特点

当一个连接创建后,不会需要对应一个线程,这个连接会被注册到多路复用器,所以一个连接只需要一个线程即可,所有的连接需要一个线程就可以操作,该线程的多路复用器会轮训,发现连接有请求时,才开启一个线程处理。

在这里插入图片描述

如上图所示,IO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据。

而在NIO模型中,他把这么多while死循环变成一个死循环,这个死循环由一个线程控制,那么他又是如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢? 这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据,下面我再举个非常简单的生活中的例子说明IO与NIO的区别

在一家幼儿园里,小朋友有上厕所的需求,小朋友都太小以至于你要问他要不要上厕所,他才会告诉你。幼儿园一共有100个小朋友,有两种方案可以解决小朋友上厕所的问题:

  1. 每个小朋友配一个老师。每个老师隔段时间询问小朋友是否要上厕所,如果要上,就领他去厕所,100个小朋友就需要100个老师 来询问,并且每个小朋友上厕所的时候都需要一个老师领着他去上,这就是IO模型,一个连接对应一个线程。
  2. 所有的小朋友都配同一个老师。这个老师隔段时间询问所有的小朋友是否有人要上厕所,然后每一时刻把所有要上厕所的小朋友批 量领到厕所,这就是NIO模型,所有小朋友都注册到同一个老师,对应的就是所有的连接都注册到一个线程,然后批量轮询。
2.2.3、NIO服务器与客户端交互代码实现

简单讲完了JDK NIO的解决方案之后,我们接下来使用NIO的方案替换掉IO的方案,我们先来看看,如果用JDK原生的NIO来实现服务端,该怎么做

服务端

package com.lagou.server;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.security.Key;
import java.util.Iterator;
import java.util.Scanner;

public class NIOServer extends Thread{
    
    

    //1. 定义多路复用器
    private Selector selector;

    //2. 定义读写缓冲区
    private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

    // 3. 定义构造方法 --> 初始化 端口
    public NIOServer(int port) {
    
    
        // 类构造前的初始化
        init(port);
    }

    // 4、定义初始化方法
    private void init(int port) {
    
    

        try {
    
    
            System.out.println("服务器正在启动");

            // 1) 开启多路复用器
            this.selector = Selector.open();

            // 2) 开启服务通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

            // 3) 设置为非阻塞
            serverSocketChannel.configureBlocking(false);

            // 4) 绑定端口
            serverSocketChannel.bind(new InetSocketAddress(port));

            // 5) 注册,标记服务通标状态
            serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);

            System.out.println("服务器启动完毕!!");

        }catch (Exception e){
    
    
            e.printStackTrace();
        }

    }


    // 5 定义 Thread 的 run 方法
    public void run(){
    
    
        while (true){
    
    

            try {
    
    
                //1) 当有至少一个 通道被选中,执行此方法
                this.selector.select();

                //2) 获取选中的通道编号集合
                Iterator<SelectionKey> keys = this.selector.selectedKeys().iterator();

                //3) 遍历keys
                while(keys.hasNext()){
    
    

                    SelectionKey key = keys.next();

                    //4) 当前key 需要从keys集合中 移出,如果不移出,下次还会循环会执行对应的逻辑,造成业务混乱
                    keys.remove();

                    //5) 判断通道是否有效
                    if(key.isValid()){
    
    

                        //6) 判断是否可用
                        try {
    
    
                            if(key.isAcceptable()){
    
    
                                accept(key);
                            }

                        }catch (Exception e){
    
    
                            key.cancel();
                        }

                        //7) 判断是否可读
                        try {
    
    
                            if(key.isReadable()){
    
    
                                read(key);
                            }

                        }catch (Exception e){
    
    
                            key.cancel();
                        }


                        //8) 判断是否可写
                        try {
    
    
                            if(key.isWritable()){
    
    
                                write(key);
                            }

                        }catch (Exception e){
    
    
                            key.cancel();
                        }

                    }

                }


            }catch (Exception e){
    
    
                e.printStackTrace();
            }

        }

    }



    private void accept(SelectionKey key) {
    
    

        try {
    
    
            // 1、当前通道 init方法中注册代理selector 中 的ServerSocketChannel
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();

            // 2、 阻塞方法,客户端发起后请求 返回
            SocketChannel channel = serverSocketChannel.accept();

            //3、serverSocketChannel 设置为非阻塞
            channel.configureBlocking(false);

            // 4、设置对应客户端的通道标记,设置次通道为可读时使用
            channel.register(this.selector,SelectionKey.OP_READ);

        }catch (Exception e){
    
    

            e.printStackTrace();
        }

    }


    // 使用通道 读取数据
    private void read(SelectionKey key) {
    
    

        try {
    
    
            // 清空缓存
            this.readBuffer.clear();

            // 获取当前通道对象
            SocketChannel channel = (SocketChannel) key.channel();

            // 将通道的数据(客户发送的data)读到缓存中
            int readLen = channel.read(readBuffer);

            //如果通道中没有数据,退出
            if(readLen == -1){
    
    
                //关闭通道
                key.channel().close();

                //关闭连接
                key.cancel();
                return;
            }

            // Buffer 中有游标,游标不会重置,需要我们调用flip重置,否则读取不一致
            this.readBuffer.flip();

            // 创建有效字节长度数组
            byte[] bytes = new byte[readBuffer.remaining()];

            // 读取buffer中 数据保存在字节数组
            readBuffer.get(bytes);
            System.out.println("收到了从客户端 "+ channel.getRemoteAddress() + " : "+ new String(bytes,"UTF-8"));

            // 注册通道,标记为写操作
            channel.register(this.selector,SelectionKey.OP_WRITE);



        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }

    private void write(SelectionKey key) {
    
    
        try {
    
    

            // 清空缓存
            this.writeBuffer.clear();

            // 获取当前通道对象
            SocketChannel channel = (SocketChannel) key.channel();

            // 录入数据
            Scanner scanner = new Scanner(System.in);

            try {
    
    
                System.out.println("即将发送数据到客户端..");

                String line = scanner.nextLine();

                // 把录入数据写到 Buffer中
                writeBuffer.put(line.getBytes("UTF-8"));

                // 重置缓存游标
                writeBuffer.flip();

                channel.write(writeBuffer);
                channel.register(this.selector,SelectionKey.OP_READ);

            }catch (Exception e){
    
    
                e.printStackTrace();
            }


        }catch (Exception e){
    
    
            e.printStackTrace();
        }
    }

    // 6 main方法启动线程
    public static void main(String[] args) {
    
    
        new Thread(new NIOServer(8888)).start();
    }

}

Client客户端

package com.lagou.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class NIOClient {
    
    


    public static void main(String[] args) {
    
    

        // 创建远程地址
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);

        SocketChannel channel = null;

        // 定义缓存
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try{
    
    
            // 开启通道
            channel = SocketChannel.open();
            // 连接远程服务器
            channel.connect(address);

            Scanner scanner = new Scanner(System.in);

            while (true){
    
    

                System.out.println("客户端即将给服务器发送数据...");

                String line = scanner.nextLine();

                if("exit".equals(line)){
    
    
                    break;
                }

                // 控制台输入数据写到缓存
                buffer.put(line.getBytes("UTF-8"));

                // 重置buffer 游标
                buffer.flip();

                // 发送到数据
                channel.write(buffer);

                // 清空 缓存数据
                buffer.clear();

                // 读取服务器返回的数据
                int readLen = channel.read(buffer);
                if(readLen == -1){
    
    
                    break;
                }

                // 重置buffer 游标
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                // 读取 数据到字节数组中
                buffer.get(bytes);

                System.out.println("收到了服务器发送的数据 : "+ new String(bytes,"UTF-8"));
                buffer.clear();

            }

        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            if (null != channel){
    
    
                try {
    
    
                    channel.close();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }


    }


}

2.3、AIO模型剖析

异步非阻塞IO。A代表asynchronize

当有流可以读时,操作系统会将可以读的流传入read方法的缓冲区,并通知应用程序,对于写操作,OS将write方法的流写入完毕是操作系统会主动通知应用程序。因此read和write都是异步 的,完成后会调用回调函数。

使用场景:连接数目多且连接比较长(重操作)的架构,比如相册服务器。重点调用了OS参与并发操作,编程比较复杂。
Java7开始支持
在这里插入图片描述

三、Netty

3.1 Netty简介

3.1.1 Netty 介绍

Netty 是由 JBOSS 提供一个异步的、 基于事件驱动的网络编程框架。

Netty 可以帮助你快速、 简单的开发出一 个网络应用, 相当于简化和流程化了 NIO 的开发过程。 作为当前最流行的 NIO 框架, Netty 在互联网领域、 大数据分布式计算领域、 游戏行业、 通信行业等获得了广泛的应用, 知名的Elasticsearch 、 Dubbo 框架内部都采用了 Netty。

在这里插入图片描述

3.1.2 为什么使用Netty
  • NIO缺点
  1. NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannelSocketChannel、 ByteBuwer 等.
  2. 可靠性不强,开发工作量和难度都非常大
  3. NIO 的 Bug。例如 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。
  • Netty优点
  1. 对各种传输协议提供统一的 API
  2. 高度可定制的线程模型——单线程、一个或多个线程池
  3. 更好的吞吐量,更低的等待延迟
  4. 更少的资源消耗
  5. 最小化不必要的内存拷贝

3.2 Netty线程模型

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

Netty 抽象出两组线程池, BossGroup 专门负责接收客 户端连接, WorkerGroup 专门负责网络读写操作。

NioEventLoop 表示一个不断循环执行处理 任务的线程, 每个 NioEventLoop 都有一个 selector, 用于监听绑定在其上的 socket 网络通道。 NioEventLoop 内部采用串行化设计, 从消息的读取->解码->处理->编码->发送, 始终由 IO 线 程 NioEventLoop 负责。

3.3 Netty常见组件

ChannelHandler 及其实现类
ChannelHandler 接口定义了许多事件处理的方法, 我们可以通过重写这些方法去实现具 体的业务逻辑
我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter, 然后通过 重写相应方法实现业务逻辑, 我们接下来看看一般都需要重写哪些方法:

public void channelActive(ChannelHandlerContext ctx), 通道就绪事件
public void channelRead(ChannelHandlerContext ctx, Object msg), 通道读取数据事件
public void channelReadComplete(ChannelHandlerContext ctx) , 数据读取完毕事件
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause), 通道发生异常事件

ChannelPipeline
ChannelPipeline 是一个 Handler 的集合, 它负责处理和拦截 inbound 或者 outbound 的事 件和操作, 相当于一个贯穿 Netty 的链。

-ChannelPipeline addFirst(ChannelHandler… handlers), 把一个业务处理类(handler) 添加到链中的第一个位 置
ChannelPipeline addLast(ChannelHandler… handlers), 把一个业务处理类(handler) 添加到链中的最后一个 位置
在这里插入图片描述

ChannelHandlerContext
这 是 事 件 处 理 器 上 下 文 对 象 , Pipeline 链 中 的 实 际 处 理 节 点 。 每 个 处 理 节 点ChannelHandlerContext 中 包 含 一 个 具 体 的 事 件 处 理 器 ChannelHandler , 同 时 ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler 进行调用。 常用方法如下所示

ChannelFuture close(), 关闭通道
ChannelOutboundInvoker flush(), 刷新
ChannelFuture writeAndFlush(Object msg) , 将 数 据 写 到 ChannelPipeline 中 当 前
ChannelHandler 的下一个 ChannelHandler 开始处理(出站)

ChannelFuture
表示 Channel 中异步 I/O 操作的结果, 在 Netty 中所有的 I/O 操作都是异步的, I/O 的调 用会直接返回, 调用者并不能立刻获得结果, 但是可以通过 ChannelFuture 来获取 I/O 操作 的处理状态。 常用方法如下所示:

Channel channel(), 返回当前正在进行 IO 操作的通道
ChannelFuture sync(), 等待异步操作执行完毕

EventLoopGroup 和其实现类 NioEventLoopGroup

EventLoopGroup 是一组 EventLoop 的抽象, Netty 为了更好的利用多核 CPU 资源, 一般 会有多个 EventLoop 同时工作, 每个 EventLoop 维护着一个 Selector 实例。 EventLoopGroup 提供 next 接口, 可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。 在 Netty 服务器端编程中, 我们一般都需要提供两个 EventLoopGroup, 例如: BossEventLoopGroup 和 WorkerEventLoopGroup。

public NioEventLoopGroup(), 构造方法
public Future<?> shutdownGracefully(), 断开连接, 关闭线程

ServerBootstrap 和 Bootstrap

ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置; Bootstrap 是 Netty 中的客户端启动助手, 通过它可以完成客户端的各种配置。 常用方法如下 所示:

-public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于 服务器端, 用来设置两个 EventLoop
public B group(EventLoopGroup group) , 该方法用于客户端, 用来设置一个 EventLoop
public B channel(Class<? extends C> channelClass), 该方法用来设置一个服务器端的通道实现
public B option(ChannelOption option, T value), 用来给 ServerChannel 添加配置
public ServerBootstrap childOption(ChannelOption childOption, T value), 用来给接收到的 通 道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler), 该方法用来设置业务处理类(自定 义的 handler)
public ChannelFuture bind(int inetPort) , 该方法用于服务器端, 用来设置占用的端口号
public ChannelFuture connect(String inetHost, int inetPort) 该方法用于客户端, 用来连接服务器端

3.3 Netty实现

目标: 使用netty客户端给服务端发送数据,服务端接收消息打印
首先引入Maven依赖

<dependency>
	<groupId>io.netty</groupId> 
	<artifactId>netty-all</artifactId>
	<version>4.1.6.Final</version>
</dependency>

3.3.1 服务端的实现

public class NettyServer {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    

        // 1、创建 NioEventLoopCroup的两个实例:bossGroup workGroup
        // 当前这两个实例代表两个线程,默认线程数为CPU 核心数乘2
        // bossGroup 接受客户端 传过来的请求
        // workGroup 处理请求


        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();


        // 2、创建 服务启动辅助类:组装一些必要的组件
        ServerBootstrap serverBootstrap = new ServerBootstrap();

        // 设置组,第一个bossGroup 负责连接,workGroup 负责连接之后的io处理
        serverBootstrap.group(bossGroup,workGroup)
                // channel 方法指定服务器监听的通道类型
                .channel(NioServerSocketChannel.class)
                // 设置channel handler ,每一个客户端 连接后,给定一个监听器进行处理
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
    
    
                    @Override
                    protected void initChannel(NioSocketChannel nioServerSocketChannel) throws Exception {
    
    

                        // 传输通道
                        ChannelPipeline pipeline = nioServerSocketChannel.pipeline();

                        // 在通道 上添加对通道的处理器,该处理器可能还是要给监听器
                        pipeline.addLast(new StringDecoder());
                        pipeline.addLast(new StringEncoder());

                        pipeline.addLast(new SimpleChannelInboundHandler<String>() {
    
    
                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
    
    

                                System.out.println(s);
                            }
                        });

                    }
                });

        //bind 监听端口
        ChannelFuture channelFuture = serverBootstrap.bind(9999).sync();

        System.out.println(" tcp server start suceess");

        // 会阻塞等待 知道服务器的channel 关闭
        channelFuture.channel().closeFuture().sync();

    }

}



3.3.2 客户端的实现
package com.lagou.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.sctp.nio.NioSctpServerChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Date;

public class NettyClient {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    

        // 客户端的启动辅助类
        Bootstrap bootstrap = new Bootstrap();
        // 线程池的实例
        NioEventLoopGroup group = new NioEventLoopGroup();

        bootstrap.group(group)
                // channel方法 指定通道类型
                .channel(NioServerSocketChannel.class)
                // 通道初始化了
                .handler(new ChannelInitializer<Channel>() {
    
    
                    @Override
                    protected void initChannel(Channel channel) throws Exception {
    
    

                        channel.pipeline().addLast(new StringEncoder());

                    }
                });

        Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();

        while (true){
    
    
            System.out.println(111);
            channel.writeAndFlush(new Date() + ": hello world");
            Thread.sleep(2000);
        }

    }


}


猜你喜欢

转载自blog.csdn.net/qq_42082278/article/details/113176576