网络编程-(BIO,NIO,AIO)

BIO

在 JDK1.4 之前,我们建立网络连接的时候只能采用 BIO,需要先在服务端启动一个ServerSocket,然后
在客户端启动 Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,
而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的
话,客户端线程会等待请求结束后才继续执行,这就是阻塞式IO。
在这里插入图片描述

服务端:

public static void main(String[] args) throws IOException {
       System.out.println("在9999进行启动");
       ServerSocket serverSocket = new ServerSocket(9999);
       while (true) {
           Socket accept = serverSocket.accept();
           InputStream inputStream = accept.getInputStream();
           byte[] bytes = new byte[1024];
           String clientIP = accept.getInetAddress().getHostAddress();
           // 数据的长度
           int len = inputStream.read(bytes);
           System.out.println(String.format("收到客户端的消息 %s : %s",clientIP, new String(bytes, 0, len)));;
           Scanner scanner = new Scanner(System.in);
           String rsp = scanner.nextLine();
           OutputStream outputStream = accept.getOutputStream();
           outputStream.write(rsp.getBytes());
           accept.close();
       }
   }

客户端

public static void main(String[] args) throws IOException {
   while (true) {
       Socket socket = new Socket("127.0.0.1", 9999);
       OutputStream outputStream = socket.getOutputStream();
       Scanner scanner = new Scanner(System.in);
       String msg = scanner.nextLine();
       outputStream.write(msg.getBytes());
       InputStream inputStream = socket.getInputStream();
       byte[] data = new byte[1024];
       int len = inputStream.read(data);
       System.out.println(new String(data, 0, len));
   }
}

这样客户端和服务端就能正常的通信,但是阻塞的。服务端没有收到数据会一直等待,同样的客户端没有收到响应也会一直阻塞。

NIO

在这里插入图片描述

从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。
NIO 和 BIO 有着相同的目的和作用,但是它们的实现方式完全不同;

  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 IO 的效率比流 IO 高很多。
  • NIO 是非阻塞式的,这一点跟 BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。
    NIO 主要有三大核心部分:
  • Channel通道
  • Buffer缓冲区
  • Selector选择器
    传统的 BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel和 Buffer进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
    原图连接,编程指北
    在这里插入图片描述
Buffer

缓冲区(Buffer):实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel 提供从文件、网络读取数据的渠道, 但是读取或写入的数据都必须经由 Buffer
在这里插入图片描述

  • public abstract ByteBuffer put(byte[] b); 存储字节数据到缓冲区
  • public abstract byte[] get(); 从缓冲区获得字节数据
  • public final byte[] array(); 把缓冲区数据转换成字节数组
  • public static ByteBuffer allocate(int capacity); 设置缓冲区的初始容量
  • public static ByteBuffer wrap(byte[] array); 把一个现成数组放到缓冲区中使用
  • public final Buffer flip(); 翻转缓冲区,重置位置到初始位置(缓冲区有一个指针从头开始读取数据,读到缓冲区尾部时,可以使用这个方法,将指针重新定位到头)
Channel

类似于 BIO 中的 stream用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的 stream 是单向的,(InputStream,OutputStream ),而 NIO 中的通道(Channel)是双向的, 既可以用来进行读操作,也可以用来进行写操作。
在这里插入图片描述

  • FileChannel 用于文件的数据读写
  • DatagramChannel 用于 UDP 的数据读写
  • ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。

1 操作本地文件(读写)

public static void main(String[] args) throws IOException {
     String path = "E:\\data.txt";
     FileOutputStream outputStream = new FileOutputStream(path);
     FileChannel channel = outputStream.getChannel();
     Scanner scanner = new Scanner(System.in);
     String str = scanner.nextLine();
     ByteBuffer allocate = ByteBuffer.allocate(1024);
     allocate.put(str.getBytes());
     allocate.flip();
     channel.write(allocate);
     System.out.println("写完毕开始读入");
     FileInputStream inputStream = new FileInputStream(path);
     FileChannel inChannel = inputStream.getChannel();
     inChannel.read(allocate);
     System.out.println(new String(allocate.array(), 0, allocate.position()));
 }

allocate.flip();的作用
在这里插入图片描述

2 复制文件
BIO实现

