NIO+BIO+AIO. Интенсивная лекция по режиму ввода-вывода в учебном пособии по Java. Продолжение темной лошадки (2)

Напишите здесь название каталога

Глава 4 Углубленный анализ JAVA NIO

Прежде чем объяснять использование NIO для реализации коммуникационной архитектуры, нам необходимо понять основные характеристики и использование NIO.

4.1 Базовое введение в Java NIO

  • Java NIO (новый ввод-вывод), также известный как неблокирующий ввод-вывод Java, представляет собой новый API-интерфейс ввода-вывода, появившийся после версии Java 1.4, который может заменить стандартный API ввода-вывода Java. NIO имеет ту же функцию и назначение, что и исходный IO, но используется совершенно по-другому: NIO поддерживает операции ввода-вывода , ориентированные на буфер и канал . NIO будет выполнять операции чтения и записи файлов более эффективно. NIO можно понимать как неблокирующий ввод-вывод. Чтение и запись традиционного ввода-вывода может только блокировать выполнение. Поток не может выполнять другие действия во время чтения и записи ввода-вывода. Например, при вызове socket.read(), если сервер не передал данные, поток остановится, он всегда заблокирован, а сокет можно настроить в неблокирующем режиме в NIO.

  • Классы, связанные с NIO, помещены в пакет и подпакеты java.nio, и многие классы исходного пакета java.io переписаны.

  • NIO состоит из трех основных частей: Channel (канал), Buffer (буфер), Selector (селектор)

  • Неблокирующий режим Java NIO позволяет потоку отправлять запрос или считывать данные из канала, но он может получать только доступные в данный момент данные.Если в данный момент нет доступных данных, ничего не будет получено, а поток будет заблокирован. Поэтому, пока данные не станут доступны для чтения, поток может продолжать заниматься другими делами. То же самое верно и для неблокирующих записей: поток запрашивает запись некоторых данных в канал, но ему не нужно ждать, пока они будут полностью записаны, поток может одновременно заниматься другими делами.

  • Популярное понимание: NIO может выполнять несколько операций в одном потоке. Допустим запросов 1000, в зависимости от реальной ситуации на обработку может быть выделено 20 или 80 потоков. В отличие от предыдущего блокирующего ввода-вывода, необходимо выделить 1000.

4.2 Сравнение NIO и BIO

  • BIO обрабатывает данные в потоковом режиме, тогда как NIO обрабатывает данные в блочном режиме, и эффективность блочного ввода-вывода намного выше, чем у потокового ввода-вывода.
  • BIO блокирует, NIO не блокирует
  • BIO работает на основе потока байтов и потока символов, тогда как NIO работает на основе Channel (канала) и Buffer (буфера).Данные всегда считываются из канала в буфер или записываются из буфера в канал. Селектор (селектор) используется для мониторинга событий нескольких каналов (например: запрос на подключение, поступление данных и т. д.), поэтому несколько клиентских каналов можно отслеживать с помощью одного потока.
НИО БИО
Буферно-ориентированный (Buffer) Потоковый (Stream)
Неблокирующий (неблокирующий ввод-вывод) Блокирующий ввод-вывод (блокирующий ввод-вывод)
Селекторы

4.3 Схематическая диаграмма трех основных принципов NIO

NIO состоит из трех основных частей: Channel (канал), Buffer (буфер), Selector (селектор)

Буферный буфер

Буфер — это, по сути, блок памяти, в который данные могут быть записаны, а затем прочитаны. Эта часть памяти упакована как объект буфера NIO, и предоставляется набор методов для легкого доступа к этой части памяти. По сравнению с прямыми операциями над массивами Buffer API проще в эксплуатации и управлении.

Канал (канал)

Канал Java NIO похож на поток, но есть некоторые отличия: данные можно читать из канала, а данные можно записывать в канал. Но потоковое (ввод или вывод) чтение и запись обычно односторонние. Каналы можно читать и записывать без блокировки, а каналы могут поддерживать чтение или запись буферов, а также асинхронное чтение и запись.

Селектор селектор

Selector — это компонент Java NIO, который может проверять один или несколько каналов NIO и определять, какие каналы готовы для чтения или записи. Таким образом, один поток может управлять несколькими каналами, тем самым управляя несколькими сетевыми соединениями и повышая эффективность.

вставьте сюда описание изображения

  • Каждый канал соответствует буферу.
  • Поток соответствует селектору, а селектор соответствует нескольким каналам (соединениям).
  • На какой канал переключается программа, определяется событием.
  • Селектор будет включать каждый канал в соответствии с различными событиями.
  • Буфер — это блок памяти, а нижний слой — это массив.
  • Чтение и запись данных осуществляется через буфер, BIO — это либо входной поток, либо выходной поток, который не может быть двунаправленным, но буфер NIO может быть прочитан или записан.
  • Ядром системы Java NIO являются: канал (Channel) и буфер (Buffer). Канал представляет собой открытое соединение с IO-устройством (например, файлом, сокетом). Если вам нужно использовать систему NIO, вам необходимо получить канал, используемый для подключения устройства ввода-вывода, и буфер, используемый для хранения данных. Затем управляйте буфером и обрабатывайте данные. Короче говоря, Channel отвечает за передачу, а Buffer — за доступ к данным.

