netty底层——nio知识点 ByteBuffer+Channel+Selector

本文所涉及到的知识点主要为Nio。
知识来源:哔哩哔哩——nio

Nio三大组件

  • Channel
    指数据的传输通道,以前我们学过的输入输出流也是数据的传输通道,只不过那是单向的,而channel是双向的数据通道。
  • Buffer
    既然要输入输出数据,就需要将数据先临时存储在内存中,Buffer就是一个缓冲区,用来暂存从channel中读取或写入的数据。最常见的就是ByteBuffer。
  • Selector

对于Selector需要结合服务器的设计演化来理解
刚开始是多线程版设计
服务器的应用程序开发肯定要处理多个客户端通信,一个客户端建立连接代码的角度就是创建一个socket。进行读写操作就是通过socket来完成,所以一个客户端来了就给它开一个线程。如果有多个客户端连接就要开多个线程,每个线程管一个客户端连接。

请添加图片描述

缺点也很明显,那就是内存占用率过高并且线程之间切换成本高。


接着出现了问题,就找解决方法。这时候就出现了线程池设计,可以使用线程池来限制线程数过多的情况。

在这里插入图片描述
但有会出现另一个问题,那就是一个客户端占用了其中的一个线程,但是就只是占用着,什么操作也不进行,这个线程就被白白的限制住了,也不能去处理其他的客户端连接请求。


最后就出现了Select
在这里插入图片描述
它最大的好处就是一个select可以管理多个channel,当其中的一个channel中发生了读写事件,selector就会通知线程去处理这个事件,也可以理解为一个线程可以接收多个客户端发送的消息,当客户端1发送消息后,selector就会告诉线程,然线程去处理,当把这个客户端发送的消息接收完后。客户端2又发送了消息,selector又会通知这个线程去处理客户端2发送过来的消息。避免了一个客户端连接占用一个线程的情况出现。

ByteBuffer

ByteBuffer在Nio中用的比较多,所以需要对它也要有一定的了解

ByteBuffer的使用步骤:

  1. 向buffer中写入数据,例如调用channel.read(buffer);
  2. 调用buffer.flip()切换至读模式
  3. 从buffer中读取数据,例如调用buffer.get();
  4. 调用buffer.clear()或者compact()切换至写状态
  5. 重复1~4步骤

那么为什么使用要按照这个步骤去使用ByteBuffer并且要切换读写模式嘞?

内部结构

首先ByteBuffer有以下几个属性:

  • Capacity -----> Buffer的容量
  • position -----> 读写指针
  • limit -----> 读写限制

首先刚开始时,创建Buffer时是这个样子的。capacity肯定是指向最后的位置,limit刚开始表示写入限制,也是最后的位置,position是开头。
在这里插入图片描述
写模式下,position表示写入的位置,limit等于容量,下图表示写入四个字节后的状态
在这里插入图片描述
假如就只有这几个字节,写入完后,再进行filp()将buffer切换为读模式后,limit就变为的读取限制
在这里插入图片描述
读取完四个字节后
在这里插入图片描述
数据读取完后再调用clear()方法切换回写模式,这里只是动了指针,数据其实还是在里面,后面再写会覆盖以前的内容
在这里插入图片描述
另一种切换为写状态。compact()方法,是把未读取完的部分向前移动,然后切换至写模式
在这里插入图片描述

常用方法


创建相关:

ByteBuffer.allocate(int number); 创建一个容量为number的一个字节缓冲对象,这里的容量就固定了
ByteBuffer.allocateDirect(int number); 也是分配容量,区别是上面的方法创建的ByteBuffer是堆内存,而这里使用的是计算机的字节内存,堆内存读写效率低,而且会收到垃圾回收机制的影响。而使用直接内存的ByteBuffer读写效率高,分配速度较慢,使用完如果不合理释放会造成内存泄漏。

切换相关:

buffer.filp(); 切换为读模式
buffer.clear(); 切换为写模式
buffer.compact(); 把未读取完的部分向前移动,然后切换至写模式

读取相关

buffer.get(); 从buffer中读取数据,使用无参的方法会使用指针往后面移动,也可以往方法中传一个 int类型的数据 表示指针的索引,这种就直接读取这个位置的值,但是不会移动指针
buffer.get(Byte[] byte); 一次性读取多个数据,读取的数据个数为字节数组的容量,会移动position指针。
buffer.rewind(); 将数据指针position变为0,也就是可以实现读取完后还能重新读取一遍数据。
buffer.make(); + buffer.reset(); 通常这两个方法结合起来使用,是对rewind()方法的一个增强,rewind()方法只能是让 position变为0重新读取,如果想反复读取中间的一些数据就可以使用这两个方法
channel.write(Buffer buffer); 从buffer中读取数据,然后通过channel写出

写入相关

buffer.put(); 往buffer中写数据
channel.read(Buffer buffer); 往buffer中写数据

字符串与ByteBuffer互转

package com.hs.netty;

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

public class ByteBufferToStringTest {
   