public static void main(String[] args) throws IOException {
   FileOutputStream outputStream = new FileOutputStream("E:\\2.txt");
   FileInputStream inputStream = new FileInputStream("E:\\1.txt");
   byte[] buffer = new byte[1024];
   int len = 0;
   while ((len = inputStream.read(buffer)) != -1) {
       outputStream.write(buffer, 0, len);
   }
   outputStream.close();
   inputStream.close();
}

NIO实现

public static void main(String[] args) throws IOException {
    FileOutputStream outputStream = new FileOutputStream("E:\\2.txt");
    FileInputStream inputStream = new FileInputStream("E:\\1.txt");
    FileChannel outputStreamChannel = outputStream.getChannel();
    FileChannel inputStreamChannel = inputStream.getChannel();
    outputStreamChannel.transferFrom(inputStreamChannel,0, inputStreamChannel.size());
    outputStream.close();
    inputStream.close();
}
Selector选择器

能够检测多个注册的通道上是否有事件发生(读、写、连接),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销.
在这里插入图片描述
该类的常用方法如下所示:

  • public static Selector open(),得到一个选择器对象
  • public int select(long timeout),监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的
  • SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
  • public Set selectedKeys(),从内部集合中得到所有的 SelectionKey
SelectionKey

代表了 Selector 和网络通道的注册关系
一共四种(就是连接事件)

  • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
  • int OP_CONNECT:代表连接已经建立,值为 8
  • int OP_READ 和 int OP_WRITE:代表了读、写操作,值为 1 和 4

该类的常用方法如下所示:

  • public abstract Selector selector(),得到与之关联的 Selector 对象
  • public abstract SelectableChannel channel(),得到与之关联的通道
  • public final Object attachment(),得到与之关联的共享数据
  • public abstract SelectionKey interestOps(int ops),设置或改变监听事件
  • public final boolean isAcceptable(),是否可以 accept
  • public final boolean isReadable(),是否可以读
  • public final boolean isWritable(),是否可以写
ServerSocketChannel

用来在服务器端监听新的客户端 Socket 连接
常用方法如下所示:

  • public static ServerSocketChannel open(),得到一个 ServerSocketChannel 通道
  • public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
  • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式, 取 值 false 表示采用非阻塞模式
  • public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
  • public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
SocketChannel

网络 IO 通道,具体负责进行读写操作
NIO 总是把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
常用方法如下所示:

  • public static SocketChannel open(),得到一个 SocketChannel 通道
  • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式, 取值 false 表示采用非阻塞模式
  • public boolean connect(SocketAddress remote),连接服务器
  • public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成连接操作
  • public int write(ByteBuffer src),往通道里写数据
  • public int read(ByteBuffer dst),从通道里读数据
  • public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
  • public final void close(),关闭通道
    基于上面的知识,我们写一个小案例,客户端与服务端的通信
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        Selector selector = Selector.open();
        serverSocketChannel.bind(new InetSocketAddress(9999));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            if (selector.select(2000) == 0) {
                System.out.println("没有任务,不阻塞");
                continue;
            }
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                if (next.isAcceptable()) {
                    // 建立连接 并且注册 SelectionKey
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("建立连接");
                }
                if (next.isReadable()) {
                    // 已经建立了连接了 可以进行读取了
                    SocketChannel channel = (SocketChannel) next.channel();
                    ByteBuffer buffer = (ByteBuffer) next.attachment();
                    channel.read(buffer);
                    System.out.println(new String(buffer.array(), 0, buffer.position()));
                    buffer.clear();
                }
                // 将已经处理过的事件移除
                iterator.remove();
            }
        }
    }
}

1 建立一在服务端起一个ServerSocketChannel 并绑定IP和端口
2 注册一个Selector 用于注册SelectionKey
3 等待客户端的连接,如果发来的请求SelectionKey isAcceptable 则表示建立连接,会往Selector 注册一个SelectionKey
4 如果是isReadable对发送的数据进行读取。

public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 9999);
        if (!channel.connect(addr)) {
            while (!channel.finishConnect()) {
                System.out.println("非阻塞去作别的事情");
            }
            Scanner scanner = new Scanner(System.in);
            String msg = new String();
            // 接收输入不断的往服务器写数据
            while (!(msg = scanner.nextLine()).equals("end")) {
                ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());
                channel.write(wrap);
            }
            System.in.read();
        }
    }
}

1 连接服务端
2 服务端注册SelectionKey 与 这个通道完成绑定
3 向服务端发送数据。

AIO

在这里插入图片描述

