Java NIO使用案例和说明

server/2025/1/3 3:33:42/

Java NIO(New Input/Output)和传统的 Java Socket 编程提供了不同的方法来处理网络通信。Java NIO 引入了非阻塞 I/O 和多路复用的概念,这使得它在处理大量并发连接时比传统的阻塞式 Socket 更加高效。

传统 Java Socket 编程有几个特点:

  • 阻塞式 I/O:传统的 Socket 类是阻塞式的,意味着当你调用 accept(), read(), 或 write() 方法时,线程会一直等待直到操作完成。
  • 一对一模型:每个连接都需要一个独立的线程来处理客户端请求,这在面对大量并发连接时可能导致资源耗尽。
  • 简单易用:API 相对简单,适合初学者或小型应用。

Java NIO是 JDK1.4开始提供的一套新的 I/O API,旨在提供更高效的非阻塞 I/O 操作和更灵活的缓冲区管理。NIO 与传统的 Java I/O API 相比,提供了更好的性能和可扩展性,特别是在处理大量并发连接时。对于需要处理大量并发连接的应用程序,Java NIO 明显优于传统的 Socket 编程。由于它可以使用单个线程管理多个连接,因此减少了线程切换带来的开销。以下是关于 Java NIO 的详细介绍。

Java NIO 的主要特性

  1. 缓冲区(Buffer)
    • 概念:Buffer 是一个容器对象,用于存储基本数据类型的值。它提供了一种机制来操作字节、字符等数据。
    • 类型:常见的 Buffer 类型包括 ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, 和 DoubleBuffer
    • 方法:Buffer 提供了诸如 put(), get(), flip(), clear() 等方法来操作数据。
  2. 通道(Channel)
    • 概念:Channel 是一个能够进行 I/O 操作的对象。与传统的流不同,Channel 可以同时进行读写操作,并且可以是非阻塞的。
    • 类型:常见的 Channel 类型包括 FileChannel, SocketChannel, ServerSocketChannel, 和 DatagramChannel
    • 操作:Channel 可以通过 read()write() 方法与 Buffer 进行数据交换。
  3. 选择器(Selector)
    • 概念:Selector 允许单个线程管理多个 Channel,从而实现非阻塞 I/O。这对于处理大量并发连接非常有用。
    • 注册:Channel 需要注册到 Selector 上,并指定感兴趣的事件类型(如读、写)。
    • 选择:通过 select() 方法,Selector 会等待直到至少有一个 Channel 准备好进行 I/O 操作。

下面是一个完整的 Java NIO 客户端和服务端示例。我们将创建一个简单的回显服务器(Echo Server),它接收客户端发送的消息并将其回显给客户端。这个例子将展示如何使用 Selector 来管理多个客户端连接,以及如何通过非阻塞 I/O 进行通信。

服务端代码

