NettyのBufferChannelSelector用法

这一篇介绍Buffer&Channel&Selector的常见API使用案例

1、Buffer

        1.1、从Buffe中读取/写入

        以ByteBuffer为例。Buffer需要和Channel结合使用(在上一篇中提到,通道是数据传输的载体,缓冲区是数据的临时存储区)。

        那么如何获取Channel对象?我们可以通过FileInputStream的.getChannel() 方法获取。

new FileInputStream(new File("D:\\Idea_workspace\\2024\\netty\\src\\data.txt")).getChannel()

        获取ByteBuffer对象需要通过.allocate() 静态方法获取,同时需要指定容量:

 ByteBuffer byteBuffer = ByteBuffer.allocate(10);

        从channel中读取数据,写入ByteBuffer,则用到了channel.read()方法,它的返回值如果为-1则代表读取结束。

int len = channel.read(byteBuffer);

        如果需要读取ByteBuffer中的内容,需要将Buffer从写模式切换到读模式,用到了Buffer的.flip() 方法,然后利用.get()方法进行读取:

byteBuffer.flip();

        完整案例:

@Slf4j
public class TestByteBuffer {public static void main(String[] args) {try (FileChannel channel = new FileInputStream(new File("D:\\Idea_workspace\\2024\\netty\\src\\data.txt")).getChannel()) {//准备缓冲区 指定容量为10ByteBuffer byteBuffer = ByteBuffer.allocate(10);while (true) {//从channel中读取数据,写入ByteBufferint len = channel.read(byteBuffer);log.debug("获取到的长度:{}",len);//len == -1 说明已读取完成if (len == -1){break;}//读取buffer的内容//flip():将Buffer从写模式切换到读模式。byteBuffer.flip();while (byteBuffer.hasRemaining()){log.debug("获取到的字节:{}",(char)byteBuffer.get());}//clear():清空Buffer,准备写入。byteBuffer.clear();}} catch (IOException e) {}}
}

        在前一篇中提到,缓冲区会维护一个类似于数组的结构,其中包含了position(指针)、limit(限制)和capacity(容量)。三个关键属性,下面我们再通过一个案例结合图片分析:

        创建一个容量为10的缓冲区:

 ByteBuffer allocate = ByteBuffer.allocate(10);
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 00 00 00 00 00 00 00                   |abc.......      |
+--------+-------------------------------------------------+----------------+

         放入三个元素:

allocate.put(new byte[]{97,98,99});
+--------+-------------------- all ------------------------+----------------+
position: [3], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 00 00 00 00 00 00 00                   |abc.......      |
+--------+-------------------------------------------------+----------------+

        切换成读模式,并且获取一个元素:

 allocate.flip();allocate.get();
position: [1], limit: [3]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 00 00 00 00 00 00 00                   |abc.......      |
+--------+-------------------------------------------------+----------------+

        如果不切换成读模式呢?那么指针在position: [3] 读取到的是00

        1.1.1、compact()

        利用compact()切换成写模式。将未读的数据复制到Buffer的开头,然后将位置设到最后一个未读元素的后面。(此时数组中读取了索引为0的元素,调用 compact()方法后,就将1,2索引上的元素复制到0,1上,如果下次写入是从2索引开始)

allocate.compact();
position: [2], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 63 63 00 00 00 00 00 00 00                   |bcc.......      |
+--------+-------------------------------------------------+----------------+

         再次放入三个元素,覆盖掉2索引上的63:

allocate.put(new byte[]{100,101,102});
+--------+-------------------- all ------------------------+----------------+
position: [5], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 63 64 65 66 00 00 00 00 00                   |bcdef.....      |
+--------+-------------------------------------------------+----------------+

        利用clear()切换到写模式,清空数组中的元素:

allocate.clear();
+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [10]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 62 63 64 65 66 00 00 00 00 00                   |bcdef.....      |
+--------+-------------------------------------------------+----------------+

        切换成读模式,除了常规的filp()方法,还有rewind()、mark() & reset()方法:

        1.1.2、rewind()

