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() 方法;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/162140.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习,神经网络中,自注意力跟卷积神经网络之间有什么样的差异或者关联?

如图 6.38a 所示&#xff0c;如果用自注意力来处理一张图像&#xff0c;假设红色框内的“1”是要考虑的像素&#xff0c;它会产生查询&#xff0c;其他像素产生 图 6.37 使用自注意力处理图像 键。在做内积的时候&#xff0c;考虑的不是一个小的范围&#xff0c;而是整张图像的…

代码的艺术-Writing Code Like a Pianist | 京东云技术团队

前言 如何评定一个系统的质量&#xff1f;什么样的系统或者软件可以称之为高质量&#xff1f;可以从三个角度来看&#xff0c;一是架构设计&#xff0c;例如技术选型、分布式系统中的数据一致性考虑等&#xff0c;二是项目管理&#xff0c;无论是敏捷开发还是瀑布式开发&#…

【SpringCloud微服务项目实战-mall4cloud项目(4)】——mall4cloud-rbac

mall4cloud-rbac角色权限访问控制模块 系统架构与模块介绍系统架构rbac模型介绍 相关代码权限校验接口代码 补充 代码地址 github地址 fork自github原始项目 gitee地址 fork自gitee原始项目 系统架构与模块介绍 系统架构 从图中可以看到&#xff0c;微服务集群中&#xff0c;…

基于Qt QSlider滑动条小项目

QSlider 是滑动条控件,滑动条可以在一个范围内拖动,并将其位置转换为整数 1. 属性和方法 QSlider 继承自 QAbstractSlider,它的绝大多数属性都是从 QAbstractSlider 继承而来的。 2.QSlider信号 - `valueChanged(int value)`: 当滑块的值改变时发出信号,传递当前滑块的值…

基于SVM+Webdriver的智能NBA常规赛与季后赛结果预测系统——机器学习算法应用(含python、ipynb工程源码)+所有数据集(三)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 特征提取3. 模型训练及评估1&#xff09;常规赛预测模型2&#xff09;季后赛模型创建 4. 模型训练准确率 相关其它博客工程源代码下载其它资料下载 前言 本项目使用了从NBA官方网站获得的数据&…

JOSEF约瑟 漏电继电器 JHOK-ZBG1 φ25mm AC220V 0.1A/0.1S 分体式

系列型号 JHOK-ZBG1 φ25mm漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBG2 φ25mm漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBG1 φ45mm漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBG2 φ45mm 漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBG1 φ100mm漏电&a…

猜数字游戏(Rust实现)

文章目录 游戏说明游戏效果展示游戏代码游戏代码详解生成神秘数字读取用户输入解析用户输入进行猜测比较 游戏说明 游戏说明 游戏运行逻辑如下&#xff1a; 随机生成一个1-100的数字作为神秘数字&#xff0c;并提示玩家进行猜测。如果玩家猜测的数字小于神秘数字&#xff0c;则…

初始web项目tomcat部署报错404

问题 简单地创建了一个web项目&#xff0c;结果一运行就404咧&#xff0c;真滴烦。。。接下来的项目也没法继续了 问题原因&#xff1a;缺少文件 其实造成这样问题的原因有不少&#xff0c;但在这里我是踩了一个坑。在出问题之前&#xff0c;我运行的其他项目都是可以跑的&…

2. 验证1101序列(Mealy)

题目要求&#xff1a; 用 M e a l y \rm Mealy Mealy型状态机验证 1101 1101 1101序列 题目描述&#xff1a; 使用状态机验证 1101 1101 1101序列&#xff0c;注意&#xff1a;允许重复子序列。 方法一&#xff1a; 去掉 M o o r e \rm Moore Moore的 s 4 s_4 s4​&#xff…

掌握.NET基础知识(一)