4.4 NIO Core 1: Буфер

Буфер

Контейнер для определенного примитивного типа данных. Все буферы, определенные пакетом java.nio, являются подклассами абстрактного класса Buffer. Буфер в Java NIO в основном используется для взаимодействия с каналом NIO, данные считываются из канала в буфер и записываются из буфера в канал.
вставьте сюда описание изображения

Класс буфера и его подклассы

Буфер подобен массиву, который может содержать несколько данных одного типа. В соответствии с различными типами данных существуют следующие общие подклассы Buffer:

  • Байтбуфер
  • CharBuffer
  • Шортбуфер
  • IntBuffer
  • Длинный буфер
  • Плавающий буфер
  • Двойной буфер

Все вышеперечисленные классы Buffer используют схожие методы для управления данными, но типы данных, которыми они управляют, различаются. Оба получают объект Buffer с помощью следующих методов:

static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象

Основные свойства буфера

Важные концепции в Buffer:

  • Емкость : как блок памяти, буфер имеет определенный фиксированный размер, также известный как «емкость».Емкость буфера не может быть отрицательной и не может быть изменена после создания.
  • Ограничение (limit) : Указывает размер оперативных данных в буфере (данные не могут быть прочитаны или записаны после ограничения). Предел буфера не может быть отрицательным и не может превышать его емкость. В режиме записи ограничение равно емкости буфера. В режиме чтения limit равен количеству записываемых данных .
  • position : индекс следующих данных для чтения или записи. Позиция буфера не может быть отрицательной и не может превышать его предел.
  • Mark (отметка) и reset (сброс) : Метка является индексом, укажите конкретную позицию в Буфере с помощью метода mark() в Буфере, а затем восстановите эту позицию, вызвав метод reset(). ,
    limit , пропускная способность подчиняется следующим инвариантам: 0 <= метка <= позиция <= предел <= пропускная способность
  • Иллюстрация:
    Вы можете видеть, что отсчет начинается с 0.
    вставьте сюда описание изображения

Общие методы буфера

Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 将缓冲区的界限设置为当前位置,并将当前位置重置为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回Buffer的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新limit的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置position
Buffer position(int n) 将设置缓冲区的当前位置为n,并返回修改后的Buffer对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark

Обработка буферных данных

Buffer 所有子类提供了两个用于数据操作的方法:get()put()方法获取Buffer中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)放入到数据Bufferput(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动position)

Использование Buffer для чтения и записи данных обычно состоит из следующих четырех шагов:

  • 1. Запись данных в буфер
  • 2. Вызовите метод flip(), чтобы переключиться в режим чтения
  • 3. Чтение данных из буфера
  • 4. Вызовите метод buffer.clear() или метод buffer.compact(), чтобы очистить буфер.

Новый
модуль Имя модуля: nio_buffer
вставьте сюда описание изображения
Новый класс
вставьте сюда описание изображения

Презентация кейса

API простое приложение
BufferTest.java

public class BufferTest {
    
    
    @Test
    public void test01() {
    
    
        // 1.分配一个缓冲区,容量是10
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 2.
        System.out.println("当前缓冲区的起始位置:" + buffer.position()); // 0
        System.out.println("当前缓冲器的限制位置:" + buffer.limit()); // 10
        System.out.println("当前缓冲器的容量:" + buffer.capacity()); // 10
        System.out.println("------------------------------");
        // 3.缓冲区中添加数据
        String name = "itheima";
        buffer.put(name.getBytes());
        System.out.println("put后缓冲区的起始位置:" + buffer.position()); // 7
        System.out.println("put后缓冲器的限制位置:" + buffer.limit()); // 10
        System.out.println("put后缓冲器的容量:" + buffer.capacity()); // 10
        System.out.println("------------------------------");

        // 4.flip()方法 将缓冲区的界限设置为当前位置,并将当前位置重置为0 可读模式
        buffer.flip();
        System.out.println("flip后缓冲区的起始位置:" + buffer.position()); // 0
        System.out.println("flip后缓冲器的限制位置:" + buffer.limit()); // 7
        System.out.println("flip后缓冲器的容量:" + buffer.capacity()); // 10
        System.out.println("------------------------------");

        // 5. get读取数据
        char ch = (char) buffer.get();
        System.out.println("字符ch:" + ch); // i
        System.out.println("get后缓冲区的起始位置:" + buffer.position()); // 1
        System.out.println("get后缓冲器的限制位置:" + buffer.limit()); // 7
        System.out.println("get后缓冲器的容量:" + buffer.capacity()); // 10

    }
}

Выходной результат:

当前缓冲区的起始位置:0
当前缓冲器的限制位置:10
当前缓冲器的容量:10
------------------------------
put后缓冲区的起始位置:7
put后缓冲器的限制位置:10
put后缓冲器的容量:10
------------------------------
flip后缓冲区的起始位置:0
flip后缓冲器的限制位置:7
flip后缓冲器的容量:10
------------------------------
字符ch:i
get后缓冲区的起始位置:1
get后缓冲器的限制位置:7
get后缓冲器的容量:10

