Переосмысление BIO и NIO с практической точки зрения

Java

предисловие

За это время я читал некоторые вещи, такие как BIO и NIO на Java. Я прочитал много блогов и обнаружил, что все виды концепций о NIO являются высокомерными. Можно сказать, что они очень полны. Я все еще в состоянии мало понимания, поэтому в этой статье не будут упоминаться многие концепции, но с практической точки зрения, напишите некоторые из моих собственных мнений о NIO и вернитесь к концепции с точки зрения практики.Будет лучшее понимание.

Реализовать простой однопоточный сервер

Чтобы понять BIO и NIO, в первую очередь, мы должны сами реализовать простой сервер, который не слишком сложен, всего один поток.

  • Зачем использовать один поток в качестве демонстрации

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

  • Сервер

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("服务器已启动并监听8080端口");
    			while (true) {
    				System.out.println();
    				System.out.println("服务器正在等待连接...");
    				Socket socket = serverSocket.accept();
    				System.out.println("服务器已接收到连接请求...");
    				System.out.println();
    				System.out.println("服务器正在等待数据...");
    				socket.getInputStream().read(buffer);
    				System.out.println("服务器已经接收到数据");
    				System.out.println();
    				String content = new String(buffer);
    				System.out.println("接收到的数据:" + content);
    			}
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    }
    
  • клиент

    public class Consumer {
    	public static void main(String[] args) {
    		try {
    			Socket socket = new Socket("127.0.0.1",8080);
    			socket.getOutputStream().write("向服务器发数据".getBytes());
    			socket.close();
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    }
    
  • Разбор кода

    Сначала мы создали класс сервера, в котором был создан экземпляр SocketServer и привязан к порту 8080. Затем вызовите метод accept, чтобы получить запрос на подключение, и вызовите метод чтения, чтобы получить данные, отправленные клиентом. Наконец, распечатайте полученные данные.

    После завершения проектирования сервера давайте реализуем клиент, сначала создадим экземпляр объекта Socket и привяжем IP-адрес к 127.0.0.1 (локальный), номер порта к 8080 и вызовем метод записи для отправки данных на сервер.

    简易服务器

  • результат операции

    Когда мы запускаем сервер, но клиент не установил соединение с сервером, результаты консоли следующие:

    简易服务器结果1

    Когда клиент запускается и отправляет данные на сервер, результаты консоли следующие:

    简易服务器结果2

  • в заключении

    Из приведенных выше результатов работы, по крайней мере, мы можем видеть, что после запуска сервера, когда клиент не подключился к серверу, сервер всегда будетблокироватьдо тех пор, пока клиент запросит подключиться к серверу.

Расширить функциональность клиента

В приведенном выше примере логика клиента, которую мы реализовали, в основном состоит в том, чтобы установить сокет --> подключиться к серверу --> отправить данные.Наши данные отправляются после подключения к серверу.отправить немедленноДа, теперь давайте расширим клиент, когда мы подключаемся к серверу, мы не отправляем данные сразу, а ждем, пока консоль вручную введет данные, прежде чем отправлять их на сервер. (код сервера остается прежним)

  • код

    public class Consumer {
    	public static void main(String[] args) {
    		try {
    			Socket socket = new Socket("127.0.0.1",8080);
    			String message = null;
    			Scanner sc = new Scanner(System.in);
    			message = sc.next();
    			socket.getOutputStream().write(message.getBytes());
    			socket.close();
    			sc.close();
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    }
    
  • контрольная работа

    При запуске сервера клиент не запросил подключение к серверу, результаты консоли следующие:

    扩展客户端1

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

    扩展客户端2

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

    扩展客户端3

  • в заключении

    Из приведенных выше результатов видно, что после запуска сервера ему сначала нужно дождаться запроса на подключение от клиента (первый блок), если нет подключения клиента, то сервер всегда будет блокироваться и ждать, а затем, когда клиент подключится, сервер будет ждать, пока клиент отправит данные (второй блок), если клиент не отправляет данные, сервер всегда будет блокироваться в ожидании отправки данных клиентом. От старта сервера до получения клиентских данных будет два блокирующих процесса. ЭтоОчень важная особенность БИО, BIO заблокируется дважды, первый раз вВ ожидании подключенияблокировка, второй раз вОжидание данныхблокировать.

BIO

  • Слабые стороны BIO в однопоточном режиме

    В приведенном выше примере мы реализовали простой сервер, который работает с одним потоком.На самом деле нетрудно заметить, что когда наш сервер получает соединение и не получает данные, отправленные клиентом, он будет заблокирован в режиме чтения (), то если в это время будет другой запрос клиента, сервер не сможет ответить. Другими словами,BIO не может обрабатывать несколько клиентских запросов без учета многопоточности..

  • Как BIO обрабатывает параллелизм

    Только что в серверной реализации мы реализовали однопоточную версию сервера BIO.Нетрудно заметить, что однопоточная версия BIO не может обрабатывать запросы от нескольких клиентов, так как же BIO может обрабатывать несколько клиентские запросы.

    На самом деле, не сложно подумать, когда нам нужно только, чтобы каждый запрос на подключение приходил, создайте поток для выполнения запроса на подключение, он может обрабатывать несколько клиентских запросов в BIO, поэтому BIO — это концепция, котораяРежим реализации сервера — одно соединение и один поток, то есть, когда у клиента есть запрос на соединение, серверу необходимо запустить поток для обработки..

  • Простая реализация многопоточного БИО-сервера

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("服务器已启动并监听8080端口");
    			while (true) {
    				System.out.println();
    				System.out.println("服务器正在等待连接...");
    				Socket socket = serverSocket.accept();
    				new Thread(new Runnable() {
    					@Override
    					public void run() {
    						System.out.println("服务器已接收到连接请求...");
    						System.out.println();
    						System.out.println("服务器正在等待数据...");
    						try {
    							socket.getInputStream().read(buffer);
    						} catch (IOException e) {
    							// TODO Auto-generated catch block
    							e.printStackTrace();
    						}
    						System.out.println("服务器已经接收到数据");
    						System.out.println();
    						String content = new String(buffer);
    						System.out.println("接收到的数据:" + content);
    					}
    				}).start();
    				
    			}
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    }
    
  • результат операции

    多线程BIO

    多线程BIO2

    Очевидно, что состояние нашего сервера сейчас таково, что один поток соответствует одному запросу, другими словами, сервер создает поток для обработки каждого запроса на соединение.

  • Недостатки многопоточных серверов BIO

    Bio Multi-Threaded Server решает слабые стороны однопоточных одновременных био не может обрабатывать, но это также приноситпроблема: если к нашему серверу подключается большое количество запросов, ноне отправлять сообщение, то наш сервер также будет создавать отдельный поток для этих запросов, которые не отправляют сообщения, поэтому, если количество подключений будет небольшим, это нормально, но большое количество подключений вызовет большую нагрузку на сервер. Итак, если таких неактивных потоков много, мы должны принять однопоточное решение, но однопоточное не может справиться с параллелизмом, который попадает в очень противоречивое состояние, поэтому есть NIO.

NIO

  • Введение НИО

    Давайте сначала посмотрим на код сервера BIO в однопоточном режиме, на самом деле NIO нужно решитьсамая фундаментальная проблемаЭто два блока, которые существуют в BIO, которыеБлокировка во время ожидания соединенияиБлокировка во время ожидания данных.

    public class Server {
    	public static void main(String[] args) {
    		byte[] buffer = new byte[1024];
    		try {
    			ServerSocket serverSocket = new ServerSocket(8080);
    			System.out.println("服务器已启动并监听8080端口");
    			while (true) {
    				System.out.println();
    				System.out.println("服务器正在等待连接...");
    				//阻塞1:等待连接时阻塞
    				Socket socket = serverSocket.accept();
    				System.out.println("服务器已接收到连接请求...");
    				System.out.println();
    				System.out.println("服务器正在等待数据...");
    				//阻塞2:等待数据时阻塞
    				socket.getInputStream().read(buffer);
    				System.out.println("服务器已经接收到数据");
    				System.out.println();
    				String content = new String(buffer);
    				System.out.println("接收到的数据:" + content);
    			}
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    }
    

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

    Тогда наш вопрос переходит кКак заставить однопоточный сервер по-прежнему получать новые запросы на подключение клиентов, ожидая поступления клиентских данных.

  • Аналоговые решения NIO

    Если вы хотите решить упомянутую выше проблему, что однопоточный сервер блокируется при получении данных и не может принимать новые запросы, то вы можете фактически заставить сервер ждать данныхНе входить в состояние блокировки, разве проблема не решена?

    • Первое решение (без блокировки во время ожидания соединения и ожидания данных)

      public class Server {
      	public static void main(String[] args) throws InterruptedException {
      		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      		try {
      			//Java为非阻塞设置的类
      			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      			serverSocketChannel.bind(new InetSocketAddress(8080));
      			//设置为非阻塞
      			serverSocketChannel.configureBlocking(false);
      			while(true) {
      				SocketChannel socketChannel = serverSocketChannel.accept();
      				if(socketChannel==null) {
      					//表示没人连接
      					System.out.println("正在等待客户端请求连接...");
      					Thread.sleep(5000);
      				}else {
      					System.out.println("当前接收到客户端请求连接...");
      				}
      				if(socketChannel!=null) {
                          //设置为非阻塞
      					socketChannel.configureBlocking(false);
      					byteBuffer.flip();//切换模式  写-->读
      					int effective = socketChannel.read(byteBuffer);
      					if(effective!=0) {
      						String content = Charset.forName("utf-8").decode(byteBuffer).toString();
      						System.out.println(content);
      					}else {
      						System.out.println("当前未收到客户端消息");
      					}
      				}
      			}
      		} catch (IOException e) {
      			// TODO Auto-generated catch block
      			e.printStackTrace();
      		}
      	}
      }
      
    • результат операции

      NIO解决方案1

      Нетрудно заметить, что при таком решении он хоть и не блокируется при приеме клиентских сообщений, но снова начинает принимать запросы к серверу, пользователь не успевает вводить сообщения, и сервер переключается на прием других клиентских запросов. слова, серверПотерян текущий запрос клиента.

    • Решение 2 (кешировать Socket, опросить, готовы ли данные)

      public class Server {
      	public static void main(String[] args) throws InterruptedException {
      		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
      		
      		List<SocketChannel> socketList = new ArrayList<SocketChannel>();
      		try {
      			//Java为非阻塞设置的类
      			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
      			serverSocketChannel.bind(new InetSocketAddress(8080));
      			//设置为非阻塞
      			serverSocketChannel.configureBlocking(false);
      			while(true) {
      				SocketChannel socketChannel = serverSocketChannel.accept();
      				if(socketChannel==null) {
      					//表示没人连接
      					System.out.println("正在等待客户端请求连接...");
      					Thread.sleep(5000);
      				}else {
      					System.out.println("当前接收到客户端请求连接...");
      					socketList.add(socketChannel);
      				}
      				for(SocketChannel socket:socketList) {
      					socket.configureBlocking(false);
      					int effective = socket.read(byteBuffer);
      					if(effective!=0) {
      						byteBuffer.flip();//切换模式  写-->读
      						String content = Charset.forName("UTF-8").decode(byteBuffer).toString();
      						System.out.println("接收到消息:"+content);
      						byteBuffer.clear();
      					}else {
      						System.out.println("当前未收到客户端消息");
      					}
      				}
      			}
      		} catch (IOException e) {
      			// TODO Auto-generated catch block
      			e.printStackTrace();
      		}
      	}
      }
      
    • результат операции

      NIO解决方案2

      NIO解决方案3

    • Разбор кода

      существуетРешение первоеВ , мы приняли неблокирующий метод, но обнаружили, что когда-то неблокирующий, ожидание отправки клиентом сообщения больше не блокируется, а напрямую повторно получает запрос на соединение нового клиента, что вызоветСоединение с клиентом потеряно, а во втором решении мы сохраняем соединения в коллекции списков и переходим к ней каждый раз, когда ждем сообщения клиента.голосование, чтобы узнать, готово ли сообщение, и если да, распечатать сообщение напрямую. Видно, что мы не запускали второй поток от начала до конца, а использовалиодин потокДля обработки подключения нескольких клиентов такой режим может прекрасно решить проблему, заключающуюся в том, что BIO не может обрабатывать многоклиентские запросы в однопоточном режиме, и решает проблемуСоединение потеряно в неблокирующем состоянииПроблема.

  • Проблемы (решение второе)

    Только сейчас по результатам выполнения видно, что сообщение не потеряно и программа не заблокирована.но, может быть что-то не так со способом получения сообщений, мы используемголосованиеСпособ получения сообщений, каждый раз опрашивать все соединения, чтобы увидеть, готово ли сообщение, в тестовом примере всего три соединения, так что проблем нет, но мы предполагаем, что соединений 10 миллионов, а то и больше, используйте Этот метод опроса крайне неэффективен. Кроме того, среди 10 миллионов подключений у нас может быть только 1 миллион сообщений, а остальные 9 миллионов не будут отправлять никаких сообщений, поэтому эти программы подключения все равно должны каждый раз опрашивать, что явно неуместно.

  • Как это решить в реальном NIO

    В реальном NIO опрос не выполняется на уровне Java, а этап опроса передается нашей операционной системе, и он меняет код опроса на системный вызов уровня операционной системы (функция выбора, epoll в среде linux), вызовите функцию select на уровне операционной системы, чтобы активно определять сокет с данными.

Разница между использованием select/epoll и опросом непосредственно на прикладном уровне

Ранее мы реализовали логику использования Java для опроса нескольких клиентских подключений, но на самом деле это не реализовано в реальном исходном коде NIO.NIO использует системный вызов опроса в нижней части операционной системы для выбора/epoll(windows: select , linux: epoll), то почему бы не реализовать его напрямую и не вызвать систему для проведения опроса?

  • выбрать базовую логику

    select

    Предполагая, что к серверу одновременно подключаются пять подключений A, B, C, D и E, тогда, в соответствии с нашим дизайном выше, программа будет проходить через эти пять подключений, опрашивать каждое подключение и получать соответствующие данные о готовности. , тогдаИ напишем свою программу какая разница?

    Прежде всего, сущность написанной нами Java-программы также требует вызова системной функции при опросе каждого сокета, поэтому опрос и однократный вызов вызовут ненужные накладные расходы на переключение контекста.

    А Select будет пятью запросами из пользовательского пространстваполная копияОдна копия в пространстве состояния ядра, в пространстве состояния ядра, чтобы определить, готов ли каждый запрос к данным, полностью избегая частого переключения контекста. Так эффективность выше, чем мы пишем опрос прямо на прикладном уровне.

    Если select не запрашивает запрос с данными, он будет постоянно блокироваться (да, select — блокирующая функция). Если один или несколько запросов имеют готовые данные, тогда select сначала установит файловый дескриптор с данными.Должность, затем выберите возврат. Пройти после возвращениятраверсПосмотрите, в каком запросе есть данные.

    Недостатки выбора:

    1. Базовое хранилище использует растровое изображение, а количество обрабатываемых запросов ограничено 1024.

    2. Файловый дескриптор будет установлен, поэтому, если установленный файловый дескриптор необходимо использовать повторно, ему необходимо повторно присвоить нулевое значение.

    3. Копирование fd (файлового дескриптора) из пользовательского режима в режим ядра по-прежнему связано с накладными расходами.

    4. После возврата select его нужно пройти снова, чтобы узнать, в каком запросе есть данные.

  • Основная логика функции опроса

    Принцип работы poll очень похож на принцип select.Давайте сначала рассмотрим структуру, используемую внутри poll.

    struct pollfd{
        int fd;
        short events;
        short revents;
    }
    

    Poll также копирует все запросы в состояние ядра. Как и select, poll также является блокирующей функцией. Когда один или несколько запросов имеют данные, он также будет установлен, но установит структуру pollfd Вместо того, чтобы устанавливать сам fd, события или ревенты в нем устанавливаются, поэтому нет необходимости переназначать нулевое значение при следующем его использовании. опрос внутренней памятиНе зависит от растрового изображения, вместо этого используйте pollfdмножествоДля такой структуры данных размер массива должен быть больше 1024. Это устраняет недостатки выбора 1 и 2.

  • epoll

    epoll — новейшая функция мультиплексирования ввода-вывода. Здесь только поговорим о его характеристиках.

    epoll и функции двух самых больших различий в том, что это fdобщийМежду пользовательским режимом и режимом ядра, поэтому нет необходимости делать копию из пользовательского режима в режим ядра, что может сэкономить системные ресурсы; кроме того, в select и poll, если данные запроса готовы, они будут все запросы возвращаются программе, чтобы просмотреть, какой запрос содержит данные, но epoll вернет только запрос с данными, потому что, когда epoll обнаружит, что в запросе есть данные, он сначала сделает запрос.переставлятьОперация, поместите все fds с данными в переднюю позицию, а затем вернитесь (возвращаемое значение — это число N запросов данных), тогда нашей программе верхнего уровня не нужно опрашивать все запросы, а напрямую проходит через epoll Первые N возвращены запросы, все эти запросы являются запросами с данными.

Концепция BIO и NIO в Java

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

Понятия организованы в:blog.CSDN.net/Блестящая машина…

Давайте рассмотрим пример, чтобы понять концепцию, взяв в качестве примера банковское снятие средств:

  • Синхронизация: лично выйти в банк, чтобы снять деньги с банковской карты (при использовании синхронного ввода-вывода Java обрабатывает чтение и запись ввода-вывода самостоятельно).
  • Асинхронный: Доверьте младшему брату отнести банковскую карту в банк для снятия денег, а затем отдать вам (при использовании асинхронного ввода-вывода Java делегирует чтение и запись ввода-вывода ОС, а адрес и размер буфера данных необходимо передать в ОС (банковская карта и пароль), ОС должна поддерживать API операций асинхронного ввода-вывода).
  • Блокировка: очереди банкоматов для снятия денег, вы можете только ждать (при использовании блокировки ввода-вывода вызов Java будет блокироваться до завершения чтения и записи перед возвратом).
  • Без блокировки: снимите деньги на кассе, возьмите номер, а затем сядьте на стул и займитесь другими делами. Трансляция знака равенства уведомит вас об этом. Вы не можете пройти, пока не придет номер. Вы можете продолжать спрашивать в лобби. диспетчера, если линия свободна.Если менеджер лобби говорит да, вы не можете пройти, пока он не придет (при использовании неблокирующего ввода-вывода, если вы не можете читать или писать Java, вызов вернется немедленно, и когда событие ввода-вывода диспетчер сообщит, что вы можете читать и писать, продолжать чтение и запись и продолжать цикл, пока чтение и запись не будут завершены).

Поддержка Java для BIO и NIO:

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

Анализ применимых сценариев для BIO и NIO:

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

Эпилог

Эта статья знакомит с некоторыми понятиями о JavaBIO и NIO с точки зрения их собственной практической работы. Лично я думаю, что понимание BIO и NIO таким образом будет иметь более глубокое понимание, чем просто чтение концепций. Результат выполнения программы дает собственное понимание JavaBIO и NIO.

Добро пожаловать в мой личный блог:Object's Blog