Netty教程01:BIO and NIO

本文详细介绍了Java NIO(非阻塞I/O)的相关概念,包括Netty、BIO、NIO和AIO的比较、缓冲区、通道、选择器及其在实际案例中的应用。通过示例代码展示了NIO如何提高网络编程的效率,如文件读写、网络通信的非阻塞处理。文章还讨论了NIO的零拷贝技术和在群聊系统中的应用,强调了NIO在处理高并发连接时的优越性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、什么是netty

  • Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github上的独立项目。
  • Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
  • Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
  • 要透彻理解Netty , 需要先学习 NIO , Netty的底层实现是NIO

2、Netty的应用场景

  • 大数据行业:Hadoop
  • 互联网行业:RPC框架,Dubbo
  • 游戏行业:Netty 作为高性能的基础通信组件,提供了 TCP/UDP 和 HTTP 协议栈,方便定制和开发私有协议栈,账号登录服务器

3、BIO

1、IO模型概述

在这里插入图片描述

  • I/O 模型理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
    Java共支持3种网络编程模型/IO模式:BIONIOAIO

  • Java BIO
    同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销

  • Java NIO
    同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理

  • Java AIO(NIO.2) : 异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor
    模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

同步:你给我发一个消息,我给你发一个消息,你要不给我发,我也不回复你

异步:你给我发一个消息,我给你回一个消息,你不理我我也继续给你发消息,不需要非得等你

2、BIO工作机制

  1. 服务器端启动一个ServerSocket
  2. 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户 建立一个线程与之通讯
  3. 客户端发出请求后, 先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
  4. 如果有响应,客户端线程会等待请求结束后,在继续执行

3、BIO案例演示

需求
使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
要求使用线程池机制改善,可以连接多个客户端.
服务器端可以接收客户端发送的数据(telnet 方式即可)。

新建maven项目

