第一章 Hello World - Netty(二)

107 阅读10分钟

1. ByteBuffer 详解

1.1 ByteBuffer 是抽象类

有两个实现类

  1. HeapByteBuffer
    • 在JVM内的堆内存中 -> 读写操作效率低 会受到GC的影响
  2. MappedByteBuffer(DirectByteBuffer)
    • 在OS内存中 -> 读写操作效率高 不会受到GC影响
    • 如果不主动释放 会造成内存泄漏

1.2 ByteBuffer的创建

  1. ByteBuffer.allocate(10)
    • 直接分配内存,分配10个字节的内存空间。
  2. ByteBuffer.allocateDirect(10)
    • 直接分配内存,分配10个字节的内存空间。
  3. ByteBuffer.wrap(byte[] b)
    • 将数组b包装成一个新的ByteBuffer。

注意:ByteBuffer一旦被分配了空间就不能再被修改 也就是不会自动扩容

1.3 ByteBuffer的特点

  1. 缓冲区,可以理解为一个数组,可以存放多个数据。
  2. 缓冲区可以存放基本数据类型,也可以存放引用数据类型。
  3. 缓冲区可以进行读写,可以指定读写的起始 位置,可以指定读写的长度。
  4. 缓冲区可以进行排序。

1.4 ByteBuffer 的 API

  1. put() 写入数据
  2. get() 读取数据
  3. flip() 切换读写模式
  4. rewind() 重置读写位置
  5. clear() 清空缓冲区
  6. mark() 标记读写位置
  7. reset() 重置读写位置
  8. compact() 压缩缓冲区
  9. equals() 判断两个缓冲区是否相等
  10. compareTo() 比较两个缓冲区

1.5 核心结构

一个Buffer中包含三个属性:position、limit、capacity。

  • position:表示当前的位置,初始值为0。buffer当前缓存的下标,在读取操作时记录读到了那个位置,在写操作时记录写到了那个位置。从0开始,每读取一次,下标+1
  • limit:表示当前缓冲区的上限,初始值和capacity值相同。读写限制,在读操作时,设置了你能读多少字节的数据,在写操作时,设置了你还能写多少字节的数据
  • capacity:表示缓冲区的容量,初始值和创建时指定的容量相同。 buffer的容量,类似于数组的size

2. ByteBuffer 的 读写模式

2.1 写模式

写入Buffer数据之前要设置写模式

  1. 新创建的Buffer自动是写模式
  2. 调用了clear,compact方法

2.2 读模式

读取Buffer数据之前要设置读模式
调用flip方法

// buffer中写入数据[写模式 创建一个bytebuffer ,clear(),compact()]
// 1. channel的read方法  
channel.read(buffer)  
// 2. buffer的put方法  
buffer.put(byte);
buffer.put((byte)'a');
buffer.put(byte[]);

// 从buffer中读出数据
1. channel的write方法  
  
2. buffer的get方法 //每调用一次get方法会影响,position的位置。  
  
3. rewind方法(手风琴),可以将postion重置成0 ,用于复读数据。  
  
4. mark&reset方法,通过mark方法进行标记(position),通过reset方法跳回标记,从新执行.  
  
5. get(i) 方法,获取特定position上的数据,但是不会对position的位置产生影响。

ByteBuffer 对字符串操作示例

// ByteBuffer buffer = Charset.forName("UTF-8").encode("hello");  
  
// 1、encode方法自动 把字符串按照字符集编码后,存储在ByteBuffer.  
// 2、自动把ByteBuffer设置成读模式,且不能手工调用flip方法。  
  
ByteBuffer buffer = StandardCharsets.UTF_8.encode("hello");  
 
while (buffer.hasRemaining()) {  
    System.out.println("buffer.get() = " + (char) buffer.get());  
}  
buffer.clear();

// Buffer中的数据转换成字符串
ByteBuffer buffer = ByteBuffer.allocate(10); // 新建ByteBuffer 默认为写模式
buffer.put("world".getBytes());  
  
buffer.flip();  // 切换读模式
CharBuffer result = StandardCharsets.UTF_8.decode(buffer);  
System.out.println("result.toString() = " + result.toString());

3.粘包与半包

3.1 粘包

粘包是指发送方在发送数据时,多个小包被合并为一个大包发送到接收方,导致接收方无法准确区分每个小包的边界。
就是一条消息没有边界粘连在一起 hello world!你好世界!
正常消息应该是
hello world!
你好世界!
两句话粘连在了一起
所以我们自己区分消息的边界,在消息结尾加上自定义分隔符,比如\n _end都可以
在Netty中也有这样的实现,不过Netty已经帮我们实现好了多种解决粘包半包的方法

