当前位置: 首页 > news >正文

Netty系列教程之NIO基础知识

近30集的孙哥视频课程,看完一集整理一集来的,内容有点多,请大家放心食用~

1. 网络通讯的演变

1.1 多线程版网络通讯

在传统的开发模式中,客户端发起一个 HTTP 请求的过程就是建立一个 socket 通信的过程,服务端在建立连接之后,会创建单独的线程来处理当前请求。如下图所示:
在这里插入图片描述
其中,客户端示例代码如下:

Socket socket = new Socket("127.0.0.1",8080);
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
printWriter.write("send data to server");

服务端示例代码如下:

ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = null;
while (true) {socket = serverSocket.accept();// 每一个消息都单独创建一个线程去处理new Thread(new MsgServerHandler(socket)).start();
}

随着越来越多的请求发起,按上述模式,服务端会 对每一个请求单独创建线程 处理:
在这里插入图片描述
在这种模式下,会存在以下几个问题:

  1. 线程创建开销:线程是通过 JVM 调用操作系统来创建;
  2. 内存占用高:线程是占用存储资源的;
  3. CPU使用率高:(CPU轮转)线程之间上下文切换;

1.2 线程池版网络通讯

为了解决传统网络通讯开发所带来的问题,可通过在服务端 创建线程池 的方式来使得线程的创建可控(不能来一个请求就创建一个线程去处理);

在这里插入图片描述
服务端示例代码如下:

// 使用线程池,预先创建线程
static{threadPoolExecutor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), 20, 120L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
}ServerSocket serverSocket = new ServerSocket(8080);
Socket socket;
while (true){socket = serverSocket.accept();// 通过提交到线程池处理threadPoolExecutor.execute(new MsgServerHandler(socket));
}

这样一来就解决了传统网络开发中的3个问题,但是又带来了新的问题:

  • 当连接池中的线程被占用(由于客户端等待输入或其它操作导致)完,新的请求不能获取到线程,需要进入到队列中进行 等待 ~

1.3 NIO非阻塞网络通讯

可使用 NIO 来解决上述阻塞的问题,在整个数据传输过程中,NIO 与上述两种通讯方式存在以下区别:

  1. 传统的数据传输方式采用 流(inputStream、outputStream)NIO 采用 管道(channel) 来进行数据传输;
  2. NIO服务端 除了使用 ServerSocket 外,在网络编程中NIO 还引入了 选择器(selector)

在这里插入图片描述

在引入 selector 之后,服务端能对客户端的 Channel 进行监控,如果能正常读写,则分配线程处理,反之发现某些客户端 阻塞 之后,selector 可以释放已分配给当前 Channel 的线程供其它 Channel 使用;

2. NIO的两个核心

2.1 Channel简介

ChannelIO 的通讯管道,类似于 InputStreamOutputStream ,但没有方向性(流有方向性);

在这里插入图片描述

常见的Channel

  • 文件操作:
    • FileChannel :读写文件中的数据;
  • 网络操作:
    • SocketChannel :通过TCP读写网络中的数据;
    • ServerSocketChannel :监听新进来的 TCP 连接,并对每一个连接都创建 SocketChannel
    • DatagramChannel :通过 UDP 读写网络中的数据;

2.2 Buffer简介

Channel 读取或写入的数据,都要写到Buffer中,才可以被程序操作!
在这里插入图片描述

在文件读取的过程中,由于 Channel 没有方向性,所以 Buffer 为了区分读写,引入了 读模式写模式 进行区分(均站在程序的角度来看),文件通过管道( Channel )将数据存入缓存( Buffer )中供程序操作使用!

  • 读模式:将文件数据读取到程序(flip());
  • 写模式:将程序中的数据保存到文件(新创建,clear(),compact);

读、写模式只能存在一个,默认新创建为写模式!

常见的Buffer

  • ByteBuffer:应用最广泛的Buffer;
  • CharBuffer:
  • DoubleBuffer:
  • FloatBuffer:
  • IntBuffer:
  • LongBuffer:
  • ShortBuffer:
  • MapperByteBuffer:ByteBuffer的子类,用于直接内存操作!

3. 初识NIO程序

此处我们通过读取一个文件数据的程序来加深理解 ChannelBuffer ~

准备好我们的测试文件 data.txt ,并写入以下的测试数据:

1234567890

创建 Channel 的方式有以下几种

  1. FileInputStream
  2. RandomAccessFile
  3. FileChannel.open()

3.1 FileInputStream实现

创建并运行以下测试代码:

