LG-Netty学习
学习链接
RPC架构设计 - 小黄学编程。
- 个人百度网盘和个人阿里云盘中都有(包括笔记、代码、文档)
文章目录
- 学习链接
- 1. Socket回顾与I/0模型
- 1.1 Socket网络编程回顾
- 1.1.1 Socket概述
- 1.1.2 Socket整体流程
- 1.1.3 代码实现
- test01
- ServerDemo
- ClientDemo
- test02
- ServerDemo
- ClientDemo
- test03
- ServerDemo
- ClientDemo
- test04
- ServerDemo
- ClientDemo
- test05
- ServerDemo
- ClientDemo
- test06
- ServerDemo
- ClientDemo
- 1.2 I/O模型
- 1.2.1 Java I/O模型说明
- 阻塞与非阻塞
- 同步和异步
- AI理解 :如何正确理解java io编程中的,阻塞 与 非阻塞,同步 与 异步
- 一、阻塞 vs 非阻塞(Blocking vs Non-Blocking)
- 二、同步 vs 异步(Synchronous vs Asynchronous)
- 三、组合模式与 Java 实现
- 四、对比总结
- 五、关键区别
- 六、示例代码片段
- 七、选择建议
- 1.2.2 BIO(同步并阻塞)
- 1.2.3 NIO(同步非阻塞)
- 1.2.4 AIO(异步非阻塞)
- 1.2.5 BIO、NIO、AIO 适用场景分析
- 2.NIO编程
- 2.1 NIO介绍
- 2.2 NIO和 BIO的比较
- 2.3 NIO 三大核心原理示意图
- 2.4 缓冲区(Buffer)
- 2.4.1 基本介绍
- 2.4.2 Buffer常用API介绍
- 1、Buffer 类及其子类
- 2、缓冲区对象创建
- 示例代码
- 3. 缓冲区对象添加数据
- 示例代码
- 4. 缓冲区对象读取数据
- 图解:flip()方法
- 图解:clear()方法
- 示例代码
- 2.5 通道(Channel)
- 2.5.1 基本介绍
- 2.5.2 Channel常用类介绍
- 2.5.3 ServerSocketChannel
- 示例代码
- 2.5.4 SocketChannel
- 示例代码
- AI理解:socketChannel的connect和finishConnect的理解
- 1. connect() 方法
- 2. finishConnect() 方法
- 关键流程总结
- 常见错误与注意事项
- 最佳实践
- AI理解:SocketChannel非阻塞模式读写理解
- 1. socketChannel.read(ByteBuffer) 的行为
- 2. socketChannel.write(ByteBuffer) 的行为
- 3. 示例代码模式
- 4. 需要特别注意的陷阱
- 总结
1. Socket回顾与I/0模型
1.1 Socket网络编程回顾
1.1.1 Socket概述
Socket
,套接字就是两台主机之间逻辑连接的端点
。TCP/IP协议是传输层协议,主要解决数据如何在网络中传输
,而HTTP是应用层协议,主要解决如何包装数据
。Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。
1.1.2 Socket整体流程
Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536
,但是0到1024是为特权服务保留的端口号
,可以选择任意一个当前没有被其他进程使用的端口。
客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
1.1.3 代码实现
test01
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");// 会阻塞在这里,一直到有客户端连上来,才停止阻塞,获取到与客户端交互的socket对象Socket socket = serverSocket.accept();log.info("接收到客户端连接");InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[10];// byte[] bytes = new byte[50];log.info("服务端读取客户端发送的数据 - 开始");// 1. 每次客户端使用write 方法发送数据,这里最多接收到bytes.length个字节的数据;// 1.1 如果客户端发送的数据长度小于bytes.length,那么这里只会接收到客户端发送的数据长度的字节;// 1.2 如果客户端发送的数据长度大于bytes.length,那么这里只会接收到bytes.length个字节的数据,剩下的数据需要再次调用read去读取;// 2. 如果客户端没有发送数据,这里会阻塞,直到有数据发送过来// 2.1 在阻塞读取的过程中,如果客户端调用socket.close()关闭连接,这里会读取到-1// 2.2 在阻塞读取的过程中,如果客户端直接挂了,这里的read方法将抛出异常// 3. 三种情况// 3.1 客户端发送了16字节数据, 这里读取到10字节数据,然后客户端调用socket.close(),服务端这里能正常读到剩下的6个字节,再次读取会读到-1// 3.2 客户端发送了16字节数据, 这里读取到10字节数据,然后客户端挂了,服务端再次读取会读到-1,就读不到剩下的6个字节了// 3.2 客户端发送了16字节数据, 这里读取到10字节数据,然后客户端调用socket.close(),然后客户端挂了,服务端这里能正常读到剩下的6个字节,再次读取会读到-1int len = inputStream.read(bytes);log.info("服务端读取客户端发送的数据 - 结束, {}", len);log.info("服务端读取到的数据是:{}", new String(bytes, 0, len));} catch (IOException e) {log.info("服务端抛出异常, ", e);throw new RuntimeException(e);}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {try {log.info("启动客户端");// 如果服务器开启了,则可以连接;如果服务器没开启,直接就抛异常了Socket socket = new Socket("127.0.0.1", 8080);log.info("创建好了客户端");log.info("客户端准备向服务端要写数据了");OutputStream outputStream = socket.getOutputStream();outputStream.write("123456789abcdefg".getBytes());log.info("客户端调用完了write方法");// socket.close();} catch (IOException e) {log.info("客户端抛出异常, ", e);throw new RuntimeException(e);}}}
test02
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");// 1. 这样去循环,显然不行,读到前面1个客户端的数据了,才能去接受下一个客户端的连接,// 前面的客户端如果不发数据,则无法接受下一个客户端的连接,// 前面的客户端发送了1次数据,则可能1次没有读完,又去处理下一个客户端了,// 不合理// 2. 因为需要单独的再开1个线程去处理这个客户端while (true) {log.info("服务器准备接收连接");Socket socket = serverSocket.accept();log.info("接收到客户端连接");try {InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[10];log.info("服务器准备接收客户端发送的数据");int len = inputStream.read(bytes);log.info("服务器接收客户端发送的数据, len: {}", len);if (len > 0) {log.info("服务器接收客户端发送的数据, data: {}", new String(bytes, 0, len));} else if (len == -1) {log.info("客户端主动断开连接");}} catch (IOException e) {log.info("服务端读取客户端数据发生异常, ", e);}}} catch (IOException e) {log.info("服务端异常, ", e);throw new RuntimeException(e);}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.Socket;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {try {log.info("客户端准备连接服务端");Socket socket = new Socket("127.0.0.1", 8080);log.info("客户端连接上了服务端");log.info("客户端给服务器写入数据");socket.getOutputStream().write("123456789abcdefg".getBytes());log.info("客户端给服务器写完数据");socket.close();} catch (IOException e) {log.info("");throw new RuntimeException(e);}}}
test03
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");while (true) {log.info("服务器准备接收连接");Socket socket = serverSocket.accept();log.info("接收到客户端连接");// 1、使用单独的线程去处理与这个客户端的交互// 2、但显然这样也有问题,只交互了1次就没了new Thread(()->{try {InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[10];log.info("服务器准备接收客户端发送的数据");int len = inputStream.read(bytes);log.info("服务器接收客户端发送的数据, len: {}", len);if (len > 0) {log.info("服务器接收客户端发送的数据, data: {}", new String(bytes, 0, len));} else if (len == -1) {log.info("客户端主动断开连接");}} catch (IOException e) {log.info("服务端读取客户端数据发生异常, ", e);}}).start();}} catch (IOException e) {log.info("服务端异常, ", e);throw new RuntimeException(e);}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.Socket;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {try {log.info("客户端准备连接服务端");Socket socket = new Socket("127.0.0.1", 8080);log.info("客户端连接上了服务端");log.info("客户端给服务器写入数据");socket.getOutputStream().write("123456789abcdefg".getBytes());log.info("客户端给服务器写完数据");socket.close();} catch (IOException e) {log.info("");throw new RuntimeException(e);}}}
test04
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");// 目前案例,只是客户端发送数据,服务端接收数据。// 还没有服务端发送数据,客户端也得去接收服务端的数据。// 并且,并不是客户端发1次,服务端就回1次,应当是客户端随时都能收发,服务端随时也都能收发。while (true) {log.info("服务器准备接收连接");Socket socket = serverSocket.accept();log.info("接收到客户端连接");new Thread(()->{try {InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[10];while (true) {log.info("服务器准备接收客户端发送的数据");// 如果客户端突然终止, 那么阻塞在这里读取的线程将会抛出异常, 因此客户端在终止之前最好调用一下socket.close()来友好关闭// 这样,服务端这里读取就不会抛出异常,而是返回-1。所以,对socket.close的理解是给对方1个关闭信号。int len = inputStream.read(bytes);log.info("服务器接收客户端发送的数据, len: {}", len);if (len > 0) {log.info("服务器接收客户端发送的数据, data: {}", new String(bytes, 0, len));} else if (len == -1) {// 客户端调用socket.close, 主动断开连接了,再次去读取仍然会返回-1,因此再去读也无用,所以这里跳出循环log.info("客户端主动断开连接");break;}}} catch (IOException e) {log.info("服务端读取客户端数据发生异常, ", e);}}).start();}} catch (IOException e) {log.info("服务端异常, ", e);throw new RuntimeException(e);}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {try {log.info("客户端准备连接服务端");Socket socket = new Socket("127.0.0.1", 8080);log.info("客户端连接上了服务端");Scanner sc = new Scanner(System.in);while (true) {log.info("请输入内容");String line = sc.nextLine();if (line == null || line.length() == 0) {continue;}if ("exit".equals(line)) {log.info("客户端准备断开连接");socket.close();log.info("客户端断开了连接");} else {log.info("客户端给服务器写入数据");socket.getOutputStream().write(line.getBytes());log.info("客户端给服务器写完数据");}}} catch (IOException e) {log.info("");throw new RuntimeException(e);}}}
test05
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {// 至此实现了,服务端和客户端之间能够相互交互try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");while (true) {log.info("服务器准备接收连接");Socket socket = serverSocket.accept();log.info("接收到客户端连接");new Thread(()->{try {// 服务端主动发送数据给客户端new Thread(()->{Scanner sc = new Scanner(System.in);while (true) {try {log.info("请输入内容");String line = sc.nextLine();if (line == null || line.isEmpty()) {continue;}if ("exit".equals(line)) {log.info("服务端准备断开与客户端的连接");// 如果线程正在读取socket的线程,而这里的这个线程再socket.close,那么正在读取socket的线程将不可避免的抛出IO异常socket.close();log.info("服务端断开了与客户端的连接");break;} else {log.info("服务端给客户端写入数据");socket.getOutputStream().write(line.getBytes());log.info("服务端给客户端写完数据");}} catch (IOException e) {log.info("服务端发送数据异常, ", e);throw new RuntimeException(e);}}}).start();// 处理来自客户端发送过来的数据InputStream inputStream = socket.getInputStream();byte[] bytes = new byte[10];while (true) {log.info("服务器准备接收客户端发送的数据");// 有几种情况:// 1. 客户端发送数据过来 - 返回读取到的数据// 2. 客户端socket.close()主动断开连接 - 返回-1// 3. 客户端突然挂了,还没来得及socket.close()主动断开连接 - 抛出异常// 4. 服务端自己socket.close() - 抛出异常int len = inputStream.read(bytes);log.info("服务器接收客户端发送的数据, len: {}", len);if (len > 0) {log.info("服务器接收客户端发送的数据, data: {}", new String(bytes, 0, len));} else if (len == -1) {// 客户端调用socket.close, 主动断开连接了,再次去读取仍然会返回-1,因此再去读也无用,所以这里跳出循环log.info("客户端主动断开连接");break;}}} catch (IOException e) {log.info("服务端读取客户端数据发生异常, ", e);}}).start();}} catch (IOException e) {log.info("服务端异常, ", e);throw new RuntimeException(e);}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {try {log.info("客户端准备连接服务端");Socket socket = new Socket("127.0.0.1", 8080);log.info("客户端连接上了服务端");// 处理来自服务端发送过来的数据new Thread(() -> {byte[] bytes = new byte[10];while (true) {try {InputStream inputStream = socket.getInputStream();log.info("客户端准备读取服务端发送的数据");int len = inputStream.read(bytes);log.info("客户端读取服务端发送的数据, len: {}", len);if (len > 0) {log.info("客户端读取服务端发送的数据, data: {}", new String(bytes, 0, len));} else if (len == -1) {log.info("服务端关闭了连接, 客户端准备关闭连接");socket.close();TimeUnit.SECONDS.sleep(5);break;}} catch (IOException e) {log.info("客户端读取服务端发送的数据发生异常, ", e);throw new RuntimeException(e);} catch (InterruptedException e) {throw new RuntimeException(e);}}}).start();// 发送数据给服务端Scanner sc = new Scanner(System.in);while (true) {log.info("请输入内容");String line = sc.nextLine();if (line == null || line.isEmpty()) {continue;}if ("exit".equals(line)) {log.info("客户端准备断开连接");socket.close();log.info("客户端断开了连接");} else {log.info("客户端给服务器写入数据");socket.getOutputStream().write(line.getBytes());log.info("客户端给服务器写完数据");}}} catch (IOException e) {log.info("客户端发送数据异常, ", e);throw new RuntimeException(e);}}}
test06
ServerDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;@Slf4j(topic = "【服务器】:")
public class ServerDemo {public static void main(String[] args) {ExecutorService executor = Executors.newCachedThreadPool();try {log.info("启动服务");ServerSocket serverSocket = new ServerSocket(8080);log.info("服务器启动成功");while (true) {log.info("服务器准备接收连接");Socket socket = serverSocket.accept();log.info("接收到客户端连接");executor.submit(() -> {handle(socket);});}} catch (IOException e) {throw new RuntimeException(e);}}private static void handle(Socket socket) {try {byte[] bytes = new byte[1024];InputStream inputStream = socket.getInputStream();log.info("开始读取客户端发送的数据");int len = inputStream.read(bytes);if (len > 0) {String data = new String(bytes, 0, len);log.info("读取到客户端发送的数据, 长度: {}, 数据: {}", len, data);log.info("服务端给客户端发送数据");socket.getOutputStream().write(("hello, i am server" + data).getBytes());} else if (len == -1) {log.info("客户端关闭了连接");}} catch (IOException e) {e.printStackTrace();} finally {try {socket.close();log.info("服务端关闭连接");} catch (IOException e) {log.error("服务器关闭连接发生异常");e.printStackTrace();}}}}
ClientDemo
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;@Slf4j(topic = "【客户端】:")
public class ClientDemo {public static void main(String[] args) {while (true) {try {Socket socket = new Socket("127.0.0.1", 8080);Scanner sc = new Scanner(System.in);log.info("请输入要发送的数据:");String data = sc.nextLine();OutputStream outputStream = socket.getOutputStream();outputStream.write(data.getBytes());log.info("客户端发送数据成功");log.info("客户端准备接收服务端的数据");byte[] bytes = new byte[1024];int read = socket.getInputStream().read(bytes);log.info("客户端接收服务端数据成功, data: {}", new String(bytes, 0, read));socket.close();} catch (IOException e) {log.info("客户端发生异常, e");e.printStackTrace();}}}
}
1.2 I/O模型
1.2.1 Java I/O模型说明
- I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的
性能 - Java共支持 3 种
网络编程/IO模型
BIO(同步并阻塞)
NIO(同步非阻塞)
AIO(异步非阻塞)
阻塞与非阻塞
主要指的是访问IO的线程是否会阻塞(或处于等待)
线程访问资源,该资源是否准备就绪的一种处理方式
同步和异步
主要是指的数据的请求方式
同步和异步是指访问数据的一种机制
AI理解 :如何正确理解java io编程中的,阻塞 与 非阻塞,同步 与 异步
嗯,用户问的是如何正确理解Java IO编程中的阻塞、非阻塞、同步和异步这些概念。首先,我需要回顾这些术语的定义以及它们在Java中的具体应用。可能用户在学习或使用Java IO/NIO时遇到了困惑,想弄清楚这些概念之间的区别和联系。
首先,我应该先分别解释每个术语的基本含义。阻塞和非阻塞通常指的是程序在等待IO操作完成时的行为
。阻塞模式下,线程会一直等待直到操作完成;非阻塞模式下,线程会立即返回,不管操作是否完成,这样可以继续做其他事情,之后可能需要轮询或回调来获取结果。
然后是同步和异步。同步指的是方法调用者需要主动等待或检查结果,而异步则是被调用方在完成后通知调用者
,通常通过回调、事件或者Future等方式。这两个概念有时候会和阻塞非阻塞混淆,所以需要明确区分。
接下来,我需要考虑这些概念在Java中的具体实现。例如,传统的Java IO(java.io包)是基于阻塞模式的,每个IO操作都会阻塞线程直到完成
。而Java NIO(java.nio包)引入了非阻塞模式,特别是Selector和Channel的结合,允许一个线程处理多个通道,提高效率
。此外,Java的NIO.2(在Java 7引入的java.nio.file等)支持异步IO,比如AsynchronousFileChannel,这属于异步非阻塞的方式
。
用户可能混淆这些概念,或者不清楚如何在实际应用中选择合适的模型。比如,阻塞IO简单但资源利用率低,非阻塞IO复杂但高效;同步需要主动获取结果,异步则由系统通知。可能需要用例子来说明不同场景下的使用方式。
还要注意,在Java中,同步和异步可以结合不同的阻塞模式。例如,NIO的非阻塞模式属于同步非阻塞,因为应用程序需要自己检查数据是否就绪;而异步IO(如AIO)则是异步非阻塞,应用程序发起操作后可以继续执行,完成后通过回调处理。
用户可能的疑问点包括:阻塞和非阻塞的区别是否等同于同步和异步?或者在Java中哪些类库实现了这些模式?如何在实际项目中决定使用哪种模型?
可能需要举一些代码片段,比如使用传统的InputStream.read()(阻塞)对比NIO的Selector.select()(非阻塞),以及AsynchronousFileChannel的异步读取。同时,解释各个模型的优缺点,比如阻塞IO的线程开销问题,非阻塞的复杂性,异步的高效但需要更复杂的错误处理等。
此外,应该提到常见的应用场景。例如,阻塞IO适合连接数少且简单的应用;非阻塞IO适合高并发,如聊天服务器;异步IO适合文件操作或高吞吐量的网络应用。
最后,总结对比表格可能会有帮助,让用户一目了然地看到各个概念的特点和实现方式。需要确保解释清晰,避免术语混淆,并且提供足够的上下文帮助用户理解何时使用哪种IO模型。
在 Java I/O 编程中,阻塞/非阻塞和同步/异步是两组不同维度的概念
,它们共同描述了 I/O 操作的行为模式。以下是它们的核心区别与联系:
一、阻塞 vs 非阻塞(Blocking vs Non-Blocking)
-
阻塞(Blocking)
-
定义:线程发起 I/O 操作后,必须等待操作完成才能继续执行后续代码。
-
行为:
- 线程在读写数据时会被挂起(进入阻塞状态),直到数据就绪或操作完成。
- 例如:InputStream.read() 会阻塞线程,直到有数据可读。
-
优点:编程简单,代码直观。
-
缺点:高并发场景下线程资源消耗大(每个连接需要独立线程)。
-
-
非阻塞(Non-Blocking)
- 定义:线程发起 I/O 操作后,立即返回一个状态值,无需等待操作完成。
- 行为:
- 线程可以继续执行其他任务,通过轮询或事件通知机制检查操作是否完成。
- 例如:NIO 的 SocketChannel.configureBlocking(false) 设置为非阻塞模式。
- 优点:单线程可处理多个 I/O 操作,适合高并发。
- 缺点:需主动轮询或处理事件,编程复杂度较高。
二、同步 vs 异步(Synchronous vs Asynchronous)
-
同步(Synchronous)
- 定义:调用方需要主动等待或检查 I/O 操作的结果。
- 行为:
- 无论是阻塞还是非阻塞,调用方需自行处理数据的读写过程。
- 例如:
- 同步阻塞:传统 java.io 的 InputStream.read()。
- 同步非阻塞:NIO 的 Selector 轮询就绪的 Channel。
- 关键点:I/O 操作的实际工作(如数据拷贝)由调用线程完成。
-
异步(Asynchronous)
- 定义:调用方发起 I/O 操作后立即返回,由系统在后台完成操作并通过回调通知结果。
- 行为:
- 调用线程无需关心数据何时就绪,操作系统或框架会主动通知。
- 例如:NIO.2 的 AsynchronousFileChannel.read(),通过 CompletionHandler 回调。
- 优点:彻底释放调用线程,适合高吞吐场景。
- 缺点:回调地狱问题(可通过 Future 或 CompletableFuture 缓解)。
三、组合模式与 Java 实现
-
同步阻塞(BIO)
-
实现:java.io 包(如 InputStream、OutputStream)。
-
场景:简单客户端或低并发服务端。
-
-
同步非阻塞(NIO)
-
实现:java.nio 包(Channel、Selector、Buffer)。
-
机制:通过 Selector 轮询多个 Channel 的就绪事件,单线程处理多请求。
-
场景:高并发服务端(如聊天服务器)。
-
-
异步非阻塞(AIO)
-
实现:java.nio.channels.AsynchronousChannel(NIO.2,Java 7+)。
-
机制:通过回调或 Future 获取结果,操作系统完成 I/O 后通知应用。
-
场景:文件操作或高吞吐网络服务(如大文件下载)。
-
四、对比总结
维度 | 阻塞(Blocking) | 非阻塞(Non-Blocking) |
---|---|---|
线程行为 | 挂起等待操作完成 | 立即返回,可执行其他任务 |
资源消耗 | 高(每连接一线程) | 低(单线程处理多连接) |
复杂度 | 简单 | 较高(需处理事件/轮询) |
维度 | 同步(Synchronous) | 异步(Asynchronous) |
---|---|---|
结果获取 | 调用方主动等待或轮询 | 由系统回调通知 |
I/O 执行方 | 应用线程参与数据拷贝 | 内核/框架完成数据拷贝 |
典型实现 | java.io 、java.nio | java.nio.channels.AsynchronousChannel |
五、关键区别
阻塞 vs 非阻塞:关注线程在等待 I/O 时的状态(是否被挂起)。
同步 vs 异步:关注 I/O 操作的完成通知机制(主动等待 vs 被动回调)。
六、示例代码片段
-
同步阻塞(BIO)
try (Socket socket = new Socket("host", port);InputStream in = socket.getInputStream()) {int data = in.read(); // 阻塞直到数据到达 }
-
同步非阻塞(NIO)
Selector selector = Selector.open(); SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ);while (true) {int readyChannels = selector.select(); // 阻塞直到有就绪事件Set<SelectionKey> keys = selector.selectedKeys();for (SelectionKey key : keys) {if (key.isReadable()) {// 处理读事件}} }
-
异步非阻塞(AIO)
AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get("file.txt")); ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer attachment) {// 异步读取完成后的回调} });
七、选择建议
-
BIO:适合简单场景,快速原型开发。
-
NIO:适合高并发网络服务(需处理复杂事件循环)。
-
AIO:适合文件操作或需要极致吞吐的场景(需注意平台支持,如 Linux 对 AIO 的实现限制)。
理解这些概念后,可以根据具体需求选择合适的 I/O 模型。
1.2.2 BIO(同步并阻塞)
Java BIO就是传统的 socket编程.
BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程(为什么呢?因为它是阻塞的,线程会阻塞在这里,一直等到有结果为止,所以必须要有个线程耗在这里使劲的等。),即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
工作机制
生活中的例子:
BIO问题分析
- 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
- 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
1.2.3 NIO(同步非阻塞)
同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理
生活中的例子:
1.2.4 AIO(异步非阻塞)
AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事件,这也就是操作系统异步 IO 的主要模型。
1.2.5 BIO、NIO、AIO 适用场景分析
- BIO(同步并阻塞) 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解
- NIO(同步非阻塞) 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持
- AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。
2.NIO编程
2.1 NIO介绍
Java NIO 全称java non-blocking IO ,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的.
-
NIO 有三大核心部分:
Channel(通道)
,Buffer(缓冲区)
,Selector(选择器)
-
NIO是 面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
-
Java NIO 的
非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞
,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。
通俗理解:NIO 是可以做到用一个线程来处理多个操作的
。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个
2.2 NIO和 BIO的比较
- BIO 以流的方式处理数据,而 NIO 以缓冲区的方式处理数据,缓冲区 I/O 的效率比流 I/O 高很多
- BIO 是阻塞的,NIO则是非阻塞的
- BIO 基于字节流和字符流进行操作,而
NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作
,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件
(比如:连接请求, 数据到达等),因此使用单个线程就可以监听多个客户端通道
2.3 NIO 三大核心原理示意图
一张图描述 NIO 的 Selector 、 Channel 和 Buffer 的关系
-
每个 channel 都会对应一个 Buffer
这句话感觉有误:Channel用于传输数据,而Buffer是数据的容器。也就是说,当从Channel读取数据时,数据会被读到Buffer里;同样,向Channel写入数据时,也是需要将数据先置入buffer,然后channel也是从Buffer里取出来数据然后写入的。
// 读取数据 // channel将数据读取到buffer ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // len 可能是 正数->读到的字节数;-1->读到末尾;0-本次没有读到数据 // (socketChannel切换为非阻塞模式,读取只是读取当时能读到的数据。如果是阻塞读取,就至少要读到1个字节的数据,除非读到流末尾,返回-1) int len = socketChannel.read(byteBuffer);
// 写入数据 // channel将buffer中的数据写入 // len 可能是 0-> 没有输入数据,正数->写入的字节数 // (socketChannel切换为非阻塞模式,写入只是写入当时能写入多少字节数。如果是阻塞写入,那就一定要把数据全部写完,调用才结束) int len = socketChannel.write(ByteBuffer.wrap("服务端已收到数据".getBytes()));
所以,channel是双向的,buffer是数据的容器载体
-
Selector 对应一个线程, 一个线程对应多个 channel(连接)
-
每个 channel 都注册到 Selector选择器上
-
Selector不断轮询查看Channel上的事件, 事件是通道Channel非常重要的概念
-
Selector 会根据不同的事件,完成不同的处理操作
-
Buffer 就是一个内存块 , 底层是有一个数组
-
NIO中数据的读取写入是通过 Buffer, 这个和 BIO ,
BIO 中要么是输入流,或者是输出流, 不能双向
,但是NIO 的 Buffer 是可以读也可以写 , channel 是双向的
.
2.4 缓冲区(Buffer)
2.4.1 基本介绍
缓冲区(Buffer)
:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
2.4.2 Buffer常用API介绍
1、Buffer 类及其子类
在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应
byte,short, int, long,float,double,char 7种
2、缓冲区对象创建
方法名 | 说明 |
---|---|
static ByteBuffer allocate(int capacity) | 创建byte类型的指定长度的缓冲区 |
static ByteBuffer wrap(byte[] array) | 创建一个有内容的byte类型缓冲区 |
示例代码
import java.nio.ByteBuffer;/*** 创建缓冲区*/
public class CreateBufferDemo {public static void main(String[] args) {//1.创建一个指定长度的缓冲区(容量限制), 以ByteBuffer为例ByteBuffer byteBuffer = ByteBuffer.allocate(5);for (int i = 0; i < 5; i++) {// 每读1位, 索引向后移动1位// 读取到的是1个byte字节System.out.println(byteBuffer.get());}//在此调用会报错--后续再读缓冲区时着重讲解//System.out.println(byteBuffer.get());//2.创建一个有内容的缓冲区ByteBuffer wrap = ByteBuffer.wrap("lagou".getBytes());for (int i = 0; i < 5; i++) {// 将读取到的byte转为字符System.out.println((char) wrap.get());}}
}
3. 缓冲区对象添加数据
方法名 | 说明 |
---|---|
int position()/position(intnewPosition) | 获得当前要操作的索引/修改当前要操作的索引位置 |
int limit()/limit(int newLimit) | 最多能操作到哪个索引/修改最多能操作的索引位置 |
int capacity() | 返回缓冲区的总长度 |
int remaining()/boolean hasRemaining() | 还有多少能操作索引个数/是否还有能操作 |
put(byte b)/put(byte[] src) | 添加一个字节/添加字节数组(position + 1/ posistion + n) |
put(int index, byte b) | 添加一个字节到指定的索引位置(position仍保留在原来的位置) |
图解:
示例代码
import java.nio.ByteBuffer;
import java.util.Arrays;/*** 添加缓冲区*/
public class PutBufferDemo {public static void main(String[] args) {//1.创建一个指定长度的缓冲区, 以ByteBuffer为例ByteBuffer byteBuffer = ByteBuffer.allocate(10);System.out.println(byteBuffer.position()); //0 获取当前索引所在位置(position是当前待操作的位置)System.out.println(byteBuffer.limit()); //10 最多能操作到哪个索引(该limit所指的索引位置不能被操作)System.out.println(byteBuffer.capacity()); //10 返回缓冲区总长度 (capacity为缓冲区总长度不变)System.out.println(byteBuffer.remaining());//10 还有多少个能操作 (remaining值为limit - position,即剩余可操作的字节数)//修改当前索引位置// byteBuffer.position(1);//修改最多能操作到哪个索引位置// byteBuffer.limit(9);// System.out.println(byteBuffer.position()); //1 获取当前索引所在位置// System.out.println(byteBuffer.limit()); //9 最多能操作到哪个索引// System.out.println(byteBuffer.capacity()); //10 返回缓冲区总长度// System.out.println(byteBuffer.remaining());//8 还有多少个能操作//添加一个字节byteBuffer.put((byte) 97); // (每put1个字节,position就会加1)System.out.println(byteBuffer.position()); //1 获取当前索引所在位置System.out.println(byteBuffer.limit()); //10 最多能操作到哪个索引System.out.println(byteBuffer.capacity()); //10 返回缓冲区总长度System.out.println(byteBuffer.remaining());//9 还有多少个能操作//添加一个字节数组byteBuffer.put("abc".getBytes());System.out.println(byteBuffer.position());//4 获取当前索引所在位置System.out.println(byteBuffer.limit()); //10 最多能操作到哪个索引System.out.println(byteBuffer.capacity());//10 返回缓冲区总长度System.out.println(byteBuffer.remaining());//6 还有多少个能操作//当添加超过缓冲区的长度时会报错byteBuffer.put("012345".getBytes());System.out.println(byteBuffer.position()); //10 获取当前索引所在位置(此时,position与limit等值了,将无法操作当前位置)System.out.println(byteBuffer.limit()); //10 最多能操作到哪个索引System.out.println(byteBuffer.capacity()); //10 返回缓冲区总长度System.out.println(byteBuffer.remaining());//0 还有多少个能操作System.out.println(byteBuffer.hasRemaining());// false 是否还能有操作的数组// 如果缓存区存满后, 可以调整position位置可以重复写,这样会覆盖之前存入索引的对应的值byteBuffer.position(0);byteBuffer.put("012345".getBytes());System.out.println(Arrays.toString(byteBuffer.array()));}
}
4. 缓冲区对象读取数据
方法名 | 介绍 |
---|---|
flip() | 切换读模式 limit设置position位置, position设置0 |
get() | (从当前position位置开始)读一个字节(position + 1) |
get(byte[] dst) | (从当前position位置开始)读dst.length个字节到dst数组中(如果当前缓存区从当前position开始到limit,不够dst.length个字节,则会报错)(position + dst.length) |
get(byte[] dst, int offset, int length) | (从当前position位置开始)读length个字节到dst数组中,并从offset索引开始,共length个字节(如果当前缓存区从当前position开始到limit,不够length个字节,则会报错)(position + length) |
get(int index) | 读指定索引的字节(position仍然保留在原来的位置) |
get(int index, byte[] dst) | 从byteBuffer指定索引处开始读取dst.length个字节到字节数组中(如果ByteBuffer从指定索引处到limit,不够dst.length个字节,则会报错)(position仍然保留在原来的位置) |
get(int index, byte[] dst, int offset, int length) | 从byteBuffer指定索引处开始读取length个字节到dst字节数组中,并从dst字节数组索引为offset处开始写起(如果ByteBuffer从指定索引处到limit,length个字节,则会报错)(position仍然保留在原来的位置) |
rewind() | 将position设置为0,mark置为-1,可以重复读 |
clear() | 切换写模式 position设置为0 , limit 设置为 capacity |
array() | 将缓冲区转换成字节数组返回 |
图解:flip()方法
图解:clear()方法
示例代码
import java.nio.ByteBuffer;/*** 从缓冲区中读取数据*/
public class GetBufferDemo {public static void main(String[] args) {//1.创建一个指定长度的缓冲区ByteBuffer allocate = ByteBuffer.allocate(10);allocate.put("0123".getBytes());System.out.println("position:" + allocate.position());//4System.out.println("limit:" + allocate.limit()); //10System.out.println("capacity:" + allocate.capacity());//10System.out.println("remaining:" + allocate.remaining());//6//切换读模式(一定要切换到读模式,否则,仍然从当前position位置开始操作)System.out.println("读取数据--------------");allocate.flip(); // (limit设置position位置, position设置0)System.out.println("position:" + allocate.position());//0System.out.println("limit:" + allocate.limit()); //4System.out.println("capacity:" + allocate.capacity());//10System.out.println("remaining:" + allocate.remaining());//4// 刚好是从position为0的索引开始读取,一直到allocate.limit()值(且该limit所在位置不能读取)for (int i = 0; i < allocate.limit(); i++) {System.out.println(allocate.get());}//读取完毕后.继续读取会报错。(该limit所在位置不能读取)//System.out.println(allocate.get());//读取指定索引字节System.out.println("读取指定索引字节--------------");System.out.println(allocate.get(1));System.out.println("读取多个字节--------------");// 重复读取(rewind会将position置为0,mark置为-1)allocate.rewind();byte[] bytes = new byte[4];allocate.get(bytes); // (如果byteBuffer中可读字节数不够bytes.length,则会报错)System.out.println(new String(bytes));// 将缓冲区转化字节数组返回System.out.println("将缓冲区转化字节数组返回--------------");byte[] array = allocate.array();System.out.println(new String(array));// 切换写模式,覆盖之前索引所在位置的值System.out.println("写模式--------------");allocate.clear(); // (clear()将position置为0, mark置为-1, limit设置为capacity)allocate.put("abc".getBytes());System.out.println(new String(allocate.array()));}
}
注意事项:
- capacity:容量(长度)limit: 界限(最多能读/写到哪里)posotion:位置(读/写哪个索引)
- 获取缓冲区里面数据之前,需要调用flip方法
- 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失。
2.5 通道(Channel)
2.5.1 基本介绍
通常来说NIO中的所有IO都是从 Channel(通道) 开始的。NIO 的通道类似于流,但有些区别如下:
- 通道可以读,也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)
- 通道可以异步读写
- 通道总是基于缓冲区Buffer来 读 写
2.5.2 Channel常用类介绍
- Channel接口
常用 的Channel实现类类 有 :FileChannel , DatagramChannel ,ServerSocketChannel和
SocketChannel 。FileChannel 用于文件的数据读写, DatagramChannel 用于 UDP 的数据读
写,ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写
。【ServerSocketChanne
类似 ServerSocket , SocketChannel 类似 Socket】
- SocketChannel 与ServerSocketChannel
类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作.
2.5.3 ServerSocketChannel
服务端实现步骤:
- 打开一个服务端通道
- 绑定对应的端口号
- 通道默认是阻塞的,需要设置为非阻塞
- 检查是否有客户端连接 有客户端连接会返回对应的通道
- 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
- 给客户端回写数据
- 释放资源
示例代码
/*** 服务端*/
public class NIOServer {public static void main(String[] args) throws IOException, InterruptedException {//1. 打开一个服务端通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//2. 绑定对应的端口号serverSocketChannel.bind(new InetSocketAddress(9999));//3. 通道默认是阻塞的,需要设置为非阻塞// true 为通道阻塞 false 为非阻塞serverSocketChannel.configureBlocking(false);System.out.println("服务端启动成功..........");while (true) {//4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null// (当设置了非阻塞模式时,这里的accept()方法调用不会阻塞)SocketChannel socketChannel = serverSocketChannel.accept();if (socketChannel == null) {System.out.println("没有客户端连接...我去做别的事情");Thread.sleep(2000);continue;}// (问题一:这里没有将获取的socketChannel设置为非阻塞模式)//5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//返回值:// 正数: 表示本次读到的有效字节个数.// 0: 表示本次没有读到有效字节.// -1: 表示读到了末尾// (问题二:无论时阻塞模式下,还是非阻塞模式下,这里并不一定能一次性读完,这里返回的read是当次读到的字节数;与socket的inputStream的read(byte[])相同;)// (如果是在非阻塞模式下,这里只是在读取时刻所读取的字节数。如果读取时刻没有数据,那就是返回0)int read = socketChannel.read(byteBuffer);System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read, StandardCharsets.UTF_8));//6. 给客户端回写数据// (问题三:在非阻塞模式下,这里并不一定能一次性写完,返回值是当次写入的字节数;与socket的outputStream的write(byte[])不同,它一定会阻塞到写完为止,或抛出异常;)// (如果是在非阻塞模式下,这里只是在写入时刻所写入的字节数。如果写入时刻缓冲区是满的,那就是没有写入任何字节,即返回0)socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));//7. 释放资源socketChannel.close();}}
}
2.5.4 SocketChannel
实现步骤
- 打开通道
- 设置连接IP和端口号
- 写出数据
- 读取服务器写回的数据
示例代码
public class NIOClient {public static void main(String[] args) {try {//1.打开通道SocketChannel socketChannel = SocketChannel.open();// (问题一:没有开启socketChannel的非阻塞模式)//2.设置连接IP和端口号// (在非阻塞模式下,connect方法的返回值可能是true,也可能是false。通常连接本地服务器,本地回环地址的连接可能非常快,可能立即完成,返回的是true。// 当返回false时,表示连接尚未建立,后面需要调用finishConnect()完成建立连接)// (在阻塞模式下,connect方法调用会一直阻塞到连接建立或因无法建立连接而抛出异常)socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));//3.写出数据socketChannel.write(ByteBuffer.wrap("老板, 该还钱拉 !".getBytes(StandardCharsets.UTF_8)));//4.读取服务器写回的数据ByteBuffer readBuffer = ByteBuffer.allocate(1024);int read = socketChannel.read(readBuffer);System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read, StandardCharsets.UTF_8));//5.释放资源socketChannel.close();} catch (IOException e) {throw new RuntimeException(e);}}}
AI理解:socketChannel的connect和finishConnect的理解
嗯,我现在要理解SocketChannel的connect和finishConnect方法。让我先回忆一下Java NIO的相关知识。SocketChannel是用于非阻塞Socket操作的类,属于java.nio.channels包。非阻塞模式的话,和传统的阻塞IO不同,所以在连接的时候应该有不同的处理方式。
首先,connect方法应该用于发起连接请求。如果是阻塞模式的话,这个方法会一直阻塞直到连接建立成功或者失败。但如果是非阻塞模式的话,调用connect会立即返回,可能返回false,表示连接还没建立,这时候需要通过某种方式来判断连接是否完成,这时候可能就需要用到finishConnect方法了。
那具体来说,当SocketChannel被设置为非阻塞模式时,调用connect方法会立即返回,如果返回true,说明连接已经建立好了,可以直接进行读写操作。如果返回false,说明连接还在进行中,这时候需要注册OP_CONNECT事件到Selector上,或者之后通过finishConnect方法来检查连接是否已经完成。
那finishConnect的作用应该是在非阻塞模式下,当connect返回false之后,后续需要不断调用finishConnect来判断连接是否完成。这个方法可能会抛出异常,比如连接失败的时候。如果finishConnect返回true,说明连接成功建立;如果还在进行中,可能返回false,或者抛出异常表示失败。
但是,可能需要更详细地理解这两个方法的调用流程。比如,在非阻塞模式下,调用connect之后,如果返回false,那么必须调用finishConnect来完成连接过程。在调用finishConnect时,可能会需要多次尝试,直到返回true或者抛出异常。而在这个过程中,可能还需要处理Selector的OP_CONNECT事件,当SocketChannel准备好完成连接时,Selector会通知我们,这时候可以调用finishConnect来确认连接结果。
有没有可能connect在非阻塞模式下返回true?比如本地回环地址的连接可能非常快,可能立即完成。这时候就不需要调用finishConnect了?
另外,在阻塞模式下,connect会一直阻塞直到连接完成,这时候finishConnect可能不会被使用到,或者调用时直接返回true,因为连接已经建立。
那在代码中如何处理这两种情况?比如,当使用非阻塞模式时,通常的流程是:
-
配置SocketChannel为非阻塞模式。
-
调用connect方法,传入目标地址。
-
如果connect返回true,直接进行读写。
-
如果返回false,注册OP_CONNECT到Selector,等待连接就绪。
-
当Selector触发OP_CONNECT事件时,调用finishConnect方法完成连接。
-
处理可能的异常情况,比如连接被拒绝。
比如,代码示例:
SocketChannel channel = SocketChannel.open();channel.configureBlocking(false);boolean connected = channel.connect(new InetSocketAddress("example.com", 80));if (!connected) {// 注册到Selector,关注OP_CONNECT事件selector.register(channel, SelectionKey.OP_CONNECT);
}// 在Selector的循环中处理OP_CONNECT事件时:
if (key.isConnectable()) {SocketChannel ch = (SocketChannel) key.channel();if (ch.finishConnect()) {// 连接成功,可以进行读写操作key.interestOps(SelectionKey.OP_READ);} else {// 处理连接失败的情况}
}
这时候,如果在调用finishConnect的时候,如果返回true,说明连接成功;否则可能需要继续等待,或者处理异常。不过,可能finishConnect在失败时会抛出异常,比如IOException,所以需要捕获异常。
另外,finishConnect的返回值在什么情况下返回true或false?根据Java文档,finishConnect的返回值是boolean类型。如果在调用时,连接操作已经完成,则返回true;如果还在进行中,则返回false。但是如果连接失败的话,调用finishConnect会抛出异常,比如ConnectException。
所以,正确的做法是,在调用finishConnect的时候,如果返回true,继续处理;如果返回false,可能需要继续等待;如果抛出异常,则处理异常,关闭通道等。
比如,在非阻塞模式下,调用connect之后,如果返回false,那么必须后续调用finishConnect来完成连接过程,直到它返回true或者抛出异常。而在此期间,可能需要处理其他任务,或者等待Selector的通知。
还有一个问题是,如果多次调用finishConnect会发生什么?比如在连接尚未完成时,多次调用是否安全?根据文档,finishConnect方法可以在连接过程中被多次调用,直到连接完成或失败。所以,在非阻塞模式下,可以多次调用,直到返回true或者抛出异常。
总结:
-
connect方法用于启动连接过程,非阻塞模式下可能立即返回false,此时需要通过finishConnect完成连接。
-
finishConnect方法用于检查连接是否完成,在非阻塞模式下必须被调用,直到连接成功或失败。
-
如果connect返回true,说明连接已经完成,无需调用finishConnect。
-
在阻塞模式下,connect会阻塞直到连接完成,此时finishConnect可能返回true或者已经不需要调用。
-
处理OP_CONNECT事件时,必须调用finishConnect来完成连接,并根据结果调整感兴趣的事件。
可能出现的错误点:
-
忘记在非阻塞模式下处理finishConnect,导致连接未完成就进行读写。
-
没有正确处理finishConnect抛出的异常,导致程序崩溃。
-
在非阻塞模式下,没有注册OP_CONNECT事件,导致无法得知何时可以调用finishConnect。
举个例子,假设在非阻塞模式下调用connect返回false,然后直接尝试写入数据,会导致异常,因为连接还未建立。必须等待finishConnect返回true之后才能进行读写操作。
所以,正确的流程应该是在非阻塞模式下,调用connect,根据返回值决定是否注册OP_CONNECT,并在适当的时候调用finishConnect完成连接,处理可能的异常。
在Java NIO中,SocketChannel的connect和finishConnect方法在非阻塞模式下协同工作以建立网络连接。以下是它们的核心理解及使用要点:
1. connect() 方法
-
作用:发起异步连接请求。
-
行为:
- 阻塞模式:调用后阻塞,直到连接成功或抛出异常。
- 非阻塞模式:立即返回boolean值:
- true:连接已立即建立(如本地回环地址)。
- false:连接正在进行中,需后续调用finishConnect完成。
-
示例:
SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); boolean connected = channel.connect(new InetSocketAddress("example.com", 80)); if (!connected) {// 注册Selector监听OP_CONNECT事件selector.register(channel, SelectionKey.OP_CONNECT); }
2. finishConnect() 方法
- 作用:在非阻塞模式下完成连接过程。
- 行为:
- 若连接成功,返回true,可进行读写。
- 若连接仍在进行,返回false,需继续等待。
- 若连接失败,抛出异常(如IOException或ConnectException)。
- 调用时机:
- 当connect()返回false后,需通过Selector监听OP_CONNECT事件,触发后调用此方法。
- 示例:
if (key.isConnectable()) {SocketChannel ch = (SocketChannel) key.channel();try {if (ch.finishConnect()) {// 连接成功,切换为读写操作key.interestOps(SelectionKey.OP_READ);}} catch (IOException e) {// 处理连接失败,关闭通道ch.close();} }
关键流程总结
- 非阻塞模式:
- connect() → 返回false → 注册OP_CONNECT → 触发事件后调用finishConnect()。
- 若finishConnect()返回true,连接建立;否则处理异常。
- 阻塞模式:
- connect()阻塞直至成功或失败,无需finishConnect()。
常见错误与注意事项
-
未处理finishConnect:在非阻塞模式下直接读写未完成的连接会抛出NotYetConnectedException。
-
未捕获异常:finishConnect可能抛出异常,需妥善处理以避免程序崩溃。
-
重复调用finishConnect:安全但需避免冗余操作,建议仅在OP_CONNECT事件触发后调用。
最佳实践
-
结合Selector:利用Selector监听OP_CONNECT事件,避免忙等。
-
资源清理:连接失败时及时关闭通道,释放资源。
-
模式切换:确认连接成功后,再切换关注的事件(如从OP_CONNECT到OP_READ)。
通过合理使用connect和finishConnect,可在非阻塞模式下高效管理连接流程,确保程序的健壮性和响应性。
AI理解:SocketChannel非阻塞模式读写理解
在使用socketChannel,并开启非阻塞模式时,我是这么理解read方法和write方法的,你看我理解的对否?对于read方法,调用socketChannel.read(byteBuffer),会从channel通道中读取到数据到byteBuffer中,如果此时channel中没有数据,那么立刻返回0,就是说没有读到数据。如果数据比较少,那么这些数据就会天道byteBuffer中,并返回读取到的字节数。如果数据比较多,可能byteBuffer定义的长度不够导致1次读不完,那么就需要再次读取,当时当次返回的时byteBuffer的capacity个字节。对于socket.channel.write(byteBuffer),是将byteBuffer中的数据即刻写入到channel中,如果写入缓冲区能够全部容纳byteBuffer,那么这一次就都写完了,如果缓冲区不能全部容纳,那就返回写入的字节数,如果缓冲区是满的,那么立刻返回0,表示什么都没写入。
好的,我现在需要仔细检查用户对SocketChannel非阻塞模式下read和write方法的理解是否正确。首先,用户对read的理解部分,我需要回忆Java NIO的相关知识。用户提到调用socketChannel.read(byteBuffer)时,如果通道没有数据,会立即返回0,这是正确的,因为非阻塞模式下read不会阻塞,没有数据就返回0。当有数据但不足以填满buffer时,会返回实际读取的字节数。这部分是对的。但用户提到数据很多时,可能一次读不完,返回buffer的capacity个字节。这里可能需要纠正,因为实际读取的字节数可能小于等于buffer的剩余空间,不一定每次都填满buffer。例如,可能底层的数据虽然很多,但每次read调用可能返回部分数据,取决于系统的可用数据量和缓冲区的情况。
接下来是write方法。用户认为如果写入缓冲区可以容纳整个byteBuffer,就会全部写入,否则返回已写入的字节数,如果缓冲区满则返回0。这里的问题在于,非阻塞模式下,write返回的是实际写入的字节数,可能部分写入,也可能完全无法写入(返回0)。但是写入缓冲区是否满可能需要进一步澄清,例如,如果底层TCP缓冲区已满,write会返回0,否则返回实际写入的字节数,可能部分写入。用户的理解基本正确,但需要强调write可能部分写入,而不一定只能写入全部或0。此外,用户是否正确处理了buffer的position和limit?例如,write是从buffer的position到limit的数据,写入后position会前进,但用户是否意识到需要多次调用write直到buffer没有剩余数据?
可能用户没有考虑到在非阻塞模式下,即使数据没有完全写入,也需要继续关注写事件,并在Channel准备好时继续写入。例如,处理写操作时,可能需要注册OP_WRITE事件,以便在缓冲区可写时继续写入剩余数据。
总结用户的正确点:非阻塞模式下read和write立即返回,返回值表示实际读写的字节数,包括0的情况。需要纠正的地方:read在数据足够时可能返回buffer的剩余空间,但实际可能返回比剩余空间小的数值;write返回的是实际写入的字节数,可能部分写入,需多次处理。此外,处理write时可能需要处理缓冲区满的情况,并注册OP_WRITE来继续写入。用户可能没有完全理解如何处理部分读写的情况,比如需要循环调用read/write直到返回0或缓冲区满,或者需要结合Selector来管理通道的就绪状态。
你对于非阻塞模式下 SocketChannel 的 read() 和 write() 方法的基本理解是正确的,但有一些细节需要补充和修正,尤其是在处理边界条件和实际网络行为时。以下是更准确的解释:
1. socketChannel.read(ByteBuffer) 的行为
- 通道无数据:如果此时通道中没有数据可读,方法会立即返回 0(不会阻塞)。这是非阻塞模式的核心特征。
- 有部分数据:如果有数据但不足以填满 ByteBuffer 的剩余空间(即 buffer.remaining()),则会将数据拷贝到缓冲区,并返回实际读取的字节数(可能小于 buffer 的容量)。
- 数据量很大:如果数据量远大于 buffer 的剩余空间,read() 会尽可能填充 buffer 的剩余空间,并返回实际填充的字节数(例如 buffer.remaining(),即填满剩余空间)。但不一定每次都会填满,因为底层网络栈的可用数据量和内核缓冲区状态会影响实际读取的字节数。
- 流结束(EOF):如果读到流的末尾(如连接关闭),read() 会返回 -1,此时应关闭通道。
关键点:
- 返回值是实际读取的字节数,可能为 0(无数据)、正数(部分或全部填充)或 -1(EOF)。
- 必须循环调用 read(),直到返回 0 或 -1,才能确保读取所有当前可用的数据。
2. socketChannel.write(ByteBuffer) 的行为
- 缓冲区可写:如果底层网络缓冲区有足够空间,write() 会尽可能写入 ByteBuffer 中剩余的数据(从 position 到 limit),并返回实际写入的字节数(可能小于 buffer.remaining())。
- 缓冲区满:如果网络缓冲区已满(例如对方接收慢或网络拥塞),write() 会立即返回 0(不会阻塞)。
- 部分写入:即使 buffer 中仍有数据未写入,只要网络缓冲区无法容纳剩余数据,write() 就可能返回一个小于 buffer.remaining() 的正数。
关键点:
- 返回值是实际写入的字节数,可能为 0(缓冲区满)或正数(部分或全部写入)。
- 必须循环调用 write(),直到返回 0 或 buffer 无剩余数据(buffer.remaining() == 0)。
- 可能需要注册 OP_WRITE:如果一次写入未能完全发送数据,通常需要向 Selector 注册 OP_WRITE 事件,以便在通道再次可写时继续发送剩余数据。
3. 示例代码模式
读取数据(非阻塞):
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead;
while ((bytesRead = socketChannel.read(buffer)) > 0) {buffer.flip();// 处理 buffer 中的数据...buffer.clear();
}
if (bytesRead == -1) {// 连接关闭socketChannel.close();
}
写入数据(非阻塞):
ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes());
while (buffer.hasRemaining()) {int bytesWritten = socketChannel.write(buffer);if (bytesWritten == 0) {// 网络缓冲区已满,注册 OP_WRITE 等待后续写入key.interestOps(SelectionKey.OP_WRITE);break;}
}
4. 需要特别注意的陷阱
-
ByteBuffer 状态管理:
- 读写操作会改变 buffer 的 position 和 limit。在重复使用 buffer 前,务必调用 buffer.clear()(重置为写模式)或 buffer.compact()(保留未处理数据并切换为写模式)。
-
处理返回值:
- 永远不要假设 read()/write() 会一次性处理完所有数据。必须检查返回值并循环调用。
-
OP_WRITE 事件的处理:
- OP_WRITE 几乎总是就绪的(除非网络缓冲区满),因此通常只在需要继续写入剩余数据时才注册它,并在写入完成后取消注册,避免 CPU 空转。
总结
你的理解基本正确,但需要更强调以下几点:
- 非阻塞 I/O 必须配合循环和返回值检查,不能假设单次调用就能完成所有操作。
- 实际读写字节数可能小于预期,甚至为 0,但这不一定是错误(如 write() 返回 0 表示网络拥塞)。
- 结合 Selector 和事件驱动模型,才能高效处理非阻塞 I/O,避免忙等待。
这些细节是编写健壮高性能网络代码的关键!