1、服务端
package com.lian.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
    public static void main(String[] args) throws Exception {
        /**
         * 思路
         * 1、创建一个线程池
         * 2、如果由客户端连接,就创建一个线程,与之通讯(单独写一个方法)
         */
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,socket连接客户端和服务端
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务端启动啦");
        //一直等待客户端连接
        while (true){
            System.out.println("等待客户端连接...");
            //服务端要接收客户端的消息,所以要accept,监听等待客户端连接
            final Socket socket = serverSocket.accept();
            //服务端收到客户端连接消息后打印到控制台
            System.out.println("有客户端连接啦!");

            //创建一个线程与客户端通讯(为了通讯单独写一个方法,在run方法里直接调用,显得干净)
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    handler(socket);
                }
            });
        }
    }

    //handler方法专门处理与客户端的连接
    public static void handler(Socket socket){
        try {
            //创建一个字节数组,用来存客户端输入的数据
            byte[] bytes = new byte[1024];
            //获取输入流,客户端的数据
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端的数据
            while (true){
                System.out.println("线程id"+Thread.currentThread().getId()+"线程名字"+Thread.currentThread().getName());
                System.out.println("正在读数据中...");
                //将输入流的数据读入到bytes数组中保存,并返回数据长度
                int read = inputStream.read(bytes);
                //将保存的数据(下标从0到尾)打印到控制台,需要字节类型转为字符串类型,如果数据下标不等于-1,说明还有数据
                if (read != -1){
                    System.out.println(new String(bytes,0,read));
                    //如果下标为-1,说明数据已经读取完毕
                } else {
                    //就跳出全部循环方法
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭服务端和客户端的连接");
            try {
                //最后需要关闭客户端和服务端的连接
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}
2、客户端

cmd命令窗口客户端连接服务端测试

-- 客户端连接服务端
telnet 127.0.0.1 6666

-- 客户端连接成功后提示如下
欢迎使用 Microsoft Telnet Client
-- cmd命令窗口输入 'CTRL+]'
Escape 字符为 'CTRL+]'
-- 发送数据 send msg
Microsoft Telnet> send hello
发送字符串 hello
Microsoft Telnet>

4、NIO

1、NIO介绍

  • Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java
    提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
  • NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  • NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  • NIO是 面向缓冲区 ,或者面向 块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
  • 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
  • HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。
  • 案例说明NIO里的Buffer
//Buffer案例演示

package com.lian.nio;

import java.nio.IntBuffer;

/**
 * 演示Buffer的使用
 */
public class BasicBuffer {
    public static void main(String[] args) {
        //创建一个buffer写入数据,大小为5,可以存放5个int
        IntBuffer buffer = IntBuffer.allocate(5);
        //向buffer中存放数据
        //buffer.put(10);

        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put(i*2);
        }

        //flip有翻转的意思,此处是切换读写功能
        buffer.flip();

        //从buffer中读取数据
        while (buffer.hasRemaining()){
            System.out.println(buffer.get());
        }
    }
}

2、NIO 和 BIO 的比较

在这里插入图片描述

  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  • BIO 是阻塞的,NIO 则是非阻塞的
  • BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

3、NIO 三大核心原理示意图

Selector 、Channel 、 Buffer 的关系图
在这里插入图片描述

  • 每个channel 都会对应一个Buffer
  • Selector 对应一个线程, 一个线程对应多个channel(连接)
  • 该图反应了有三个channel 注册到 该selector //程序
  • 程序切换到哪个channel 是由事件决定的
  • Selector 会根据不同的事件,在各个通道上切换
  • Buffer 就是一个内存块 , 底层是有一个数组
  • 数据的读取写入是通过Buffer, 这个和BIO , BIO 中要么是输入流,或者是输出流, 不能双向,但是NIO的Buffer是可以读也可以写, 需要 flip 方法切换
  • channel 是双向的, 可以返回底层操作系统的情况, 比如Linux , 底层的操作系统通道就是双向的.

4、缓冲区(Buffer)

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer

NIO 中,Buffer 是一个顶层父类,常用Buffer子类

ByteBuffer,存储字节数据到缓冲区
ShortBuffer,存储字符串数据到缓冲区
CharBuffer,存储字符数据到缓冲区
IntBuffer,存储整数数据到缓冲区
LongBuffer,存储长整型数据到缓冲区
DoubleBuffer,存储小数到缓冲区
FloatBuffer,存储小数到缓冲区

在这里插入图片描述
Buffer及子类方法

public abstract class Buffer {
    //JDK1.4时,引入的api
    public final int capacity( )//返回此缓冲区的容量
    public final int position( )//返回此缓冲区的位置
    public final Buffer position (int newPositio)//设置此缓冲区的位置
    public final int limit( )//返回此缓冲区的限制
    public final Buffer limit (int newLimit)//设置此缓冲区的限制
    public final Buffer mark( )//在此缓冲区的位置设置标记
    public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
    public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
    public final Buffer flip( )//反转此缓冲区
    public final Buffer rewind( )//重绕此缓冲区
    public final int remaining( )//返回当前位置与限制之间的元素数
    public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
    public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
 
    //JDK1.6时引入的api
    public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
    public abstract Object array();//返回此缓冲区的底层实现数组
    public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
    public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}

ByteBuffer 类(二进制数据),该类的主要方法如下:

public abstract class ByteBuffer {
    //缓冲区创建相关api
    public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
    public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
    public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
    //构造初始化位置offset和上界length的缓冲区
    public static ByteBuffer wrap(byte[] array,int offset, int length)
     //缓存区存取相关API
    public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
    public abstract byte get (int index);//从绝对位置get
    public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
    public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
 }

5、通道(Channel)

  • NIO的通道类似于流,但有些区别如下: 通道可以同时进行读写,而流只能读或者只能写 通道可以实现异步读写数据,通道可以从缓冲读数据,也可以写数据到缓冲
  • BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。
  • Channel在NIO中是一个接口public interface Channel extends Closeable{}
  • 常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似Socket】
  • FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。

FileChannel用来对本地文件进行 IO 操作,常见的方法有

  • public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
  • public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
  • public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
  • public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道

6、Channel案例演示

1、本地文件写数据

需求:
使用前面学习后的ByteBuffer(缓冲) 和 FileChannel(通道), 将 “hello,皮蛋” 写入到file01.txt 中
如果文件不存在就创建

package com.lian.nio;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class NioFileChannel01 {
    public static void main(String[] args) throws Exception {
        //创建一个字符串
        String s = "pidan";
        //创建文件路径
        File file = new File("E:\\file01.txt");
        //创建一个输出流
        FileOutputStream outputStream = new FileOutputStream(file);
        //通过输出流得到FileChannel,把流放在通道里
        FileChannel channel = outputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将字符串字节数据放在缓冲区
        byteBuffer.put(s.getBytes(StandardCharsets.UTF_8));
        //对缓冲区byteBuffer进行反转,由读操作变为写操作
        byteBuffer.flip();
        //将缓存区中数据写到通道里
        channel.write(byteBuffer);
        //关闭输出流
        outputStream.close();
    }
}
2、本地文件读数据

需求:
使用ByteBuffer(缓冲) 和 FileChannel(通道), 将 file01.txt 中的数据读入到程序,并显示在控制台屏幕
假定文件已经存在

package com.lian.nio;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NioFileChannel02 {
    public static void main(String[] args) throws Exception {
        //获取要读取文件的路径
        File file = new File("E:\\file01.txt");
        //将文件放入到输入流中
        FileInputStream fileInputStream = new FileInputStream(file);
        //将流放入到通道里
        FileChannel fileChannel = fileInputStream.getChannel();
        //创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
        //将通道里的数据读入到缓冲区中
        fileChannel.read(byteBuffer);
        //将缓冲区的字节数据变为String
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }
}
3、使用一个Buffer完成文件读取

需求:
使用 FileChannel(通道) 和 方法 read , write,完成文件的拷贝
拷贝一个文本文件 1.txt , 放在项目下即可

第1种方法

package com.lian.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 拷贝指定目录下的文件
 * 思路:
 *  1、先从本地读取上指定目录文件的数据到服务端,本地客户端 ——> 缓冲区 ——> 通道 ——> 服务端
 *  2、再从服务端获取数据写入到本地指定的目录里,服务端 ——> 通道 ——> 缓冲区 ——> 本地客户端
 */
public class NioFileChannel03 {
    public static void main(String[] args) throws Exception{
        //指定要拷贝的文件的目录
        File srcFile = new File("E:\\file01.txt");
        File desFile = new File("E:\\file02.txt");
        //新建输入输出流
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileOutputStream fileOutputStream = new FileOutputStream(desFile);
        //根据输入流获取通道
        FileChannel fileChannel = fileInputStream.getChannel();
        //新建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)srcFile.length());
        //将通道里的数据读入到缓冲区中
        fileChannel.read(byteBuffer);
        //切换缓冲区,由读变为写
        byteBuffer.flip();
        //将缓存区内的数据写入到通道中
        FileChannel channel = fileOutputStream.getChannel();
        channel.write(byteBuffer);
        //输入输出流关闭
        fileInputStream.close();
        fileOutputStream.close();
    }
}

第2种方法

package com.lian.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 拷贝指定目录下的文件
 * 另外一种解法
 */
public class NioFileChannel03_01 {
    public static void main(String[] args) throws Exception{
        //指定要拷贝的目录下的文件
        File srcFile = new File("E:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        FileChannel fileChannel1 = fileInputStream.getChannel();
        //指定要拷贝文件的目录及文件名
        File desFile = new File("E:\\file02.txt");
        FileOutputStream fileOutputStream = new FileOutputStream(desFile);
        FileChannel fileChannel2 = fileOutputStream.getChannel();
        //创建缓存区
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        /**
         * 持续读数据,直到读完为止
         * 比如数据有100g,而缓冲区设定了容量只能存10g,因为通道要读取100g数据,所以需要缓存区循环的存10次
         * 好比缸里水有10斤,1瓢只能装1斤,所以需要循环的舀10瓢水
         *     底层其实是重置
         *     private int mark = -1;
         *     private int position = 0;
         *     private int limit;
         *     private int capacity;
         */
        while (true){
            //好比水瓢每次装水后都要倒进管道里,才能有容量继续装下一瓢
            byteBuffer.clear();
            //将缓冲区中的数据读入到通道中,read代表数据的大小
            int read = fileChannel1.read(byteBuffer);
            //如果读取数据完毕
            if (read == -1){
                //跳出循环结束
                break;
            }
            //将缓冲区反装操作,由读操作变为写操作
            byteBuffer.flip();
            //将通道中的数据写入到缓冲区中
            fileChannel2.write(byteBuffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }
}
4、拷贝文件使用transferFrom方法

需求:
使用 FileChannel(通道) 和 方法 transferFrom ,完成文件的拷贝
拷贝一张图片

package com.lian.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 使用transferFrom方法实现拷贝
 */
public class NioFileChannel04 {
    public static void main(String[] args) throws Exception {
        
        //新建源文件,要拷贝的文件
        File srcFile = new File("E:\\1.png");
        //拷贝采用文件输入流
        FileInputStream fileInputStream = new FileInputStream(srcFile);
        //获取源通道
        FileChannel channelSrc = fileInputStream.getChannel();
        //新建目标目录和文件名
        File desFile = new File("E:\\2.png");
        //采用输出流将源文件写入到指定目录下
        FileOutputStream fileOutputStream = new FileOutputStream(desFile);
        //目标文件的通道
        FileChannel channelDes = fileOutputStream.getChannel();
        //新建缓存区,作为两个通道的中转站
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        //利用transferFrom方法将源文件拷贝到目标目录下
        channelDes.transferFrom(channelSrc, 0, channelSrc.size());
        //关闭通道
        channelSrc.close();
        channelDes.close();
        //关闭流
        fileInputStream.close();
        fileOutputStream.close();
    }
}

7、Buffer 和 Channel的注意点

  • ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能BufferUnderflowException 异常。
  • 可以将一个普通Buffer 转成只读Buffer,NIO 还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成.
  • 前面读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作

MappedByteBuffer案例演示

package com.lian.nio;

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 修改指定目录下文件内容 比如文件内容pidan hello 改为iidao hello
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        //获取指定目录下文件且设置为可读写
        RandomAccessFile randomAccessFile = new RandomAccessFile("E:\\file01.txt","rw");
        //获取对应通道
        FileChannel fileChannel = randomAccessFile.getChannel();
        /**
         * 参数1:FileChannel.MapMode.READ_WRITE 读写模式
         * 参数2:0 可以直接修改的起始位置
         * 参数3:5 映射到内存的大小
         * 可以直接修改的范围是0到5
         */
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        //修改数据
        mappedByteBuffer.put(4,(byte)'o');
        mappedByteBuffer.put(0,(byte) 'i');
        //关闭流
        randomAccessFile.close();
        System.out.println("修改成功");
    }
}

ScatteringAndGathering案例演示

package com.lian.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

/**
 * Scatter分散和gather聚合
 * Scatter,将数据写入buffer时,可以采用buffer数组,依次写入
 * Gather,从buffer中读取数据时,可以采用buffer数组,依次读取
 *
 * 当缓存区大小有限,读取的数据太大时,可以设多个缓存区成为数组
 */
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception{
        //获取一个通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //获取绑定端口号
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7777);
        //通道绑定端口到socket并启动
        serverSocketChannel.bind(inetSocketAddress);
        //创建一个buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        //给数组内的byteBuffer分别分配容量大小
        byteBuffers[0]=ByteBuffer.allocate(5);
        byteBuffers[1]=ByteBuffer.allocate(3);
        //等待客户端连接(客户端测试用telnet命令)
        SocketChannel socketChannel = serverSocketChannel.accept();
        //假定从客户端接收8个字节
        int messageLength = 8;
        //循环读取数据
        while (true){
            //定义读取到的字节数是0个
            int byteRead = 0;
            //如果读取的字节数小于客户端接收的字节数8个
            while (byteRead < messageLength) {
                //通道读取缓存区数组内的全部数据,读取到的大小为read
                long l = socketChannel.read(byteBuffers);
                //累计读取的字节数量
                byteRead += l;
                System.out.println("读取的字节数量是:" + byteRead);
                //使用流打印,看当前buffer的position和limit
                //将byteBuffers数组变为列表做流计算
                Arrays.asList(byteBuffers).stream().map(buffer -> "position:" + buffer.position() + "limit:" + buffer.limit()).forEach(System.out::println);
            }
            //将所有的buffer反转
            //Arrays.asList(byteBuffers).stream().map(byteBuffer -> byteBuffer.flip());
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());
            //定义bytewrite为0
            long byteWrite = 0;
            //当写入的数据小于定义的数据8时
            while (byteWrite < messageLength){
                //将数据写出到客户端
                long l = socketChannel.write(byteBuffers);
                byteWrite += l;
            }
            //对所有的byteBuffer缓冲区进行复位
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
            System.out.println("byteRead:"+byteRead+"byteWrite:"+byteWrite+"messageLength:"+messageLength);
        }
    }
}

8、Selector(选择器)

1、介绍
  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
  • Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销
2、看图说话

在这里插入图片描述

  • Netty 的 IO 线程 NioEventLoop 聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
  • 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  • 线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
  • 由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
  • 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O,一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3、Selector类方法
public abstract class Selector implements Closeable { 
//得到一个选择器对象
public static Selector open();
//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public int select(long timeout);
//从内部集合中得到所有的 SelectionKey	
public Set<SelectionKey> selectedKeys();
}

NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket

selector 相关方法说明

selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
selector.wakeup();//唤醒selector
selector.selectNow();//不阻塞,立马返还
4、NIO 非阻塞 网络编程原理分析图

NIO 非阻塞 网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel) 关系梳理图
在这里插入图片描述

  1. 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
  2. Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
  3. 将socketChannel注册到Selector上, register(Selector sel, int ops),一个selector上可以注册多个SocketChannel
  4. 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
  5. 进一步得到各个 SelectionKey (有事件发生)
  6. 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
  7. 可以通过得到的 channel , 完成业务处理