3.2 半包

半包是指发送方在发送数据时,一个大包被拆分为多个小包发送到接收方,导致接收方无法完整地接收到每个小包。这种情况可能发生在网络传输过程中发生了分割、丢包或其他错误的情况下。接收方在接收到不完整的数据时,需要进行特定的处理才能确保完整地接收到每个小包。
How are you?\n I am
fine!Thank you!\n
一句话被拆分得不完整,导致出现半包现象。

为了后续的学习,我们自己用NIO来封装一下

public static void main ( String[] args ) {  
    ByteBuffer byteBuffer = ByteBuffer.allocate(50);  
    byteBuffer.put("hello world!\n你好世界!\n".getBytes());  //粘包
    doLineSplit(byteBuffer);
    byteBuffer.put("how are you?\n i m".getBytes());  
    doLineSplit(byteBuffer);  
    byteBuffer.put(" fine, think you\n".getBytes());  // 半包
    doLineSplit(byteBuffer);
}  
  
// ByteBuffer接受的数据 \n  
private static void doLineSplit(ByteBuffer buffer) {  
    buffer.flip();  
    for (int i = 0; i < buffer.limit(); i++) {  
        if (buffer.get(i) == '\n') {  
            int length = i + 1 - buffer.position();  
            ByteBuffer target = ByteBuffer.allocate(length);  
            for (int j = 0; j < length; j++) {  
                target.put(buffer.get());  
            }  

            //截取工作完成  
            target.flip();  
            System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());  
        }  
    }  
    buffer.compact();  
}

buffer.compact();进行数据处理,把第一次没有读取完的数据,向前移动和后面的内容进行整合。
就比如读完了how are you?\n 一条消息还剩i m没有读完,就把i m往前移,后面读到的数据fine, think you\n加到i m后面,就能得到一条完整的消息。

4. NIO的开发使用

4.1 文件操作

使用 channel 写入数据到文件

public static void main ( String[] args ) throws IOException {  

    //1 获得Channel FileOutputStream, RandomAccessFile  
    FileChannel channel = new FileOutputStream("data1").getChannel();  

    //2 获得Buffer  
    ByteBuffer buffer = Charset.forName("UTF-8").encode("hello world");  

    //3write  
    channel.write(buffer);  
  
}

4.2 网络编程

服务端接收请求通过 -> ServerSocketChannel
进行实际通信是 -> SocketChannel

先来写一下NIO的客户端
连接服务端 向服务端写入数据

public class MyClient {  
    public static void main(String[] args) throws IOException {  
        SocketChannel socketChannel = SocketChannel.open();  
        socketChannel.connect(new InetSocketAddress(8000));  
        System.out.println("---------");  
        socketChannel.write(StandardCharsets.UTF_8.encode("hello world\n"));  
    }  
}

服务端

public class MyServer {  
    /**  
    * 通过这版代码 证明了 服务器端 存在2种阻塞  
    * 1. 连接阻塞 ----> accept方法存在阻塞---> ServerSocketChannel阻塞。  
    * 2. IO阻塞 ----> channel的read方法存在阻塞---> SocketChannel阻塞。   
    * @param args  
    * @throws Exception  
    */  
    public static void main(String[] args) throws Exception {  
        //1. 创建ServerSocketChannel 
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
        //2. 设置服务端的监听端口:---》client通过网络进行访问 ip:port http://localhost:8989  
        serverSocketChannel.socket().bind(new InetSocketAddress(8000));  
        List<SocketChannel> channelList = new ArrayList<>();  
        ByteBuffer buffer = ByteBuffer.allocate(20);  
        //3. 接收client的连接  
        while ( true ){  
            //4. SocketChannle 代表 服务端与Client链接的一个通道  
            System.out.println("等待连接服务器...");  
            SocketChannel socketChannel = serverSocketChannel.accept();//阻塞 程序等待client  
            System.out.println("服务器已经连接..."+socketChannel);  

            channelList.add(socketChannel);  
            //5. client与服务端 通信过程 NIO代码  
            for (SocketChannel channel : channelList) {  
                System.out.println("开始实际的数据通信....");  
                channel.read(buffer);//阻塞 对应的IO通信的阻塞  
                buffer.flip();  
                CharBuffer decode = Charset.forName("UTF-8").decode(buffer);  
                System.out.println("decode.toString() = " + decode.toString());  
                buffer.clear();  
                System.out.println("通信已经结束....");  
            }  
        }  
    }  
}

可以看到服务端一直在死循环监听客户端的连接,一直在空转,非常的消耗CPU的性能,可能会打满CPU,该怎么办呢?
这时候就需要引入Selector了。