package com.wuxiaolong.socket.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.nio.charset.StandardCharsets;
import java.util.Iterator;public class EchoServer {private static final int PORT = 8080; // 服务器监听的端口号private Selector selector; // 选择器用于管理多个通道/*** 构造函数:初始化选择器并设置服务器通道。*/public EchoServer() throws IOException {// 打开选择器,用于多路复用 I/O 操作selector = Selector.open();// 打开服务器通道,并绑定到指定端口ServerSocketChannel serverChannel = ServerSocketChannel.open();serverChannel.socket().bind(new InetSocketAddress(PORT));serverChannel.configureBlocking(false); // 设置为非阻塞模式// 注册选择器,监听连接事件(OP_ACCEPT)serverChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务器已启动,监听端口 " + PORT);}/*** 启动服务器并进入主循环,处理客户端连接和消息。*/public void start() throws IOException {while (true) {// 选择已就绪的键(有新连接或可读取的数据)  如果没有事件响应  这里会阻塞selector.select();// 获取已就绪的键集合,并迭代处理每个键Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();while (selectedKeys.hasNext()) {SelectionKey key = selectedKeys.next();selectedKeys.remove(); // 从集合中移除已处理的键if (!key.isValid()) { // 如果键无效,跳过continue;}try {if (key.isAcceptable()) {// 处理新的客户端连接ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();SocketChannel clientChannel = serverChannel.accept();if (clientChannel != null) {clientChannel.configureBlocking(false); // 设置为非阻塞模式clientChannel.register(selector, SelectionKey.OP_READ); // 注册读取事件System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());}} else if (key.isReadable()) {// 读取客户端消息SocketChannel clientChannel = (SocketChannel) key.channel();
//                        ByteBuffer readBuffer = ByteBuffer.allocate(256); // 堆上创建缓冲区ByteBuffer readBuffer = ByteBuffer.allocateDirect(256); // 直接内存上创建缓冲区int readBytes = clientChannel.read(readBuffer); // 从通道读取数据到缓冲区if (readBytes == -1) {// 客户端关闭连接clientChannel.close();System.out.println("客户端断开连接");} else if (readBytes > 0) {// 准备回显数据readBuffer.flip(); // 切换缓冲区为读取模式byte[] messageBytes = new byte[readBuffer.remaining()];readBuffer.get(messageBytes);String message = new String(messageBytes, StandardCharsets.UTF_8);System.out.println("收到消息: " + message);// 创建一个新的缓冲区来存储回显数据ByteBuffer writerBuffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));// 回显消息给客户端while (writerBuffer.hasRemaining()) {clientChannel.write(writerBuffer); // 将缓冲区中的数据写回客户端}writerBuffer.clear(); // 清空原始缓冲区以备下次使用}}} catch (IOException e) {// 捕获并处理 I/O 异常,确保通道关闭key.cancel();if (key.channel() != null) {try {key.channel().close();} catch (IOException ex) {ex.printStackTrace();}}}}}}/*** 主方法:创建并启动服务器。*/public static void main(String[] args) throws IOException {new EchoServer().start();}
}

客户端代码

package com.wuxiaolong.socket.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;public class EchoClient {private static final String SERVER_HOST = "localhost"; // 服务器主机地址private static final int SERVER_PORT = 8080; // 服务器监听的端口号private SocketChannel socketChannel; // 客户端通道/*** 构造函数:初始化客户端通道并连接到服务器。*/public EchoClient() throws IOException {// 打开客户端通道并尝试连接到服务器socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));socketChannel.configureBlocking(false); // 设置为非阻塞模式System.out.println("已连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);}/*** 发送消息给服务器并接收回显消息。** @param message 要发送的消息字符串*/public void sendMessage(String message) throws IOException {// 发送消息到服务器ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));socketChannel.write(writeBuffer);// 等待服务器回显writeBuffer.clear(); // 清空缓冲区// 不断读取直到没有更多数据while (true){ByteBuffer readBuffer = ByteBuffer.allocate(256);int bytesRead = socketChannel.read(readBuffer);if (bytesRead > 0) {readBuffer.flip(); // 切换缓冲区为读取模式byte[] responseBytes = new byte[readBuffer.remaining()];readBuffer.get(responseBytes);String resp = new String(responseBytes, StandardCharsets.UTF_8);readBuffer.clear(); // 清空缓冲区以备下次读取System.out.println("服务器回显: " + resp);break;}}}/*** 关闭客户端通道。*/public void close() throws IOException {socketChannel.close();System.out.println("客户端已断开连接");}/*** 主方法:创建客户端并允许用户通过命令行输入消息。*/public static void main(String[] args) {try{EchoClient client = new EchoClient();Scanner scanner = new Scanner(System.in);System.out.println("请输入要发送的消息(输入 'exit' 退出):");while (true) {System.out.print("您: ");String message = scanner.nextLine();if ("exit".equalsIgnoreCase(message)) {break;}client.sendMessage(message);}scanner.close();} catch (IOException e) {e.printStackTrace();}}
}