5、案例演示

需求:
编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
目的:理解NIO非阻塞网络编程机制

服务端

package com.lian.nio;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class NioServer {
    public static void main(String[] args) throws Exception{
        //创建serverSocketChannel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //获取一个selector
        Selector selector = Selector.open();
        //绑定端口号
        SocketAddress socketAddress = new InetSocketAddress(6666);
        //通道监听客户端的端口号
        serverSocketChannel.socket().bind(socketAddress);
        //设置通道为非阻塞
        serverSocketChannel.configureBlocking(false);
        //通道注册进selector选择器中,关心事件为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //循环等待客户端连接
        while (true){
            //如果选择器阻塞1秒等于0,就代表没有事件发生,就返回
            if (selector.select(1000)==0){
                System.out.println("服务器等待了1秒,无连接");
                continue;
            }
            /**
             * 如果返回大于0,就获取到SelectionKey的集合,获取到了关注的事件,
             * selector.selectedKeys()返回关注事件的集合
             *
             */
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //使用迭代器遍历selectionKeys集合
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                //获取到SelectionKey
                SelectionKey key = keyIterator.next();
                //如果是OP_ACCEPT事件,说明有新的客户端连接
                if (key.isAcceptable()){
                    //给该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将当前客户端的socketChannel通道也注册到selector选择器上,关注事件是OP_READ,
                    // 同时给socketChannel关联一个Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                //如果发生OP_READ事件
                if (key.isReadable()){
                    //通过key反向获取到对应的channel
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    //将当前通道的数据读入到缓存区
                    socketChannel.read(buffer);
                    //再将缓存区的数据转为字符串类型打印到控制台
                    System.out.println(new String(buffer.array()));
                }
                //手动从集合中移动当前的selectionKey,防止重复操作
                keyIterator.remove();
            }
        }
    }
}

