序文
ネットワークプログラミングの基礎知識は上で紹介しましたが、BIOのネットワークプログラミングはJavaをベースに書かれています。BIO モデルには、C10K 問題などの大きな問題があることがわかっています。その本質はブロックの理由によるものです。そのため、より多くのリクエストを処理したい場合は、十分なスレッドが必要ですが、十分なスレッドがあるとメモリが発生します。使用上の問題 、CPU コンテキストの切り替えによって引き起こされるパフォーマンスの問題。これによりサーバーがクラッシュします。この問題を解決するにはどうすればよいでしょうか? 最適化のため、後で NIO、AIO、IO 多重化が行われます。この記事では、これらのモデルについて詳しく説明し、Java に基づいて NIO を作成します。
基本的な考え方
I/O ブロックはどこでどのようにブロックされますか? まずいくつかの基本概念を理解します
- ユーザー空間: ユーザー プロセスのコード、データ、スタックを保存するためにユーザー プロセスに割り当てられた仮想アドレス空間。
- カーネル空間: オペレーティング システムの基礎であり、コンピュータのハードウェア リソースの管理とシステム コール インターフェイスの提供を担当し、またユーザー空間とハードウェア間の橋渡しも行います。
オペレーティング システムのセキュリティと安定性を確保するために、ユーザー プロセスとオペレーティング システム カーネルは分離されており、ユーザー プロセスはカーネル空間に直接アクセスできず、システム コールなどを通じてカーネルへのリクエストを開始する必要があります。そしてカーネルはユーザープロセスに代わって操作を実行します。
つまり、アプリケーションは、ネットワーク カードやディスクなどのハードウェア デバイスにデータを読み書きするときに、カーネルを経由する必要があります。以下では、BIO、NIO、および IO 多重化モデルを 1 つずつ紹介し、各モデルの IO プロセスについて詳しく説明します。
BIOプロセス
まず、IO ブロッキングと呼ばれるものは、ユーザー プロセス、つまりユーザー空間のプログラムがハードウェア デバイスから読み取っているプロセスであることを明確にしておきます。ユーザーは常に待機する必要があります。これはブロッキング IO と呼ばれます。プロセスは次のとおりです。
プロセスがカーネルへの呼び出しを開始した後、データが返されるまでプロセス全体がブロックされていることがわかりますが、Java BIO プログラミングと組み合わせると、つまり、このプロセスがブロックされ、いくつかの問題が発生しますinputStream.read()
。
- ブロックすると現在のスレッドが占有されて他の操作が実行できなくなるため、新しいリクエストがあった場合には新しいスレッドのみ作成できます。Linux システムでは、各スレッドのデフォルトのスタック サイズは 8MB であり、他の要素を考慮しない場合、8G サーバーは最大 1000 個のリクエストを処理できます。
- リクエスト量が増加するとスレッド数も増加するため、大量のスレッドがブロックされて起動すると、CPUによる頻繁なコンテキストスイッチングが発生し、パフォーマンスの低下につながります。
この問題は C10K 問題の本質です。非常に直感的に思えますが、複数の IO を処理するために使用するスレッドの数を減らすことで解決できますか? 引き続き NIO プロセスを確認します。
NIOプロセス
NIO については、ノンブロッキングについて説明しています。BIO の説明を通じて、NIO のノンブロッキングは、次の図に示すように、データの有無に関係なく、ユーザー プロセスに直接応答することに反映されています。
recvfrom()
ユーザー プロセスが関数を呼び出した後は直接応答しますが、データを取得する前にポーリングと呼び出しを続けていることがわかります。ブロッキングによる CPU コンテキストの切り替えはありませんが、CPU は常にアイドル状態にあり、完全に活用できません。CPUの役割。BIO と同様、単一スレッドの場合、IO イベントは順次にのみ処理でき、単一スレッドは依然として複数の IO イベントを処理できません。
IO多重化処理
BIO と同様に、NIO はブロックによって引き起こされる可能性のある C10K 問題を解決できないため、1 つのスレッドで複数の IO イベントを処理するにはどうすればよいでしょうか? これは次のようになりますか。スレッドを使用してこれらの IO を監視し、いずれかの IO にデータが入ったらデータを受信します。次の図に示すように、IO 多重化はこの原理です。
select()
もう 1 つ関数呼び出しがあり、select()
指定された FD (Linux ではソケットを含むすべてがファイルであることに注意してください) を監視し、カーネルは FD に対応するソケットを監視することがわかります。1 つ以上のソケットにデータがある場合は、それらが返されます。このとき、受信側ソケットのデータがselect()
呼び出されるため、単一のスレッドで複数の I/O 操作を処理でき、システムの効率とパフォーマンスが向上します。recvfrom()
Linux では、select、poll、epoll という 3 つの一般的に使用される I/O 多重化方法があります。
-
選択とポーリングの原理はポーリングに基づいています。つまり、登録されているすべての I/O イベントを継続的にクエリし、イベントが発生するとアプリケーションに即座に通知します。この方法は、各クエリがすべての I/O イベントを走査する必要があるため、非効率的です。
-
epoll の原理はイベント通知に基づいています。つまり、I/O イベントが発生した場合にのみアプリケーションに通知されます。このアプローチは無効なクエリを回避するため、より効率的です。
Java NIO プログラミング
Java BIO プログラミングと比較すると、Java NIO プログラミングはそれほど直観的に理解できるわけではありませんが、複数の IO モデル (特に IO 多重化) を理解すると、比較的容易に理解できます。Java NIO は実際には IO 多重化です。
Java NIO のコア概念
Java NIO プログラミングには、理解する必要のあるいくつかの中心的な概念 (コンポーネント) があります。
-
チャネル (チャネル): チャネルは生の I/O 操作を抽象化したもので、データの読み取りと書き込みに使用できます。ファイルやソケットなどと対話できます。
-
バッファ (Buffer): バッファはデータを保存するためのコンテナです。読み取りおよび書き込み操作を実行するときは、まずデータがバッファに読み取られ、次にバッファに書き込まれるか、バッファから読み取られます。
-
セレクター: セレクターは、Java NIO によって提供される多重化メカニズムであり、1 つのスレッドを通じて複数のチャネルの I/O 操作を管理できます。
BIO と比較すると、開発者は Socket と直接やり取りするのではなく、Selector
複数のChannel
ソケットBuffer
とやり取りすることでバッファーの容量、場所、制限を管理するメソッドを提供し、これらの属性を設定することで、データの読み取りおよび書き込みの場所と範囲を制御できます。 。つまり、NIO は、IO 処理の効率とパフォーマンスを向上させながら、より豊富な機能をサポートします。
Java NIO の例
以下は、NIO ベースのサーバーとクライアントを作成するための簡単な Java NIO ネットワーク プログラミングの例です。
サーバーコード:
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 {
// 创建Selector
selector = Selector.open();
// 创建ServerSocketChannel,并绑定端口
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(8888));
// 将ServerSocketChannel注册到Selector上,并监听连接事件。当接收到一个客户端连接请求时就绪。该操作只给服务器使用。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 8888");
// 循环等待事件发生
while (true) {
// 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除当前处理的SelectionKey
keys.remove();
if (key.isAcceptable()) {
// 处理连接请求
handleAccept(key);
}
if (key.isReadable()) {
// 处理读数据请求
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 监听到ServerSocketChannel连接事件,获取到连接的客户端
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 将clientChannel注册到Selector上,并监听读事件,当操作系统读缓冲区有数据可读时就绪(该客户端的)。
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);
// 回复客户端
String response = "Server response: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
クライアントコード:
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)) {
// 如果发送的消息是"bye",则关闭连接并退出循环
client.doDisConnect();
break;
}
client.sendMsg(message);
}
}
private void doDisConnect() {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendMsg(String message) {
// 发送消息到服务器
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();
// 创建SocketChannel并连接服务器
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) {
// 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
selector.select();
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
// 移除当前处理的SelectionKey
keys.remove();
if (key.isReadable()) {
// 处理读数据请求
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);
}
}
要約する
この記事の導入により、各 IO モデルの原則を理解することができ、次のような多くの概念をより明確に理解できます。 ブロッキングは、ユーザー プロセスがシステム
コール インターフェイスを開始した後、データの有無に関係なく反映されます。 、結果に直接反応するかどうか?直接応答がノンブロッキングの場合、待機はブロッキングです。IO
多重化の原理は、単一のスレッドが複数の I/O 操作を処理することで、システムの効率とパフォーマンスが向上します。IO
多重化を理解することで、 Java NIO プログラミングの概要。