性能优势

  • 非阻塞 I/O:NIO 支持非阻塞模式,允许单个线程管理多个 I/O 操作,从而提高了并发性能。
  • 零拷贝技术:某些情况下,NIO 可以减少数据在用户空间和内核空间之间的拷贝次数,例如使用 sendfile() 或内存映射文件。
  • 直接缓冲区ByteBuffer.allocateDirect() 创建的直接缓冲区位于堆外内存中,减少了垃圾回收的压力。

实际应用

NIO 在需要高性能网络通信的应用场景中非常有用,比如:

  • Web 服务器:处理大量并发 HTTP 请求。
  • 数据库驱动:高效地与数据库进行交互。
  • 实时系统:要求低延迟和高吞吐量的数据传输。

总结

Java NIO 提供了一套强大而灵活的 API,使得开发者可以构建高效、可扩展的 I/O 应用程序。理解 NIO 的核心概念和机制,可以帮助在实际项目中更好地利用这些特性,提升应用程序的性能和可靠性。


http://www.ppmy.cn/server/154687.html

相关文章

ssh免密登录服务器

一、免密登录原理 免密登录原理通过RSA公开密钥算法的一种应用。RSA是公开密钥密码体制的一种使用不同的加密密钥与解密密钥&#xff0c;“由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制(非对称加密) 。在公开密钥密码体制中&#xff0c;加密密钥&#xff08;即公…

Windows配置IE浏览器不自动跳转到Edge

一&#xff1a;使用 IE 浏览器自身设置&#xff08;部分情况有效&#xff09; 打开 IE 浏览器设置&#xff1a;启动 IE 浏览器&#xff0c;点击右上角的 “工具”&#xff08;齿轮形状&#xff09;图标&#xff0c;选择 “Internet 选项”。设置启动选项&#xff1a;在 “Inte…

防火墙技术与网络安全

网络已经成为了人类所构建的最丰富多彩的虚拟世界&#xff0c;网络的迅速发展&#xff0c;给我们的工作和学习生活带来了巨大的改变。我们通过网络获得信息&#xff0c;共享资源。如今&#xff0c;Internet遍布世界任何一个角落&#xff0c;并且欢迎任何一个人加入其中&#xf…

【C++11】类型分类、引用折叠、完美转发

目录 一、类型分类 二、引用折叠 三、完美转发 一、类型分类 C11以后&#xff0c;进一步对类型进行了划分&#xff0c;右值被划分纯右值(pure value&#xff0c;简称prvalue)和将亡值 (expiring value&#xff0c;简称xvalue)。 纯右值是指那些字面值常量或求值结果相当于…

redis清除策略

redis清除策略 Redis对于过期键有三种清除策略 被动删除&#xff1a;当读/写一个已经过期的key时&#xff0c;会触发惰性删除策略&#xff0c;直接删除掉这个过期key主动删除&#xff1a;由于惰性删除策略无法保证冷数据被及时删掉&#xff0c;所以Redis会定期(默认每100ms)主…

【centos8 镜像修改】centos8 镜像修改阿里云

要将 CentOS 8 的镜像源修改为阿里云镜像&#xff0c;你需要编辑 /etc/yum.repos.d/ 目录下的 .repo 文件。以下是具体的步骤&#xff1a; 备份原始的 .repo 文件&#xff1a; 在编辑之前&#xff0c;建议备份原始的 .repo 文件&#xff0c;以便在出现问题时可以恢复。 sudo cp…

游戏引擎学习第61天

回顾并计划接下来的事情 我们现在的目标是通过创建一个占位符版本的游戏来展示我们所做的工作。这个版本的游戏包含了许多基本要素&#xff0c;目的是快速构建一些东西&#xff0c;进行测试&#xff0c;并观察代码结构的形成。这些代码的实施是为了理解系统如何工作&#xff0…

MySQL如何执行.sql 文件:详细教学指南

在使用MySQL数据库过程中&#xff0c;我们经常需要执行包含SQL语句的.sql文件。这些文件通常用于数据库的备份和恢复或批量执行SQL脚本。本文将详细介绍如何在不同环境下执行MySQL的.sql文件。 前置准备 在开始之前&#xff0c;请确保以下条件已经满足&#xff1a; 已经安装…