4.3 Selector

定义: Selector(选择器)是Java NIO(New IO)库中的一个关键组件,用于实现非阻塞IO(Non-blocking IO)。Selector允许单个线程同时监听多个通道(Channels),并在这些通道就绪时进行处理,从而实现高效的IO操作。
概念:

  1. 选择器(Selector):
    选择器是Selector类的实例,用于管理一组通道的就绪状态。它允许单个线程同时监听多个通道,并在通道就绪时进行处理。通过使用选择器,可以实现非阻塞IO,避免了每个通道都需要一个线程进行阻塞等待的情况。
  2. 注册通道(Registering Channels):
    通过调用通道的register()方法将通道注册到选择器上,以便选择器能够监听该通道的就绪状态。可以指定感兴趣的操作类型(如读、写、连接、接收)来告诉选择器监听哪些事件。
  3. 选择操作(Selecting Operations):
    选择器提供了select()方法,用于阻塞地等待通道就绪事件。当至少一个通道就绪时,该方法会返回,然后可以通过调用选择器的其他方法(如selectedKeys())获取就绪通道的集合,进一步处理这些就绪事件。
  4. 就绪通道集合(Selected Channel Set):
    当选择器中的通道就绪时,可以通过调用选择器的selectedKeys()方法获取就绪通道的集合,它返回一个SelectionKey对象的集合。SelectionKey包含了就绪通道的信息,可以通过它来获取通道、操作类型等相关信息。
  5. 多路复用(Multiplexing):
    Selector使用操作系统提供的多路复用机制来实现高效的IO操作。在大多数操作系统中,多路复用使用了一些底层的系统调用(如select、poll、epoll等)来监听多个IO事件,从而避免了阻塞式IO操作带来的性能问题。

下面我们来改造一下服务端。

public class MyServer2 {  
    public static void main ( String[] args ) throws IOException {  
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
    serverSocketChannel.bind(new InetSocketAddress(8000));  
    serverSocketChannel.configureBlocking(false);//Selector 只有在非阻塞的情况下 才可以使用。  

    //引入监管者  
    Selector selector = Selector.open();//1. 工厂,2. 单例  

    //监管者 管理谁? selector.xxxx(ssc); //管理者 ssc ---> Accept  
    SelectionKey selectionKey = serverSocketChannel.register(selector, 0, null);  
    // selector监控 SSC ACCEPT  
    // selector  
    // keys --> HashSet  
    // register注册 ssc  
    selectionKey.interestOps(SelectionKey.OP_ACCEPT);  

    System.out.println("MyServler2.main");  

    //监控  
    while ( true ) {  
        selector.select();//等待.只有监控到了 有实际的连接 或者 读写操作 ,才会处理。  
        //对应的 有ACCEPT状态的SSC 和 READ WRITE状态的 SC 存起来  
        // SelectionsKeys HashSet  

        System.out.println("-------------------------");  

        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();  
        while ( iterator.hasNext() ) {//ServerSocketChannel ScoketChannel  
            SelectionKey key = iterator.next();  
            //用完之后 就要把他从SelectedKeys集合中删除掉。 ServerSocketChannel---SelectedKeys删除 ,后续 SSC建立新的连接  
            iterator.remove();  

            if ( key.isAcceptable() ) {  
                //serverSocketChannel.accept();  
                ServerSocketChannel channel = (ServerSocketChannel) key.channel();  
                SocketChannel sc = channel.accept();  
                sc.configureBlocking(false);  
                //监控sc状态 ---> keys  
                SelectionKey sckey = sc.register(selector, 0, null);  
                sckey.interestOps(SelectionKey.OP_READ);  
            } else if ( key.isReadable() ) {  
                try {  
                    SocketChannel sc = (SocketChannel) key.channel();  
                    ByteBuffer buffer = ByteBuffer.allocate(5);  
                    int read = sc.read(buffer);  
                    if ( read == - 1 ) {  
                        key.cancel();  
                    } else {  
                        buffer.flip();  
                        System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());  
                    }  
                } catch ( IOException e ) {  
                    e.printStackTrace();  
                    key.cancel();  
                }  
            }  
            }  
        }  
    }  
}

前面写了处理粘包半包的,咱们是不是得用上,再加上对缓冲区的扩容,(小声)这些在Netty都帮我们自动实现了。