Простое приложение API 2
BufferTest.java

    @Test
    public void test02() {
    
    
        // 1.分配一个缓冲区容量为10
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        System.out.println("缓冲区的起始位置:" + byteBuffer.position()); // 0
        System.out.println("缓冲区的限制位置:" + byteBuffer.limit()); // 10
        System.out.println("缓冲区的容量:" + byteBuffer.capacity()); // 10
        System.out.println("------------------------------");

        String name = "itheima";
        byteBuffer.put(name.getBytes());
        System.out.println("put后缓冲区的起始位置:" + byteBuffer.position()); // 7
        System.out.println("put后缓冲区的限制位置:" + byteBuffer.limit()); // 10
        System.out.println("put后缓冲区的容量:" + byteBuffer.capacity()); // 10
        System.out.println("------------------------------");

        // 2.清除缓冲区
        byteBuffer.clear();
        System.out.println("clear后缓冲区的起始位置:" + byteBuffer.position()); // 0
        System.out.println("clear后缓冲区的限制位置:" + byteBuffer.limit()); // 10
        System.out.println("clear后缓冲区的容量:" + byteBuffer.capacity()); // 10
        System.out.println((char) byteBuffer.get()); // i 这里并不会清除,而只是把position变回了0,覆盖写操作才会清除原有数据
        System.out.println("------------------------------");
    }

Выходной результат:

缓冲区的起始位置:0
缓冲区的限制位置:10
缓冲区的容量:10
------------------------------
put后缓冲区的起始位置:7
put后缓冲区的限制位置:10
put后缓冲区的容量:10
------------------------------
clear后缓冲区的起始位置:0
clear后缓冲区的限制位置:10
clear后缓冲区的容量:10
i

Простое приложение API 3
BufferTest.java

@Test
    public void test03() {
    
    
        // 3.定义一个缓冲区
        String name = "itheima";
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        byteBuffer.put(name.getBytes());
        byteBuffer.flip();
        // 读取数据
        byte[] bytes = new byte[2];
        byteBuffer.get(bytes);
        String rs = new String(bytes);
        System.out.println(rs); // it
        System.out.println("当前缓冲区的起始位置:" + byteBuffer.position()); // 2 说明读取了前2个位置了(0,1),这个时候从第3个位置2开始
        System.out.println("当前缓冲区的限制位置:" + byteBuffer.limit()); // 7 itheima flip之后,前7个位置可以读取
        System.out.println("当前缓冲区的容量:" + byteBuffer.capacity()); // 10
        System.out.println("------------------------------");

        // mark
        byteBuffer.mark(); // 标记此刻的位置2
        byte[] bs = new byte[3];
        byteBuffer.get(bs);
        System.out.println(new String(bs)); // hei
        System.out.println("当前缓冲区的起始位置:" + byteBuffer.position()); // 5
        System.out.println("当前缓冲区的限制位置:" + byteBuffer.limit()); // 7
        System.out.println("当前缓冲区的容量:" + byteBuffer.capacity()); // 10
        System.out.println((char) byteBuffer.get()); // m
        System.out.println("------------------------------");

        // reset
        byteBuffer.reset();
        if (byteBuffer.hasRemaining()) {
    
    
            System.out.println(byteBuffer.remaining()); // 5
        }
    }

Выходной результат:

it
当前缓冲区的起始位置:2
当前缓冲区的限制位置:7
当前缓冲区的容量:10
------------------------------
hei
当前缓冲区的起始位置:5
当前缓冲区的限制位置:7
当前缓冲区的容量:10
m
------------------------------
5

Прямые и непрямые буферы

Что такое прямая память и непрямая память
Согласно описанию официального документа:

byte byfferСуществует два типа: один основан на прямой памяти (т. е. памяти без кучи); другой — непрямой памяти (т. е. памяти кучи). Для прямой памяти JVM будет иметь более высокую производительность в операциях ввода-вывода, поскольку она напрямую воздействует на операции ввода-вывода локальной системы. Вместо прямой памяти, то есть данных в памяти кучи, если должны выполняться операции ввода-вывода, они сначала будут скопированы из памяти процесса в прямую память, а затем обработаны локальным вводом-выводом.

С точки зрения потока данных косвенная память представляет собой следующую цепочку действий:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

А прямая память это:

本地IO-->直接内存-->本地IO

Очевидно, что при выполнении операций ввода-вывода, таких как отправка большого объема данных по сети, прямая память будет более эффективной. Прямая память создается с помощью allocateDirect , но она требует более высокой производительности, чем применение для обычной памяти кучи. Однако эта часть данных находится за пределами JVM, поэтому она не занимает память приложения. Итак, когда у вас есть большой объем данных для кэширования, и их жизненный цикл очень длинный, то лучше использовать прямую память. Вообще говоря, если это не приносит очевидных улучшений производительности, рекомендуется использовать динамическую память напрямую. Является ли байтовый буфер прямым или непрямым, можно определить, вызвав его метод isDirect() .