    public static void main(String[] args) {
   
        // 字符串转换为ByteBuffer
        // 1. 传统方式,直接将字符串先转换为字节数组然后再添加
        // 添加进去这几个字符后还是写的状态,手动改为读模式后才能读取数据
        ByteBuffer buffer = ByteBuffer.allocate(16);
        buffer.put("hello".getBytes());
        buffer.flip();
        System.out.println("第一种方式读取的字符:" + buffer.get());
        // 2. Charset方式,它本身就是一种字符集类,可以处理字符串和ByteBuffer之间的转换
        // 将字符串添加进ByteBuffer后会自动切换为读模式
        ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("Hello...");
        System.out.println("第二种方式读取的字符:" + buffer1.get());
        // 3. wrap 它是nio提供的一个工具类,用来字节数组与ByteBuffer之间转换
        // 也会再添加完成后自动切换为读模式
        ByteBuffer buffer2 = ByteBuffer.wrap("hello..".getBytes());

        // 反之,ByteBuffer转字符串。可以使用Charset转换,当把数据读取完后 ByteBuffer还是读的状态
        String str1 = StandardCharsets.UTF_8.decode(buffer).toString();
    }
}



小案例

解决黏包半包问题

package com.hs.netty;

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

/**
 * ByteBuffer的测试,接收数据的黏包与丢包
 * 题目如下:
 * 假设网络上有多条数据发送给服务器,数据之间使用/n分割,但由于某些原因这些数据在接收时,被进行了重新组合。
 * 原消息如下:
 * Hello,World/n
 * I'm hushang/n
 * How are you?/n
 * 变成了下面两个ByteBuffer
 * Hello,World/nI'm hushang/nHo
 * w are you?/n
 * 现将错乱的数据恢复成按/n分割的数据
 *
 * @author hs
 * @date 2021/07/08
 */
public class ByteBufferContestTest {
   

    private static ByteBuffer messageBuffer;

    public static void main(String[] args) {
   

        //初始化存放每条正确消息的buffer
        messageBuffer = ByteBuffer.allocate(20);

        //创建出题目要求的情况
        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer.put("Hello,World\nI'm hushang\nHo".getBytes());
        split2(buffer);
        buffer.put("w are you?\n".getBytes());
        split2(buffer);
    }
    
    /**
     * 思路:
     * 首先获取老buffer的长度,然后遍历,通过get(i)获取每个位置的字节,再进行判断,如果为换行的话就创建一个新buffer
     * 然后利用当前i + 1 - 起始位置(position) 求出新buffer的应该存储数据的长度。然后再用获取一个字符 添加一个字符。
     * 最后还有使用compact()方法将未读取的两个字符放到buffer前面去,然后切换为写模式。
     * @param buffer
     */
    private static void split2(ByteBuffer buffer){
   
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
   
            // 新buffer的长度
            int length = i + 1 - buffer.position();
            if (buffer.get(i) == '\n'){
   
                ByteBuffer target = ByteBuffer.allocate(length);
                for (int j = 0 ; j < length ; j++){
   
                    target.put(buffer.get());
                }
                // 输出新buffer
                target.flip();
                System.out.println(StandardCharsets.UTF_8.decode(target).toString());
            }
        }
        buffer.compact();
    }
}



文件编程

如果不感兴趣可以跳过本章

FileChannel

它只能工作在阻塞模式下,也不能直接打开FileChannel,必须通过FileInputStream、FileOutputStream或者是RandomAccessFile来获取FileChannel,它们都有getChannel()方法。

  • 通过FileInputStream获取的FileChannel只能进行读
  • 通过FileOutputStream获取的FileChannel只能写
  • 通过RandomAccessFile获取的FileChannel,根据构造RandomAccessFile时的读写模式决定

读取数据,调用channel.read(Buffer buffer) , 读取的数据暂存在buffer中

写入数据,正确步骤如下

ByteBuffer buffer= Bytebuffer.allocate(10);
buffer.put(...);
buffer.flip();
// 需要先将buffer切换为读模式后才能使用buffer往channel中写入数据
// 这里并不能保证一次将buffer中的内容全部读取出来,所以要加一个while
while(buffer.hasRemaining()){
   
    channel.write(buffer) 
}

最后就是关闭,Channel必须要关闭。

操作系统出于性能的考虑,channel.write(buffer) ; 会将数据缓存,不是立刻写入磁盘 。最后channel关闭是会将所有的数据保存到磁盘里。也可以调用force(true)方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

两个channel传输数据

将channel1中的数据传输到channel2中

// 元素channel.transferTO(数据开始位置,数据数量,目标channel);
channel1.transferTO(0,channel1.size(),channel2);

transferTo()方法传输的文件大小是有限制了,一次只能传输2G的数据,所以可以做如下改进

// 总长度
long size = channel1.size();
// 判断是否还有数据未读取完
if(long left = size; left>0 ; ){
   
    // transferTo()方法的返回值就是传输的多少数据,所以直接让left相减
    left -= channel1.transferTo((size-left),left,channel2);
}

改进后就会分多次传输这个大于2G的数据

Path

