NIO-Buffer 使用
从 channel 读取数据到 buffer 中
URL url = ByteBufferExample.class.getClassLoader().getResource("data.txt");
if (url != null) {
try (FileInputStream resource = new FileInputStream(url.getPath())) {
FileChannel channel = resource.getChannel();
// position = 0 | capacity = 1024 | limit = capacity
ByteBuffer buffer = ByteBuffer.allocate(1024);
// position = readLen
while (channel.read(buffer) != -1) { // 将 channel 中的数据写入到 buffer
// limit = position | position = 0
buffer.flip(); // 切换至读模式
while (buffer.hasRemaining()) {
byte data = buffer.get();
System.out.println((char) data);
}
// position = 0 | limit = capacity
buffer.clear(); // 切换为写模式
}
} catch (IOException e) {
log.info("读取失败", e);
}
}
在堆或内存上分配空间
// 在堆内存上分配空间,读写效率低,受 GC 影响
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 在直接内存上分配空间,读写效率高,不收 GC 影响,分配内存的效率低(使用了系统内存,可能造成内存泄露)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
buffer 的读取操作
ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{'a', 'b', 'c'});
buffer.flip();
byte[] dst = new byte[3];
buffer.get(dst); // 从头读取到 dst
log.info("position-{}", buffer.position());
buffer.rewind(); // 设置为再次读取 position 置为 0
log.info("position-{}", buffer.position());
buffer.get(dst);
log.info("position-{}", buffer.position());
// mark & reset
buffer.rewind();
buffer.mark(); // 标记当前 position 位置
buffer.get(dst);
buffer.reset(); // 重置到 mark 标记位置
buffer.get(dst); // 再次读取
log.info(new String(dst));
// 读取指定位置,不会移动 position 指针
log.info("position-{}", buffer.position());
byte data = buffer.get(0);
System.out.println((char) data);
log.info("position-{}", buffer.position());
buffer & 字符串的转换
// 字符串转 buffer 方式一
String str = "你好,世界";
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(str.getBytes());
// 字符串转 buffer 方式二(encode 会自动切换到读模式)
ByteBuffer encodeBuffer = StandardCharsets.UTF_8.encode(str);
// buffer 转字符串
String convertStr = StandardCharsets.UTF_8.decode(encodeBuffer).toString();
log.info(convertStr);
粘包 & 半包
针对如下原始数据
Hello World\n
Im zhang san\n
How arw you\n
经过网络传输后,组合为了如下形式的两个 buffer
Hello World\nIm zhang san\nHo
w are you\n
原本不同行的消息都合并到了一起(粘包),原本一起的消息被拆分、截断(半包),这主要是由消息发送方的的缓冲区大小限制的
针对这种情况,需要手动将消息进行拆分,有如下示例代码
public static void example() {
// 模拟分两次接受消息
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello World\nIm zhang san\nHo".getBytes());
split(buffer);
buffer.put("w are you\n".getBytes());
split(buffer);
}
// 0 1 2 3 4
public static void split(ByteBuffer source) {
source.flip(); // 切换至读模式
// 以 \n 为依据,尝试找到一条完整消息
for (int i = 0; i < source.limit(); i++) {
if (source.get(i) == '\n') {
// 获取消息的长度
int len = i + 1 - source.position();
// 将完整消息写入新的 buffer
ByteBuffer partBuffer = ByteBuffer.allocate(len);
for (int j = 0; j < len; j++) {
partBuffer.put(source.get()); // 从 source 读取
}
partBuffer.flip();
byte[] dts = new byte[partBuffer.limit()];
partBuffer.get(dts);
log.info(new String(dts));
}
}
// 将未读取的内容往前移动至 0 索引位置,position 置为当前未读取内容长度索引位置,相当于继续写入
// 这里会将未找到换行符的剩余消息移动至开头,与后面继续写入的消息进入下一次拆分流程
source.compact();
}
FileChannel
channel 数据传输
String formPath = Optional.ofNullable(ChannelExample.class.getClassLoader().getResource("form.txt")).orElseThrow().getPath();
String toPath = Optional.ofNullable(ChannelExample.class.getClassLoader().getResource("to.txt")).orElseThrow().getPath();
try (FileInputStream fis = new FileInputStream(formPath);
FileOutputStream fos = new FileOutputStream(toPath)) {
FileChannel form = fis.getChannel();
FileChannel to = fos.getChannel();
// 底层使用操作系统的零拷贝进行优化, 单次传输存在 2G 的上限
form.transferTo(0, form.size(), to);
} catch (Exception e) {
log.error("err", e);
}
超出2G上限,分多次传输
String formPath = "D:\\randomfile.bin";
String toPath = "D:\\randomfileto.bin";
try (FileInputStream fis = new FileInputStream(formPath);
FileOutputStream fos = new FileOutputStream(toPath)) {
FileChannel form = fis.getChannel();
FileChannel to = fos.getChannel();
long size = form.size();
// 分多次传输
for (long left = form.size(); left > 0; ) {
// left 当前剩余的大小; size - left 即下次传输的位置
left -= form.transferTo(size - left, left, to);
}
} catch (Exception e) {
log.error("err", e);
}
阻塞 & 非阻塞
客户端
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress("localhost", 8080));
sc.write(StandardCharsets.UTF_8.encode("Hello"));
log.info("waiting...");
} catch (Exception e) {
log.error("err", e);
}
服务端阻塞通信
try (ServerSocketChannel ssc = ServerSocketChannel.open();) {
ByteBuffer buffer = ByteBuffer.allocate(16);
// 绑定监听端口
ssc.bind(new InetSocketAddress(8080));
// 客户端连接集合
List<SocketChannel> channels = new ArrayList<>();
log.debug("connecting...");
while (true) {
// accept 建立与客户端连接,阻塞模式下,线程会停止运行,等待一个客户端连接后继续往下执行
SocketChannel sc = ssc.accept();
log.debug("connected... {}", sc);
channels.add(sc);
for (SocketChannel channel : channels) {
// 接收客户端发送的数据
log.info("read...");
channel.read(buffer); // 阻塞模式下,线程停止运行
buffer.flip();
log.info("receive:{}", StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
log.debug("after read...{}", channel);
}
}
} catch (Exception e) {
log.error("err", e);
}
阻塞模式下,单线程无法很好的处理多个客户端,如果每个客户端都开辟线程,势必会导致 OOM,如果使用线程池,多个线程的上下文切换效率也不高
服务端非阻塞通信
try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
ByteBuffer buffer = ByteBuffer.allocate(16);
ssc.configureBlocking(false); // 非阻塞模式
ssc.bind(new InetSocketAddress(8080));
List<SocketChannel> channels = new ArrayList<>();
while (true) {
SocketChannel sc = ssc.accept(); // 非阻塞,线程会继续运行,如果没有连接建立,则 sc 为 null
if (sc != null) {
log.debug("connected... {}", sc);
sc.configureBlocking(false); // 非阻塞模式
channels.add(sc);
}
for (SocketChannel channel : channels) {
// 接收客户端发送的数据
int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
if (read > 0) {
buffer.flip();
log.info("receive:{}", StandardCharsets.UTF_8.decode(buffer));
buffer.clear();
log.debug("after read...{}", channel);
}
}
}
} catch (Exception e) {
log.error("err", e);
}
非阻塞模式下,线程空转,浪费 cpu 性能且数据读取的过程中,线程仍然是阻塞的
Selector
非阻塞 Server & Client
通过 selector 可以对多个 channel 进行统一管理,每个 channel 可以注册自己自己关心的事件,在没有 channel 注册的事件发生时,selector 处于阻塞状态,只有对应 channel 注册的事件发生时,线程才会继续执行,这样就避免了线程长时间空转消耗 cpu
// 创建 selector 管理多个 channel, 同时创建服务端的 channel: ssc
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false); // 非阻塞模式
ssc.bind(new InetSocketAddress(8080));
// 将服务端的 channel 注册到 selector 上(注意:selector 可以管理多个 channel)
SelectionKey serverSocketChannelKey = ssc.register(selector, 0, null);
/*
几种事件:
accept 有连接请求时触发
connect 客户端建立连接后触发
read 可读事件
write 可写事件
*/
// 对于 ServerSocketChannel,只需要关心 accept 连接事件
serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);
log.info("connecting...");
while (true) {
// 无事件发生,阻塞(这是通过 selector 管理 channel 的统一 api)
selector.select();
// 拿到所有可用事件集合(只针对注册到此 selector 上的 channel)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> itr = selectionKeys.iterator();
while (itr.hasNext()) {
SelectionKey selectionKey = itr.next();
// 区分事件类型执行对应操作
if (selectionKey.isAcceptable()) {
// 拿到事件对应的通道,因为是 accept 事件,这里转为服务端的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
/*
建立客户端连接,或者取消处理事件:selectionKey.cancel(); 不能不处理
*/
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 将与客户端建立的 channel 也注册到 selector,下次遍历时,就会得到对应的 SelectionKey
SelectionKey socketChannelKey = socketChannel.register(selector, 0, null);
socketChannelKey.interestOps(SelectionKey.OP_READ); // 与客户端的通道暂时只关心 read 事件
log.info("connected...");
} else if (selectionKey.isReadable()) {
// 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = socketChannel.read(buffer);
if (read == -1) { // 正常断开,read() 返回值为 -1
log.info("客户端:{},断开", selectionKey);
selectionKey.cancel();
}
if (read != 0) {
buffer.flip();
log.info("receive:{}", Charset.defaultCharset().decode(buffer));
buffer.clear();
}
} catch (Exception e) {
// 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
log.error("客户端强制断开:", e);
selectionKey.cancel();
}
}
itr.remove(); // 处理完的 key 需要删除,否则下次再次处理时会返回 null
}
}
} catch (Exception e) {
log.error("err", e);
}
客户端发送的消息存在粘包、半包的情况,这里通过 \n 处理消息的边界,引入对消息进行拆分的逻辑,如下
public static void split(ByteBuffer source, Consumer<ByteBuffer> consumer) {
source.flip();
for (int i = 0; i < source.limit(); i++) {
if (source.get(i) == '\n') {
// 从当前位置开始计算数据的长度
int len = i - (source.position() - 1);
ByteBuffer partBuffer = ByteBuffer.allocate(len);
for (int j = 0; j < len; j++) {
partBuffer.put(source.get());
}
consumer.accept(partBuffer);
}
}
source.compact();
}
服务的对客户端消息的处理逻辑改造如下
// 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = socketChannel.read(buffer);
if (read == -1) { // 正常断开,read() 返回值为 -1
log.info("客户端:{},断开", selectionKey);
selectionKey.cancel();
}
if (read != 0) {
// 以 \n 拆分消息
split(buffer, data -> {
data.flip();
log.info("receive:{}", Charset.defaultCharset().decode(data));
});
}
} catch (IOException e) {
// 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
log.error("客户端强制断开:", e);
selectionKey.cancel();
}
然而除了对消息的边界进行处理,消息容量大小的超出也要考虑到,客户端发送消息的长度可能会超出服务端 Buffer 缓冲区的大小,为此需要对 Buffer 进行扩容
而当客户端消息的长度超过 Buffer 容量时,没有读完的消息会触发多次 read 事件,目前的 Buffer 是作为一个局部变量进行创建的,如果一次性没有读取完数据,第二次读取时,又创建了新的 Buffer,这会导致之前的消息丢失,扩容操作也无法进行
如果将 Buffer 的创建抽取到事件处理外部,这又会导致多个 channel 都使用同一个 Buffer,实际上应该为每一个 channel 都分配自己的 Buffer,这在注册 channel 到 selector 时,可以通过att参数指定,注册 channel 的代码修改如下
// 通过第三个参数指定 attachment(附件),附件可以是任意类型,这里将其用作 ByteBuffer 的关联使用
SelectionKey socketChannelKey = socketChannel.register(selector, 0, ByteBuffer.allocate(16));
这样当 read 事件发生时,通过如下方式获取关联的 Buffer
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 拿到当前 selectionKey 关联的附件
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int read = socketChannel.read(buffer);
接下来是 Buffer 的扩容逻辑
if (read != 0) {
// 以 \n 拆分消息
split(buffer, data -> {
data.flip();
log.info("receive:{}", Charset.defaultCharset().decode(data));
});
/*
compact() 方法执行后,position 会被置为剩余未读字节数 limit 会被置为 buffer 的 capacity
如果剩余未读字节数等于容量,就意味着需要扩容
*/
if (buffer.position() == buffer.limit()) {
// 扩容为两倍大小
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip(); // 拷贝的旧 buffer 需要切换为读模式
newBuffer.put(buffer);
selectionKey.attach(newBuffer); // 替换附件
buffer.clear();
}
}
最终代码如下
// 创建 selector 管理多个 channel, 同时创建服务端的 channel: ssc
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false); // 非阻塞模式
ssc.bind(new InetSocketAddress(8080));
// 将服务端的 channel 注册到 selector 上(注意:selector 可以管理多个 channel)
SelectionKey serverSocketChannelKey = ssc.register(selector, 0, null);
/*
几种事件:
accept 有连接请求时触发
connect 客户端建立连接后触发
read 可读事件
write 可写事件
*/
// 对于 ServerSocketChannel,只需要关心 accept 连接事件
serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);
log.info("connecting...");
while (true) {
// 无事件发生,阻塞(这是通过 selector 管理 channel 的统一 api)
selector.select();
// 拿到所有可用事件集合(只针对注册到此 selector 上的 channel)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> itr = selectionKeys.iterator();
while (itr.hasNext()) {
SelectionKey selectionKey = itr.next();
// 区分事件类型执行对应操作
if (selectionKey.isAcceptable()) {
// 拿到事件对应的通道,因为是 accept 事件,这里转为服务端的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
/*
建立客户端连接,或者取消处理事件:selectionKey.cancel(); 不能不处理
*/
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 将与客户端建立的 channel 也注册到 selector,下次遍历时,就会得到对应的 SelectionKey
SelectionKey socketChannelKey = socketChannel.register(selector, 0, ByteBuffer.allocate(16));
socketChannelKey.interestOps(SelectionKey.OP_READ); // 与客户端的通道暂时只关心 read 事件
log.info("connected...");
} else if (selectionKey.isReadable()) {
// 除了一般的 read 事件,客户端正常断开或异常断开也会产生 read 事件
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 拿到当前 selectionKey 关联的附件
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
int read = socketChannel.read(buffer);
if (read == -1) { // 正常断开,read() 返回值为 -1
log.info("客户端:{},断开", selectionKey);
selectionKey.cancel();
}
if (read != 0) {
// 以 \n 拆分消息
split(buffer, data -> {
data.flip();
log.info("receive:{}", Charset.defaultCharset().decode(data));
});
/*
compact() 方法执行后,position 会被置为剩余未读字节数 limit 会被置为 buffer 的 capacity
如果剩余未读字节数等于容量,就意味着需要扩容
*/
if (buffer.position() == buffer.limit()) {
// 扩容为两倍大小
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip(); // 拷贝的旧 buffer 需要切换为读模式
newBuffer.put(buffer);
selectionKey.attach(newBuffer); // 替换附件
buffer.clear();
}
}
} catch (IOException e) {
// 异常断开的 read() 方法会抛出异常,需要取消客户端在 selector 中的注册
log.error("客户端强制断开:", e);
selectionKey.cancel();
}
}
itr.remove(); // 处理完的 key 需要删除,否则下次再次处理时会返回 null
}
}
} catch (Exception e) {
log.error("err", e);
}
消息拆分
// 1 2 3 4 5 6
// 0 1 2 3 4 5 6 7 8 9
// 1 2 3 \n 4 5 6 \n 7 8
public static void split(ByteBuffer source, Consumer<ByteBuffer> consumer) {
source.flip();
for (int i = 0; i < source.limit(); i++) {
if (source.get(i) == '\n') {
// 从当前位置开始计算数据的长度
int len = i - (source.position() - 1);
ByteBuffer partBuffer = ByteBuffer.allocate(len);
for (int j = 0; j < len; j++) {
partBuffer.put(source.get()); // 从 source 读取
}
consumer.accept(partBuffer);
}
}
source.compact();
}
写入内容过多
如下代码,演示了服务端一次性写入了过多的内容,客户端一次性未处理完的情况
服务端
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> itr = selectionKeys.iterator();
while (itr.hasNext()) {
SelectionKey selectionKey = itr.next();
if (selectionKey.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 向客户端发送大量数据
StringBuilder data = new StringBuilder();
for (int i = 0; i < 30000000; i++) {
data.append('a');
}
ByteBuffer buffer = Charset.defaultCharset().encode(data.toString());
while (buffer.hasRemaining()) { // 数据过多,分多次写入
// 返回值代表实际写入的字节数
int writeLen = socketChannel.write(buffer);
log.info("写入:{}", writeLen);
}
}
itr.remove();
}
}
} catch (IOException e) {
log.error("error", e);
}
客户端
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress(8080));
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
while (true) {
int read = sc.read(buffer); // 数据过多,分多次读取
if (read != 0) {
buffer.flip();
String readContent = Charset.defaultCharset().decode(buffer).toString();
log.info("读取到字节长度:{}", read);
buffer.clear();
}
}
} catch (Exception e) {
log.info("error:", e);
}
服务端部分打印如下
23:51:14.664 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:655355
23:51:14.671 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:1179639
23:51:14.675 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:917497
23:51:14.678 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:1048568
23:51:14.681 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:786426
23:51:14.683 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.685 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.688 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.690 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
23:51:14.692 [main] INFO com.netty.example.exer.server_client.WriteServer -- 写入:0
可以看到,由于内容写入过度,使得客户端在未处理完写入内容时,服务端依然在尝试写入,导致实际写入的字节数为0,这不符合非阻塞的概念,应该在可以写入的时候进行写入操作
修改后的服务端代码如下(事件处理部分)
try (Selector selector = Selector.open(); ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> itr = selectionKeys.iterator();
while (itr.hasNext()) {
SelectionKey selectionKey = itr.next();
itr.remove();
if (selectionKey.isAcceptable()) {
// 与客户端建立链接
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 注册客户端的 channel,暂时监听读事件
SelectionKey socketChannelKey = socketChannel
.register(selector, SelectionKey.OP_READ);
// 向客户端发送大量数据
ByteBuffer buffer = Charset.defaultCharset().encode("a".repeat(30000000));
int write = socketChannel.write(buffer);
log.info("写入长度:{}", write);
// 还有剩余,监听可写事件,实现非阻塞写入
if (buffer.hasRemaining()) {
/*
socketChannelKey.interestOps() + SelectionKey.OP_WRITE
意为在原来监听的事件基础上,再加上 OP_WRITE 事件的监听,否则直接
监听 OP_WRITE 事件的话,会覆盖原来的事件
*/
socketChannelKey.interestOps(socketChannelKey.interestOps() + SelectionKey.OP_WRITE);
socketChannelKey.attach(buffer); // 将未写完的数据作为附件
}
}
if (selectionKey.isWritable()) {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
if (buffer.hasRemaining()) {
int write = socketChannel.write(buffer);
log.info("写入长度:{}", write);
} else {
selectionKey.attach(null); // 数据写入完毕,清理
// 当前内容写入完毕,暂时无需关注 OP_WRITE 事件
selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);
}
}
}
}
} catch (IOException e) {
log.error("error", e);
}