public class NIOTest {public static void main(String[] args) throws IOException {// 1. 创建Channel FileChannelFileChannel channel = new FileInputStream("D:\\Rivamed\\awesome\\message\\data.txt").getChannel();// 2. 创建缓冲区,此处分配了10字节ByteBuffer byteBuffer = ByteBuffer.allocate(10);// 3. 将读取的数据放入缓冲区channel.read(byteBuffer);// 4. 通过程序读取Buffer中的内容,设置缓冲区为读模式byteBuffer.flip();// 5. 循环读取缓冲区中的数据while (byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("(char)b = " + (char)b);}// 6. 读完之后设置为写模式byteBuffer.clear();}
}

上述代码能正常运行并读取打印 data.txt 文件中的内容,但如果 data.txt 中的内容超过10位,如:

1234567890abc

则上述代码将不能打印出 abc 这三个字符,原因是我们设置的 ByteBuffer 缓冲区大小为 10 个字节,程序在第一次读取完之后就正常结束了!显然不符合预期~我们需要改造成以下代码来循环读取!

public class NIOTest {public static void main(String[] args) throws IOException {// 1. 创建Channel FileChannelFileChannel channel = new FileInputStream("D:\\Rivamed\\awesome\\message\\data.txt").getChannel();// 2. 创建缓冲区,此处分配了10字节ByteBuffer byteBuffer = ByteBuffer.allocate(10);while (true){// 3. 将读取的数据放入缓冲区,read为实际读取的字节数,如果没有内容则返回 -1int read = channel.read(byteBuffer);if(read == -1) break;// 4. 通过程序读取Buffer中的内容,设置缓冲区为读模式byteBuffer.flip();// 5. 循环读取缓冲区中的数据while (byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("(char)b = " + (char)b);}// 6. 读完之后设置为写模式byteBuffer.clear();}}
}

3.2 RandomAccessFile实现

使用 RandomAccessFile 实现文件读取的代码如下所示,并添加异常处理等:

public class NIOTest2 {public static void main(String[] args) {FileChannel fileChannel = null;try {// 1. 创建Channel FileChannelfileChannel = new RandomAccessFile("D:\\Rivamed\\awesome\\message\\data.txt","rw").getChannel();// 2. 创建缓冲区,此处分配了10字节ByteBuffer byteBuffer = ByteBuffer.allocate(10);while (true){// 3. 将读取的数据放入缓冲区,read为实际读取的字节数,如果没有内容则返回 -1int read = fileChannel.read(byteBuffer);if(read == -1) break;// 4. 通过程序读取Buffer中的内容,设置缓冲区为读模式byteBuffer.flip();// 5. 循环读取缓冲区中的数据while(byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("(char)b = " + (char)b);}// 6. 读完之后设置为写模式byteBuffer.clear();}}catch (Exception e){e.printStackTrace();}finally {// 7. 关闭通道if(fileChannel!=null){try {fileChannel.close();} catch (IOException e) {throw new RuntimeException(e);}}}}
}

3.3 FileChannel.open()实现

使用 FileChannel.open() 实现文件读取的代码如下所示:

public class NIOTest3 {public static void main(String[] args) {FileChannel fileChannel = null;try {// 1. 创建Channel FileChannelfileChannel = FileChannel.open(Paths.get("D:\\Rivamed\\awesome\\message\\data.txt"), StandardOpenOption.READ);// 2. 创建缓冲区,此处分配了10字节ByteBuffer byteBuffer = ByteBuffer.allocate(10);while (true){// 3. 将读取的数据放入缓冲区,read为实际读取的字节数,如果没有内容则返回 -1int read = fileChannel.read(byteBuffer);if(read == -1) break;// 4. 通过程序读取Buffer中的内容,设置缓冲区为读模式byteBuffer.flip();// 5. 循环读取缓冲区中的数据while(byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("(char)b = " + (char)b);}// 6. 读完之后设置为写模式byteBuffer.clear();}}catch (Exception e){e.printStackTrace();}finally {// 7. 关闭通道if(fileChannel!=null){try {fileChannel.close();} catch (IOException e) {throw new RuntimeException(e);}}}}
}

3.4 使用try-resource重构

JDK1.7 之后引入了 try-resource 的机制,它能帮我们自动完成在 finally 块中对资源的关闭操作,如下为改造之后的代码示例:

public class NIOTest4 {public static void main(String[] args) {// 1. 创建Channel FileChanneltry(FileChannel fileChannel = FileChannel.open(Paths.get("D:\\Rivamed\\awesome\\message\\data.txt"), StandardOpenOption.READ)) {// 2. 创建缓冲区,此处分配了10字节ByteBuffer byteBuffer = ByteBuffer.allocate(10);while (true){// 3. 将读取的数据放入缓冲区,read为实际读取的字节数,如果没有内容则返回 -1int read = fileChannel.read(byteBuffer);if(read == -1) break;// 4. 通过程序读取Buffer中的内容,设置缓冲区为读模式byteBuffer.flip();// 5. 循环读取缓冲区中的数据while(byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("(char)b = " + (char)b);}// 6. 读完之后设置为写模式byteBuffer.clear();}} catch (IOException e) {throw new RuntimeException(e);}}
}

注意

 ByteBuffer byteBuffer = ByteBuffer.allocate(10);

ByteBuffer 一旦定义就不能动态调整

4. ByteBuffer详解

4.1 ByteBuffer 主要实现类

ByteBuffer抽象类 ,它的主要实现类为:

  1. HeadByteBuffer:用的是 JVM 内的堆内存,受 GC (堆内存不够时)的影响,在 IO 操作中效率不高;

    在这里插入图片描述

  2. MappedByteBuffer(DirectByteBuffer):用的操作系统上的内存,一步操作文件系统(不会受到 GC 影响,但可能造成内存泄漏);

    在这里插入图片描述

内存泄漏和内存溢出的区别

  • 内存泄漏 :已分配的内存 没有正常释放 或者存在 内存碎片 ,导致后续处理过程中出现所需内存不足的情况;

    在这里插入图片描述

  • 内存溢出 :程序运行或者处理时需要用到的内存大于能提供的最大内存的情况;

    在这里插入图片描述

4.2 ByteBuffer 核心结构

ByteBuffer 是一个类似数组的结构,在整个结构中包括三个主要的状态:

  1. Capacity :缓存的容量,类似于数组中的size;
  2. Position :当前缓存的下标,在读取时记录当前读取的位置,在写操作的时候记录写的位置,从0开始,每读取一次,下标+1;
  3. Limit :读写限制,在读写操作时,帮我们限制了能读多少数据和还能写多少数据;

读写的本质就是 Position 和 Limit 的相互作用,如下如所示:

在这里插入图片描述

不同写模式设置的区别

  • 调用 byteBuffer.clear() 设置写模式:
    在这里插入图片描述

  • 调用 compact() 设置写模式:

    在这里插入图片描述

4.3 ByteBuffer 核心API

使用 ByteBuffer 无非就是数据的存取,即往 buffer 中写和从 buffer 中读;

  • 写入数据(创建ByteBuffer、clear()、compact())的方法包含:

    1. channel 的 read 方法:从文件、IO中往buffer中写数据;1. channel.read(buffer)
    2. buffer 的 put() 方法:直接写入byte数据;1. buffer.put(byte)2. buffer.put(byte[])
    
  • 读取数据(flip())

    1. channel 的 write 方法(从buffer中读数据并往文件写,与上述read相反)
    2. buffer 的 get() 方法,每调用一次会影响 Position 的位置;
    3. rewind() 方法,可以将Position重置为0,用于复读数据;
    4. mark()&reset() 方法,通过mark标记Position,通过reset方法调回标记,从新执行;
    5. get(i) 方法,获取特定Position位置上的数据,但是不会对Position位置产生影响且不受读写模式的影响;
    

4.4 ByteBuffer 字符串操作

  • 字符串存储到buffer中:

    public static void main(String[] args) {ByteBuffer byteBuffer = ByteBuffer.allocate(10);// 调用 string 的 getBytes() 方法即可;byteBuffer.put("Lannis".getBytes());// 设置读模式byteBuffer.flip();while (byteBuffer.hasRemaining()){System.out.println("byteBuffer = " + (char)byteBuffer.get());}byteBuffer.clear();
    }
    

    也可使用字符集编码处理:

