JB1-9-网络编程(二)

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 流的底层流程就是先将磁盘上的数据读取到内核空间,然后再复制到用户空间的程序中进行操作。

数据读取流程如下

  1. 管理员从磁盘把数据搬到「内核仓库」(内核内存)。
  2. 管理员把数据从「内核仓库」复制到「用户仓库」(程序内存)。
  3. 程序使用数据。

为何需要两次操作:为了防止程序乱改硬件数据(比如误删系统文件),虽然额外多出了依次操作,但整体过程会更加安全。

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 的堆缓冲区

  1. 开发测试方法:
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 类

  1. 在本地开发一个 D:\xxx\HelloWorld.java 文件,内容随意。
  2. 开发测试方法:
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 类

  1. 开发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();
    }
}
  1. 开发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对应状态状态描述
CONNECTChannel 已经准备好完成连接序列了,此时IP,端口等均已搭建完成
ACCEPTChannel 可以调用 accept()
READChannel 可以调用 read()
WRITEChannel 可以调用 write()

武技:测试缓存通道选择器

  1. 开发 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();
    }
}
  1. 开发 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-网络编程(二)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值