前言 本文将讲解一些.NET基础。NET基础是指在计算机编程中使用.NET框架所需要的基础知识..NET的认识: .NET是由微软开发的一个跨平台的应用程序开发框架。它包括一个运行时环境和一个面向对象的程序库&#xff0c;可以用于开发各种类型的应用程序&#xff0c;包括桌面应用程序、…

数据驱动娱乐的未来:揭秘爱奇艺如何利用Apache Spark实现个性化推荐和内容分析

Apache Spark 在爱奇艺的现状与应用 Apache Spark 是爱奇艺大数据平台的核心组件&#xff0c;被广泛应用于数据处理、数据同步和数据查询分析等关键场景。在数据处理方面&#xff0c;爱奇艺通过数据开发平台支持开发者提交 Spark Jar 包任务或 Spark SQL 任务&#xff0c;进行…

【JVM系列】- 类加载子系统与加载过程

类加载子系统与加载过程 &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学习心得&#xff0c;欢迎指正…

25.1 MySQL SELECT语句

1. SQL概述 1.1 SQL背景知识 1946年, 世界上诞生了第一台电脑, 而今借由这台电脑的发展, 互联网已经成为一个独立的世界. 在过去几十年里, 许多技术和产业在互联网的舞台上兴衰交替. 然而, 有一门技术却从未消失, 甚至日益强大, 那就是SQL.SQL(Structured Query Language&…

进化策略算法

前言 进化策略 (Evolution Strategy) 后面都简称 ES&#xff0c;其本质就是&#xff1a;种群通过交叉产生后代&#xff0c;我们只保留较好的父代和子代&#xff0c;一直这样迭代下去&#xff0c; 我们的保留方式是&#xff1a; 父代产生后代&#xff0c;然后将后代DNA和原来的…

02_单片机及开发板介绍

单片机简介 单片机&#xff0c;又称为微控制器&#xff08;Microcontroller&#xff09;&#xff0c;是一种集成了微处理器核心、存储器、输入/输出接口及各种功能模块的集成电路芯片。它通常由中央处理器&#xff08;CPU&#xff09;、存储器、输入/输出接口以及各种外设组成&…

【Leetcode】 707. 设计链表

你可以选择使用单链表或者双链表&#xff0c;设计并实现自己的链表。 单链表中的节点应该具备两个属性&#xff1a;val 和 next 。val 是当前节点的值&#xff0c;next 是指向下一个节点的指针/引用。 如果是双向链表&#xff0c;则还需要属性 prev 以指示链表中的上一个节点…

保序回归与金融时序数据

保序回归在回归问题中的作用是通过拟合一个单调递增或递减的函数&#xff0c;来保持数据点的相对顺序特性。 一、保序回归的作用 主要用于以下情况&#xff1a; 1. 有序数据&#xff1a;当输入数据具有特定的顺序关系时&#xff0c;保序回归可以帮助保持这种顺序关系。例如&…

rust学习—— 控制流if 表达式

控制流 根据条件是否为真来决定是否执行某些代码&#xff0c;或根据条件是否为真来重复运行一段代码&#xff0c;是大部分编程语言的基本组成部分。Rust 代码中最常见的用来控制执行流的结构是 if 表达式和循环。 if 表达式 if 表达式允许根据条件执行不同的代码分支。你提供…

vue-cli脚手架创建项目时报错Error: command failed: npm install --loglevel error

项目背景 环境&#xff1a;vue-cli 5.x 在工程文件中&#xff0c;后端模块wms已经创建完成&#xff0c;现在想新建一个名为vue-web的前端模块 执行命令vue create vue-web时&#xff0c; 报错Error: command failed: npm install --loglevel error 问题分析及解决 排查过程…

Linux性能优化--使用性能工具发现问题

9.0 概述 本章主要介绍综合运用之前提出的性能工具来缩小性能问题产生原因的范围。阅读本章后&#xff0c;你将能够&#xff1a; 启动行为异常的系统&#xff0c;使用Linux性能工具追踪行为异常的内核函数或应用程序。启动行为异常的应用程序&#xff0c;使用Linux性能工具追…