前言
上文介紹了網(wǎng)絡(luò)編程的基礎(chǔ)知識,并基于 Java 編寫了 BIO 的網(wǎng)絡(luò)編程。我們知道 BIO 模型是存在巨大問題的,比如 C10K 問題,其本質(zhì)就是因其阻塞原因,導(dǎo)致如果想要承受更多的請求就必須有足夠多的線程,但是足夠多的線程會帶來內(nèi)存占用問題、CPU上下文切換帶來的性能問題,從而造成服務(wù)端崩潰的現(xiàn)象。怎么解決這一問題呢?優(yōu)化唄,所以后面就有了NIO、AIO、IO多路復(fù)用。本文將對這幾個模型詳細說明并基于 Java 編寫 NIO。
基本概念
I/O阻塞是哪里阻塞、怎么阻塞?先簡單了解一些基本概念
- 用戶空間:被分配給用戶進程的虛擬地址空間,用來存儲用戶進程的代碼、數(shù)據(jù)和堆棧等。
- 內(nèi)核空間:操作系統(tǒng)的基礎(chǔ),負責(zé)管理計算機的硬件資源和提供系統(tǒng)調(diào)用接口,同時也是用戶空間和硬件之間的橋梁。
為了保證操作系統(tǒng)的安全性和穩(wěn)定性,用戶進程和操作系統(tǒng)內(nèi)核是隔離的,用戶進程不能直接訪問內(nèi)核空間,而是需要通過系統(tǒng)調(diào)用等方式向內(nèi)核發(fā)起請求,由內(nèi)核代表用戶進程執(zhí)行操作。
也就是說我們的應(yīng)用程序在向硬件設(shè)備,比如網(wǎng)卡、磁盤等讀取或?qū)懭霐?shù)據(jù)時需要經(jīng)過內(nèi)核。下面對BIO、NIO、IO多路復(fù)用模型逐一介紹,詳細了解各模型IO過程。
BIO過程
首先明確一下,我們所說的IO阻塞是用戶進程也就是用戶空間中的程序在向硬件設(shè)備讀取的這個過程,在還沒有數(shù)據(jù)時給用戶的反映是需要一直等待的,這個我們叫阻塞IO。過程如下圖:
我們可以看到,在進程向內(nèi)核發(fā)起調(diào)用后直到數(shù)據(jù)返回,整個過程都是阻塞的,結(jié)合Java BIO 編程,也就是說在 inputStream.read()
這個過程是阻塞的,就存在幾個問題:
- 由于阻塞會占用當(dāng)前線程,使之不能進行其他操作,當(dāng)有新的請求時只能新建線程。在 Linux 系統(tǒng)中,每個線程的默認棧大小為 8MB,在不考慮其他因素的情況下,一個 8G 的服務(wù)器最多也就承載1000個請求量。
- 由于線程數(shù)會隨著請求量增大而增大,當(dāng)有大量的線程阻塞喚醒,CPU 頻繁的切換上下文會導(dǎo)致性能的下降。
這個問題也就是C10K問題的本質(zhì),看上去很直觀,使用少的線程就處理多個IO是不是就可以解決呢?繼續(xù)看NIO過程。
NIO過程
NIO我們說的是非阻塞,通過對BIO的說明,NIO的非阻塞體現(xiàn)在:無論有無數(shù)據(jù)都直接響應(yīng)給用戶進程,如下圖:
我們可以看到,確實是在用戶進程調(diào)用recvfrom()
函數(shù)后直接響應(yīng),但是在沒有拿到數(shù)據(jù)之前一直在輪詢調(diào)用,雖然沒有因為阻塞造成CPU上下文的切換,但是CPU一直處于空轉(zhuǎn)狀態(tài),不能充分發(fā)揮CPU的作用。與BIO一樣,在單線程的情況下,只能依次處理IO事件,單個線程依然處理不了多個IO事件。
IO多路復(fù)用過程
既然NIO與BIO一樣并不能解決因阻塞可能會造成的C10K問題,那如何讓一個線程可以處理多個IO事件呢?可不可以這樣:用一個線程專門監(jiān)聽這些IO,一旦哪個IO有數(shù)據(jù)了再去接收數(shù)據(jù)。IO多路復(fù)用就是這個原理,如下圖:
我們可以看到,多了一個select()
函數(shù)調(diào)用,select()
會去監(jiān)聽指定的FD(這里注意一下,在Linux中,一切皆文件,包括socket),內(nèi)核去監(jiān)聽FD對應(yīng)的sockets。任意一個或多個socket有數(shù)據(jù)了就返回給select()
,這個時候再去調(diào)用recvfrom()
接收sockets的數(shù)據(jù),從而實現(xiàn)了單個線程處理多個I/O操作,提高系統(tǒng)的效率和性能。
在Linux下,常用的I/O多路復(fù)用方式有三種:select、poll和epoll。
-
select和poll的原理是基于輪詢,即不斷地查詢所有注冊的I/O事件,如果有事件發(fā)生就立即通知應(yīng)用程序。這種方式的效率較低,因為每次查詢都需要遍歷所有的I/O事件。
-
epoll的原理是基于事件通知,即只有當(dāng)I/O事件發(fā)生時,才會通知應(yīng)用程序。這種方式的效率更高,因為它避免了無效的查詢。
Java NIO編程
相比Java BIO編程,Java NIO編程理解起來沒有那么直觀,不過在理解多個IO模型(尤其是IO多路復(fù)用)后就相對容易理解了,Java NIO實際上就是IO多路復(fù)用。
Java NIO 核心概念
在Java NIO編程中,有幾個核心的概念(組件)需要了解:
-
通道(Channel):通道是對原始I/O操作的抽象,可以用于讀取和寫入數(shù)據(jù)。它可以與文件、套接字等進行交互。
-
緩沖區(qū)(Buffer):緩沖區(qū)是一個容器,用于存儲數(shù)據(jù)。在進行讀寫操作時,數(shù)據(jù)會先被讀取到緩沖區(qū)中,然后從緩沖區(qū)中寫入或讀取。
-
選擇器(Selector):選擇器是Java NIO提供的一種多路復(fù)用機制,可以通過一個線程管理多個通道的I/O操作。
相對于BIO,開發(fā)者不直接與Socket交互,而是通過Selector
與多個Channel
交互,同時Buffer
提供了方法來管理緩沖區(qū)的容量、位置和限制,通過設(shè)置這些屬性,可以控制數(shù)據(jù)的讀寫位置和范圍??傊甆IO在提升IO處理效率和性能的同時支持更豐富的功能,
Java NIO 示例
以下是一個簡單的Java NIO網(wǎng)絡(luò)編程示例,用于創(chuàng)建一個基于NIO的服務(wù)器和客戶端:
服務(wù)端代碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer {
private Selector selector;
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.startServer();
}
public void startServer() throws IOException {
// 創(chuàng)建Selector
selector = Selector.open();
// 創(chuàng)建ServerSocketChannel,并綁定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8888));
// 將ServerSocketChannel注冊到Selector上,并監(jiān)聽連接事件。當(dāng)接收到一個客戶端連接請求時就緒。該操作只給服務(wù)器使用。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8888");
// 循環(huán)等待事件發(fā)生
while (true) {
// 等待事件觸發(fā),阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除當(dāng)前處理的SelectionKey
keys.remove();
if (key.isAcceptable()) {
// 處理連接請求
handleAccept(key);
}
if (key.isReadable()) {
// 處理讀數(shù)據(jù)請求
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 監(jiān)聽到ServerSocketChannel連接事件,獲取到連接的客戶端
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 將clientChannel注冊到Selector上,并監(jiān)聽讀事件,當(dāng)操作系統(tǒng)讀緩沖區(qū)有數(shù)據(jù)可讀時就緒(該客戶端的)。
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客戶端斷開連接
key.cancel();
clientChannel.close();
System.out.println("Client disconnected ");
return;
}
byte[] data = new byte[bytesRead];
buffer.flip();
buffer.get(data);
String message = new String(data).trim();
System.out.println("Received message from client: " + message);
// 回復(fù)客戶端
String response = "Server response: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
客戶端代碼:文章來源:http://www.zghlxwxcb.cn/news/detail-622929.html
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
public class NIOClient {
private Selector selector;
private SocketChannel socketChannel;
public static void main(String[] args) {
NIOClient client = new NIOClient();
new Thread(() -> client.doConnect("localhost", 8888)).start();
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
if ("bye".equals(message)) {
// 如果發(fā)送的消息是"bye",則關(guān)閉連接并退出循環(huán)
client.doDisConnect();
break;
}
client.sendMsg(message);
}
}
private void doDisConnect() {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMsg(String message) {
// 發(fā)送消息到服務(wù)器
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
try {
socketChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
private void doConnect(String host, int port) {
try {
selector = Selector.open();
// 創(chuàng)建SocketChannel并連接服務(wù)器
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(host, port));
// 等待連接完成
while (!socketChannel.finishConnect()) {
// 連接未完成,可以做一些其他的事情
}
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("連接成功!");
while (true) {
// 等待事件觸發(fā),阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除當(dāng)前處理的SelectionKey
keys.remove();
if (key.isReadable()) {
// 處理讀數(shù)據(jù)請求
handleRead(key);
}
}
}
} catch (IOException e) {
System.out.println("連接失?。。?!");
e.printStackTrace();
}
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 釋放資源
key.cancel();
clientChannel.close();
return;
}
byte[] data = new byte[bytesRead];
buffer.flip();
buffer.get(data);
String message = new String(data).trim();
System.out.println("Received message from server: " + message);
}
}
總結(jié)
通過本文介紹,我們可以了解各個IO模型原理,并且對很多概念有了更清晰的認識,比如:
阻塞體現(xiàn)在:用戶進程發(fā)起系統(tǒng)調(diào)用接口后,不論有無數(shù)據(jù),是否直接響應(yīng)結(jié)果?如果直接響應(yīng)就是非阻塞,等待就是阻塞;
IO多路復(fù)用原理就是單個線程處理多個I/O操作,從而提高系統(tǒng)的效率和性能;
并且通過對IO多路復(fù)用的理解快速的入門了Java NIO 編程。文章來源地址http://www.zghlxwxcb.cn/news/detail-622929.html
到了這里,關(guān)于BIO、NIO、IO多路復(fù)用模型詳細介紹&Java NIO 網(wǎng)絡(luò)編程的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!