JDK7引入的Path和Paths类,Path用来表示文件路径,Paths是一个工具类 用来获取Path实例对象

Path source = Paths.get("D:\\aaa\\bb...");
Path source = Paths.get("D:/aaa/bb...");
Path source = Paths.get("D:\\aaa","bb"); // 这里这表示D://aaa//bb

// 输出正常的文件路径
System.out.printlb(path.normalize());

Path 也支持 . 和 … 表示当前路径与上一级路径

Files

检查文件或文件夹是否存在:Files.exists(Path path)

创建一级目录:Files.createDirectory(Paths.get("E:\\1bbb\\测试创建文件夹")),如果目录已经存在或者是创建多级目录是会有异常的。

创建多级目录:Files.creatDirectories(Path path)

拷贝文件:Files.copy(Path source , Path target);第一个参数就是原文件,第二个参数就是拷贝到哪去,如果文件已经存在也会抛异常。

Path source = Paths.get("E:\\1bbb\\照片\\2852CF8E7B2523436666674951FA17BE.jpg");
Path target = Paths.get("E:\\1bbb\\测试创建文件夹\\testCopy.jpg");
Files.copy(source,target);

如果希望即使target文件已经存在,但还是想要source替换掉target可以在copy()方法中加一个参数

Files.copy(source,target, StandardCopyOption.REPLACE_EXISTING);

移动文件 StandardCopyOption.ATOMIC_MOVE 参数保证文件移动时的原子性

Path source = Paths.get("E:\\1bbb\\照片\\2852CF8E7B2523436666674951FA17BE.jpg");
Path target = Paths.get("E:\\1bbb\\照片\\2852CF8E7B2523436666674951FA17BE.jpg");
Files.move(source,target, StandardCopyOption.ATOMIC_MOVE);

删除文件:Files.delete(Path path) 如果文件不存在会抛异常

删除目录: 也是调用delete()方法,但是如果文件夹中有内容的话会报异常,只能删除空文件夹

遍历目录:

package com.hs.netty.file;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

/**
 * 测试Fils工具类操作文件
 * @author hs
 * @date 2021/07/11
 */
public class FilesTest {
   
    public static void main(String[] args) throws IOException {
   
        Path source = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_231");
        // walkFileTree(参数1是要遍历的目录Path对象,参数2是遍历后要执行的操作)
        Path path = Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
   
            /**
            * 注意下面重写的方法 返回值不要变 就这样写
             * 进入到该目录之前执行的方法
             */
            @Override
          public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
   
              System.out.println("---> 进入目录:" + dir);
              return super.preVisitDirectory(dir, attrs);
          }

            /**
             * 遍历文件夹中的每个文件时执行的方法
             */
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
   
                System.out.println("文件:" + file);
                return super.visitFile(file, attrs);
            }

            /**
             * 遍历文件执行失败执行的方法
             */
            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
   
                return super.visitFileFailed(file, exc);
            }

            /**
             * 文件遍历完后 执行的方法
             */
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
   
                System.out.println("<-----退出目录:" + dir );
                return super.postVisitDirectory(dir, exc);
            }
        });
    }
}

如果要删除一个多级目录,那就可以先在重写的visitFile()方法中删除每个文件,然后在重写的postVisitDirectory()方法中删除文件夹。

如果要拷贝一个文件夹,那就可以先在preVisitDirectory()方法中在要生成的位置创建一个文件夹,然后在重写的visitFile()方法中copy每个文件。但是这里有一个路径字符串替换的问题,需要在创建文件夹和复制文件时要把原路径字符串替换成新的要生成的路径,

就比如原始路径是E:\1bbb\照片,拷贝后的路径是D:\aa\bb

创建是文件夹就要从E:\1bbb\照片\image 改为 D:\aa\bb\image 。 就是需要把原始路径的前面一部分替换成新路径。


网络编程

阻塞模式

我们必定接触过的JDK提供的Socket网络编程,它其实就是比较典型的阻塞模式。接下来就详细讲讲这阻塞二字。
我们服务器端启动后,通过accept()方法来处理客户端的连接,当客户端连接成功后,会通过read()方法来接收客户端发送的数据。这两个方法都是阻塞的,程序运行到accept()方法就会被阻塞住,当有客户端连接成功才会解除阻塞继续运行,然后运行到read()方法又会阻塞,直到客户端发送数据菜户解除阻塞。
那么就会出现这样的一个情况,当一个客户端连接成功后,在read()方法阻塞住,这时候另一个客户端是连接不上的。
如果执行accept()方法等待客户端连接时,这个时候其他已连接的客户端发送数据是接收不到的。这就是阻塞。当然也就解决方法,那就是来一个客户端连接就开一个线程。
服务器端代码:

package com.hs.netty.network;

import com.google.common.base.Charsets;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;

/**
 * 测试阻塞模式下,服务器端的代码
 * @author hs
 * @date 2021/07/12
 */
public class ServerSocketChannelTest {
   
    public static void main(String[] args) throws IOException {
   
        // 首先创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(16);
        // 创建服务器
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置端口为8080
        serverSocketChannel.bind(new 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值