zoukankan      html  css  js  c++  java
  • 网络编程之ByteBuffer

    一、简介

      数据的传输不是按照原有的磨样进行的,是经过一定的转换的,我们经常用到的也就是ByteBuffer,除此之外还有ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer。Bytebuffer还有一些子类MappedByteBuffer、DirectByteBuffer、HeapByteBuffer,详见下图

    二、ByteBuffer的核心属性

    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    • capacity:缓冲区的容量。通过构造函数赋予,一旦设置,无法更改
    • limit:缓冲区的界限。位于limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量
    • position下一个读写位置的索引(类似PC)。缓冲区的位置不能为负,并且不能大于limit
    • mark:记录当前position的值。position被改变后,可以通过调用reset() 方法恢复到mark的位置。

    以上四个属性必须满足以下要求:mark <= position <= limit <= capacity

    看一段代码

    public class TestByteBuffer {
        public static void main(String[] args) {
            // 获得FileChannel
            try (FileChannel channel = new FileInputStream("stu.txt").getChannel()) {
                // 获得缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(10);
                int hasNext = 0;
                StringBuilder builder = new StringBuilder();
                while((hasNext = channel.read(buffer)) > 0) {
                    // 切换模式 limit=position, position=0
                    buffer.flip();
                    // 当buffer中还有数据时,获取其中的数据
                    while(buffer.hasRemaining()) {
                        builder.append((char)buffer.get());
                    }
                    // 切换模式 position=0, limit=capacity
                    buffer.clear();
                }
                System.out.println(builder.toString());
            } catch (IOException e) {
            }
        }
    }

    关键的几个步骤是

    1、向 buffer 写入数据,例如调用 channel.read(buffer)

    2、调用 flip() 切换至读模式,flip会使得buffer中的limit变为position,position变为0,调用 buffer.get()从 buffer 读取数据

    3、调用 clear() 或者compact()切换至写模式,调用clear()方法时position=0,limit变为capacity,调用compact()方法时,会将缓冲区中的未读数据压缩到缓冲区前面

     三、ByteBuffer的核心方法

    1、put()方法

    • put()方法可以将一个数据放入到缓冲区中。
    • 进行该操作后,postition的值会+1,指向下一个可以放入的位置。capacity = limit ,为缓冲区容量的值。

     2、flip()方法

    • flip()方法会切换对缓冲区的操作模式,由写->读 / 读->写
    • 进行该操作后
      • 如果是写模式->读模式,position = 0 , limit 指向最后一个元素的下一个位置,capacity不变
      • 如果是读->写,则恢复为put()方法中的值

     3、get()方法

    • get()方法会读取缓冲区中的一个值
    • 进行该操作后,position会+1,如果超过了limit则会抛出异常
    • 注意:get(i)方法不会改变position的值

    4、rewind()方法

    • 该方法只能在读模式下使用
    • rewind()方法后,会恢复position、limit和capacity的值,变为进行get()前的值

    5、clean()方法

    • clean()方法会将缓冲区中的各个属性恢复为最初的状态,position = 0, capacity = limit
    • 此时缓冲区的数据依然存在,处于“被遗忘”状态,下次进行写操作时会覆盖这些数据

    mark()和reset()方法

    • mark()方法会将postion的值保存到mark属性中
    • reset()方法会将position的值改为mark中保存的值

    compact()方法

    此方法为ByteBuffer的方法,而不是Buffer的方法

    • compact会把未读完的数据向前压缩,然后切换到写模式
    • 数据前移后,原位置的值并未清零,写时会覆盖之前的值

    clear() VS compact()

    clear只是对position、limit、mark进行重置,而compact在对position进行设置,以及limit、mark进行重置的同时,还涉及到数据在内存中拷贝(会调用arraycopy)。所以compact比clear更耗性能。但compact能保存你未读取的数据,将新数据追加到为读取的数据之后;而clear则不行,若你调用了clear,则未读取的数据就无法再读取到了,所以需要根据情况来判断使用哪种方法进行模式切换。

     四、粘包与半包

      网络上有多条数据发送给服务端,数据之间使用 进行分隔,但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为:

      Hello,nikou

      I’m yumi

      How are you?

    变成了

      Hello,nikou I’m y

      umi How are you?

    粘包的原因:发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去。

    半包的原因:接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象。

    解决的办法有很多,这里先看其中的一种,

    1、通过get(index)方法遍历ByteBuffer,遇到分隔符时进行处理。注意:get(index)不会改变position的值。

      记录该段数据长度,以便于申请对应大小的缓冲区;

      将缓冲区的数据通过get()方法写入到target中;

    2、调用compact方法切换模式,因为缓冲区中可能还有未读的数据。

    public class ByteBufferDemo {
        public static void main(String[] args) {
            ByteBuffer buffer = ByteBuffer.allocate(32);
            // 模拟粘包+半包
            buffer.put("Hello,world
    I'm Nyima
    Ho".getBytes());
            // 调用split函数处理
            split(buffer);
            buffer.put("w are you?
    ".getBytes());
            split(buffer);
        }
    
        private static void split(ByteBuffer buffer) {
            // 切换为读模式
            buffer.flip();
            for(int i = 0; i < buffer.limit(); i++) {
    
                // 遍历寻找分隔符
                // get(i)不会移动position
                if (buffer.get(i) == '
    ') {
                    // 缓冲区长度
                    int length = i+1-buffer.position();
                    ByteBuffer target = ByteBuffer.allocate(length);
                    // 将前面的内容写入target缓冲区
                    for(int j = 0; j < length; j++) {
                        // 将buffer中的数据写入target中
                        target.put(buffer.get());
                    }
                    // 打印查看结果
                    ByteBufferUtil.debugAll(target);
                }
            }
            // 切换为写模式,但是缓冲区可能未读完,这里需要使用compact
            buffer.compact();
        }
    }

    参考连接:https://nyimac.gitee.io/2021/04/18/Netty%E5%AD%A6%E4%B9%A0%E4%B9%8BNIO%E5%9F%BA%E7%A1%80/

  • 相关阅读:
    模拟出栈
    全排列 next_permutation 用法
    区间覆盖
    BFS GPLT L2-016 愿天下有情人都是失散多年的兄妹
    GPLT L2-014 列车调度
    图的联通分量个数统计(判断图是否联通)
    堆排序 GPLT L2-012 关于堆的判断
    牛客挑战赛 30 A 小G数数
    由树的中后序遍历求树的前层序遍历
    【HDOJ4699】Editor(对顶栈,模拟)
  • 原文地址:https://www.cnblogs.com/sglx/p/15357332.html
Copyright © 2011-2022 走看看