JDK 7 引入了 Asynchronous IO,即 AIO,叫做异步不阻塞的 IO,也可以叫做NIO2。在进行 IO 编程中,常用到两种模式:Reactor模式 和 Proactor模式。

  • NIO采用 Reactor 模式。在NIO中,当有事件发生时,服务器端会得到通知,然后进行相应的处理。这个模式要求应用程序在就绪事件上执行实际的I/O操作,因此程序员需要自行处理就绪事件和数据的读取。
  • AIO采用 Proactor 模式。在AIO中,应用程序继续执行而不必等待I/O操作完成。当I/O操作完成时,操作系统会通知应用程序。这意味着程序员可以专注于应用逻辑,而不必在意I/O操作的状态。一般适用于连接数较多且连接时间较长的应用。
    NIO 和 AIO还有一个非常重要的区别就是NIO需要周期的检查状态,而AIO是通过回调函数来通知应用程序。
    1 单线程模型
    在这里插入图片描述
    2 多线程模型
    在这里插入图片描述

也是就使用一个线程负责接收请求,一个线程池来负责处理具体的任务。

Netty

Netty模型概览:
在这里插入图片描述
Netty 抽象出两组线程池:BossGroup、WorkerGroup

  • BossGroup 专门负责接收客户端连接
  • WorkerGroup 专门负责网络读写操作
  • BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup,相当于一个事件循环组

NioEventLoopGroup 可以有多个线程,即含有多个NioEventLoop

NioEventLoop 表示一个不断循环的执行处理任务的线程

  • 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
    • Selector 上可以注册监听多个 NioChannel,也就是监听Socket网络通信
    • 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
    • 每个 NioChannel 都绑定有一个自己的 ChannelPipeline
  • NioEventLoop 内部采用串行化(Pipeline)设计:责任链模式
    - 消息读取 ==> 解码 ==> 处理(handlers) ==> 编码 ==> 发送,始终由IO线程NioEventLoop 负责

一个Client连接的执行流程

  • Boss的NioEventLoop 循环执行步骤:
    1 选择器注册:在Boss的NioEventLoop启动时,会创建一个java.nio.channels.Selector,并将服务器端监听的Channel注册到这个Selector上。
    2 循环处理:NioEventLoop会进入一个循环,不断地处理事件。
    3 事件轮询:在每一次循环中,Boss的NioEventLoop会调用Selector的select()方法来轮询就绪事件,这些事件包括新的客户端连接事件、读取事件、写入事件等。
    4 处理事件:当有就绪事件时,NioEventLoop会遍历就绪的SelectionKey,并根据具体的事件类型进行处理。对于ServerSocketChannel来说,会处理新的客户端连接事件。
    5 接受连接:对于新的客户端连接事件,Boss的NioEventLoop会接受这个连接,并将与客户端通信的Channel注册到另一个负责处理I/O操作的EventLoop上,如Worker的NioEventLoop。
    6 执行任务:除了处理网络事件外,Boss的NioEventLoop还可能处理其他任务,如定时任务、定时器事件等。
    7 事件处理完成:一次循环完成后,NioEventLoop会继续下一轮的事件轮询,直到被关闭。
  • Worker的NIOEventLoop 循环执行步骤:
    • 轮询read、write 事件
    • 在对应NioSocketChannel中,处理业务相关操作(ChannelHandler)
    • 处理任务队列的任务,即 runTasks
  • 每个Worker的NioEventLoop 处理业务时会使用管道Pipeline。Pipeline中包含了 Channel,通过管道可以获取到对应Channel,Channel 中维护了很多的Handler处理器。
Netty核心API
  1. ServerBootstrap 和 Bootstrap
  • ServerBootstrap 是 Netty 中的服务端启动助手,通过它可以完成服务端的各种配置;
  • Bootstrap 是 Netty 中的客户端启动助手,通过它可以完成客户端的各种配置。