客户端

package com.lian.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NioClient {
    public static void main(String[] args) throws Exception{
        //获取客户端通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置通道非阻塞
        socketChannel.configureBlocking(false);
        //提供给服务器端ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(inetSocketAddress)){
           while (!socketChannel.finishConnect()){
               System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作");
           }
        }
        //如果连接成功,就发送数据
        String s = "hello pidan";
        //缓存区打包一个字节类型的数组,缓存区和字节数组的大小相同
        ByteBuffer buffer = ByteBuffer.wrap(s.getBytes());
        //发送数据,将buffer数据写入channel
        socketChannel.write(buffer);
        //代码停在这里,返回一个整型字节数据
        System.in.read();
    }
}

9、SelectionKey

表示 Selector 和网络通道的注册关系, 共四种:

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

源码中:

public static final int OP_READ = 1 << 0; 
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

SelectionKey相关方法

public abstract class SelectionKey {
	public abstract Selector selector();//得到与之关联的 Selector 对象
	public abstract SelectableChannel channel();//得到与之关联的通道Channel
	public final Object attachment();//得到与之关联的共享数据,也就是ByteBuffer缓冲区
	public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
	public final boolean isAcceptable();//是否可以 accept
	public final boolean isReadable();//是否可以读
	public final boolean isWritable();//是否可以写
}

