NIO基础

NIO 非阻塞IO

三大组件之Channel & Buffer

channel类似于stream,是读写数据的双向通道,可以从channel将数据读入buffer,也可以把buffer的数据写入channel,而之前的stream要么是输入,要么是输出,channel比stream更为底层

常见Channel

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

常见buffer

buffer用来缓冲读写数据

  • ByteBuffer

    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer

  • IntBuffer

  • LongBuffer

  • FloatBuffer

  • DoubleBuffer

  • CharBuffer

读取文件数据

package org.example.bytebuffer;

import lombok.extern.slf4j.Slf4j;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

@Slf4j
public class TestByteBuffer {
    public static void main(String[] args) {

        // FileChannel
        // 1.输入流输出流 2.RandomAccessFile
        try (FileChannel fileChannel = new FileInputStream("data.txt").getChannel()) {
            // 创建ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(10);
            while (true) {
                // 读取到buffer里
                int len = fileChannel.read(byteBuffer);
                log.debug("读取到的字节数: {}", len);
                if (len == -1) { // 没有内容了
                    break;
                }
                byteBuffer.flip(); // 切换读模式
                // 获取byteBuffer
                while (byteBuffer.hasRemaining()) { //是否还有剩余未读数据
                    char ch = (char) byteBuffer.get();
                    log.debug("读取到的字节: {}", ch);
                }
                // 读完一次以后,buffer切换为写模式
                byteBuffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ByteBuffer内部结构

有下面重要属性:

  • capacity
  • position
  • limit

ByteBufferUtil

package org.example.util;

import java.nio.ByteBuffer;

public class ByteBufferUtil {

    public static void debug(ByteBuffer buffer) {
        int pos = buffer.position();
        int limit = buffer.limit();
        int cap = buffer.capacity();

        // 定义列宽
        int offsetWidth = 8;
        int hexWidth = 49; // 16字节 * 2 + 15个空格 + 1个中间分隔符 = 49
        int charWidth = 16;

        // 计算表格总宽度
        int totalTableWidth = offsetWidth + hexWidth + charWidth + 8; // 8是边框和分隔符的宽度

        // 居中标题
        String titleText = " ByteBuffer Debug View ";
        printCenteredBorder("┌", "─", "┐", titleText, totalTableWidth);

        // 显示缓冲区信息
        String infoText = String.format("Position: %d  │  Limit: %d  │  Capacity: %d", pos, limit, cap);
        printCenteredLine(infoText, totalTableWidth);

        // 显示标记说明
        String legendText = "Arrows: ↑P = Position  ↑L = Limit-1  ↑* = Both";
        printCenteredLine(legendText, totalTableWidth);

        // 列分隔符
        printSeparator("├", "┼", "┤", offsetWidth, hexWidth, charWidth);

        // 列标题
        String offsetHeader = centerText("Offset", offsetWidth);
        String hexHeader = centerText("Hex Dump", hexWidth);
        String charHeader = centerText("ASCII", charWidth);
        System.out.printf("│%s│%s│%s│%n", offsetHeader, hexHeader, charHeader);

        printSeparator("├", "┼", "┤", offsetWidth, hexWidth, charWidth);

        // 创建一个临时的只读副本来安全地读取所有位置的数据
        ByteBuffer readBuffer = buffer.asReadOnlyBuffer();
        readBuffer.clear(); // 重置position=0, limit=capacity,这样可以读取整个缓冲区

        // 数据行
        for (int i = 0; i < cap; i += 16) {
            StringBuilder hexPart = new StringBuilder();
            StringBuilder charPart = new StringBuilder();
            StringBuilder arrowPart = new StringBuilder(); // 新增:箭头行
            String offset = String.format("%08X", i);

            // 处理每行的16个字节
            for (int j = 0; j < 16; j++) {
                int idx = i + j;
                if (idx < cap) {
                    // 使用绝对位置读取,不影响原缓冲区的position
                    byte b = readBuffer.get(idx);

                    // 格式化十六进制值(不再添加标记)
                    String hexValue = String.format("%02X", b & 0xFF);
                    hexPart.append(hexValue);

                    // 添加ASCII字符
                    char c = (b >= 32 && b <= 126) ? (char) b : '·';
                    charPart.append(c);

                    // 添加箭头标记
                    String arrow = getArrow(idx, pos, limit);
                    arrowPart.append(arrow.isEmpty() ? "  " : (" " + arrow));
                } else {
                    // 填充空白
                    hexPart.append("  ");
                    charPart.append(" ");
                    arrowPart.append("  ");
                }

                // 添加间隔(第8个字节后添加额外空格)
                if (j < 15) {
                    hexPart.append(j == 7 ? "  " : " ");
                    arrowPart.append(j == 7 ? "  " : " ");
                }
            }

            // 确保字符部分长度正确
            while (charPart.length() < charWidth) {
                charPart.append(" ");
            }

            // 确保十六进制部分长度正确
            String hexStr = hexPart.toString();
            if (hexStr.length() < hexWidth) {
                hexStr = String.format("%-" + hexWidth + "s", hexStr);
            }

            // 确保箭头部分长度正确
            String arrowStr = arrowPart.toString();
            if (arrowStr.length() < hexWidth) {
                arrowStr = String.format("%-" + hexWidth + "s", arrowStr);
            }

            // 输出数据行
            System.out.printf("│%s│%s│%s│%n", offset, hexStr, charPart.toString());

            // 检查这一行是否需要显示箭头
            boolean hasArrows = false;
            for (int j = 0; j < 16 && i + j < cap; j++) {
                int idx = i + j;
                if (!getArrow(idx, pos, limit).isEmpty()) {
                    hasArrows = true;
                    break;
                }
            }

            // 如果有箭头,输出箭头行
            if (hasArrows) {
                System.out.printf("│%s│%s│%s│%n",
                        " ".repeat(offsetWidth),
                        arrowStr,
                        " ".repeat(charWidth));
            }
        }

        // 底部边框
        printSeparator("└", "┴", "┘", offsetWidth, hexWidth, charWidth);

        // 添加额外信息
        if (cap > 0) {
            System.out.println();
            System.out.printf("Buffer Info: %d bytes total, %d bytes readable (pos=%d to limit=%d)%n",
                    cap, Math.max(0, limit - pos), pos, limit);
        }
    }

    private static String getArrow(int index, int position, int limit) {
        boolean isPosition = (index == position);
        boolean isLimitMinus1 = (index == limit - 1 && limit > 0);

        if (isPosition && isLimitMinus1) {
            return "↑*"; // 同时是position和limit-1
        } else if (isPosition) {
            return "↑P"; // position箭头
        } else if (isLimitMinus1) {
            return "↑L"; // limit-1箭头
        }
        return "";
    }

    private static String getMarker(int index, int position, int limit) {
        boolean isPosition = (index == position);
        boolean isLimitMinus1 = (index == limit - 1 && limit > 0);

        if (isPosition && isLimitMinus1) {
            return "*"; // 同时是position和limit-1
        } else if (isPosition) {
            return "P"; // position标记
        } else if (isLimitMinus1) {
            return "L"; // limit-1标记
        }
        return "";
    }

    private static void printCenteredBorder(String left, String fill, String right, String text, int width) {
        int textLen = text.length();
        int fillLen = width - textLen - 2; // -2 for left and right borders
        int leftFill = fillLen / 2;
        int rightFill = fillLen - leftFill;

        System.out.print(left);
        System.out.print(fill.repeat(leftFill));
        System.out.print(text);
        System.out.print(fill.repeat(rightFill));
        System.out.println(right);
    }

    private static void printCenteredLine(String text, int width) {
        String centeredText = centerText(text, width - 2); // -2 for borders
        System.out.println("│" + centeredText + "│");
    }

    private static void printSeparator(String left, String middle, String right, int... widths) {
        System.out.print(left);
        for (int i = 0; i < widths.length; i++) {
            System.out.print("─".repeat(widths[i]));
            if (i < widths.length - 1) {
                System.out.print(middle);
            }
        }
        System.out.println(right);
    }

    private static String centerText(String text, int width) {
        if (text.length() >= width) {
            return text.substring(0, width);
        }
        int paddingTotal = width - text.length();
        int padLeft = paddingTotal / 2;
        int padRight = paddingTotal - padLeft;
        return " ".repeat(padLeft) + text + " ".repeat(padRight);
    }

    // 示例用法
    public static void main(String[] args) {
        System.out.println("=".repeat(80));
        System.out.println("ByteBuffer Debug Tool - Test Cases");
        System.out.println("=".repeat(80));

        // 测试案例1:正常缓冲区
        System.out.println("\n【测试1】正常缓冲区,包含可打印字符");
        ByteBuffer buffer1 = ByteBuffer.allocate(32);
        String testStr = "Hello, ByteBuffer World!";
        buffer1.put(testStr.getBytes());
        buffer1.flip();
        buffer1.position(7); // 设置position到逗号后
        buffer1.limit(20);   // 限制到World的d
        debug(buffer1);

        // 测试案例2:小缓冲区,position和limit-1重合
        System.out.println("\n【测试2】position和limit-1重合的情况");
        ByteBuffer buffer2 = ByteBuffer.allocate(10);
        buffer2.put(new byte[]{0x41, 0x42, 0x43, 0x44, 0x45}); // ABCDE
        buffer2.flip();
        buffer2.position(2); // C
        buffer2.limit(3);    // limit在D,所以limit-1是C
        debug(buffer2);

        // 测试案例3:包含不可打印字符
        System.out.println("\n【测试3】包含不可打印字符和二进制数据");
        ByteBuffer buffer3 = ByteBuffer.allocate(24);
        for (int i = 0; i < 24; i++) {
            buffer3.put((byte) (i + 1)); // 1-24的字节值
        }
        buffer3.flip();
        buffer3.position(5);
        buffer3.limit(18);
        debug(buffer3);

        // 测试案例4:空缓冲区
        System.out.println("\n【测试4】空缓冲区");
        ByteBuffer buffer4 = ByteBuffer.allocate(0);
        debug(buffer4);

        // 测试案例5:单字节缓冲区
        System.out.println("\n【测试5】单字节缓冲区");
        ByteBuffer buffer5 = ByteBuffer.allocate(1);
        buffer5.put((byte) 'X');
        buffer5.flip();
        debug(buffer5);

        // 测试案例6:大缓冲区截断显示
        System.out.println("\n【测试6】较大缓冲区");
        ByteBuffer buffer6 = ByteBuffer.allocate(50);
        String longStr = "The quick brown fox jumps over the lazy dog. 1234567890";
        buffer6.put(longStr.substring(0, Math.min(longStr.length(), 50)).getBytes());
        buffer6.flip();
        buffer6.position(10);
        buffer6.limit(35);
        debug(buffer6);
    }
}

看看效果

package org.example.bytebuffer;

import lombok.extern.slf4j.Slf4j;


import java.nio.ByteBuffer;

import static org.example.util.ByteBufferUtil.debug;
@Slf4j
public class TestByteBuffer {
    public static void main(String[] args) {
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        debug(byteBuffer);
        byteBuffer.put((byte) 'a');
        byteBuffer.put((byte) 'b');
        byteBuffer.put((byte) 'c');
        debug(byteBuffer);
        byteBuffer.flip();
        debug(byteBuffer);
        for (int i = 0; i < 3; i++) {
            byteBuffer.get();
        }
        debug(byteBuffer);
    }
}

ByteBuffer常用方法

分配空间

        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
package org.example.bytebuffer;

import lombok.extern.slf4j.Slf4j;


import java.nio.ByteBuffer;


@Slf4j
public class TestByteBuffer {
    public static void main(String[] args) {
        System.out.println(ByteBuffer.allocate(1024).getClass());
        System.out.println(ByteBuffer.allocateDirect(1024).getClass());
    }
}

向buffer写入数据

从buffer读取数据

读取数据的mark和reset

package org.example.bytebuffer;

import lombok.extern.slf4j.Slf4j; // Lombok annotation for logging, though not used in this main example
import java.nio.ByteBuffer;

import static org.example.util.ByteBufferUtil.debug; // Assuming ByteBufferUtil.debug is your custom utility

/**
 * Demonstrates common operations and state transitions of a Java NIO ByteBuffer.
 * This class showcases:
 * 1. Allocation of a ByteBuffer.
 * 2. Writing data (put) into the buffer.
 * 3. Switching to read mode (flip).
 * 4. Reading data (get) from the buffer.
 * 5. Marking a position (mark) and returning to it (reset).
 */
@Slf4j // Lombok annotation, provides a 'log' field automatically
public class TestByteBuffer {

    public static void main(String[] args) {
        System.out.println("========== TestByteBuffer Execution Start ==========");

        // 1. Allocate ByteBuffer
        // ---------------------------------------------------------------------
        System.out.println("\n--- 1. Allocating ByteBuffer (capacity 10) ---");
        ByteBuffer byteBuffer = ByteBuffer.allocate(10);
        // State after allocation:
        // position = 0 (current position for writing/reading)
        // limit = 10 (how much can be written/read, initially capacity)
        // capacity = 10 (total size of the buffer)
        // mark = undefined

        // 2. Put data into the buffer (Write Mode)
        // ---------------------------------------------------------------------
        System.out.println("\n--- 2. Putting \"helloyyds\" (9 bytes) into buffer ---");
        byteBuffer.put("helloyyds".getBytes()); // "helloyyds" is 9 bytes long
        // After put:
        // position advances by the number of bytes written (position = 9)
        // limit remains capacity (limit = 10)
        debug(byteBuffer); // Display buffer state and content

        // 3. Flip the buffer (Switch to Read Mode)
        // ---------------------------------------------------------------------
        System.out.println("\n--- 3. Flipping buffer (to prepare for reading) ---");
        byteBuffer.flip();
        // After flip:
        // limit is set to current position (limit = 9, the extent of written data)
        // position is reset to 0 (to start reading from the beginning of written data)
        // mark is discarded if set
        debug(byteBuffer);

        // 4. Get one byte (Read Mode)
        // ---------------------------------------------------------------------
        System.out.println("\n--- 4. Getting one byte from buffer ---");
        byteBuffer.get(); // Reads the byte at position 0 ('h') and increments position
        // After get():
        // position advances by 1 (position = 1)
        // limit remains 9
        debug(byteBuffer);

        // 5. Mark current position and read some bytes
        // ---------------------------------------------------------------------
        System.out.println("\n--- 5. Marking position and reading 4 bytes ---");
        byteBuffer.mark(); // Sets mark = current position (mark = 1)
        System.out.print("Read characters: ");
        for (int i = 0; i < 4; i++) {
            char c = (char) byteBuffer.get(); // Reads 'e', 'l', 'l', 'o'
            System.out.print(c + " ");
        }
        System.out.println();
        // After reading 4 bytes:
        // position advances by 4 (position = 1 + 4 = 5)
        // limit remains 9
        // mark remains 1
        debug(byteBuffer);

        // 6. Reset position to the mark
        // ---------------------------------------------------------------------
        System.out.println("\n--- 6. Resetting position to the mark ---");
        byteBuffer.reset(); // Sets position = mark (position = 1)
        // After reset:
        // position is now 1
        // limit remains 9
        debug(byteBuffer);

        // 7. Read the same bytes again after reset
        // ---------------------------------------------------------------------
        System.out.println("\n--- 7. Reading 4 bytes again after reset ---");
        System.out.print("Read characters again: ");
        for (int i = 0; i < 4; i++) {
            char c = (char) byteBuffer.get(); // Reads 'e', 'l', 'l', 'o' again
            System.out.print(c + " ");
        }
        System.out.println();
        // After reading 4 bytes again:
        // position advances by 4 (position = 1 + 4 = 5)
        // limit remains 9
        debug(byteBuffer); // Final state

        System.out.println("\n========== TestByteBuffer Execution End ==========");
    }
}





字符串与bytebuffer的互相转换

package org.example.bytebuffer;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class StringToByteBuffer {
    public static void main(String[] args) {

        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("helloworld");
        ByteBuffer byteBuffer1 = Charset.forName("UTF-8").encode("hello");
        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
        System.out.println(charBuffer.toString());
        CharBuffer charBuffer1 = Charset.forName("UTF-8").decode(byteBuffer1);
        System.out.println(charBuffer1.toString());

        ByteBuffer byteBuffer2 = ByteBuffer.wrap("helloworld".getBytes(StandardCharsets.UTF_8));
        CharBuffer charBuffer2 = Charset.forName("UTF-8").decode(byteBuffer2);
        System.out.println(charBuffer2);

    }
}

分散读集中写

package org.example.bytebuffer;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

import static org.example.util.ByteBufferUtil.debug;

public class MultiPartByteBuffer {
    public static void main(String[] args) {

        try (RandomAccessFile randomAccessFile = new RandomAccessFile("3parts.txt", "r")) {
            FileChannel fileChannel = randomAccessFile.getChannel();

            // 分配三个 ByteBuffer
            ByteBuffer a1 = ByteBuffer.allocate(3);
            ByteBuffer a2 = ByteBuffer.allocate(3);
            ByteBuffer a3 = ByteBuffer.allocate(5);

            // 分散读取
            fileChannel.read(new ByteBuffer[]{a1, a2, a3});

            // 切换到读模式
            a1.flip();
            a2.flip();
            a3.flip();

            // 调试输出
            debug(a1);
            debug(a2);
            debug(a3);

            // Process a1 fully
            while (a1.hasRemaining()) {
                System.out.print((char) a1.get());
            }
// Process a2 fully
            while (a2.hasRemaining()) {
                System.out.print((char) a2.get());
            }
// Process a3 fully
            while (a3.hasRemaining()) {
                System.out.print((char) a3.get());
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class MultiPartFileChannelWriter {
    public static void main(String[] args) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile("multi.txt","rw")) {
            FileChannel fileChannel = randomAccessFile.getChannel();
            ByteBuffer a1 = StandardCharsets.UTF_8.encode("meowrain");
            ByteBuffer a2 = StandardCharsets.UTF_8.encode("yyds");
            ByteBuffer a3 = StandardCharsets.UTF_8.encode("hooo");
            fileChannel.write(new ByteBuffer[]{a1,a2,a3});
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

黏包和半包