сцены, которые будут использоваться

  • 1 Необходимо хранить большое количество данных, и их жизненный цикл очень долог.
  • 2 Подходит для частых операций ввода-вывода, таких как сценарии параллелизма в сети.

4.5 NIO Core II: канал (канал)

Обзор канала

Канал (Channel): определяется пакетом java.nio.channels. Канал представляет собой соединение, открытое источником и целью ввода-вывода. Каналы похожи на традиционные «потоки». Просто сам Канал не может напрямую обращаться к данным, а Канал может взаимодействовать только с Буфером.

1. Каналы NIO похожи на потоки, но некоторые отличия заключаются в следующем:

  • Каналы могут читать и записывать одновременно, в то время как потоки могут только читать или записывать

  • Каналы могут читать и записывать данные асинхронно

  • Каналы могут читать данные из буферов и записывать данные в буферы:

2. Поток в BIO однонаправленный, например, объект FileInputStream может только читать данные, а канал (Channel) в NIO двунаправленный и может быть прочитан или записан.

3. Канал — это интерфейс в NIO

public interface Channel extends Closeable{
    
    }

Обычно используемые классы реализации канала

  • FileChannel: канал для чтения, записи, отображения и управления файлами.
  • DatagramChannel: чтение и запись каналов данных в сети через UDP.
  • SocketChannel: чтение и запись данных в сети через TCP.
  • ServerSocketChannel: он может отслеживать новые входящие TCP-соединения и создавать SocketChannel для каждого нового входящего соединения.
    【ServerSocketChannel похож на ServerSocket, SocketChannel похож на Socket】

Класс FileChannel

Один из способов получить канал — вызвать метод getChannel() для объекта, который поддерживает каналы . Классы, поддерживающие каналы, следующие:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • ДейтаграммСокет
  • Разъем
  • Другой способ для ServerSocket
    получить канал — использовать статический метод newByteChannel() класса Files для получения байтового канала. Или открыть и вернуть указанный канал через статический метод open() канала

Общие методы FileChannel

int read(ByteBuffer dst)Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts)Channel中的数据“分散”到ByteBuffer[]
int write(ByteBuffer src)ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs)ByteBuffer[]中的数据“聚集”到 Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

Структура проекта нового модуля
nio_channel
вставьте сюда описание изображения
выглядит следующим образом:
вставьте сюда описание изображения

Случай 1 - запись данных в локальный файл

Требования: Используйте ранее изученный ByteBuffer (буфер) и FileChannel (канал), чтобы написать «привет, темная лошадка Java-программист!» в data.txt.