10、ServerSocketChannel

ServerSocketChannel 在服务器端监听新的客户端 Socket 连接

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

客户端连接成功,服务端会返给客户端SocketChannel通道

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区

public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
	//得到一个 SocketChannel 通道
	public static SocketChannel open();
	//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
	public final SelectableChannel configureBlocking(boolean block);
	//连接服务器
	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();
}

11、NIO 网络编程案例-群聊系统

实例要求:
编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
实现多人群聊
服务器端:可以监测用户上线,离线,并实现消息转发功能
客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)
目的:进一步理解NIO非阻塞网络编程机制

最终实现效果图

服务端

Server 端
服务器接收到消息 时间: [2019-09-29 12:02:35] -> 服务器 ok.......
127.0.0.1:56018 上线 ...
127.0.0.1:56029 上线 ...
服务器接收到消息 时间: [2019-09-29 12:02:55] -> 127.0.0.1:56029 说: jack                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
服务器进行消息转发 ...
服务器接收到消息 时间: [2019-09-29 12:03:07] -> 127.0.0.1:56018 说: tom                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
服务器进行消息转发 ...
服务器接收到消息 时间: [2019-09-29 12:03:14] -> 127.0.0.1:56029 说: 以前玩                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
服务器进行消息转发 ...
服务器接收到消息 时间: [2019-09-29 12:03:51] -> 127.0.0.1:56018 离线了 ...
服务器接收到消息 时间: [2019-09-29 12:03:56] -> 127.0.0.1:56029 离线了 ...