        我们再次创建一个容量为10的缓冲区,并且初始化4个元素,使用filp()切换到读模式,再获取两个元素:

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put(new byte[]{97,98,99,100});
buffer.flip();
buffer.get();
buffer.get();

        此时的position在2位置。

 position: [2], limit: [4]

         +-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+
+--------+-------------------- all ------------------------+----------------+

        rewind() 方法的底层会将position重新设置为0:

         调用rewind() 方法:

buffer.rewind();

         position重新回到0的位置

+--------+-------------------- all ------------------------+----------------+
position: [0], limit: [4]
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+

        1.1.3、mark() & reset()

         mark() & reset() 通常会结合使用。 mark() 方法的作用是标记当前的position,reset() 方法的作用是重置position为 mark() 方法标记的位置。

        接上面的案例,经过了rewind() 方法, position重新回到0的位置。我们进行四次读取,但是在第二次读取结束后使用 mark() 方法:

buffer.get();
buffer.get();
buffer.mark();
buffer.get();
buffer.get();
+--------+-------------------- all ------------------------+----------------+
position: [4], limit: [4]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+

        调用reset() 方法:

buffer.reset();
+--------+-------------------- all ------------------------+----------------+
position: [2], limit: [4]+-------------------------------------------------+|  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 61 62 63 64 00 00 00 00 00 00                   |abcd......      |
+--------+-------------------------------------------------+----------------+
         1.1.4、get(index)

        get(index)是get()方法的重载。和get()方法的区别在于获取元素不会移动指针。

         1.2、半包、粘包

        半包现象:

        一个完整的应用层数据包在传输过程中被分成了多个TCP包发送和接收。接收方在读取数据时,可能只能读到一个完整包的一部分:

        假设发送端要发送一个大小为1024字节的数据包,但由于网络或缓冲区限制,这个数据包被拆分成两个TCP包,第一个包包含前512字节,第二个包包含后512字节。接收方在读取时可能先读到前512字节,接下来再读到后512字节。

        其原因在于:

  • 在网络传输过程中,数据包可能会被拆分成多个较小的TCP包进行传输。
  • 发送端的缓冲区限制了每次可以发送的数据量,因此较大的数据包可能会被拆分。
  • 接收方读取数据的速度可能赶不上数据到达的速度,导致每次读取时只能获取部分数据。

        粘包现象

        多个应用层数据包在传输过程中被粘合在一起,接收方在读取数据时一次性读取到了多个数据包的内容:

        假设发送端发送了两个大小分别为512字节的数据包,接收方由于读取速度较慢,可能一次性读取到1024字节的数据,这样两个数据包的内容就粘在了一起。

        其原因在于:

  • 发送端以较快的速度发送多个数据包,而接收方读取数据的速度较慢,导致多个数据包积累在接收缓冲区中。
  • TCP是面向字节流的协议,没有明确的消息边界,多个应用层数据包可能在TCP层被拼接在一起。        

        为了处理半包和粘包现象,可以采用以下几种常见的方法:

  1. 定长消息:预先规定每个消息的长度,接收方根据约定的长度读取固定大小的字节数据。
  2. 分隔符:在每个消息之间插入特殊的分隔符,接收方读取数据时根据分隔符进行拆分。
  3. 消息头:在每个消息前添加固定长度的消息头,消息头中包含消息的长度信息,接收方首先读取消息头,然后根据消息头中的长度信息读取相应长度的消息体。

下面是一种通过设置消息头进行解决的案例:

发送方

public void send(SocketChannel socketChannel, byte[] data) throws IOException {//假设我们data的长度为6//创建 ByteBuffer 其大小为消息头(4 字节)加上消息体(6 字节)的长度:ByteBuffer buffer = ByteBuffer.allocate(4 + data.length);//将消息体长度(6)放入缓冲区,作为消息头://写入 4 字节的消息头,内容为 6buffer.putInt(data.length);// 将消息体放入缓冲区buffer.put(data);// 切换缓冲区为读模式,准备写入到SocketChannel中buffer.flip();// 循环写入SocketChannel,确保缓冲区中的数据全部发送出去while (buffer.hasRemaining()) {socketChannel.write(buffer);}
}

接收方

public void receive(SocketChannel socketChannel) throws IOException {// 创建一个ByteBuffer用于读取消息头(4字节)ByteBuffer headerBuffer = ByteBuffer.allocate(4);// 确保消息头全部读入缓冲区while (headerBuffer.hasRemaining()) {socketChannel.read(headerBuffer);}// 切换缓冲区为读模式,准备读取消息头中的数据headerBuffer.flip();// 读取消息头,获取消息体的长度// length 的值为 6int length = headerBuffer.getInt();// 创建一个ByteBuffer用于读取消息体// length 的值为 6ByteBuffer dataBuffer = ByteBuffer.allocate(length);// 确保消息体全部读入缓冲区while (dataBuffer.hasRemaining()) {socketChannel.read(dataBuffer);}// 切换缓冲区为读模式,准备读取消息体中的数据dataBuffer.flip();// 从缓冲区中读取消息体数据byte[] data = new byte[length];dataBuffer.get(data);}

2、Channel

        我们重点介绍与网络编程有关的SocketChannel和ServerSocketChannel。

       SocketChannel和ServerSocketChannel,又分为阻塞和非阻塞两种模式:

        2.1、阻塞模式

        首先需要创建服务器:

/*** nio的阻塞模式 服务端*/
@Slf4j
public class Server {public static void main(String[] args) throws IOException {//缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(16);//创建服务器ServerSocketChannel ssc = ServerSocketChannel.open();//绑定端口ssc.bind(new InetSocketAddress(8080));//连接集合ArrayList<SocketChannel> channels = new ArrayList<>();while (true){//接受客户端的信息log.debug("开始链接...");SocketChannel socketChannel = ssc.accept();//没有连接时会阻塞log.debug("链接完成...{}",socketChannel);channels.add(socketChannel);for (SocketChannel channel : channels) {//把信息读取到缓冲区log.debug("开始读取");channel.read(byteBuffer);//读取不到数据时会阻塞byteBuffer.flip();debugRead(byteBuffer);byteBuffer.clear();log.debug("读取结束");}}}
}

        创建前端:

/*** nio的阻塞模式 前端*/
public class Client {public static void main(String[] args) throws IOException {SocketChannel socketChannel = SocketChannel.open();socketChannel.connect(new InetSocketAddress("localhost",8080));// 发送消息到服务器String message = "Hello, Server!";ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());socketChannel.write(buffer);}
}

        这里的阻塞主要体现在两个地方,第一在于,服务器接受客户端连接时,如果一直没有连接,会阻塞。第二在于,连接建立上,但是客户端一直没有发送消息,同样会阻塞。

        同时启动前端和服务端:

        此时前端还没有连接上服务器,服务器一直阻塞在SocketChannel socketChannel = ssc.accept()

        前端尚未发出消息:

        服务器一直阻塞在channel.read(byteBuffer);

        2.2、非阻塞模式

        非阻塞模式和阻塞模式的代码大致相同,在服务器对于SocketChannel和ServerSocketChannel设置configureBlocking属性为false。

        只启动服务器,很显然此时是没有任何一个客户端连接上的,没有像阻塞模式那样在SocketChannel socketChannel = ssc.accept();这一行一直等待。

        前端连接上之后,还没有发送消息:

        服务器没有在int read = channel.read(byteBuffer);阻塞。

3、Selector

        使用Channel的阻塞模式效率很低,而非阻塞模式,如果一直没有客户端连接或者读取不到数据,就会在循环中空转,也是对cpu的一种浪费,实际开发中也不会采用这样的模式。

        为了改进上面的弊端,引入了Selector(选择器),核心思想是一个Selector去管理多个Channel,根据Channel注册的不同事件类型去进行操作

/*** NIO selector*/
@Slf4j
public class ServerSelector {public static void main(String[] args) throws IOException {//创建selector,管理多个channelSelector selector = Selector.open();//创建服务器ServerSocketChannel ssc = ServerSocketChannel.open();//如果要注册到 selector上 必须先设置成非阻塞ssc.configureBlocking(false);//把ssc注册到selector上,并且设置监听accept事件SelectionKey key = ssc.register(selector, 0, null);key.interestOps(SelectionKey.OP_ACCEPT);log.debug("注册的key:{}", key);//绑定端口ssc.bind(new InetSocketAddress(8080));//连接集合ArrayList<SocketChannel> channels = new ArrayList<>();while (true) {//如果没有事件发生会阻塞selector.select();Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()){SelectionKey selectionKey = it.next();log.debug("获取的Key:{}",selectionKey);//根据获取到的SelectionKey进行分派不同的事件if (selectionKey.isAcceptable()) {ServerSocketChannel channel = ((ServerSocketChannel) selectionKey.channel());SocketChannel sc = channel.accept();log.debug("获取的连接:{}",sc);//将sc同时注册到selector上,监听读事件sc.configureBlocking(false);SelectionKey scKey = sc.register(selector, 0, null);scKey.interestOps(SelectionKey.OP_READ);}else if (selectionKey.isReadable()){SocketChannel socketChannel = (SocketChannel) selectionKey.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(16);socketChannel.read(byteBuffer);byteBuffer.flip();debugRead(byteBuffer);}}}}
}

        只启动服务端,因为没有连接事件,在selector.select()这一行陷入阻塞。

        启动客户端:

        并且尝试向服务端发送数据:

        服务端没有接收到消息,反而出现了一个空指针异常,这是什么原因?


        Selector是一个抽象类,具体实现是WindowsSelectorImpl 

System.out.println(selector.getClass());

        根据堆栈信息,出现异常在52行,我们在服务端打断点看一下原因:

        重点关注selectedKeys和keys两个成员变量:

        把ServerSocketChannel注册到selector上时,keys将其记录:

        在运行了selector.select()方法后,selectedKeys成员变量中也会记录ServerSocketChannel

        在将SocketChannel注册到selector上后,keys将其记录。

 

        此时第一次循环结束,注意,selectedKeys中的ServerSocketChannel没有被删除,下一次循环依旧会匹配到ServerSocketChannel的accept事件

        再次进入selectionKey.isAcceptable()分支。此时没有新的连接,channel.accept()会返回null。(为什么会返回null?因为设置的是非阻塞模式),从而导致空指针。

        从上面的过程中可以发现,selectedKeys 集合中的元素不会自动移除,需要我们手动删除。(也是为什么要使用迭代器而不是增强for循环的原因,如果使用增强for一边遍历一边增删集合中的元素,会导致并发修改异常。

        改进上面的代码:

SelectionKey selectionKey = it.next();
log.debug("获取的Key:{}",selectionKey);
it.remove();

小结:

       改造服务器的代码,分为以下的步骤:

  1. 创建selector,管理多个channel。
  2. 将ServerSocketChannel或SocketChannel注册到selector上(必须设置成非阻塞模式)。并且设置即将监听的事件。
  3. 调用selector的select方法。
  4. 得到selector中所有key并遍历,根据不同的key进行任务分派。
  5. 移除key
        3.1、Selector的消息边界问题

        Selector也是基于Buffer实现,那么它是如何解决半包,粘包问题的呢?同样有三种方式:

  1. 在消息传输之前固定好Buffer的容量,例如发送了两条消息,第一条消息占了8个字节,第二条消息只有2个字节,但无论消息有多大,都固定容量为10。
  2. 在消息中加入特殊的符号,根据符号进行拆分。
  3. 使用消息头+消息体。消息头固定大小,记录了消息体的大小。

        我们这次使用第二种方式:

        消息拆分方法,假设我们在消息中使用'\n'字符进行拆分:

 /*** abc123\nabc...\nab ->* abc123* abc...* ab* @param source*/private static void split(ByteBuffer source) {//切换到读取模式source.flip();//找到完整的\n字符for (int i = 0; i < source.limit(); i++) {if (source.get(i) == '\n'){int length = i + 1 - source.position();ByteBuffer target = ByteBuffer.allocate(length);for (int j = 0; j < length; j++) {target.put(source.get());}debugAll(target);}};//将剩余部分向前压缩source.compact();}

        在读取消息时,就不能将ByteBuffer作为一个局部变量了。如果触发了多次循环,局部变量每次获取到的都不是同一个ByteBuffer。我们可以在将SocketChannel或ServerSocketChannel注册到selector时,给其绑定一个专属的ByteBuffer(类似于把队列绑定到交换机上):

ByteBuffer byteBuffer = ByteBuffer.allocate(16);
SelectionKey scKey = sc.register(selector, 0,byteBuffer );

        需要使用的时候再取出:

//取出附件
ByteBuffer scByteBuffer = (ByteBuffer) selectionKey.attachment();

        改造读取消息的代码,加入扩容机制。

split(scByteBuffer);
//加入扩容机制
if (scByteBuffer.position() == scByteBuffer.limit()){ByteBuffer newByteBuffer = ByteBuffer.allocate(scByteBuffer.capacity() * 2);scByteBuffer.flip();newByteBuffer.put(scByteBuffer);selectionKey.attach(newByteBuffer);
}

        下面我们通过debug的方式加深一下印象:

        客户端即将发送的消息如下,第一条消息超过了16个字节:

        切换到读取模式前,ByteBuffer已经占满了16个字节:

        切换到读取模式,从0索引开始读:

        第一次循环结束后仍然未找到分隔符的位置,触发扩容:

        将扩容后的ByteBuffer(32长度)重新放回附件中,替换掉原先16长度的:

        下一次循环进入split方法:

        在第18个字节的位置找到了分隔符:

 

        3.2、可写事件

        如果服务器端需要向客户端一次写入较多的数据,可以利用Selector的可写事件分批完成。

        其核心思想在于,如果第一次向客户端没有完全写入,就给socketChannel向Selector注册一个可写事件, 并且将未读完的ByteBuffer放入附件中。下次循环监听到了可写事件,进入分支再次向客户端写入上一次未完成的内容。直到全部写入完毕,再释放附件,并且删除可写事件

        完整案例:

public class ServerWrite {public static void main(String[] args) throws IOException {ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));Selector selector = Selector.open();//将ServerSocketChannel注册到Selector上,默认连接模式serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,null);while (true){selector.select();Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> it = selectionKeys.stream().iterator();while (it.hasNext()) {SelectionKey selectionKey = it.next();it.remove();if (selectionKey.isAcceptable()) {SocketChannel socketChannel =  handleAccept(selector,selectionKey);//将SocketChannel注册到Selector上,默认读取模式SelectionKey scKey = socketChannel.register(selector, SelectionKey.OP_READ, null);StringBuilder sb = new StringBuilder();for (int i = 0; i < 50000000; i++) {sb.append("a");}ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(sb.toString());//未处理完if (byteBuffer.hasRemaining()) {//SocketChannel再次注册写模式scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);//将未处理完的ByteBuffer放入SocketChannel  scKey的附件中scKey.attach(byteBuffer);}//监听到读取模式}else if (selectionKey.isWritable()){//从附件中取出ByteBufferByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();//取出SocketChannelSocketChannel socketChannel = (SocketChannel) selectionKey.channel();//再次写入int write = socketChannel.write(byteBuffer);System.out.println(write);//直到写入完成,删除附件和写模式if (!byteBuffer.hasRemaining()){selectionKey.attach(null);selectionKey.interestOps(selectionKey.interestOps() - SelectionKey.OP_WRITE);}}}}}private static SocketChannel handleAccept(Selector selector,SelectionKey selectionKey) throws IOException {ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();ssc.configureBlocking(false);return ssc.accept();}
}

附录:

NIO Selector四大事件触发的时机:

  • OP_ACCEPT:当有客户端尝试连接到服务器时,ServerSocketChannel会触发OP_ACCEPT事件。
  • OP_CONNECT:当客户端发起连接请求后,连接操作完成时会触发OP_CONNECT事件。
  • OP_READ:当通道中有数据可读时,会触发OP_READ事件。这意味着客户端或服务器端的通道有数据可以读取。
  • OP_WRITE:当通道准备好写数据时,会触发OP_WRITE事件。这意味着可以向通道写入数据而不会阻塞。

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

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

相关文章

OSFP 1类LSA详解

概述 上图为1类LSA的实际报文结构 , 在开始之前一定需要说明 , 1类LSA是OSPF中最复杂的LSA类型 , 在LSA头部的文章中详细介绍了 LS Type / Link State ID / Adv Router 3种头部字段 , 在1类LSA的主体内容中还存在类似的字段十分的相似 , 很多网络从业者难以理解的点就在于此 , …

orbslam2代码解读(2):tracking跟踪线程

书接上回&#xff0c;mpTracker->GrabImageMonocular(im,timestamp)函数处理过程&#xff1a; 如果图像是彩色图&#xff0c;就转成灰度图如果当前帧是初始化的帧&#xff0c;那么在构建Frame的时候&#xff0c;提取orb特征点数量为正常的两倍&#xff08;目的就是能够在初…

vue3中$attrs与inheritAttrs的使用

Vue 3 引入了一些新特性和改进&#xff0c;其中之一就是对 $attrs 的处理方式。在 Vue 2 中&#xff0c;$attrs 包含了父组件传递给子组件的属性&#xff0c;但不包括子组件已经声明的 props。在 Vue 3 中&#xff0c;$attrs 的行为有所变化&#xff1a; 默认情况下&#xff0…

14. RTCP 协议

RTCP 协议概述 RTCP&#xff08;Real-time Transport Control Protocol 或 RTP Control Protocol 或简写 RTCP&#xff09;&#xff0c;实时传输控制协议&#xff0c;是实时传输协议&#xff08;RTP&#xff09;的一个姐妹协议。 注&#xff1a;RTP 协议和 RTP 控制协议&#…

Postgresql源码(135)生成执行计划——Var的调整set_plan_references

1 总结 set_plan_references主要有两个功能&#xff1a; 拉平&#xff1a;生成拉平后的RTE列表&#xff08;add_rtes_to_flat_rtable&#xff09;。调整&#xff1a;调整前每一层计划中varno的引用都是相对于本层RTE的偏移量。放在一个整体计划后&#xff0c;需要指向一个统一…

Material-UI create-react-app 创建移动端 H5

当使用 create-react-app 和 Material-UI 来创建移动端 H5 页面时,你需要考虑几个关键点来确保页面在移动设备上表现良好。以下是一些步骤和最佳实践: 创建 React 项目 使用 create-react-app 快速创建一个新的 React 项目: npx create-react-app my-mobile-app cd my-mobil…

京东一面测开(KPI)

京东一面测开凉经&#xff08;笔试ak&#xff09; 3.8 面试官&#xff1a;你很优秀啊&#xff0c;你不用谦虚 没问技术相关&#xff0c;问了如何设计测试用例步骤一些理论&#xff1a; 什么是软件测试&#xff1f;其目的是什么&#xff1f; 软件测试有哪些类型&#xff1f;请列…

架构设计-全局异常处理器404、405的问题

java web 项目中经常会遇到异常处理的问题&#xff0c;普遍的做法是使用全局异常处理&#xff0c;这样做有以下几种原因&#xff1a; 集中化处理&#xff1a;全局异常处理允许你在一个集中的地方处理整个应用程序中的异常。这有助于减少代码重复&#xff0c;因为你不必在每个可…

GCROOT节点有哪些?

GCROOT节点在Java虚拟机中扮演着判断对象是否存活的起点角色。它们主要包括以下几种类型&#xff1a; 虚拟机栈中引用的对象&#xff1a; 这通常指的是各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。这些对象在虚拟机栈中被直接引用&#xff0c;因此它们及其…

项目方案:社会视频资源整合接入汇聚系统解决方案(五)

目录 一、概述 1.1 应用背景 1.2 总体目标 1.3 设计原则 1.4 设计依据 1.5 术语解释 二、需求分析 2.1 政策分析 2.2 业务分析 2.3 系统需求 三、系统总体设计 3.1设计思路 3.2总体架构 3.3联网技术要求 四、视频整合及汇聚接入 4.1设计概述 4.2社会视频资源分…

QT项目实战: 五子棋小游戏

目录 内容介绍 一.添加头文件 二.画棋盘 1.宏定义 2.棋盘 三.画棋子 四.获取棋子摆放位置 五.判断棋子存在 六.判断胜利 1.变量定义和初始化 2.检查获胜条件 3.游戏结束处理 七.重绘 八.效果展示 九.代码 1.mainwindow.h 2.mainwindow.cpp 3.chessitem.h 4…

HTML 中使用 JavaScript 的具体方式

文章目录 一、JavaScript 脚本添加方式1.1 内联 JavaScript1.2 内嵌 JavaScript1.3 外部引用 JavaScript 二、外部引用 JavaScript 的注意事项 一、JavaScript 脚本添加方式 在 HTML 文件中使用 JavaScript 代码主要由以下三种方法&#xff1a; 内联内嵌外部引用 1.1 内联 J…

【python】在【机器学习】与【数据挖掘】中的应用:从基础到【AI大模型】

目录 &#x1f497;一、Python在数据挖掘中的应用&#x1f495; &#x1f496;1.1 数据预处理&#x1f49e; &#x1f496;1.2 特征工程&#x1f495; &#x1f497;二、Python在机器学习中的应用&#x1f495; &#x1f496;2.1 监督学习&#x1f49e; &#x1f496;2.2…

【MySQL】(基础篇七) —— 通配符和正则表达式

通配符和正则表达式 本章介绍什么是通配符、如何使用通配符以及怎样使用LIKE操作符进行通配搜索&#xff0c;以便对数据进行复杂过滤&#xff1b;如何使用正则表达式来更好地控制数据过滤。 目录 通配符和正则表达式LIKE操作符百分号(%)通配符下划线(_)通配符 通配符使用技巧正…

深入理解 C++ 智能指针

文章目录 一、引言二、 原始指针的问题1、原始指针的问题2、智能指针如何解决这些问题 三、智能指针的类型四、std::shared_ptr1、shared_ptr使用2、shared_ptr的使用注意事项3、定制删除器4、shared_ptr的优缺点5、shared_ptr的模拟实现 五、std::unique_ptr1、unique_ptr的使…

SpringSecurity入门(三)

12、密码加密 12.1、不指定具体加密方式&#xff0c;通过DelegatingPasswordEncoder&#xff0c;根据前缀自动选择 PasswordEncoder passwordEncoder PasswordEncoderFactories.createDelegatingPasswordEncoder();12.2、指定具体加密方式 // Create an encoder with streng…

Java中的CAS无锁并发原理是怎样的

CAS&#xff08;Compare And Swap&#xff09;即比较并交换&#xff0c;是一种无锁并发算法的核心原理。 简单来说&#xff0c;CAS 原理通过以下三个步骤来实现&#xff1a; 1. 读取当前值&#xff1a;获取目标变量当前的值。 2. 比较预期值&#xff1a;将读取到的值与预…

逆运动学IK原理举例说明

逆运动学IK原理举例说明 逆运动学(Inverse Kinematics, IK)是计算机器人各个关节的位置和角度,使机器人末端(如手或脚)达到目标位置的过程。IK 是机器人学中的一个重要问题,因为它允许我们从任务空间(如末端执行器的位置和姿态)控制机器人关节空间(如关节角度)。 逆…

【iOS】UI学习——登陆界面案例、照片墙案例

文章目录 登陆界面案例照片墙案例 登陆界面案例 这里通过一个登陆界面来复习一下前面学习的内容。 先在接口部分定义两个UILabel、两个UITextField、两个UIButton按键&#xff1a; #import <UIKit/UIKit.h>interface ViewController : UIViewController {UILabel* _lb…

2024050501-重学 Java 设计模式《实战命令模式》

重学 Java 设计模式&#xff1a;实战命令模式「模拟高档餐厅八大菜系&#xff0c;小二点单厨师烹饪场景」 一、前言 持之以恒的重要性 初学编程往往都很懵&#xff0c;几乎在学习的过程中会遇到各种各样的问题&#xff0c;哪怕别人那运行好好的代码&#xff0c;但你照着写完…