@Test
    public void test() {
    
    
        FileOutputStream fileOutputStream = null;
        FileChannel fileChannel = null;
        try {
    
    
            // 1.字节输出流通向目标文件
            fileOutputStream = new FileOutputStream("data01.txt");
            // 2.得到字节输出流的通道
            fileChannel = fileOutputStream.getChannel();
            // 3.分配缓冲区
            String text = "hello,黑马Java程序员!";
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put(text.getBytes());
            byteBuffer.flip();
            fileChannel.write(byteBuffer);

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                fileChannel.close();
                fileOutputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

Напишите результат:
вставьте сюда описание изображения

Случай 2 — чтение данных из локального файла

Требование: использовать ранее изученный ByteBuffer (буфер) и FileChannel (канал) для чтения данных из data01.txt в программу и отображения их на экране консоли.

@Test
    public void test3() {
    
    
        FileInputStream fileInputStream = null;
        FileChannel channel = null;
        try {
    
    
            // 1. 定义一个文件输入流,与源文件接通
            fileInputStream = new FileInputStream("data01.txt");
            // 2.得到文件输入流的文件通道
            channel = fileInputStream.getChannel();

            // 3. 定义一个缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 4.读取数据到缓冲区
            int count = channel.read(byteBuffer);
            // 方式一:
            //System.out.println(new String(byteBuffer.array(), 0, count));
            
            // 方式二:
            byteBuffer.flip();
            System.out.println(new String(byteBuffer.array(), 0, byteBuffer.remaining()));
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                channel.close();
                fileInputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

результат:

hello,黑马Java程序员!

Случай 3 — используйте буфер для завершения копирования файла

Используйте FileChannel (канал) для завершения копирования файла.

/**
     * @param
     * @return void
     * @description //文件拷贝
     * @date 2023/4/5 22:08
     * @author wty
     **/
    @Test
    public void test4() {
    
    
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel fileChannel = null;
        FileChannel fileOutputStreamChannel = null;
        try {
    
    
            // 定义一个文件
            File file = new File("D:\\1.txt");
            File target = new File("D:\\server\\2.txt");
            // 得到一个文件输入和输出流
            fileInputStream = new FileInputStream(file);

            fileOutputStream = new FileOutputStream(target);


            // 先创建通道
            fileChannel = fileInputStream.getChannel();
            fileOutputStreamChannel = fileOutputStream.getChannel();
            // 创建缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
            int count = 0;
            // 开始读取数据
            while (true) {
    
    
                // 必须先清空缓冲区,再写入数据
                byteBuffer.clear();
                if ((count = fileChannel.read(byteBuffer)) == -1) {
    
    
                    break;
                }
                byteBuffer.flip();

                // 把数据写出通道
                fileOutputStreamChannel.write(byteBuffer);
            }


        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                fileOutputStreamChannel.close();
                fileChannel.close();
                fileOutputStream.close();
                fileInputStream.close();
                System.out.println("拷贝完成!");
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

результат:
вставьте сюда описание изображения

Случай 4 - Разбрасывать и собирать

Рассеянное чтение (Scatter): относится к чтению данных канала Channel в несколько буферов.

Сбор относится к «сбору» данных из нескольких буферов в канал.

@Test
    public void test5() {
    
    

        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        FileChannel fileInputStreamChannel = null;
        FileChannel fileOutputStreamChannel = null;
        try {
    
    
            // 1. 定义字节输入管道
            fileInputStream = new FileInputStream("data01.txt");
            // 2. 字节输出管道
            fileOutputStream = new FileOutputStream("data03.txt");
            // 3.定义多个缓冲区
            ByteBuffer byteBuffer1 = ByteBuffer.allocate(4);
            ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);

            // 4. 缓冲区放入数组
            ByteBuffer[] byteBuffers = {
    
    byteBuffer1, byteBuffer2};

            // 5.从通道中读取数据分散到各个缓冲区
            fileInputStreamChannel = fileInputStream.getChannel();
            fileOutputStreamChannel = fileOutputStream.getChannel();

            fileInputStreamChannel.read(byteBuffers);

            // 6.从每个缓冲区中查看是否有数据读取到
            for (ByteBuffer byteBuffer : byteBuffers) {
    
    
                // 切换到读数据模式
                byteBuffer.flip();
                System.out.println(new String(byteBuffer.array(), 0, byteBuffer.remaining()));
            }

            // 7.聚集写入到通道
            fileOutputStreamChannel.write(byteBuffers);


        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                fileOutputStreamChannel.close();
                fileInputStreamChannel.close();
                fileOutputStream.close();
                fileInputStream.close();
                System.out.println("文件复制完成");
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }

    }

Посмотреть Результаты:
вставьте сюда описание изображения

Случай 5 — TransferFrom()

Скопируйте исходные данные канала из целевого канала

@Test
    public void test06() {
    
    
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;

        FileChannel fileInputStreamChannel = null;
        FileChannel fileOutputStreamChannel = null;
        try {
    
    
            // 1.获取文件源
            fileInputStream = new FileInputStream("data01.txt");
            fileOutputStream = new FileOutputStream("data03.txt");

            // 2.获取通道
            fileInputStreamChannel = fileInputStream.getChannel();
            fileOutputStreamChannel = fileOutputStream.getChannel();

            // 3.复制数据
            fileOutputStreamChannel.transferFrom(fileInputStreamChannel, fileInputStreamChannel.position(), fileInputStreamChannel.size());

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                fileOutputStreamChannel.close();
                fileInputStreamChannel.close();
                fileOutputStream.close();
                fileInputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

Удалить целевые файлы перед тестированием
вставьте сюда описание изображения

Тест: обнаружено, что репликация прошла успешно
вставьте сюда описание изображения

Случай 6 — TransferTo()

Скопируйте исходные данные канала в целевой канал

@Test
    public void test07() {
    
    
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;

        FileChannel fileInputStreamChannel = null;
        FileChannel fileOutputStreamChannel = null;
        try {
    
    
            // 1.获取文件源
            fileInputStream = new FileInputStream("data01.txt");
            fileOutputStream = new FileOutputStream("data03.txt");

            // 2.获取通道
            fileInputStreamChannel = fileInputStream.getChannel();
            fileOutputStreamChannel = fileOutputStream.getChannel();

            // 3.复制数据
            fileInputStreamChannel.transferTo(fileInputStreamChannel.position(), fileInputStreamChannel.size(), fileOutputStreamChannel);

        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                fileOutputStreamChannel.close();
                fileInputStreamChannel.close();
                fileOutputStream.close();
                fileInputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

Осознайте результат:
вставьте сюда описание изображения

4.6 NIO Core 3: Селектор

Обзор селектора

Селектор (Selector) является мультиплексором объектов SelectableChannle, Selector может одновременно отслеживать состояние ввода-вывода нескольких SelectableChannels, то есть с помощью Selector можно заставить один поток управлять несколькими каналами. Селектор — это ядро ​​неблокирующего ввода-вывода.
вставьте сюда описание изображения

  • NIO в Java использует неблокирующие методы ввода-вывода. Вы можете использовать один поток для обработки нескольких клиентских подключений, и вы будете использовать Selector (селектор).
  • Селектор может определить, есть ли события на нескольких зарегистрированных каналах (примечание: несколько каналов могут быть зарегистрированы в одном и том же селекторе в форме событий), если событие происходит, он получит событие, а затем обработает каждое событие соответствующим образом. Таким образом, для управления можно использовать только один поток. Управлять несколькими каналами, то есть управлять несколькими соединениями и запросами.
  • Только когда соединение/канал действительно имеет событие чтения и записи, чтение и запись будут выполняться, что значительно снижает нагрузку на систему и не требует создания потока для каждого соединения и не требует поддержки нескольких потоков.
  • Избегаются накладные расходы, вызванные переключением контекста между несколькими потоками.

Применение селектора

Создать селектор: создайте селектор, вызвав метод Selector.open().

Selector selector = Selector.open();

Зарегистрируйте канал с помощью селектора: SelectableChannel.register(Selector sel, int ops)

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

Когда для регистрации канала в селекторе вызывается функция register(Selector sel, int ops), событие мониторинга селектора для канала должно быть указано вторым параметром ops. Типы событий, которые можно прослушивать (обозначаются четырьмя константами, которые могут использовать SelectionKey):

  • чтение: SelectionKey.OP_READ (1)
  • Имя: SelectionKey.OP_WRITE (4)
  • соединение: SelectionKey.OP_CONNECT (8)
  • Получите: SelectionKey.OP_ACCEPT (16)
  • Если вы регистрируетесь для прослушивания более чем одного события, вы можете использовать оператор «битовое ИЛИ» для подключения.
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE 

4.7 Анализ принципа неблокирующей сетевой связи NIO

Схема селектора и описание функций

Selector может достичь: один поток ввода-вывода может одновременно обрабатывать N клиентских подключений и операции чтения и записи, что в корне решает традиционную модель синхронного блокирующего ввода-вывода одно соединение один поток, производительность архитектуры, эластичная масштабируемость и надежность были значительно улучшены.

вставьте сюда описание изображения

Серверный процесс

  • 1. Когда клиент подключится к серверу, сервер получит SocketChannel через ServerSocketChannel: 1. Получите канал
   ServerSocketChannel ssChannel = ServerSocketChannel.open();
  • 2. Переключитесь в неблокирующий режим
   ssChannel.configureBlocking(false);
  • 3. Обязательное соединение
   ssChannel.bind(new InetSocketAddress(9999));
  • 4. Получите селектор
  Selector selector = Selector.open();
  • 5. Зарегистрируйте канал в селекторе и укажите «прослушивать получение событий».
  ssChannel.register(selector, SelectionKey.OP_ACCEPT);
    1. Опрос для получения «готовых» событий в селекторах
  //轮询式的获取选择器上已经“准备就绪”的事件
   while (selector.select() > 0) {
    
    
          System.out.println("轮一轮");
          //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
          Iterator<SelectionKey> it = selector.selectedKeys().iterator();
          while (it.hasNext()) {
    
    
              //8. 获取准备“就绪”的是事件
              SelectionKey sk = it.next();
              //9. 判断具体是什么事件准备就绪
              if (sk.isAcceptable()) {
    
    
                  //10. 若“接收就绪”,获取客户端连接
                  SocketChannel sChannel = ssChannel.accept();
                  //11. 切换非阻塞模式
                  sChannel.configureBlocking(false);
                  //12. 将该通道注册到选择器上
                  sChannel.register(selector, SelectionKey.OP_READ);
              } else if (sk.isReadable()) {
    
    
                  //13. 获取当前选择器上“读就绪”状态的通道
                  SocketChannel sChannel = (SocketChannel) sk.channel();
                  //14. 读取数据
                  ByteBuffer buf = ByteBuffer.allocate(1024);
                  int len = 0;
                  while ((len = sChannel.read(buf)) > 0) {
    
    
                      buf.flip();
                      System.out.println(new String(buf.array(), 0, len));
                      buf.clear();
                  }
              }
              //15. 取消选择键 SelectionKey
              it.remove();
          }
      }
  }

клиентский процесс

    1. получить канал
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
    1. Переключить неблокирующий режим
     sChannel.configureBlocking(false);
    1. Выделить буфер указанного размера
  ByteBuffer buf = ByteBuffer.allocate(1024);
    1. отправить данные на сервер
		Scanner scan = new Scanner(System.in);
		while(scan.hasNext()){
    
    
			String str = scan.nextLine();
			buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
					+ "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		//关闭通道
		sChannel.close();

4.8 Случай входа в неблокирующую сетевую связь NIO

Создайте новый модуль
с именем: nio_selector
вставьте сюда описание изображения

Требования: сервер получает запрос на подключение от клиента и получает события, отправленные несколькими клиентами.

пример кода

/**
  客户端
 */
public class Client {
    
    
    public static void main(String[] args) {
    
    
        SocketChannel socketChannel = null;
        try {
    
    
            // 1.获取通道
            socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            // 2.切换非阻塞模式
            socketChannel.configureBlocking(false);
            // 3.分配指定缓冲区大小
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            // 4.发送数据给服务端
            Scanner scanner = new Scanner(System.in);
            while (true) {
    
    
                System.out.println("请输入:");
                String str = scanner.nextLine();
                byteBuffer.put(("波妞:" + str).getBytes());
                byteBuffer.flip();
                socketChannel.write(byteBuffer);
                byteBuffer.clear();
            }
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                socketChannel.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Сервер

/**
 服务端
 */
public class Server {
    
    
    public static void main(String[] args) {
    
    
        ServerSocketChannel serverSocketChannel = null;
        try {
    
    
            // 1.获取通道
            serverSocketChannel = ServerSocketChannel.open();
            System.out.println("服务端等待监听…………");
            // 2.切换至非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 3.指定连接的端口
            serverSocketChannel.bind(new InetSocketAddress(8888));
            // 4.获取选择器Selector
            Selector selector = Selector.open();
            // 5.将通道注册到选择器上,并且开始指定监听事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 6.使用Selector选择器轮询已经就绪的事件
            while (selector.select() > 0) {
    
    
                // 7.获取选择器中的所有注册的通道中已经就绪好的事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                // 8.遍历准备好的事件
                while (iterator.hasNext()) {
    
    
                    SelectionKey selectionKey = iterator.next();
                    // 9.判断该事件具体是什么
                    if (selectionKey.isAcceptable()) {
    
    
                        // 10.直接获取当前接入的客户端通道
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 11.切换成非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 12.将本客户端注册到选择器,并且监听读事件
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
    
    
                        // 13.获取当前选择器的读事件
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        // 14.读取数据
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int count = 0;
                        while ((count = socketChannel.read(byteBuffer)) > 0) {
    
    
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, count));
                            // 清除,归位
                            byteBuffer.clear();
                        }
                    }
                    iterator.remove();
                }
            }

        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                serverSocketChannel.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

Выходной результат:
Клиент:
вставьте сюда описание изображения

Сервер:
вставьте сюда описание изображения

4.9 Пример приложения сетевого программирования NIO — система группового чата

Новый модуль: Структура проекта nio_chat
вставьте сюда описание изображения
выглядит следующим образом:
вставьте сюда описание изображения

Цель

Требования: дальнейшее понимание механизма неблокирующего сетевого программирования NIO для реализации многогруппового чата.

  • Напишите систему группового чата NIO для реализации требований к общению между клиентом и клиентом (неблокирующий)
  • Сторона сервера: он может отслеживать пользователя в сети и в автономном режиме и реализовывать функцию пересылки сообщений.
  • Клиент: через канал сообщения могут быть отправлены всем другим пользователям-клиентам без блокировки, и в то же время могут быть приняты сообщения, перенаправленные другими пользователями-клиентами через сервер.

Реализация кода сервера

public class Server {
    
    
    // 1.定义一些成员变量、选择器、服务端通道、端口
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT = 8888;

    public Server() {
    
    
        try {
    
    
            // a.创建选择器
            this.selector = Selector.open();
            // b.获取通道
            this.serverSocketChannel = ServerSocketChannel.open();
            // c.绑定客户端连接的端口
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            // d.设置非阻塞的通信模式
            serverSocketChannel.configureBlocking(false);
            // f.注册通道到选择器上
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
    
    
        // 1.创建服务端对象
        Server server = new Server();
        // 2.开始监听各种消息,连接、群聊、离线消息
        server.listen();


    }

    /**
     * 监听事件
     */
    private void listen() {
    
    
        try {
    
    
            while (selector.select() > 0) {
    
    
                // a.获取所有选择器中的注册事件
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                // b.开始遍历这些事件
                while (iterator.hasNext()) {
    
    
                    SelectionKey selectionKey = iterator.next();
                    if (selectionKey.isAcceptable()) {
    
    
                        // 客户端接入请求
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
    
    
                        // 处理客户端消息,然后接收并且转发
                        readClientData(selectionKey);

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

    /**
     * 接收当前客户端消息的信息,转发给其它全部客户端通道
     *
     * @param selectionKey
     */
    private void readClientData(SelectionKey selectionKey) {
    
    
        SocketChannel socketChannel = null;
        try {
    
    
            socketChannel = (SocketChannel) selectionKey.channel();
            // 创建缓冲区对象,接收客户端通道的数据
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int count = 0;
            while ((count = socketChannel.read(byteBuffer)) > 0) {
    
    
                byteBuffer.flip();
                String message = new String(byteBuffer.array(), 0, count);
                System.out.println("接收到消息:" + message);
                // 把这个消息推送给全部客户端接收
                sendMessageToAllClient(message, socketChannel);

                byteBuffer.clear();

            }
        } catch (IOException e) {
    
    
            // 当前客户端离线,取消注册
            selectionKey.cancel();
            try {
    
    
                System.out.println("有人离线了:" + socketChannel.getRemoteAddress());
                socketChannel.close();
            } catch (IOException ex) {
    
    
                ex.printStackTrace();
            }
        }
    }

    /**
     * 把当前客户端的消息推送给全部在线注册的Channel
     *
     * @param message
     * @param socketChannel
     */
    private void sendMessageToAllClient(String message, SocketChannel socketChannel) throws IOException {
    
    
        System.out.println("服务端开始转发消息,当前处理的线程是:" + Thread.currentThread().getName());
        for (SelectionKey key : selector.keys()) {
    
    
            Channel channel = key.channel();
            // 不要拿自己的通道
            if (channel instanceof SocketChannel && channel != socketChannel) {
    
    
                // 将字节包装到缓冲区中
                ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
                ((SocketChannel) channel).write(byteBuffer);
            }
        }

    }
}

Реализация клиентского кода

public class Client {
    
    
    // 1.定义客户端相关属性
    private Selector selector;
    private static final int PORT = 8888;
    private SocketChannel socketChannel;

    public Client() {
    
    
        try {
    
    
            // a.创建选择器
            selector = Selector.open();
            // b.连接服务器
            socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(), 8888));
            // c.设置非阻塞模式
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("当前客户端创建完毕");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
    
    
        Client client = new Client();
        // 定义一个线程用来监听服务端发送的消息
        new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    client.readInfo();
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
    
    
            String message = scanner.nextLine();
            try {
    
    
                client.sendToServer(message);
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }

        }
    }

    /**
     * 发送消息给服务端
     *
     * @param message
     */
    private void sendToServer(String message) throws IOException {
    
    
        socketChannel.write(ByteBuffer.wrap(("波妞说:" + message).getBytes()));
    }

    /**
     * 监听事件
     */
    private void readInfo() throws IOException {
    
    
        while (selector.select() > 0) {
    
    
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isReadable()) {
    
    
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    channel.read(byteBuffer);
                    System.out.println(new String(byteBuffer.array()).trim());
                }
            }
            iterator.remove();
        }
    }

}

Тест:
сначала запустите сервер, затем запустите клиент.
Клиент 1
вставьте сюда описание изображения
Клиент 2
вставьте сюда описание изображения
Сервер
вставьте сюда описание изображения

краткое содержание

Глава 5 Углубленный анализ JAVA AIO

5.1 Программирование все-в-одном

  • Java AIO (NIO 2.0): асинхронный и неблокирующий режим реализации сервера — один эффективный запрос и один поток, а запрос ввода-вывода клиента сначала выполняется операционной системой (ОС), а затем уведомляет серверное приложение о запуске. нить для обработки.
AIO
异步非阻塞,基于NIO的,可以称之为NIO2.0
    BIO                   NIO                              AIO        
Socket                SocketChannel                    AsynchronousSocketChannel
ServerSocket          ServerSocketChannel	       AsynchronousServerSocketChannel

В отличие от NIO, при выполнении операций чтения и записи вам нужно только напрямую вызвать метод чтения или записи API.Эти два метода являются асинхронными.Для операций чтения, когда есть поток для чтения, операционная система будет передавать читаемый поток в буфер метода чтения.Для операции записи, когда операционная система заканчивает запись потока, переданного методом записи, операционная система активно уведомляет приложение

То есть можно понять, что методы чтения/записи являются асинхронными, и callback-функция будет вызываться активно после завершения. В JDK1.7 эта часть называется NIO.2, которая в основном добавляет следующие четыре асинхронных канала в пакет Java.nio.channels:

	AsynchronousSocketChannel
	AsynchronousServerSocketChannel
	AsynchronousFileChannel
	AsynchronousDatagramChannel

Глава 6 BIO, NIO, AIO Краткое изложение курса

БИО, НИО, АИО:

  • Java BIO: Синхронный и блокирующий , режим реализации сервера - один поток на соединение, то есть, когда у клиента есть запрос на соединение, серверу необходимо запустить поток для обработки.Если соединение ничего не делает, это вызовет ненужные накладные расходы на потоки, конечно, они могут быть улучшены с помощью механизма пула потоков.

  • Java NIO: синхронный и неблокирующий , режим реализации сервера — один поток на запрос, то есть запросы на подключение, отправленные клиентом, будут зарегистрированы на мультиплексоре, и мультиплексор запустится, когда он опрашивает запросы ввода-вывода на соединение Один поток выполняет обработку.

  • Java AIO (NIO.2): асинхронный и неблокирующий . Режим реализации сервера — один поток для одного эффективного запроса. Все запросы ввода-вывода клиента сначала выполняются ОС, а затем серверное приложение уведомляется об этом. запустить поток для обработки.

Анализ сценариев применения BIO, NIO и AIO:

  • Метод BIO подходит для архитектур с относительно небольшим и фиксированным числом подключений.Этот метод имеет относительно высокие требования к ресурсам сервера, а параллелизм ограничен приложениями.Это был единственный выбор до JDK1.4, но программа интуитивно понятна, простой и понятный.

  • Метод NIO подходит для архитектур с большим количеством подключений и относительно короткими подключениями (легкие операции), такими как чат-серверы. Параллелизм ограничен приложениями, а программирование сложнее. JDK1.4 начал его поддерживать.

  • Метод AIO используется в архитектурах с большим количеством подключений и относительно длинными подключениями (тяжелыми операциями), такими как серверы альбомов, которые полностью вызывают ОС для участия в параллельных операциях, а программирование более сложное JDK7 начал его поддерживать . Нетти!

Supongo que te gusta

Origin blog.csdn.net/sinat_38316216/article/details/129959193
Recomendado
Clasificación