客户端

Client 端
127.0.0.1:56198 is ok ~
127.0.0.1:56186 说: 我是jack
我是tom
127.0.0.1:56207 说: 我是tom
127.0.0.1:56207 说: 我来自北京
我来自上海
127.0.0.1:56186 说: 我来自天津
bye

案例实操

服务端

package com.lian.nio.groupchat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class GroupChatServer {
    //定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;
    //无参构造器
    public GroupChatServer() {
        try{
            //得到选择器
            selector = Selector.open();
            //初始化ServerSocketChannel
            listenChannel = ServerSocketChannel.open();
            //通道绑定端口号
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            //设置通道非阻塞
            listenChannel.configureBlocking(false);
            //将监听通道注册到selector选择器上
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //监听客户端连接处理
    public void listen(){
        try {
            //服务器端循环监听客户端
            while (true){
                //服务端阻塞2秒
                int count = selector.select();
                //有事件处理
                if (count > 0){
                    //遍历得到selectionKeys集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                    while (keyIterator.hasNext()){
                        SelectionKey key = keyIterator.next();
                        //如果是监听到Acceptable事件
                        if (key.isAcceptable()){
                            //服务端就返回给客户端一个SocketChannel通道
                            SocketChannel socketChannel = listenChannel.accept();
                            //设置通道为非阻塞
                            socketChannel.configureBlocking(false);
                            //将客户端通道注册到selector选择器上,事件为只读
                            socketChannel.register(selector,SelectionKey.OP_READ);
                            System.out.println(socketChannel.getRemoteAddress() + "上线");
                        }
                        //如果是监听到只读事件
                        if (key.isReadable()){
                            //处理读,专门写方法
                            readData(key);
                        }
                        //当处理完事件后,需要手动删除key,防止重复处理
                        keyIterator.remove();
                    }
                }else {
                    System.out.println("等待...");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {

        }
    }

    //读取客户端消息
    private void readData(SelectionKey key) throws Exception {
        //定义一个socketChannel
        SocketChannel socketChannel = null;
        try {
            socketChannel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将通道的数据读取到缓存区中
            int count = socketChannel.read(byteBuffer);
            //如果读取到了数据
            if (count > 0){
                //将数据转为字符串类型
                String msg = new String(byteBuffer.array());
                //输出到客户端
                System.out.println("from客户端"+msg);
                //向其他客户端转发消息(去掉自己),专门写一个方法处理
                sendInfoToOtherClient(msg,socketChannel);
            }
        }catch (Exception e){
            try {
                System.out.println(socketChannel.getRemoteAddress()+"离线了...");
                //取消注册
                key.cancel();
                //关闭通道
                socketChannel.close();
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
    }

    //转发消息给其他客户(通道),消息本身,排除自己
    private void sendInfoToOtherClient(String msg,SocketChannel self) throws Exception {
        System.out.println("服务器转发消息中...");
        //遍历selector中全部的事件,keys是包含服务器端的事件
        for (SelectionKey key : selector.keys()){
            //通过key取出对应的SocketChannel
            Channel targetChannel = key.channel();
            //排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self){
                //转型
                SocketChannel dest = (SocketChannel) targetChannel;
                //将msg存储到buffer缓存区
                ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
                //将buffer的数据写入到通道中
                dest.write(byteBuffer);
            }
        }
    }
    //服务端启动
    public static void main(String[] args) {
        GroupChatServer groupChatServer = new GroupChatServer();
        groupChatServer.listen();
    }
}

客户端

package com.lian.nio.groupchat;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;

public class GroupChatClient {
    //定义相关属性
    //服务器ip
    private final String HOST = "127.0.0.1";
    private final int PORT = 6667;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    //构造器
    public GroupChatClient() throws Exception{
        //创建选择器Selector
        selector = Selector.open();
        //通道绑定ip和端口
        socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
        //设置通道非阻塞
        socketChannel.configureBlocking(false);
        //将channel注册到选择器中,关注事件是OP_READ
        socketChannel.register(selector, SelectionKey.OP_READ);
        //得到username
        username = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println(username+"is ok");
    }

    //向服务器发送消息
    public void sendInfo(String info){
        info = username + "说" + info;
        try {
            //将ByteBuffer缓存区中的字节数据写入到管道中
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //读取从服务器端回复的消息
    public void readInfo(){
        try{
            //先阻塞2秒钟
            int count = selector.select();
            //如果大于0说明有可用的通道
            if (count > 0){
                //获取所有的事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //将所有的事件装到迭代器中
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                //如果迭代器中有事件
                while (keyIterator.hasNext()){
                    //就将事件取出来
                    SelectionKey key = keyIterator.next();
                    //如果事件是可读
                    if (key.isReadable()){
                        //就得到对应事件的通道
                        SocketChannel channel = (SocketChannel) key.channel();
                        //得到对应事件的缓存区
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        //将数据从通道读取到缓存区
                        channel.read(byteBuffer);
                        //将缓存区数据转为字符串类型
                        String msg = new String(byteBuffer.array());
                        //打印到控制台
                        System.out.println(msg.trim());
                    }
                }
                //删除当前的SelectionKey,防止重复操作
                keyIterator.remove();
                //如果没有可用的通道
            }else {
                //System.out.println("没有可用的通道");

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

    //客户端启动
    public static void main(String[] args) throws Exception{
        GroupChatClient groupChatClient = new GroupChatClient();
        //创建线程
        new Thread(() -> {
            //一直读取数据,每隔3秒,读取从服务器发送过来的数据
            while (true){
                groupChatClient.readInfo();
                try {
                    Thread.currentThread().sleep(3000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String s = scanner.nextLine();
            groupChatClient.sendInfo(s);
        }
    }
}

12、零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开。

在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在 OS 里,到底是怎么样的一个的设计?我们分析 mmap 和 sendFile 这两个零拷贝

1、mmap 优化

mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图
在这里插入图片描述

2、sendFile 优化

Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
提示:零拷贝从操作系统角度,是没有cpu 拷贝
在这里插入图片描述
Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结:

这里其实有 一次cpu 拷贝
kernel buffer -> socket buffer
但是,拷贝的信息很少,比如
lenght , offset , 消耗低,可以忽略
在这里插入图片描述
我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。

零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

3、mmap 和 sendFile 的区别
  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
4、NIO 零拷贝案例

案例要求:
使用传统的IO 方法传递一个大文件
使用NIO 零拷贝方式传递(transferTo)一个大文件
看看两种传递方式耗时时间分别是多少

演示传统io案例

服务端

package com.lian.nio.zerocopy;

import java.io.DataInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class OldServer {
    public static void main(String[] args) throws Exception {
        //在客户/服务器通信模式中,服务器端需要创建监听特定端口的ServerSocket,
        // ServerSocket负责接收客户连接请求,并生成与客户端连接的Socket
        ServerSocket serverSocket = new ServerSocket(7001);
        //一直等待客户端连接
        while (true){
            //等待客户端连接,客户端连接成功,服务端返回给客户端socket
            Socket socket = serverSocket.accept();
            //获取客户端的数据流
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
            try {
                //创建字节类型的数组,用来存储客户端的数据
                byte[] byteArray = new byte[1024];
                //一直读取客户端数据
                while (true){
                    //将客户端数据输入流全部输出到服务端的字节数组中,并返回读取到数据的大小
                    int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
                    //如果数据全部读取完毕
                    if (readCount == -1){
                        //就跳出循环,不再读取,如果没有读取完,会循环一直读取直到读取完毕
                        break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

客户端

package com.lian.nio.zerocopy;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.Socket;

public class OldClient {
    public static void main(String[] args) throws Exception {
        //网络中的进程是通过socket来通信的
        Socket socket = new Socket("127.0.0.1",7001);
        //初始化文件及路径
        String fileName = "E:\\temp\\soft\\Git\\Git-2.26.2-64-bit.exe";
        //获取文件输入流
        InputStream inputStream = new FileInputStream(fileName);
        //客户端获取输出流,转为数据输出流类型
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
        //新建一个字节数组,作用是作为数据输入流和输出流得中转站,输入流存到byte数组中,又输送给输出流,bute数组像是瓢,一瓢一瓢的往缸里舀水
        byte[] bytes = new byte[1024];
        //自定义读取数据的大小
        long readCount;
        //自定义默认读取到的数据大小为0
        long total = 0;
        //获取当前时间
        long startTime = System.currentTimeMillis();
        //循环将输入流读取到字节数组中,如果大于0,说明读取到数据了,输入流已经将数据输送到byte数组中,当输入流没有数据再往byte数组中存时就跳出循环
        while ((readCount = inputStream.read(bytes)) >= 0){
            //累加读取数据大小,开始默认数据大小是0,每读取数据获得数据就自增
            total += readCount;
            //只要byte数组这个瓢里有水了就循环输出到数据输出流里,将byte数组中数据输出到输出流
            dataOutputStream.write(bytes);
        }
        //输出总输出数据量耗费时间
        System.out.println("发送总字节数"+total+",耗时:"+(System.currentTimeMillis()-startTime));
        //关闭输入输出流和通信通道
        inputStream.close();
        dataOutputStream.close();
        socket.close();
    }
}

输出结果为
在这里插入图片描述

演示NIO零拷贝 io案例

服务端

package com.lian.nio.zerocopy;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class NewServer {
    public static void main(String[] args) throws Exception {
        //获取ip和端口号
        InetSocketAddress inetSocketAddress = new InetSocketAddress( 1100);
        //获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //根据通道返回ServerSocket
        ServerSocket serverSocket = serverSocketChannel.socket();
        //绑定ip和端口号
        serverSocket.bind(inetSocketAddress);
        //创建缓存区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //循环等待客户端连接
        while (true){
            //客户端连接成功后,服务端返回给客户端一个socket
            SocketChannel socketChannel = serverSocketChannel.accept();
            //自定义读取数据大小
            int readCount = 0;
            //当读取的数据大小不等于-1时,代表数据没有读完
            while (-1 != readCount){
                try {
                    //将客户端通道里的数据读取到服务端的缓存区中,如果读取完毕则跳出循环
                    readCount = socketChannel.read(byteBuffer);
                }catch (Exception e){
                    //如果数据读完不要抛出异常,直接跳出循环即可
                    break;
                }
            }
            //缓冲区倒带,position为0,mark作废
            byteBuffer.rewind();
        }
    }
}

客户端

package com.lian.nio.zerocopy;

import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;

public class NewClient {
    public static void main(String[] args) throws Exception {
        //使用服务端给返回的SocketChannel通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置通道连接端口ip
        socketChannel.connect(new InetSocketAddress("localhost", 1100));
        //自定义要传输的文件
        String fileName = "E:\\file01.txt";
        //得到文件的输入流的FileChannel文件通道
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();
        //记录文件发送前时间
        long startTime = System.currentTimeMillis();
        /**
         * linux下,tranferTo方法就可完成传输
         * windows下,一次调用transfetTo只能发送8m,需要分段传输文件
         * tranferTo底层使用零拷贝
         * 将文件通道里数据传送到SocketChannel通道里
         */
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        //输出发送文件总的数量和耗时
        System.out.println("发送总数量"+transferCount+"耗时"+(System.currentTimeMillis()-startTime));
        //关闭通道
        fileChannel.close();
    }
}

返回结果为
在这里插入图片描述

BIO、NIO、AIO对比

同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发。
同步非阻塞:到理发店理发,发现前面有其它人理发,给理发师说下,先干其他事情,一会过来看是否轮到自己.
异步非阻塞:给理发师打电话,让理发师上门服务,自己干其它事情,理发师自己来家给你理发

原生NIO存在的问题

  • NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握
    Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
  • 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
  • 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
  • JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到JDK 1.7 版本该问题仍旧存在,没有被根本解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值