    // 将字符串按指定字符集编码之后存储到 ByteBuffer 中
    public static void main(String[] args) {//使用encode方法创建ByteBufferByteBuffer byteBuffer = StandardCharsets.UTF_8.("lannis");// 如果使用encode方法时,已经自动设置了读模式,需要省略flip();// 如果此处加上flip(),limit会设置为上一次的position位置,而上一次position为0,进而导致数据获取不到;// byteBuffer.flip();while (byteBuffer.hasRemaining()){byte b = byteBuffer.get();System.out.println("byteBuffer = " + (char)byteBuffer.get());}byteBuffer.clear();
    }
    

    或者使用 ByteBuffer.wrap() 方法:

    public static void main(String[] args) {ByteBuffer byteBuffer = ByteBuffer.wrap("lannis".getBytes());// 在使用wrap方法时,已经自动设置了读模式,此处需要省略flip()// 如果此处加上flip(),limit会设置为上一次的position位置,而上一次position为0// byteBuffer.flip();while (byteBuffer.hasRemaining()){System.out.println("byteBuffer = " + (char)byteBuffer.get());}byteBuffer.clear();
    }
    
  • Buffer中的数据转为字符串

    public static void main(String[] args) {// 使用encode方法创建ByteBufferByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("文明和谐");// 使用decode方法解码CharBuffer decode = StandardCharsets.UTF_8.decode(byteBuffer);System.out.println("decode = " + decode);
    }
    

粘包和半包

  • 粘包:当前接受的数据包含下一次数据的内容;

  • 半包:当前接受的数据不完整;

5. NIO的开发使用

5.1 文件读取

读取文件的代码在上面第三节已经演示过,此处不再复述;

5.2 文件写入

以下为将数据写入文件的代码示例:

public static void main(String[] args) throws IOException {// 1. 获得Channel,可通过 FileOutputStream/RandomAccessFile 获得FileChannel data = new FileOutputStream("data").getChannel();// 2. 获得bufferByteBuffer lannis = StandardCharsets.UTF_8.encode("lannis");// 3. 写文件data.write(lannis);
}

5.3 文件复制

  • 使用输入输出流实现:

    public static void main(String[] args) throws IOException {//data1 -> data2FileInputStream fileInputStream = new FileInputStream("data1");FileOutputStream fileOutputStream = new FileOutputStream("data2");byte[] buffer = new byte[1024];while (true){int read = fileInputStream.read(buffer);if(read == -1)break;fileOutputStream.write(buffer,0,read);}
    }
    
  • 使用commons-io实现:

    // 这里引入commons-io依赖
    public static void main(String[] args) throws IOException {//data1 -> data2FileInputStream fileInputStream = new FileInputStream("data1");FileOutputStream fileOutputStream = new FileOutputStream("data2");IOUtils.copy(fileInputStream,fileOutputStream)
    }
    
  • 使用NIO方式实现(零拷贝~效率高):

    public static void main(String[] args) throws IOException {FileChannel from = new FileInputStream("data1").getChannel();FileChannel to = new FileOutputStream("data2").getChannel();from.transferTo(0,from.size(),to);
    }
    

注意:
需要注意的是,NIO传输存在文件大小上限,最大支持 2G-1kb ,当实际文件大小超过 2GB 之后,只能进行分段拷贝:

  public static void main(String[] args) throws IOException {FileChannel from = new FileInputStream("data1").getChannel();FileChannel to = new FileOutputStream("data2").getChannel();// 还剩多少没有拷贝long left = from.size();while (left > 0){left = left - from.transferTo(from.size()-left,left,to);}
}

6. NIO网络编程

6.1 代码示例

NIO 网络编程中,服务端 中用于接受请求的是 ServerSocketChannel ,进行实际通信的是 SocketChannel ,以下为创建服务端和客户端的相关代码:

/*创建服务端*/
public class NIOServer {public static void main(String[] args) throws IOException {// 创建 ServerSocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 用于保存建立的连接List<SocketChannel> socketChannels = new ArrayList<>();ByteBuffer buffer = ByteBuffer.allocate(20);// 接受客户端的连接while (true){System.out.println("等待客户端连接... ");// socketChannel 代表服务端和客户端连接的一个通道【连接阻塞】SocketChannel socketChannel = serverSocketChannel.accept();System.out.println("已于客户端建立连接...:"+socketChannel);socketChannels.add(socketChannel);// 客户端与服务端通信过程for (SocketChannel channel : socketChannels) {System.out.println("开始接受处理客户端数据...");// 读取客户端提交的数据【IO阻塞】channel.read(buffer);// 设置读模式buffer.flip();// 打印输出接收到的消息System.out.println("客户端消息: " + StandardCharsets.UTF_8.decode(buffer));// 设置写模式buffer.clear();System.out.println("通信已结束...");}}}
}
/*NIO客户端*/
public class NIOClient {public static void main(String[] args) throws IOException {// 创建 socketChannel 用于通信连接SocketChannel socketChannel = SocketChannel.open();// 连接服务端socketChannel.connect(new InetSocketAddress(8000));socketChannel.write(StandardCharsets.UTF_8.encode("Test"));// 此处断点,服务端会出现IO阻塞的情况System.out.println("--------------------------------------------------------");}
}

6.2 阻塞问题

在上述代码运行过程中,服务端存在以下两个阻塞的情况:

  • ServerSocketChannel 阻塞:服务端等待客户端连接,accept() 方法存在阻塞;

    // 可通过设置 ServerSocketChannel 为非阻塞
    serverSocketChannel.configureBlocking(false);
    

    设置完之后,serverSocketChannel.accept() 在没有客户端连接的时候,返回值为 null :
    在这里插入图片描述

    同时,如果 socketChannel 不为 null 的时候,放入上述 list 中才有意义,需要进行判断:

    ...
    // 只有不为空的时候才添加客户端
    if(socketChannel!=null){socketChannels.add(socketChannel);    
    }
    ...
    
  • SocketChannel 阻塞:客户端IO通信的阻塞,channel.read() 方法存在阻塞;