常用方法:

  • 服务端ServerBootstrap

    • ServerBootstrap group(parentGroup , childGroup), 该方法用于设置两个EventLoopGroup,连接线程组和工作线程组
    • 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) ,该方法用于设置占用端口号
  • 客户端Bootstrap

    • public B group(EventLoopGroup group) ,该方法用来设置客户端的 EventLoopGroup
    • public B channel(Class<? extends C> channelClass),该方法用来设置服务端或客户端通道的实现类型
    • public ChannelFuture connect(String inetHost, int inetPort) ,该方法用来配置连接服务端地址信息,host:port
  1. EventLoopGroup(Boss\WorkerGroup)
    在 Netty 服务端编程中,一般需要提供两个 EventLoopGroup: ①BossEventLoopGroup专门负责接
    收客户端连接、②WorkerEventLoopGroup专门负责网络读写操作。
  • Netty 为了更好的利用多核 CPU 资源,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护
    着一个 Selector 实例。
  • EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。
  • EventLoopGroup 本质是一组 EventLoop,池化管理的思想

通常一个服务端口即一个ServerSocketChannel 对应一个Selector 和一个EventLoop 线程,BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给WorkerEventLoopGroup 来进行 IO处理。
在这里插入图片描述

  • BossEventLoopGroup 通常是单线程的 EventLoop,EventLoop 维护着一个注册了ServerSocketChannel 的 Selector 实例
  • Boss的EventLoop 不断轮询 Selector 将连接事件分离出来,通常是 OP_ACCEPT 事件, 然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup
  • WorkerEventLoopGroup 会由 next 选择其中一个 EventLoop 来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的事件进行处理。

常用方法:

  • public NioEventLoopGroup(),构造方法
  • public Future<?> shutdownGracefully(),断开连接,关闭线程
  1. ChannelHandler 及其实现类

在这里插入图片描述
我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法:

  • channelActive(ChannelHandlerContext ctx),通道就绪事件
  • channelRead(ChannelHandlerContext ctx, Object msg),通道读取数据事件
  • channelReadComplete(ChannelHandlerContext ctx) ,数据读取完毕事件
  • exceptionCaught(ChannelHandlerContext ctx, Throwable cause),通道发生异常事件
  1. ChannelPipeline
    ChannelPipeline是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链(责任链模式)。

在这里插入图片描述
上图绿线的线串起来的就是Pipeline,它包含3个处理不同业务的ChannelHandler,依次通过这三个ChannelHandler。因为这3个ChannelHandler不知道彼此。所以要用ChannelHandlerContext上下文来说明,ChannelHandlerContext包含ChannelHandler、Channel、pipeline的信息。

  • ChannelPipeline addFirst(ChannelHandler… handlers),把业务处理类(handler)添加到Pipeline链中的第一个位置
  • ChannelPipeline addLast(ChannelHandler… handlers),把业务处理类(handler)添加到Pipeline链中的最后一个位置
  1. ChannelHandlerContext
    ChannelHandlerContext是事件处理器上下文对象, Pipeline链中的实际处理节点。 每个处理节点ChannelHandlerContext 中包含一个具体的事件处理器 ChannelHandler , 同时ChannelHandlerContext 中也绑定了对应的 Pipeline 和 Channel 的信息,方便对 ChannelHandler进行调用。

常用方法:

  • ChannelFuture close(),关闭通道
  • ChannelOutboundInvoker flush(),刷新
  • ChannelFuture writeAndFlush(Object msg) ,将数据写到ChannelPipeline中当前ChannelHandler 的下一个ChannelHandler 开始处理(出栈交给下一个handler将继续处理)。
  1. ChannelOption
    Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。ChannelOption 是Socket 的标准化参数而非 Netty 的独创。
    常配参数:
  • ChannelOption.SO_BACKLOG:用来初始化服务器可连接队列大小,对应 TCP/IP 协议 listen 函数中的 backlog 参数。
    • 服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
    • 如果请求连接过多,服务端将不能及时处理,多余连接放在队列中等待,backlog 参数指定了等待队列大小。
  • ChannelOption.SO_KEEPALIVE ,连接是否一直保持(是否长连接)。
  1. ChannelFuture
    ChannelFuture表示 Channel 中异步 IO 操作的未来结果,在 Netty 中异步IO操作都是直接返回,调用者并不能立刻获得结果,但是可以通过 ChannelFuture 来获取 IO 操作的处理状态。Netty异步非阻塞处理事件,如果事件很费时,会通过Future异步处理,不会阻塞。
    常用方法:
  • Channel channel(),返回当前正在进行IO操作的通道
  • ChannelFuture sync(),等待异步操作执行完毕
  1. Unpooled
    Unpooled是Netty 提供的一个专门用来操作缓冲区的工具类。
    常用方法:
    ByteBuf copiedBuffer(CharSequence string, Charset charset),通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 对象)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值