Java道经第1卷 - 第9阶 - 网络编程(二)
传送门:JB1-9-网络编程(一)
传送门:JB1-9-网络编程(二)
文章目录
S03. 同步非阻塞流
E01. 基础概念
1. 内核态与用户态
心法:操作系统可以执行所有的 CPU 指令,包括很多危险操作,所以为了程序的健壮性,CPU 指令一般被分为内核态和用户态两大类。
权限区别:电脑就像一个有门禁的大楼,内核态是「管理员专属区域」,能操作所有设备;用户态是「普通用户区域」,程序在这里运行,想动用硬件必须得找管理员申请:
- 内核态(管理员):能直接控制硬件(如磁盘、网卡),执行危险操作(如修改内存地址),就像操作系统的「核心部门」,负责管理整个大楼的基础设施。
- 用户态(普通用户):只能在「安全区域」运行程序(如微信、浏览器),不能直接碰硬件,就像大楼里的「普通办公室」,员工只能用电脑办公,不能随意动配电箱。
对比区别:内核态和用户态其他区别对比如下表:
维度 | 内核态(Kernel Mode) | 用户态(User Mode) |
---|---|---|
权限级别 | 最高权限(可执行所有 CPU 指令,包括危险操作) | 受限权限(只能执行非特权指令) |
内存空间 | 访问内核空间(0~3GB,Linux 为例) | 访问用户空间(3~4GB,Linux 为例) |
典型操作 | 内存管理、进程调度、硬件驱动(如磁盘 / 网卡控制) | 运行应用程序(如浏览器、QQ)、文件读写(需系统调用) |
风险等级 | 直接操作硬件,错误可能导致系统崩溃 | 受保护的沙盒环境,错误通常仅影响当前程序 |
2. IO流底层流程
心法:IO 流的底层流程就是先将磁盘上的数据读取到内核空间,然后再复制到用户空间的程序中进行操作。
数据读取流程如下:
- 管理员从磁盘把数据搬到「内核仓库」(内核内存)。
- 管理员把数据从「内核仓库」复制到「用户仓库」(程序内存)。
- 程序使用数据。
为何需要两次操作:为了防止程序乱改硬件数据(比如误删系统文件),虽然额外多出了依次操作,但整体过程会更加安全。
3. 常见IO流模型
心法:常见的 IO 流模型分为 BIO,MIO,AIO 和 NIO 四种。
(BIO)Block IO:同步阻塞:
- 流程:当用户发起一个 read 请求后必须等到出了结果才可以去做别的事情。
- 示例:比如赵四用的是最老式的鱼竿,得一直守着,等到鱼上钩了再拉杆。
(MIO)Multiplexing IO:也称 Event Driven IO,是多路复用型的 IO 流:
- 流程:用户先发送 select 请求,阻塞住整个进程,同时内核监视起对应的全部的socket,当任何一个 socket 中的数据准备好了,select 就返回,然后用户再发送 read 请求,将硬盘数据从内核拷贝到用户进程。
- 示例:比如广坤的鱼竿和赵四的鱼竿一样也拥有显示屏,广坤同时放好几根鱼竿,然后守在旁边,一旦显示有鱼上钩,就将对应的鱼竿拉起来。
(AIO)Asynchronous IO:异步非阻塞:
- 流程:当用户发起一个 read 请求后,立刻返回目标硬盘数据是否已经加载到内核空间,此时用户可以立刻去做其它的事,然后内核去异步执行后续的操作,一切完成后,内核会给用户进程发送一个 signal 信号,告诉它 read 操作完成了。
- 示例:比如大拿直接雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给大拿发个短信。
(NIO)Non-Block IO:同步非阻塞,是从 JDK1.4 版本开始引入的,自带缓冲区,可以进行更加高效的文件读写操作:
- 流程:当用户发起一个 read 请求后会立刻返回目标硬盘数据是否已经加载到内核空间:
- 若已经加载了,则开始从内核空间读取数据到用户空间,此过程阻塞用户进程。
- 若还未加载到,则间隔轮询,即每隔一段时间就重新询问一遍。
- 示例:比如刘能的鱼竿有个显示屏,能显示是否有鱼上钩,于是他和旁边 MM 聊天,每隔一会就检查一次,若有鱼上钩,则迅速拉杆。
E02. NIO缓冲Buffer
心法:Buffer 是 NIO 的核心之一,可以保存多个相同类型的数据,且支持双向操作。
八大基本数据类型除布尔类型外,均有对应的 Buffer 子类,如 IntBuffer/CharBuffer
等。
1. 缓冲区类型
NIO 中的 Buffer 分为直接缓冲区和堆缓冲区两种,具体区别如下:
直接缓冲区 DirectBuffer
:
- 内存消耗:位于本地内存,与 JVM 无关,创建和销毁的开销比较大,不容易被控制。
- 传输效率:本地内存和内核空间都在同一 OS 中,可以直接和本机 OS 的内核空间传输数据,所以传输效率高。
- 内存回收:需要自己想办法解决。
堆缓冲区(非直接缓冲区) HeapBuffer
:
- 内存消耗:位于 JVM 的堆内存中,创建和销毁的开销比较小,容易控制。
- 传输效率:JVM 属于另一块虚拟的 OS,想要和本机 OS 的内核空间传输数据,需要先开辟一块本地内存作为中间缓冲区,所以传输效率低。
- 内存回收:由 JVM 的 GC 负责管理。
武技:测试 Buffer 的堆缓冲区
- 开发测试方法:
package nio;
/** @author 周航宇 */
public class BufferTest {
@Test
public void testBuild() {
// 创建堆缓冲区: 初始容量1024字节
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
System.out.println(heapBuffer.isDirect() ? "直接缓冲区" : "堆缓冲区");
// 创建直接缓冲区: 初始容量1024字节
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
System.out.println(directBuffer.isDirect() ? "直接缓冲区" : "堆缓冲区");
}
}
2. 缓冲区属性
心法:缓冲区四个核心属性值的大小关系为
0 <= mark <= position <= limit <= capacity
。
初始容量 capacity
:指的是你必须存放 N 个字节的数据,初始会填满 N 个字节的空格,无默认值:
- 设置初始容量:必须在构造器中指定,且指定后不能被改变。
- 获取初始容量:可通过
buffer.capacity()
方法获取。
操作上限 limit
:指的是你只能操作 N 个字节的数据,默认值等于初始容量:
- 设置操作上限:可通过
buffer.limit(int pos)
方法设置。 - 获取操作上限:可通过
buffer.limit()
方法获取。
当前位置 position
:指的是你已经操作到了指定的位置,默认值为 0:
- 设置当前位置:可通过
buffer.position(int pos)
方法设置。 - 获取当前位置:可通过
buffer.position()
方法获取。
备份位置 mark
:指的是你在这个位置做了备份标记,默认值为 0,该属性标记为 -1 时表示失效:
- 备份当前位置:可通过
buffer.mark()
方法将当前位置备份给 mark 属性。 - 恢复当前位置:可通过
buffer.position()
方法移动当前位置到 mark 位置。
武技:测试缓冲区四个属性
package nio.buffer;
/** @author 周航宇 */
public class BufferTest {
@Test
public void testBufferField() {
// 创建堆缓冲区: 初始容量1024字节
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 设置当前位置0,操作上限1024字节
byteBuffer.position(0).limit(1024);
System.out.println("初始容量: " + byteBuffer.capacity());
System.out.println("当前位置: " + byteBuffer.position());
System.out.println("操作上限: " + byteBuffer.limit());
// 向缓冲区写入数据 "Hello"
byteBuffer.put("Hello".getBytes());
// 备份了当前位置: mark = position
byteBuffer.mark();
System.out.println("备份: " + byteBuffer.position() + "号位置");
// 向缓冲区写入数据 "World"
byteBuffer.put("World".getBytes());
// 恢复到备份的位置: position = mark
byteBuffer.reset();
System.out.println("跳回: " + byteBuffer.position() + "号位置");
// 向缓冲区写入数据 "Java"
byteBuffer.put("Java".getBytes());
// 将缓冲区中的数据转为字节数组后打印,该操作不移动position
System.out.println("查看: " + new String(byteBuffer.array()));
// 读写翻转: limit = position, position = 0, mark = -1
byteBuffer.flip();
// 在指定位置,读取缓冲区中的元素,该操作不会改变 position
System.out.print("读取: ");
System.out.print((char) byteBuffer.get(5));
System.out.print((char) byteBuffer.get(6));
// 从起始位置,读取缓冲区中的元素,该操作会自增 position
System.out.print((char) byteBuffer.get());
System.out.print((char) byteBuffer.get() + "\n");
// 倒带重读: position = 0, mark = -1
byteBuffer.rewind();
System.out.print("倒带: ");
System.out.print((char) byteBuffer.get());
System.out.print((char) byteBuffer.get() + "\n");
// 重置归零: position = 0, limit = capacity, mark = -1
byteBuffer.clear();
System.out.print("重置: ");
// 遍历堆缓冲区中的全部数据
for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
char data = (char) byteBuffer.get();
// 空格不输出
if (data != '\u0000') {
System.out.print(data);
}
}
System.out.println();
}
}
E03. NIO通道Channel
心法:BIO 两端分别是 App 和硬盘数据,通过
j.io
包类进行通信,而 NIO 两端分别是 App 中的 Buffer 和硬盘数据,通过j.nio.channels
包类进行通信。
NIO 中的 Channel 分为 FileChannel 和 SocketChannel 两种。
1. FileChannel
心法:FileChannel 是 Channel 的文件 NIO 实现类,负责文件读写,对应 FileChannel 类。
武技:测试 FileChannel 类
- 在本地开发一个 D:\xxx\HelloWorld.java 文件,内容随意。
- 开发测试方法:
package nio.channel.file;
/** @author 周航宇 */
public class FileChannelTest {
/* 源文件路径 */
private static final String SRC_PATH = "D:\\idea\\workspace\\..\\nio\\HelloWorld.java";
/* 目标文件路径 */
private static final String DEST_PATH = "D:\\idea\\workspace\\..\\nio\\HiWorld.java";
/** 复制 `HelloWorld.java` 中的内容到 `HiWorld.java` */
@SneakyThrows
@Test
public void testFileChannel() {
// 文件输入流,文件输出流,文件输入流通道,文件输出流通道
FileInputStream fis = new FileInputStream(SRC_PATH);
FileOutputStream fos = new FileOutputStream(DEST_PATH);
FileChannel fisChannel = fis.getChannel();
FileChannel fosChannel = fos.getChannel();
// 创建堆缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 缓冲区读取文件输入流中的数据
while (fisChannel.read(byteBuffer) != -1) {
// 读写翻转: 将缓冲区切换为写模式
byteBuffer.flip();
// 将缓冲区中的数据写入文件输出流通道
fosChannel.write(byteBuffer);
// 重置归零: position = 0, limit = capacity, mark = -1
byteBuffer.clear();
}
// 关流
fosChannel.close();
fisChannel.close();
fis.close();
fos.close();
System.out.println("拷贝成功");
}
}
2. SocketChannel
心法:SocketChannel 是 Channel 的 Socket 实现类,负责连接 TCP 客户端的通道,对应 ServerSocketChannel 类。
武技: 测试 ServerSocketChannel 类
- 开发Socket服务端类
BlockSocketServer
:
package nio.channel.socket;
/** @author 周航宇 */
public class BlockSocketServer {
/** 端口号 */
private static final int PORT = 9999;
@SneakyThrows
public static void main(String[] args) {
// 打开一个Socket服务端通道,并绑定Socket地址
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
System.out.println("服务端准备接受数据...");
// Socket服务端通道等待接收数据,该方法会阻塞当前线程
SocketChannel socketChannel = serverSocketChannel.accept();
// 创建堆缓冲区: 初始容量1024字节
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 创建UTF8编码实例
CharsetDecoder charsetDecoder = StandardCharsets.UTF_8.newDecoder();
// 缓冲区读取Socket服务端通道中的数据
while (socketChannel.read(byteBuffer) != -1) {
// 读写翻转: 将缓冲区切换为写模式
byteBuffer.flip();
// 解码缓冲区中的数据
CharBuffer charBuffer = charsetDecoder.decode(byteBuffer);
// 读取堆缓冲区中的全部数据
for (int i = 0, j = charBuffer.limit(); i < j; i++) {
System.out.print(charBuffer.get());
}
System.out.println();
// 重置归零: position = 0, limit = capacity, mark=-1
byteBuffer.clear();
}
// 关闭通道
socketChannel.close();
serverSocketChannel.close();
}
}
- 开发Socket客户端类
BlockSocketClient
:
package nio.channel.socket;
/** @author 周航宇 */
public class BlockSocketClient {
/** IP地址 */
private final static String IP = "localhost";
/** 端口号 */
private final static int PORT = 9999;
@SneakyThrows
public static void main(String[] args) {
// 打开一个客户端通道并连接到指定Socket地址
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(IP, PORT));
// 创建堆缓冲区
CharBuffer charBuffer = CharBuffer.allocate(1024);
System.out.println("输入消息...");
// 从控制台接收数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 创建UTF8编码实例
CharsetEncoder charsetEncoder = StandardCharsets.UTF_8.newEncoder();
// 遍历从控制台接收到的数据
String str;
while ((str = br.readLine()) != null) {
// 缓冲区读取从控制台接收到的数据
charBuffer.put("=> " + str);
// 读写翻转: 将缓冲区切换为写模式
charBuffer.flip();
// 将缓冲区中的数据编码后,写入Socket客户端通道
socketChannel.write(charsetEncoder.encode(charBuffer));
// 重置归零: position = 0, limit = capacity, mark=-1
charBuffer.clear();
}
// 关流
br.close();
socketChannel.close();
}
}
E04. NIO选择器Selector
心法:Selector 可以监控多个 Channel状态并作出对应处理,是设计NIO模型的最佳选择。
Channel对应状态 | 状态描述 |
---|---|
CONNECT | Channel 已经准备好完成连接序列了,此时IP,端口等均已搭建完成 |
ACCEPT | Channel 可以调用 accept() 了 |
READ | Channel 可以调用 read() 了 |
WRITE | Channel 可以调用 write() 了 |
武技:测试缓存通道选择器
- 开发 Socket 服务端类:
package nio.selector;
/** @author 周航宇 */
public class NonBlockSocketServer {
/** 端口号 */
private static final int PORT = 9002;
@SneakyThrows
public static void main(String[] args) {
// 打开一个Socket服务端通道,绑定Socket地址,配置非阻塞
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
System.out.println("服务端准备接受数据...");
// 开启一个Selector选择器实例
Selector selector = Selector.open();
// 将服务端通道注册到指定的Selector,并配置关注的状态为 `ACCEPT`
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
SocketChannel socketChannel = null;
// 返回Selector中已经处于就绪状态,即已准备好进行IO操作的通道个数
while (selector.select() > 0) {
// 返回Selector中就绪的Channel集合
Iterator<SelectionKey> channels = selector.selectedKeys().iterator();
// 遍历Selector中就绪的Channel集合
while (channels.hasNext()) {
// 获Channel标识
SelectionKey selectionKey = channels.next();
// 通道处于 `ACCEPT` 状态: 等待接收数据
if (selectionKey.isAcceptable()) {
System.out.println("通道处于 `ACCEPT` 状态");
// 准备接收数据,配置Channel非阻塞
socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 将服务端通道注册到指定的Selector,并配置关注的状态为 `READ`
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 通道处于 `READ` 状态: 读取数据
else if (selectionKey.isReadable()) {
System.out.println("通道处于 `READ` 状态");
// 获取当前状态对应的Channel
socketChannel = (SocketChannel) selectionKey.channel();
// 创建堆缓冲区: 初始容量1024字节
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 缓冲区读取Socket服务端通道中的数据
while (socketChannel.read(byteBuffer) > 0) {
// 读写翻转: 将缓冲区切换为写模式
byteBuffer.flip();
// 读取堆缓冲区中的全部数据
for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
System.out.print((char) byteBuffer.get());
}
System.out.println();
// 重置归零: position = 0, limit = capacity, mark = -1
byteBuffer.clear();
}
}
// 删除通道
channels.remove();
}
}
// 关流
if (socketChannel != null) {
socketChannel.close();
}
serverSocketChannel.close();
}
}
- 开发 Socket 客户端类:
package nio.selector;
/** @author 周航宇 */
public class NonBlockSocketClient {
/** IP地址 */
private static final String IP = "localhost";
/** 端口号 */
private static final int PORT = 9002;
@SneakyThrows
public static void main(String[] args) {
// 打开一个客户端通道并连接到指定Socket地址,并设置非阻塞
SocketAddress socketAddress = new InetSocketAddress(IP, PORT);
SocketChannel socketChannel = SocketChannel.open(socketAddress);
socketChannel.configureBlocking(false);
// 创建堆缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("输入消息...");
// 从控制台接收数据
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 遍历从控制台接收到的数据
String str;
while ((str = br.readLine()) != null) {
// 缓冲区读取从控制台接收到的数据
byteBuffer.put(("=> " + str).getBytes());
// 读写翻转: 将缓冲区切换为写模式
byteBuffer.flip();
// 将缓冲区中的数据编码后,写入Socket客户端通道
socketChannel.write(byteBuffer);
// 重置归零: position = 0, limit = capacity, mark=-1
byteBuffer.clear();
}
// 关流
br.close();
socketChannel.close();
}
}
Java道经第1卷 - 第9阶 - 网络编程(二)
传送门:JB1-9-网络编程(一)
传送门:JB1-9-网络编程(二)