     // 设置 socketChannel 非阻塞
    socketChannel.configureBlocking(false);
    

修改调整过后的代码如下(主要在服务端修改):

public class NIOServer {public static void main(String[] args) throws IOException {// 创建 ServerSocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 用于保存建立的连接List<SocketChannel> socketChannels = new ArrayList<>();ByteBuffer buffer = ByteBuffer.allocate(20);// 接受客户端的连接while (true){System.out.println("等待客户端连接... ");// socketChannel 代表服务端和客户端连接的一个通道【连接阻塞】SocketChannel socketChannel = serverSocketChannel.accept();System.out.println("已于客户端建立连接...:"+socketChannel);if(socketChannel!=null){// 设置 socketChannel 非阻塞socketChannel.configureBlocking(false);socketChannels.add(socketChannel);}// 客户端与服务端通信过程for (SocketChannel channel : socketChannels) {System.out.println("开始接受处理客户端数据...");// 读取客户端提交的数据【网络通讯IO阻塞】channel.read(buffer);// 设置读模式buffer.flip();// 打印输出接收到的消息System.out.println("客户端消息: " + StandardCharsets.UTF_8.decode(buffer));// 设置写模式buffer.clear();System.out.println("通信已结束...");}}}
}

::: tip 存在的问题

上述代码虽然解决的 ServerSocketChannelSocketChannel 阻塞的问题,但是存在 空转、死循环 ,会进一步导致CPU占用过高的问题;

故需要引入一个类似于 监管者 的角色(也就是后文的 selector),用来监管连接的创建和IO的通讯,即 ServerSocketChannelSocketChannel

:::

7. Selector

7.1 基础介绍

在引入 selector 之前,需要对它有一个大概的了解。

selector 并不会实时监管所有的 ServerSocketChannelSocketChannel ,而是在以下(常用)的特定场景(状态)下才会被监管:

  • accept():ServerSocketChannel 的连接建立;
  • read():SocketChannel 中的读操作;
  • write():SocketChannel 中的写操作;
  • connect():主要用于客户端中;

并且在实际使用时, selector 只有在非阻塞的情况下才生效,也就是需要添加以下配置才生效:

// 设置 ServerSocketChannel 为非阻塞
serverSocketChannel.configureBlocking(false);
// 设置 SocketChannel 为非阻塞
socketChannel.configureBlocking(false);

另外,还需要了解在 selector 中的两个重要属性:

  • keys :将需要监控的所有的 Channel 都注册到这个 keys 属性中;

    通过 channel.register() 配置 selector;
    通过 interestOps 配置需要监控的状态;
    
  • selectionKeys :存储的是实际发生以上监控状态的 Channel

    通过 selector.select() 去监听发生的特定状态的Channel;
    当监听到特定事件之后,会将 keys 中的 Channel 移动到 selectionKeys 中。
    后续就可以通过 selectedKeys() 方法获取并处理特定 Channel 事件;
    

由于 SelectionKey 中存在的 Channel 可能是 ServerSocketChannel 或者 SocketChannel,故在后续业务处理中,需要使用以下方法进行区分:

  • key.isAcceptable():如果返回true,则表示当前 selectKey 缓存的是 ServerSocketChannel 对象;

  • key.isReadable():如果返回true,则表示当前 selectKey 缓存的是 SocketChannel 对象;

  • key.isWritable():如果返回true,则表示当前 selectKey 缓存的是 SocketChannel 对象;

7.2 创建连接代码示例

为了进一步说明理解 selector ,接下来,我们将一步一步的结合代码进行测试演示,首先创建服务端的代码,并设置为非阻塞模式:

/*服务端代码*/
public class NIOServer {public static void main(String[] args) throws IOException {// 创建 ServerSocketChanneltry (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));}}
}

接着,引入 selector :

/*服务端代码*/
public static void main(String[] args) throws IOException {// 创建 ServerSocketChanneltry (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 引入 selectorSelector selector = Selector.open();// 将当前 ServerSocketChannel 注册到 selector 中,返回 selectKeySelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);// 配置 selectKey 监听 accept 状态selectionKey.interestOps(SelectionKey.OP_ACCEPT);while (true) {// 【会阻塞】开始监控,只有监控到有实际连接或读写操作才会处理selector.select();}}
}

此时的状态

运行上述服务端代码之后,在上述 12 行代码前后进行断点,执行注册代码前:

在这里插入图片描述

执行注册代码后:

在这里插入图片描述

接着代码正常运行,会在17行阻塞住,一直等待客户端的连接:
在这里插入图片描述

完成上述配置操作后,我们可以启动客户端进行连接,上述 selector.select() 方法在没有客户端连接发生的时候,会一直处于等待的状态,一但有连接发生,它就会将 keys 中监控的当前连接 Channel 复制到 selectionKeys 中,:

在这里插入图片描述
接下来,添加对 selectedKeys 中的数据进行处理的方法:

/*服务端代码*/
public static void main(String[] args) throws IOException {// 创建 ServerSocketChanneltry (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 引入 selectorSelector selector = Selector.open();// 将当前 ServerSocketChannel 注册到 selector 中,返回 selectKeyselector = {WindowsSelectorImpl@910}SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);// 配置 selectKey 监听 accept 状态selectionKey.interestOps(SelectionKey.OP_ACCEPT);while (true) {// 【会阻塞】开始监控,只有监控到有实际连接或读写操作才会处理selector.select();// 获取所有有效的SelectionKey(需要使用iterator遍历,因为后续会删除,不能使用for循环,for循环不能删除)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 只有在确认有有效状态的情况下,才会进行以下循环,避免了空转和死循环的问题while (iterator.hasNext()) {SelectionKey key = iterator.next();// 用完之后务必删除,否则会出现空指针iterator.remove();// 获取对应的 Channelif (key.isAcceptable()) {// 连接 ServerSocketChannelServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();// 或者直接使用上述创建好的 serverSocketChannel// SocketChannel socketChannel = serverSocketChannel.accept();System.out.println("channel = " + socketChannel);}}}}
}

启动客户端之后,服务端正常输出连接信息:
在这里插入图片描述

7.3 服务端读消息代码示例

针对客户端的写事件,需要在连接之后进行创建:

/*服务端代码*/
public static void main(String[] args) throws IOException {// 创建 ServerSocketChanneltry (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 引入 selectorSelector selector = Selector.open();// 将当前 ServerSocketChannel 注册到 selector 中,返回 selectKeyselector = {WindowsSelectorImpl@910}SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);// 配置 selectKey 监听 accept 状态selectionKey.interestOps(SelectionKey.OP_ACCEPT);while (true) {// 【会阻塞】开始监控,只有监控到有实际连接或读写操作才会处理selector.select();// 获取所有有效的SelectionKey(需要使用iterator遍历,因为后续会删除,不能使用for循环,for循环不能删除)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 只有在确认有有效状态的情况下,才会进行以下循环,避免了空转和死循环的问题while (iterator.hasNext()) {SelectionKey key = iterator.next();// 用完之后务必删除,否则会出现空指针iterator.remove();// 获取对应的 Channelif (key.isAcceptable()) {// 连接 ServerSocketChannelServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();// 或者直接只有上述创建好的 serverSocketChannel// SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);SelectionKey register = socketChannel.register(selector, 0, null);register.interestOps(SelectionKey.OP_READ);System.out.println("channel = " + socketChannel);}else if(key.isReadable()){// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(20);socketChannel.read(byteBuffer);// 设置读模式byteBuffer.flip();System.out.println("msg = " + StandardCharsets.UTF_8.decode(byteBuffer));}}}}
}

正常运行客户端服务端之后,服务端打印输出如下:
在这里插入图片描述

客户端示例代码如下:

/*NIO客户端*/
public class NIOClient {public static void main(String[] args) throws IOException {// 创建 socketChannel 用于通信连接SocketChannel socketChannel = SocketChannel.open();// 连接服务端socketChannel.connect(new InetSocketAddress(8000));socketChannel.write(StandardCharsets.UTF_8.encode("Test"));System.out.println("--------------------------------------------------------");}
}

注意,当客户端发送的数据长度大于服务端Buffer的长度时:

  1. 客户端只发送一次数据;
  2. 服务端会多次调用 select() 方法多次处理,直到当前消息处理完毕之后,整个流程才算结束;

在某些特殊操作下,服务器端无法处理,select() 方法就会频繁调用(如在客户端非正常关闭会发送-1的状态,服务端处理不了会一直进行 select() 方法的调用),可通过调用 selectKey.cancel() 来调用,修改调整以下代码:

...
}else if(key.isReadable()){try{// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(20);int read = socketChannel.read(byteBuffer);if(read == -1){key.cancel();}else{// 设置读模式byteBuffer.flip();System.out.println("msg = " + StandardCharsets.UTF_8.decode(byteBuffer));}}catch (Exception e){e.printStackTrace();key.cancel();}
}
...

7.4 半包和粘包

一旦buffer缓冲区设置不合理,就会出现半包和粘包的问题(第6章节的代码亦是),例如以下客户端像服务端发生Hello World消息的代码:

/*客户端代码*/
public static void main(String[] args) throws IOException {// 创建 socketChannel 用于通信连接SocketChannel socketChannel = SocketChannel.open();// 连接服务端socketChannel.connect(new InetSocketAddress(8000));// 发送数据socketChannel.write(StandardCharsets.UTF_8.encode("Hello World"));socketChannel.close();
}

为了能演示出效果,此时服务端的代码如下所示:

/*服务端代码*/
public static void main(String[] args) throws IOException {// 创建 ServerSocketChanneltry (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {// 设置 ServerSocketChannel 非阻塞serverSocketChannel.configureBlocking(false);// 设置服务端的监听端口(客户端通过网络进行访问的时候需要IP和端口)serverSocketChannel.bind(new InetSocketAddress(8000));// 引入 selectorSelector selector = Selector.open();// 将当前 ServerSocketChannel 注册到 selector 中,返回 selectKeyselector = {WindowsSelectorImpl@910}SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);// 配置 selectKey 监听 accept 状态selectionKey.interestOps(SelectionKey.OP_ACCEPT);while (true) {// 【会阻塞】开始监控,只有监控到有实际连接或读写操作才会处理selector.select();// 获取所有有效的SelectionKey(需要使用iterator遍历,因为后续会删除,不能使用for循环,for循环不能删除)Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 只有在确认有有效状态的情况下,才会进行以下循环,避免了空转和死循环的问题while (iterator.hasNext()) {SelectionKey key = iterator.next();// 用完之后务必删除,否则会出现空指针iterator.remove();// 获取对应的 Channelif (key.isAcceptable()) {// 连接 ServerSocketChannelServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();// 或者直接只有上述创建好的 serverSocketChannel// SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);SelectionKey register = socketChannel.register(selector, 0, null);register.interestOps(SelectionKey.OP_READ);System.out.println("channel = " + socketChannel);}else if(key.isReadable()){// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(10);int read = socketChannel.read(byteBuffer);if(read == -1){key.cancel();}else{// 设置读模式byteBuffer.flip();System.out.println("msg = " + StandardCharsets.UTF_8.decode(byteBuffer));}}}}}
}

注意上述第 39 行代码,我们分配的 ByteBuffer 大小为 10 个字节,此时运行服务端和客户端之后,服务端会打印输出以下内容:

在这里插入图片描述

从运行结果分析可以发现,客户端明明只发了一次消息,但是服务端却打印出两条消息,这显然是不符合业务要求的。

Hello World 为11个字节长度,此处我们有两种解决方式可选:

  1. 修改 ByteBuffer 的大小为11,保证能接收到客户端发送的消息;( ByteBuffer 一旦定义就不可修改,故此方法不可靠)
  2. 通过分割符来甄别一条完整的消息,以此解决半包和粘包的问题;(以下为详细的解决办法)

为了能完整获取客户端发送的数据,需要进行一些数据处理,例如添加分隔符(分隔符的目的是为了甄别一条完整的信息),引入以下方法:
/*解决半包粘包问题*/
private static void doLineSplit(ByteBuffer byteBuffer) {// 设置读模式byteBuffer.flip();for (int i = 0; i < byteBuffer.limit(); i++) {if ('\n' == byteBuffer.get(i)) {int length = i + 1 - byteBuffer.position();ByteBuffer target = ByteBuffer.allocate(length);// 取数据for (int j = 0; j < length; j++) {target.put(byteBuffer.get());}// 设置读模式target.flip();System.out.println("StandardCharsets.UTF_8.decode(target) = " + StandardCharsets.UTF_8.decode(target));}}byteBuffer.compact();
}

接着修改服务端读取客户端消息部分的方法:

...
}else if(key.isReadable()){// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(10);int read = socketChannel.read(byteBuffer);if(read == -1){key.cancel();}else{doLineSplit(byteBuffer);}
}
...

修改客户端发送代码,在 Hello World 后面增加 \n 如下所示:

public static void main(String[] args) throws IOException {// 创建 socketChannel 用于通信连接SocketChannel socketChannel = SocketChannel.open();// 连接服务端socketChannel.connect(new InetSocketAddress(8000));// 发送数据socketChannel.write(StandardCharsets.UTF_8.encode("Hello World\n"));socketChannel.close();
}

运行修改过后的代码,服务端输出结果如下:
在这里插入图片描述


在这里插入图片描述

为什么输出的只有 d ,前面的内容哪里去了?别慌,请听我狡辩:

在上述的 doLineSplit() 方法中,确实是能通过分隔符 \n 来获取完整的消息的,但是有一个前提就是 ByteBuffer 必须是同一个。
但是在 select() 事件监听并处理的代码中,每一次都是一个新的 ByteBuffer,还记得下面的代码吗?
在每次进入到 key.isReadable() 条件成立的方法后,我们会新建 ByteBuffer:ByteBuffer byteBuffer = ByteBuffer.allocate(10);
这样一来,就会导致select()方法两次调用处理的ByteBuffer没有关联上,第一次不会打印是因为没有读取到 \n 分隔符,在第二次读取的时候亦没有获取到前一次读取的结果,故只读取并打印到 d\n 字符;

当然,这也有解决办法,那就是将 ByteBufferChannel 绑定在一起,保证一个 Channel 多次操作中 ByteBuffer 为同一个;

还记得 SelectionKey.register(sql,ops,att) 方法吗?这个方法中有三个参数:

  1. sql:注册Channel的选择器;
  2. ops:设置要监听的状态;
  3. att:需要绑定的附件,可以为空;

我们可以通过如下设置 att 属性来给每一个 Channel 绑定一个 Channel 共享的 ByteBuffer ,修改以下服务端代码:

...if (key.isAcceptable()) {// 连接 ServerSocketChannelServerSocketChannel channel = (ServerSocketChannel) key.channel();SocketChannel socketChannel = channel.accept();socketChannel.configureBlocking(false);// 创建共享 ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(20);SelectionKey register = socketChannel.register(selector, 0, byteBuffer);register.interestOps(SelectionKey.OP_READ);System.out.println("channel = " + socketChannel);}else if(key.isReadable()){// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();// 获取共享 ByteBufferByteBuffer attachment = (ByteBuffer) key.attachment();int read = socketChannel.read(attachment);if(read == -1){key.cancel();}else{doLineSplit(attachment);}}
...

存在的问题:在上述代码调整中,我们创建了共享 ByteBuffer 来保证一个 Channel 中多次操作使用同一个 ByteBuffer ,以此确保消息能够完整的处理;

但是注意,为了避免使用 compact() 方法之后ByteBuffer 中的内容超过容量大小的问题,此处我是修改了 ByteBuffer 的容量大小哦

这样一来又会带来另外一个问题,我不能动态修改 ByteBuffer 的容量大小,如果传入的消息过长怎么办?

那就需要找一个办法去扩容 ByteBuffer ~~~

7.5 ByteBuffer扩容

在这里插入图片描述

当我们调用上述 doLineSplit() 方法对客户端的消息处理完之后,需要判断 PositionLimit 的值,如果相等,则说明当前 ByteBuffer 容量不够,需要进行扩容处理,反之则跳过,示例代码如下:

...
}else if(key.isReadable()){// 读 SocketChannelSocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(10);int read = socketChannel.read(byteBuffer);if(read == -1){key.cancel();}else{doLineSplit(byteBuffer);if(byteBuffer.position() == byteBuffer.limit()){//此时说明容量不够了,需要进行扩容ByteBuffer newByteBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);// 将原始的ByteBuffer中的数据复制到新Buffer中newByteBuffer.put(byteBuffer);// 重新绑定新ByteBufferkey.attach(newByteBuffer);}}
}
...

待优化和考虑的地方:

  1. ByteBuffer 容量不够的时候,我们进行了扩容处理,但是在后续请求中,可能接受的数据长度远远小于扩容后的大小,在多线程请求中,会造成内存浪费!除了扩容之外,还需要考虑缩容

  2. ByteBuffer 扩容时,旧 Buffer 中的数据往新 Buffer 中的数据写时,效率很低(可通过零拷贝方式解决);

  3. 为了避免频繁检索上述代码中的 \n 分隔符,可以通过头体分离的方式来保证信息完整性:

    在这里插入图片描述

7.6 服务端写消息代码示例

上述代码已经完成了服务端创建连接并读取客户端发送的数据的代码示例,接下来将继续完善服务端向客户端发送数据的功能;

此处我们在服务端和客户端连接建立之后,随即向客户端发送数据,代码如下所示:

/*NIO服务端*/
public class NIOServer {public static void main(String[] args) throws IOException {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8000));Selector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();iterator.remove();if (selectionKey.isAcceptable()) {SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);SelectionKey skey = socketChannel.register(selector, SelectionKey.OP_READ);//准备数据StringBuilder sb = new StringBuilder();for (int i = 0; i < 2000000; i++) {sb.append("abcdabcd");}ByteBuffer buffer = StandardCharsets.UTF_8.encode(sb.toString());while (buffer.hasRemaining()){int write = socketChannel.write(buffer);System.out.println("write = " + write);}}}}}
}
/*NIO客户端*/
public class NIOClient {public static void main(String[] args) throws IOException {// 创建 socketChannel 用于通信连接try (SocketChannel socketChannel = SocketChannel.open()) {// 连接服务端socketChannel.connect(new InetSocketAddress(8000));// 接受服务端数据ByteBuffer buffer = ByteBuffer.allocate(1024);int read = 0;while (true) {read += socketChannel.read(buffer);System.out.println("read = " + read);buffer.clear();}}}
}

上述代码运行之后,服务端和客户端控制台打印输出的结果如下图所示:
在这里插入图片描述

通过上面的运行结果发现,服务端发送了很多空数据,这是因为受到了发生速率的限制,为了解决这个问题,这个时候我们就可使用 isWriteable() 方法来监听 write 的状态:

...
if (selectionKey.isAcceptable()) {SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);SelectionKey skey = socketChannel.register(selector, SelectionKey.OP_READ);//准备数据StringBuilder sb = new StringBuilder();for (int i = 0; i < 2000000; i++) {sb.append("abcdabcd");}ByteBuffer buffer = StandardCharsets.UTF_8.encode(sb.toString());// 先写一次int write = socketChannel.write(buffer);System.out.println("write = " + write);// 判断是否写完if (buffer.hasRemaining()) {//说明么有写完,为当前的 SocketChannel 增加 write 的监听// READ 和 Writeskey.interestOps(skey.interestOps() + SelectionKey.OP_WRITE);// 把当前操作传给下一个操作skey.attach(buffer);}
} else if (selectionKey.isWritable()) {// 获取客户端 ChannelSocketChannel socketChannel = (SocketChannel) selectionKey.channel();// 获取 BufferByteBuffer buffer = (ByteBuffer) selectionKey.attachment();// 写操作int write = socketChannel.write(buffer);System.out.println("write = " + write);if (!buffer.hasRemaining()) {//写完了selectionKey.attach(null);selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);}
}
...

这样一来,运行改动过后的代码,服务端就不会发生过多的空数据,进而提高了服务端的处理消息的能力,服务端输出结果如下:
在这里插入图片描述

8. Reactor 模式

8.1 单线程模式

在这里插入图片描述
在单线程模式中,客户端的连接以及后续的读写操作都是由一个线程来完成的,存在效率低的问题;

8.2 主从多线程模式

在这里插入图片描述
在这种模式下,将客户端连接相关的交由一个独立的(图中Boss)线程处理,后续读写操作交由其它(图中Worker)线程处理;

8.3 代码实现

参照上述主从多线程模式的图例,我们需要将IO的读写操作用单个 Worker 线程来处理,故我们首先需要创建 Worker 线程类:

// Worker 线程类
public class Worker implements Runnable {private final String name;private Selector selector;// 多线程环境下的状态需要增加 volatileprivate volatile boolean created;// 为了传递线程间的变量private final ConcurrentLinkedDeque<Runnable> concurrentLinkedDeque = new ConcurrentLinkedDeque<>();public Worker(String name) {this.name = name;}public void register(SocketChannel sc) throws IOException {if (!created){// 每个 Worker 创建一个线程Thread thread = new Thread(this, name);selector = Selector.open();thread.start();created = true;}// 放到一个线程中保证有序执行concurrentLinkedDeque.add(()->{try {sc.register(selector, SelectionKey.OP_READ + SelectionKey.OP_WRITE);} catch (ClosedChannelException e) {throw new RuntimeException(e);}});// 唤醒阻塞的select.select()selector.wakeup();}@Overridepublic void run() {while (true) {try {selector.select();Runnable poll = concurrentLinkedDeque.poll();if(poll!=null){poll.run();}Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey scKey = iterator.next();iterator.remove();if (scKey.isReadable()) {SocketChannel socketChannel = (SocketChannel) scKey.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(30);socketChannel.configureBlocking(false);socketChannel.read(byteBuffer);byteBuffer.flip();System.out.println("Message = " + StandardCharsets.UTF_8.decode(byteBuffer));byteBuffer.clear();}}} catch (IOException e) {throw new RuntimeException(e);}}}
}

接着修改服务端的代码:

// 服务端代码
public class ReactorBossServer {public static void main(String[] args) throws IOException, InterruptedException {ServerSocketChannel ssc = ServerSocketChannel.open();ssc.configureBlocking(false);ssc.bind(new InetSocketAddress(8000));Selector selector = Selector.open();ssc.register(selector, SelectionKey.OP_ACCEPT);// 模拟多线程,此处示例为2个Worker[] workers = new Worker[2];for (int i = 0; i < workers.length; i++) {workers[i] = new Worker("worker"+i);}AtomicInteger index = new AtomicInteger();while (true) {// 监控连接selector.select();Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();iterator.remove();if (selectionKey.isAcceptable()) {ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);// hash取模 x%2 结果0或1workers[index.getAndIncrement()%workers.length].register(socketChannel);}}}}
}

最后运行,当多个客户端连接之后,服务端轮转进行处理!(此处自行进行代码测试)

9. 零拷贝

在没有任何优化操作前,以读取文件数据在到写数据的流程为例进行数据拷贝的分析,如下图所示:

在这里插入图片描述

在调用 Read() 方法之后,JVM 会通知操作系统,由操作系统调用操作文件相关的 API 来读取硬盘上的数据,随后将数据存储在操作系统的内存 (高速页缓存/内核缓冲区)中,进而过渡传递到 JVM 中的应用缓存【做了2次数据的拷贝】;同理,在调用 write() 写数据时也发生了两次数据拷贝,整个操作下来发生了【4次数据拷贝】,故此效率偏低;

9.1 内存映射

NIO 中有个 内存映射 的概念,通过内存映射可以将 高速页缓存 中的数据 共享应用缓存 ,同时减少了数据拷贝的次数,示例图如下:

在这里插入图片描述
在代码中可使用以下代码创建直接缓冲区:

ByteBuffer.allocateDirect(10);

内存映射 主要用于文件的操作;
使用直接内存的好处如上图所示,就是减少了数据拷贝的次数,但是带来的问题就是需要手动进行内存析构,否则会造成内存浪费

9.2 零拷贝

零拷贝:不涉及到虚拟机内存的拷贝;

Linux2.1Linux2.4 内核中,存在 sendFile() 方法,其两者的拷贝区别如下:

在这里插入图片描述
可以看出在 Linux2.4 的内核中,拷贝次数比 Linux2.1 又少了1次,效率又提高了;

Java 中使用 file.transferTo()file.transferFrom() 方法即可调用 sendFile() 方法;

相关文章:

Netty系列教程之NIO基础知识

近30集的孙哥视频课程&#xff0c;看完一集整理一集来的&#xff0c;内容有点多&#xff0c;请大家放心食用~ 1. 网络通讯的演变 1.1 多线程版网络通讯 在传统的开发模式中&#xff0c;客户端发起一个 HTTP 请求的过程就是建立一个 socket 通信的过程&#xff0c;服务端在建立…...

【题解 树形dp 拆位】 树上异或

「KDOI-06-S」树上异或 题目描述 给定一棵包含 n n n 个节点的树&#xff0c;第 i i i 个点有一个点权 x i x_i xi​。 对于树上的 n − 1 n-1 n−1 条边&#xff0c;每条边选择删除或不删除&#xff0c;有 2 n − 1 2^{n-1} 2n−1 种选择是否删除每条边的方案。 对于…...

SpringBoot项目访问后端页面404

检查项目的路径和mapper映射路径没问题后&#xff0c;发现本级pom文件没有加入web启动模块的pom文件中 maven做项目控制时&#xff0c;要注意将maven模块加入到web启动模块中...

设计模式-综合应用(一)

介绍 使用jQuery做一个模拟购物车的示例 用到的设计模式 工厂模式 单例模式装饰器模式 观察者模式状态模式 模板方法模式 代理模式 UML类图...

大数据Flink(一百):SQL自定义函数(UDF)和标量函数(Scalar Function)

文章目录 SQL自定义函数(UDF)和标量函数(Scalar Function)...

14、Set 和 Map 数据结构

文章目录 14、Set 和 Map 数据结构1. Set1.1 基本用法☆☆☆ 值唯一&#xff0c;没有重复的值☆☆☆ 接受数组、具有 iterable 接口的数据结构☆☆☆ 数组去重1&#xff1a;[...new Set(array)]☆☆☆ 字符串去重&#xff1a;[...new Set(ababbc)].join()☆ 两个对象总是不相等…...

数据结构与算法设计分析——动态规划

目录 一、动态规划的定义二、动态规划的基本要素和主要步骤&#xff08;一&#xff09;最优子结构&#xff08;二&#xff09;重叠子问题 三、贪心法、分治法和动态规划的对比&#xff08;一&#xff09;贪心法&#xff08;二&#xff09;分治法&#xff08;三&#xff09;动态…...

PHPExcel 字母列不够用,针对 AA、AB、AC ... ZZ 这样的列

在PHPExcel 导出功能中&#xff0c;如果字段超过26个字母时&#xff0c;会出现字母不够用A~Z后 AA~AZ来添加后续字段 php中&#xff0c;chr() 函数从指定 ASCII 值返回字符&#xff0c;可以自定义一个方法来返回对应的字母 // $num 列数 1,2,3,4,5,6,7...... function getCol…...

fastdds源码编译安装

如何根据源码编译 fastdds 如何根据源码编译 fastdds 这里是为了根据源码编译一个 fastdds 。 fastdds 依赖 fastcdr Asio TinyXMl2 下载 fastdds 源码 git clone gitgithub.com:eProsima/Fast-DDS.git 进入 下载好的 fastdds 中执行 git submodule update --init --recurs…...

第二证券:风电概念强势拉升,威力传动“20cm”涨停,双一科技等大涨

风电概念20日盘中强势拉升&#xff0c;到发稿&#xff0c;威力传动“20cm”涨停&#xff0c;双一科技涨超17%&#xff0c;顺发恒业亦涨停&#xff0c;金雷股份、大金重工涨约7%&#xff0c;新强联、海力风电涨超5%。 音讯面上&#xff0c;9月以来江苏、广东海风项目加快推动&a…...

DataFrame窗口函数操作

文章最前&#xff1a; 我是Octopus&#xff0c;这个名字来源于我的中文名--章鱼&#xff1b;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github &#xff1b;这博客是记录我学习的点点滴滴&#xff0c;如果您对 Python、Java、AI、算法有兴趣&#xff0c;可以关注我的…...

【德哥说库系列】-RHEL8环境源码编译安装MySQL8.0

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…...

Sandboxie+Buster Sandbox Analyzer打造个人沙箱

一、运行环境和需要安装的软件 实验环境&#xff1a;win7_x32或win7_x64 用到的软件&#xff1a;WinPcap_4_1_3.exe、Sandboxie-3-70.exe、Buster Sandbox Analyzer 重点是Sandboxie必须是3.70版本。下载地址&#xff1a;https://github.com/sandboxie-plus/sandboxie-old/blo…...

源码解析flink的GenericWriteAheadSink为什么做不到精确一次输出

背景 GenericWriteAheadSink是可以用于几乎是精准一次输出的场景&#xff0c;为什么说是几乎精准一次呢&#xff1f;我们从源码的角度分析一下 GenericWriteAheadSink做不到精准一次输出的原因 首先我们看一下flink检查点完成后通知GenericWriteAheadSink开始进行分段的记录…...

C#经典十大排序算法(完结)

C#冒泡排序算法 简介 冒泡排序算法是一种基础的排序算法&#xff0c;它的实现原理比较简单。核心思想是通过相邻元素的比较和交换来将最大&#xff08;或最小&#xff09;的元素逐步"冒泡"到数列的末尾。 详细文章描述 https://mp.weixin.qq.com/s/z_LPZ6QUFNJcw…...

C/C++文件操作(细节满满,part2)

该文章上一篇&#xff1a;C/C文件操作&#xff08;细节满满&#xff0c;part1&#xff09;_仍有未知等待探索的博客-CSDN博客 个人主页&#xff1a;仍有未知等待探索_C语言疑难,数据结构,小项目-CSDN博客 专题分栏&#xff1a;C语言疑难_仍有未知等待探索的博客-CSDN博客 目录 …...

web前端面试-- 手写原生Javascript方法(new、Object.create)

web面试题 本人是一个web前端开发工程师&#xff0c;主要是vue框架&#xff0c;整理了一些面试题&#xff0c;今后也会一直更新&#xff0c;有好题目的同学欢迎评论区分享 ;-&#xff09; web面试题专栏&#xff1a;点击此处 手动实现Object.create 通过Object.create&#…...

C++前缀和算法的应用:得到连续 K 个 1 的最少相邻交换次数 原理源码测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 滑动窗口 题目 给你一个整数数组 nums 和一个整数 k 。 nums 仅包含 0 和 1 。每一次移动&#xff0c;你可以选择 相邻 两个数字并将它们交换。 请你返回使 nums 中包…...

目标检测YOLO实战应用案例100讲-基于YOLOv5的航拍图像旋转目标检测

目录 前言 国内外研究历史与现状 目标检测技术的研究历史与现状...

H5前端开发——BOM

H5前端开发——BOM BOM&#xff08;Browser Object Model&#xff09;是指浏览器对象模型&#xff0c;它提供了一组对象和方法&#xff0c;用于与浏览器窗口进行交互。 通过 BOM 对象&#xff0c;开发人员可以操作浏览器窗口的行为和状态&#xff0c;实现与用户的交互和数据传…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)

服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

【磁盘】每天掌握一个Linux命令 - iostat

目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat&#xff08;I/O Statistics&#xff09;是Linux系统下用于监视系统输入输出设备和CPU使…...

在四层代理中还原真实客户端ngx_stream_realip_module

一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡&#xff08;如 HAProxy、AWS NLB、阿里 SLB&#xff09;发起上游连接时&#xff0c;将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后&#xff0c;ngx_stream_realip_module 从中提取原始信息…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:

根据万维钢精英日课6的内容&#xff0c;使用AI&#xff08;2025&#xff09;可以参考以下方法&#xff1a; 四个洞见 模型已经比人聪明&#xff1a;以ChatGPT o3为代表的AI非常强大&#xff0c;能运用高级理论解释道理、引用最新学术论文&#xff0c;生成对顶尖科学家都有用的…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程

本文较长&#xff0c;建议点赞收藏&#xff0c;以免遗失。更多AI大模型应用开发学习视频及资料&#xff0c;尽在聚客AI学院。 本文全面剖析RNN核心原理&#xff0c;深入讲解梯度消失/爆炸问题&#xff0c;并通过LSTM/GRU结构实现解决方案&#xff0c;提供时间序列预测和文本生成…...

音视频——I2S 协议详解

I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议&#xff0c;专门用于在数字音频设备之间传输数字音频数据。它由飞利浦&#xff08;Philips&#xff09;公司开发&#xff0c;以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...

基于PHP的连锁酒店管理系统

有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...

Oracle11g安装包

Oracle 11g安装包 适用于windows系统&#xff0c;64位 下载路径 oracle 11g 安装包...