Java NIO 实践

1 NIO 3个核心的组件

  • Channel(通道),包括服务端ServerSocketChannel 及SocketChannel
  • Buffer(缓冲区)
  • Selector(选择器)

2 实例代码

2.1 服务器端

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

public class BasicServerSocketChannelDemo {
    public static void main(String[] args) throws IOException {
        //创建selector,管理多个channel
        Selector selector = Selector.open();

        //创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置为非阻塞模式,只有非阻塞模式才能使用selector
        serverSocketChannel.configureBlocking(false);

        //注册,并设置关注channel 上的事件(客户端连接)
        //第二个参数也可以通过selectionKey.interestOps(SelectionKey.OP_ACCEPT) 设置,
        // register方法还有第三个参数-attachment,将来可以通过selectionKey.attachment()获得,担任也可以通过selectionKey.attach(att)设置
        SelectionKey selectionKeyx = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //绑定网络端口
        serverSocketChannel.bind(new InetSocketAddress(9900));

        while (true){
            //select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
            //select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
            int select = selector.select();

            //处理事件, selectedKeys 内部包含了所有发生的事件
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()){
                SelectionKey selectionKey = selectionKeyIterator.next();
                //要从 selectedKeys 集合中删除,否则会一直存在于selectedKeys集合中。
                selectionKeyIterator.remove();
                //按事件类型分别处理
                //如果是客户端连接
                if(selectionKey.isAcceptable()){
                    //也可以直接调用最开始创建的serverSocketChannel,或者通过以下获得serverSocketChannel
                    //ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
                    //接受客户端的连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //设置为非阻塞模式
                    socketChannel.configureBlocking(false);
                    //将socketChannel 注册到selector上,并关注此socketChannel上的可读事件
                    socketChannel.register(selector,SelectionKey.OP_READ);

                    System.out.println("客户端接入"+socketChannel.getRemoteAddress());
                }else if(selectionKey.isReadable()){
                    //可读事件,收到数据
                    try {
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
                        int read = socketChannel.read(byteBuffer);
                        //客户端正常断开
                        if(read==-1){
                            //取消 SelectionKey对应的 SocketChannel 在selector 的注册
                            selectionKey.cancel();
                            System.out.println("连接断开");
                        }else {
                            byteBuffer.flip();
                            System.out.println("收到数据:"+ StandardCharsets.UTF_8.decode( byteBuffer).toString());
                        }
                    } catch (IOException e) {
                        //客户端异常断开,如杀掉进程 socketChannel.read方法会触发IOException
                        selectionKey.cancel();
                        System.out.println("连接异常断开");
                    }
                }else if(selectionKey.isWritable()){
                    //可写事件,正常情况不能订阅可写事件,否则会一直触发,
                    //只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况,重现的方法 是发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
                    //在写完数据后 需要取消订阅可写事件
                    System.out.println("触发Writable 事件");
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer= ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8));
                    int write = socketChannel.write(byteBuffer);
                    //
                    if(!byteBuffer.hasRemaining()){
                        selectionKey.interestOps(SelectionKey.OP_READ);
                    }
                }
            }
        }

    }
}

2.2客户端

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;

public class BasicSocketChannelDemo {
    public static void main(String[] args) throws IOException {
        //创建selector
        Selector selector = Selector.open();

        //SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //设置为非阻塞模式,只有非阻塞模式才能使用selector
        socketChannel.configureBlocking(false);
        //注册,并设置关注channel 上的事件(客户端连接)
        socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);

        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9900));
        while (true){
            //select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
            //select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
            int select = selector.select();

            //处理事件, selectedKeys 内部包含了所有发生的事件
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()){
                SelectionKey selectionKey = selectionKeyIterator.next();
                 //要从 selectedKeys 集合中删除,否则会一直存在于selectedKeys集合中。
                selectionKeyIterator.remove();
                //按事件类型分别处理
                //如果是与服务端建立连接时间
                if(selectionKey.isConnectable()){
                    socketChannel.finishConnect();
                    System.out.println("与服务端连接成功");
                }else if(selectionKey.isReadable()){
                    try {
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int read = socketChannel.read(byteBuffer);
                        //正常断开
                        if(read==-1){
                            //取消 SelectionKey对应的 SocketChannel 在selector 的注册
                            selectionKey.cancel();
                            System.out.println("连接断开");
                        }else {
                            byteBuffer.flip();
                            System.out.println("收到数据:"+ StandardCharsets.UTF_8.decode( byteBuffer).toString());
                        }
                    } catch (IOException e) {
                        //异常断开,如杀掉进程 socketChannel.read方法会触发IOException
                        selectionKey.cancel();
                        System.out.println("连接异常断开");
                    }
                }else if(selectionKey.isWritable()){
                    //可写事件,正常情况不能订阅可写事件,否则会一直触发,
                    //只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况,重现的方法 是发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
                    //在写完数据后 需要取消订阅可写事件
                    ByteBuffer byteBuffer= ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8));
                    int write = socketChannel.write(byteBuffer);
                    //
                    if(!byteBuffer.hasRemaining()){
                        selectionKey.interestOps(SelectionKey.OP_READ);
                    }

                }
            }
        }

    }
}
=

2.3 概念解析

2.3.1 SelectionKey 事件类型

  • connect - 客户端连接服务端成功时触发
  • accept - 服务器端成功接受连接时触发
  • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
  • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
    可写事件,正常情况不能订阅可写事件,否则会一直触发,只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况)
    调试重现的方法:发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
    在写完数据后 需要取消订阅可写事件

2.3.2 监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

  • 方法1,阻塞直到绑定事件发生,无特殊情况都会使用此方法

int count = selector.select();

  • 方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

  • 方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

  • select 何时不阻塞
    • 事件发生时
    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据 大于 buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时(jdk linux 版本的bug)
    • 调用 selector.wakeup()
    • 调用 selector.close()
    • selector 所在线程 interrupt

2.3.3 处理事件

  • 为何要 selectionKeyIterator.remove()
    因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
    第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
    第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
  • selectionKey.cancel();
    cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件

2.3.4 消息分割的方案

因为TCP的数据没有边界的,故需要接收方和发送方约定消息分割的协议。常见的方案:

  • 固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  • 另按分隔符拆分,缺点是效率低
  • TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    Http 1.1 是 TLV 格式
    Http 2.0 是 LTV 格式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值