оригинал:chenmingyu.top/nio/
Изучение системы ввода-вывода Java сосредоточено на изучении модели ввода-вывода.После понимания различных моделей ввода-вывода вы сможете лучше понять Java IO.
Java IO — это набор API-интерфейсов, которые Java использует для чтения и записи данных (ввода и вывода). Большинство программ обрабатывают некоторый ввод и производят некоторый вывод из ввода. Java предоставляет для этого пакет java.io.
Систему ввода-вывода в Java можно разделить на три модели ввода-вывода: Bio, Nio и Aio.
- Что касается Био, нам нужно знать, что такое синхронная блокирующая модель ввода-вывода, объект работы Био: поток и как использовать Био для сетевого программирования, проблема использования Био для сетевого программирования.
- О Nio, нам нужно знать, что такое синхронная модель безблокировки IO, какова модель мультиплексирования IO, а также концепции буфера, канал, селектор в NIO и как использовать NIO для сетевого программирования
- Что касается Aio, нам нужно знать, что такое асинхронная неблокирующая модель ввода-вывода, Aio может использовать несколько способов достижения асинхронной работы и как использовать сетевое программирование Aio.
BIO
BIO — это синхронный блокирующий ввод-вывод. До JDK1.4 была только одна модель ввода-вывода. Объектом операции BIO является поток. Один поток может обрабатывать только запросы ввода-вывода одного потока. Если вы хотите обрабатывать несколько потоков одновременно, вы необходимо использовать многопоточность.
Потоки включают в себя потоки символов и потоки байтов, а потоки концептуально представляют собой непрерывный поток данных. Когда программе нужно прочитать данные, ей нужно использовать входной поток для чтения данных, а когда ей нужно записать данные, ей нужно использовать выходной поток.
Блокирующая модель ввода-вывода
В Linux, когда процесс приложения вызываетrecvfromКогда метод вызывает данные, если ядро не подготовит данные, оно не вернется немедленно, а будет ждать готовности данных, а перед возвратом данные будут скопированы из ядра в пространство пользователя. , процесс приложения будет блокироваться до тех пор, пока не вернется, поэтому он называется Blocking IO model
поток
Существует два основных типа потоков, работающих в BIO, поток байтов и поток символов.Оба типа можно разделить на входной поток и выходной поток в соответствии с направлением потока.
По типу и направлению ввода и вывода его можно разделить на:
- Входной поток байтов: InputStream
- Выходной поток байтов: OutputStream
- Поток входных символов: Reader
- Выходной поток символов: Writer
Поток байтов в основном используется для обработки байтов или двоичных объектов, а поток символов используется для обработки текста или строк символов.
использоватьInputStreamReader
Преобразует входной поток байтов во входной поток символов
Reader reader = new InputStreamReader(inputStream);
использоватьOutputStreamWriter
Преобразует выходной поток байтов в выходной поток символов
Writer writer = new OutputStreamWriter(outputStream)
Мы можем читать данные из источника данных через InputStream и Reader в программе, а затем выводить данные на целевой носитель через OutputStream и Writer в программе
При использовании потоков байтов InputStream и OutputStream оба являются абстрактными классами, мы создаем экземпляры их подклассов, и каждый подкласс имеет свою собственную область видимости.
То же самое верно при использовании потоков символов. Считыватель и писатель оба абстрактных класса. Мы создали свои подклассы, и каждый подкласс имеет свой собственный объем действий.
Возьмите чтение и запись файлов в качестве примера
Чтение данных из источника данных
Входной байтовый поток:InputStream
public static void main(String[] args) throws Exception{
File file = new File("D:/a.txt");
InputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes);
System.out.println(new String(bytes));
inputStream.close();
}
Поток входных символов:Reader
public static void main(String[] args) throws Exception{
File file = new File("D:/a.txt");
Reader reader = new FileReader(file);
char[] bytes = new char[(int) file.length()];
reader.read(bytes);
System.out.println(new String(bytes));
reader.close();
}
вывод на целевой носитель
Выходной поток байтов:OutputStream
public static void main(String[] args) throws Exception{
String var = "hai this is a test";
File file = new File("D:/b.txt");
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(var.getBytes());
outputStream.close();
}
Выходной поток символов:Writer
public static void main(String[] args) throws Exception{
String var = "hai this is a test";
File file = new File("D:/b.txt");
Writer writer = new FileWriter(file);
writer.write(var);
writer.close();
}
BufferedInputStream
При использовании INPUTSTREAM он является одним байтовым байтом, чтенным или письменным, а BufferedInputStream предоставляет буфер для потока входных байтов. При чтении данных вы будете прочитать часть данных в буфере, когда буфер после чтения данных. , входной поток снова заполнит буфер данных, пока входной поток не будет читаться, и есть буфер для улучшения многих скоростей IO.
Используйте метод для переноса входного потока в BufferedInputStream.
/**
* inputStream 输入流
* 1024 内部缓冲区大小为1024byte
*/
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);
BufferedOutputStream
BufferedOutputStream может предоставлять буферы для выходных потоков байтов, подобно BufferedInputStream.
Используйте метод для переноса выходного потока в BufferedOutputStream.
/**
* outputStream 输出流
* 1024 内部缓冲区大小为1024byte
*/
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream,1024);
Потоки байтов предоставляют буферизованные потоки, а потоки символов также должны предоставлять BufferedReader и BufferedWriter.
BufferedReader
Предоставьте буфер для входного потока символов, который используется следующим образом.
BufferedReader bufferedReader = new BufferedReader(reader,1024);
BufferedWriter
Предоставьте буфер для выходного потока символов, который используется следующим образом.
BufferedWriter bufferedWriter = new BufferedWriter(writer,1024);
Сетевое программирование модели BIO
При использовании модели BIO для программирования Socket сервер обычно использует цикл while для вызова метода accept. Когда нет запроса клиента, метод accept будет блокироваться до тех пор, пока запрос не будет получен и соответствующая обработка не будет возвращена. Этот процесс является линейным. ., последующий запрос будет принят только после обработки текущего запроса, что обычно приводит к длительной блокировке коммуникационного потока
Модель BIO обрабатывает несколько соединений:
В этом режиме мы обычно используем поток для принятия запроса, а затем используем пул потоков для обработки запроса и одновременного управления несколькими клиентскими подключениями сокетов следующим образом:
Проблема использования модели BIO для сетевого программирования заключается в отсутствии эластичной масштабируемости.Количество одновременных обращений клиентов и количество потоков сервера составляет 1:1, и обычно из-за блокировок большое количество потоков будет находиться в одном потоке. состояние ожидания, ожидание готовности входных или выходных данных. Вызывает пустую трату ресурсов. Перед лицом большого количества параллелизма, если вы не используете пул потоков для непосредственного создания новых потоков, потоки будут расширяться, а система производительность снизится, что может привести к переполнению памяти стека, а частое создание и уничтожение потоков еще более расточительно.
Использование пула потоков может быть лучшим решением, но оно не может решить проблему блокировки ввода-вывода, и также необходимо учитывать, что если количество пулов потоков установлено на небольшое число, большое количество клиентских соединений Socket будет быть отклоненным.Если количество пулов потоков установлено на большое число Когда это произойдет, это вызовет много переключений контекста, и программе необходимо выделить память для стека вызовов каждого потока.Диапазон размера по умолчанию составляет 64 КБ до 1 МБ, что тратит впустую память виртуальной машины.
Модель BIO подходит для архитектуры с фиксированным количеством ссылок и относительно небольшим количеством ссылок, но код, написанный с использованием этой модели, более интуитивно понятен, прост и понятен.
NIO
Начиная с JDK 1.4, JDK выпустила новую библиотеку классов ввода-вывода, называемую NIO, которая представляет собой синхронную неблокирующую модель ввода-вывода.
Неблокирующая модель ввода-вывода
Реализация модели синхронного неблокирующего ввода-вывода:
Неблокирующая модель ввода-вывода
вызов процесса приложенияrecvfromСистемный вызов, если данные ядра не готовы, он напрямую вернет ошибку EWOULDBLOCK, процесс приложения не будет блокироваться, но процесс приложения должен постоянно опрашивать вызовrecvfrom, пока данные ядра не будут готовы, а затем дождитесь, пока данные будут скопированы из ядра в пространство пользователя (это время будет блокироваться, но занимает очень мало времени), и вернитесь после завершения копирования
Модель повторного использования ввода-вывода
Модель мультиплексирования ввода-вывода с использованием предоставляемой системой Linuxвыбрать, опросСистемный вызов, передайте один или несколько файловых дескрипторов (клиентские ссылки в сетевом программировании) на системный вызов select или poll, процесс приложения блокируется при выборе, таким образом формируя процесс, соответствующий нескольким ссылкам Socket, а затем выберите/опросите этот набор Ссылки на сокеты будут сканироваться линейно. Когда только несколько сокетов имеют данные, эффективность падает, а select/poll ограничивается количеством дескрипторов файлов, которые он содержит. Значение по умолчанию — 1024.
Модель ввода-вывода, управляемая сигналами
Системный вызов sigaction выполняет функцию обработки сигнала. Этот системный вызов не блокирует процесс приложения. Когда данные готовы, для процесса генерируется сигнал SIGIO, и приложение через сигнал обратного вызова уведомляется о вызове recvfrom для чтения данные.
Основные концепции NIO
Buffer(буфер)
Буфер - это объект, который содержит некоторые данные для записи или чтения.В NIO все данные обрабатываются в области буфера.При чтении данных они должны быть прочитаны из буфера.При записи данных они сначала будут записаны в буфер , В этой области буфер представляет собой, по сути, массив, из которого данные могут быть записаны, а затем прочитаны, обеспечивая структурированный доступ к данным и внутреннюю поддержку информации, такой как места чтения и записи.
Создание экземпляра ByteBuffer
//创建一个容量为1024个byte的缓冲区
ByteBuffer buffer=ByteBuffer.allocate(1024);
Как использовать буфер:
- Записать данные в буфер
- перечислить
flip()
способ переключения буфера из режима записи в режим чтения - читать данные из буфера
- перечислить
clear()
метод илиcompact()
метод очистки буфера, чтобы его можно было снова записать
Смотрите это для более подробной информации:ifeve.com/buffers/
Channel(ряд)
Канальные (канальные) данные всегда читаются из канала в буфер, либо записываются из буфера в канал, Канал отвечает только за транспортировку данных, а рабочие данные - это Буфер
Каналы похожи на потоки, за исключением того, что:
- Причина в том, что канал является двунаправленным и может быть прочитан и записан одновременно, а поток однонаправлена и может быть написан только или прочитать.
- Чтение и запись потока заблокированы, и канал может быть прочитан и записан асинхронно.
Данные считываются из канала в буфер
inChannel.read(buffer);
Данные записываются из буфера в канал
outChannel.write(buffer);
Подробнее см. здесь: ifeve.com/channels/>
Возьмите копирование файлов в качестве примера
FileInputStream fileInputStream=new FileInputStream(new File(src));
FileOutputStream fileOutputStream=new FileOutputStream(new File(dst));
//获取输入输出channel通道
FileChannel inChannel=fileInputStream.getChannel();
FileChannel outChannel=fileOutputStream.getChannel();
//创建容量为1024个byte的buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
//从inChannel里读数据,如果读不到字节了就返回-1,文件就读完了
int eof =inChannel.read(buffer);
if(eof==-1){
break;
}
//将Buffer从写模式切换到读模式
buffer.flip();
//开始往outChannel写数据
outChannel.write(buffer);
//清空buffer
buffer.clear();
}
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
Selector(селектор мультиплекса)
Селектор является основой программирования NIO. Основная функция заключается в регистрации нескольких каналов на селекторе. Если происходит событие чтения или записи на канале, канал находится в состоянии готовности и будет опрашиваться селектором. Коллекция Ready Channel для операций IO
Связь между селектором и каналом, буфером
Подробнее см. здесь: ifeve.com/selectors/
Сетевое программирование модели NIO
NIO в JDK использует мультиплексированную модель ввода-вывода. Путем мультиплексирования нескольких блоков ввода-вывода в один блок выбора система может одновременно обрабатывать несколько клиентских запросов в одном потоке, что снижает нагрузку на систему. JDK был реализован на основе модели select/poll.В версиях выше JDK 1.5 update10 нижний уровень использует epoll вместо select/poll
Преимущества epoll перед select/poll:
- Количество открытых файловых дескрипторов, поддерживаемых epoll, не ограничено, а количество файловых дескрипторов, которые можно открыть с помощью select/poll, ограничено.
- Выберите / опрос использует опрос для прохождения всего набора дескрипторов файлов, а также вызовы EPOLL обратно в зависимости от функции обратного вызова каждого файлового дескриптора
выбрать, опрос, epollОба являются механизмами мультиплексирования ввода-вывода. Мультиплексирование ввода-вывода — это механизм, с помощью которого процесс может отслеживать несколько дескрипторов.Когда дескриптор готов (обычно готов к чтению или записи), он может уведомить программу о выполнении соответствующих операций чтения и записи. Но select, poll и epoll по сути являются синхронным вводом-выводом, потому что все они должны отвечать за чтение и запись после того, как события чтения и записи готовы, то есть процесс чтения и записи блокируется, а асинхронный ввод-вывод /O не требует собственной ответственности за чтение и запись
NIO предоставляет два набора различных каналов сокетов для реализации сетевого программирования на стороне сервера: ServerSocketChannel и клиентский SocketChannel, оба канала поддерживают блокирующий и неблокирующий режимы.
код сервера
Сервер принимает вывод сообщения, отправленный клиентом, и отправляет сообщение клиенту.
//创建多路复用选择器Selector
Selector selector=Selector.open();
//创建一个通道对象Channel,监听9001端口
ServerSocketChannel channel = ServerSocketChannel.open().bind(new InetSocketAddress(9001));
//设置channel为非阻塞
channel.configureBlocking(false);
//
/**
* 1.SelectionKey.OP_CONNECT:连接事件
* 2.SelectionKey.OP_ACCEPT:接收事件
* 3.SelectionKey.OP_READ:读事件
* 4.SelectionKey.OP_WRITE:写事件
*
* 将channel绑定到selector上并注册OP_ACCEPT事件
*/
channel.register(selector,SelectionKey.OP_ACCEPT);
while (true){
//只有当OP_ACCEPT事件到达时,selector.select()会返回(一个key),如果该事件没到达会一直阻塞
selector.select();
//当有事件到达了,select()不在阻塞,然后selector.selectedKeys()会取到已经到达事件的SelectionKey集合
Set keys = selector.selectedKeys();
Iterator iterator = keys.iterator();
while (iterator.hasNext()){
SelectionKey key = (SelectionKey) iterator.next();
//删除这个SelectionKey,防止下次select方法返回已处理过的通道
iterator.remove();
//根据SelectionKey状态判断
if (key.isConnectable()){
//连接成功
} else if (key.isAcceptable()){
/**
* 接受客户端请求
*
* 因为我们只注册了OP_ACCEPT事件,所以有客户端链接上,只会走到这
* 我们要做的就是去读取客户端的数据,所以我们需要根据SelectionKey获取到serverChannel
* 根据serverChannel获取到客户端Channel,然后为其再注册一个OP_READ事件
*/
// 1,获取到ServerSocketChannel
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 2,因为已经确定有事件到达,所以accept()方法不会阻塞
SocketChannel clientChannel = serverChannel.accept();
// 3,设置channel为非阻塞
clientChannel.configureBlocking(false);
// 4,注册OP_READ事件
clientChannel.register(key.selector(),SelectionKey.OP_READ);
} else if (key.isReadable()){
// 通道可以读数据
/**
* 因为客户端连上服务器之后,注册了一个OP_READ事件发送了一些数据
* 所以首先还是需要先获取到clientChannel
* 然后通过Buffer读取clientChannel的数据
*/
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead>0){
byteBuffer.flip();
System.out.println("client data :"+new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
/**
* 我们服务端收到信息之后,我们再给客户端发送一个数据
*/
byteBuffer.clear();
byteBuffer.put("客户端你好,我是服务端,你看这NIO多难".getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
} else if (key.isWritable() && key.isValid()){
//通道可以写数据
}
}
}
код клиента
После того, как клиент подключается к серверу, он сначала отправляет сообщение на сервер и принимает сообщение, отправленное сервером.
Selector selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
//将channel设置为非阻塞
clientChannel.configureBlocking(false);
//连接服务器
clientChannel.connect(new InetSocketAddress(9001));
//注册OP_CONNECT事件
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
//如果事件没到达就一直阻塞着
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isConnectable()){
/**
* 连接服务器端成功
*
* 首先获取到clientChannel,然后通过Buffer写入数据,然后为clientChannel注册OP_READ时间
*/
clientChannel = (SocketChannel) key.channel();
if (clientChannel.isConnectionPending()){
clientChannel.finishConnect();
}
clientChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
byteBuffer.put("服务端你好,我是客户端,你看这NIO难吗".getBytes("UTF-8"));
byteBuffer.flip();
clientChannel.write(byteBuffer);
clientChannel.register(key.selector(),SelectionKey.OP_READ);
} else if (key.isReadable()){
//通道可以读数据
clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
long bytesRead = clientChannel.read(byteBuffer);
while (bytesRead>0){
byteBuffer.flip();
System.out.println("server data :"+new String(byteBuffer.array()));
byteBuffer.clear();
bytesRead = clientChannel.read(byteBuffer);
}
} else if (key.isWritable() && key.isValid()){
//通道可以写数据
}
}
}
Использование нативной библиотеки класса NIO очень сложна. Библиотека Nio Class и API являются сложными и неприятными для использования. Вы должны быть очень знакомы с сетевым программированием, чтобы написать высококачественные программы NIO. Поэтому не рекомендуется использовать родной NIO. для сетевого программирования напрямую. Некоторые зрелые рамки, такие как Netty
AIO
JDK1.7 обновил библиотеку классов Nio и стал Nio2.0. Самое главное — обеспечить операции ввода-вывода для асинхронных файлов, а также ввод-вывод, управляемый событиями.Асинхронный канал сокетов AIO — это настоящий асинхронный неблокирующий ввод-вывод.
Модель асинхронного ввода-вывода
В системе Linux процесс приложения инициирует операцию чтения и может немедленно выполнять другие действия.Ядро подготовит данные и скопирует их в доступное пространство и сообщит процессу приложения, что данные были скопированы и операция чтения завершена. .
сетевое программирование модели aio
Асинхронная работа
aio может выполнять асинхронное чтение и запись без опроса зарегистрированных каналов через мультиплексор, что упрощает модель программирования NIO.
Асинхронный канал Aio предоставляет две асинхронные операции, асинхронный канал, получая результаты:
- Получить результат асинхронной операции через класс Future, но следует отметить, что future.get() является блокирующим методом, который заблокирует поток
- Асинхронный обратный вызов, обратный вызов выполняется путем передачи класса реализации CompletionHandler.CompletionHandler определяет два метода, завершенный и неудавшийся, которые соответствуют успеху и неудаче соответственно.
Каналы в Aio поддерживают два вышеуказанных метода.
AIO предоставляет соответствующий канал асинхронного сокета для реализации сетевого программирования, сервер: AsynchronousServerSocketChannel и клиент AsynchronousSocketChannel.
Сервер
Сервер отправляет сообщение клиенту и принимает сообщение, отправленное клиентом.
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9001));
//异步接受请求
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
//成功时
@Override
public void completed(AsynchronousSocketChannel result, Void attachment) {
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("我是服务端,客户端你好".getBytes());
buffer.flip();
result.write(buffer, null, new CompletionHandler<Integer, Void>(){
@Override
public void completed(Integer result, Void attachment) {
System.out.println("服务端发送消息成功");
}
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("发送失败");
}
});
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
result.read(readBuffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println(new String(readBuffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("读取失败");
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
//失败时
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
//防止线程执行完
TimeUnit.SECONDS.sleep(1000L);
клиент
Клиент отправляет сообщение на сервер и получить сообщение, отправленное сервером
AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9001));
//阻塞,获取连接
future.get();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读数据
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
//成功时调用
@Override
public void completed(Integer result, Void attachment) {
System.out.println(new String(buffer.array()));
}
//失败时调用
@Override
public void failed(Throwable exc, Void attachment) {
System.out.println("客户端接收消息失败");
}
});
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("我是客户端,服务端你好".getBytes());
writeBuffer.flip();
//阻塞方法
Future<Integer> write = client.write(writeBuffer);
Integer r = write.get();
if(r>0){
System.out.println("客户端消息发送成功");
}
//休眠线程
TimeUnit.SECONDS.sleep(1000L);
Суммировать
Сравнение различных моделей ввода-вывода:
Псевдоасинхронный ввод-вывод относится к модели Bio, которая использует пул потоков для обработки запросов.
Ссылаться на:
Полное руководство по Netty, второе издание
если eve.com/Java-you-oh-ali…Сеть параллельного программирования
Специальности.Meituan.com/2016/11/04/…Техническая команда Meituan
Если есть какие-либо нарушения изображений в тексте, пожалуйста, свяжитесь со мной, чтобы удалить