private static void doLineSplit ( ByteBuffer buffer ) {  
    buffer.flip();  
    for ( int i = 0; i < buffer.limit(); i++ ) {  
        if ( buffer.get(i) == '\n' ) {  
            int length = i + 1 - buffer.position();  
            ByteBuffer target = ByteBuffer.allocate(length);  
            for ( int j = 0; j < length; j++ ) {  
                target.put(buffer.get());  
            }  

            //截取工作完成  
            target.flip();  
            System.out.println("StandardCharsets.UTF_8.decode(target).toString() = " + StandardCharsets.UTF_8.decode(target).toString());  
        }  
    }  
    buffer.compact();  
}
public static void main ( String[] args ) throws IOException {
----------------------------省略部分,完整代码请参考上文-----------

    if ( key.isAcceptable() ) {  
        //serverSocketChannel.accept();  
        ServerSocketChannel channel = (ServerSocketChannel) key.channel();  
        SocketChannel sc = channel.accept();  
        sc.configureBlocking(false);  
        //监控sc状态 ---> keys  
        ByteBuffer buffer = ByteBuffer.allocate(7);  
        SelectionKey sckey = sc.register(selector, 0, buffer); // channel 绑定 buffer  
        sckey.interestOps(SelectionKey.OP_READ);  
    } else if ( key.isReadable() ) {  
        try {  
            SocketChannel sc = (SocketChannel) key.channel();  
            // ByteBuffer buffer = ByteBuffer.allocate(7); // 创建了新的 bytebuffer  
            ByteBuffer buffer = (ByteBuffer) key.attachment();// 拿到绑定的buffer  
            int read = sc.read(buffer);  
            if ( read == - 1 ) {  
                key.cancel();  
            } else {  
                // buffer.flip();  
                //System.out.println("Charset.defaultCharset().decode(buffer).toString() = " + Charset.defaultCharset().decode(buffer).toString());  
                // 处理半包粘包  
                doLineSplit(buffer);
                if ( buffer.position() == buffer.limit() ) {  
                    // 容量不够了 缓冲区该扩容了  
                    // 怎么扩容 ---》 空间扩大 2.旧的缓冲区 复制到新的缓冲区  
                    ByteBuffer newByteBuffer = ByteBuffer.allocate(buffer.capacity() * 2);  
                    buffer.flip();  
                    newByteBuffer.put(buffer.get());  
                    // 3. 绑定新的缓冲区  
                    key.attach(newByteBuffer);  
                 }
            }  
        } catch ( IOException e ) {  
            e.printStackTrace();  
            key.cancel();  
        }  
    }
}

那么客户端可以向服务端发送数据,服务端接收数据,那么客户端可不可以接收服务端发送的数据?
答案是肯定的。

// 客户端接收服务端数据
public class MyClient1 {  
    public static void main ( String[] args ) throws IOException {  
        SocketChannel socketChannel = SocketChannel.open();  
        socketChannel.connect(new InetSocketAddress(8000));  

        int read = 0;  
        while ( true ){  
            ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);  
            read += socketChannel.read(buffer);  
            System.out.println("read = " + read);  
            buffer.clear();  
        }  
    }  
}
// 服务端写出数据
public class MyServer5 {  
    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 sscKey = iterator.next();  
                iterator.remove();  

                if ( sscKey.isAcceptable() ) {  
                    SocketChannel sc = serverSocketChannel.accept();  
                    sc.configureBlocking(false);  
                    SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);  

                    // 准备数据  
                    StringBuilder sb = new StringBuilder();  
                    for ( int i = 0; i < 200000; i++ ) {  
                        sb.append("s");  
                    }  
                    // NIO buffer 存数据 channel 写  
                    ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());  

                    int write = sc.write(buffer);  
                    System.out.println("write = " + write);  
                    if ( buffer.hasRemaining() ){  
                        // 为了当前的SocketChannel 增加 write监听  
                        // read + write 监听  
                        scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);  
                        // 把剩余数据的buffer传过去  
                        scKey.attach(buffer);  
                    }  
                    } else if ( sscKey.isWritable() ){  
                        //  
                        SocketChannel sc = (SocketChannel) sscKey.attachment();  
                        ByteBuffer buffer = (ByteBuffer) sscKey.attachment();  

                        int write = sc.write(buffer);  
                        System.out.println("write = " + write);  

                        if ( !buffer.hasRemaining() ){  
                            sscKey.attach(null);  
                            sscKey.interestOps(sscKey.interestOps() - SelectionKey.OP_WRITE);  
                    }  
                }  

            }  
        }  
    }  
}

至此NIO网络编程写完了,可以看出使用NIO编程有点繁琐,还要注意内存的回收,这更加体现了了Netty在网络编程的简易和便捷性,后续我们开始探讨怎么用Netty开发网络编程,敬请期待!

吐槽:掘金的代码块怎么没有格式化 不太好用

码文不易,给个赞支持一下吧。

文章参考:孙帅suns Netty应用