NIO的整体介绍和Demo

 BIO的步骤流程说明:

 

BIO代码实现:

// 用于存储读取到的数据
// 用于存储读取到的数据
byte[] bytes = new byte[1024];
ServerSocket serverSocket= null;
try {
    serverSocket = new ServerSocket();
    serverSocket.bind(new InetSocketAddress(8080)) ; // 绑定监听的端口
    for(;;){
        //线程阻塞,等待连接
        Socket socket = serverSocket.accept();
        // 如果有客户端连接进来就往下走,如果没有读取到数据会堵塞
        InputStream input = socket.getInputStream();
        try{
            int read = input.read(bytes);
            String content = new String(bytes);
            System.out.println(content);
        }} catch (IOException e) {
            e.printStackTrace();
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

 客户端:

try {
    Socket socket = new Socket("127.0.0.1",8080);
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("hello".getBytes());
} catch (IOException e) {
    e.printStackTrace();
}

 总结:

其中BIO可以通过采用多线程完成并发请求处理,accept方法处阻塞,不变,如果监听到有客户端连接进来,则开启一个线程,并且将获取到的和客户端长连接交互的socket 作为对象传入子线程中,然后让read方法的阻塞在子线程中出现,而主线程则继续执行,然后循环到accept()方法中等待第二个客户端线程的链接。

其中负责监听的socket对象在创建之后会返回一个文件描述符,用于bind一个端口,这个bind调用的是操作系统的内核函数,调用listen 方法用于监听,而accept方法则阻塞住不往下走,直到客户端通过操作系统完成三次握手的过程之后accept会接受到这个客户端,返回一个文件描述符(简单理解就是和客户端交互的socket对象),然后进入read方法,进行阻塞。可以通过开辟线程来执行read,而accept则循环监控连接;

缺点:线程频繁创建和销毁(线程栈消耗),上线文切换频繁,不安全。无法解决单线程问题

NIO non-blocking IO(非阻塞IO)关键组成:

  1. channel 通道
  2. buffer 缓冲区
  3. selector 选择器

1、channel通道:

Java NIO的通道类似流,但又有些不同,channel既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。通道可以异步地读写。通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。

通道的类型(参考右边链接):http://ifeve.com/channels/

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

以ServerSocketChannel为例:首先先开启ServerSocketChannel,调用open()方法,并且可以非阻塞的监听新进连接,如果不设置为非阻塞也会和BIO 一样会放弃cpu的执行权,直到有新连接被监听到才会往下执行;该对象可产生一个用于监听的ServerSocket对像,ServerSocket可以绑定监听指定的端口;注意:FileChannel不能切换到非阻塞模式

// 开启通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 获取服务端监听套接字
ServerSocket serverSocket = serverChannel.socket();
// 绑定监听的端口 也可以直接用ServerSocketChannel 来监听   
// serverChannel.bind(new InetSocketAddress(8080));
serverSocket.bind(new InetSocketAddress(8080));
// 设置通道为非阻塞 accept()方法会立即返回,如果没有监听到新连接返回null,监听到
//新连接则返回一个与新连接交互的SocketChannel通道
serverChannel.configureBlocking(false);

 

通道和缓冲区的数据交互

//将 Buffer 中数据写入 Channel
channel.write(buff)
//从 Channel 读取数据到 Buffer
channel.read(buff)

channel 注册到选择器上,register()如果已经注册过的,更新对象和更新channel对什么事件感兴趣,注册之后返回一个selectorKey,用于标识channel 和channel感兴趣的事件,如果是有客户端连接这个channel,并且事件是这个channel感兴趣的 ,那么操作系统会将这个连接分配给这个channel。关于底层应用的的共享内存啊、内核轮询之类的可以自己去看马士兵的NIO里面介绍的挺多的。

2、buffer 缓冲区:

缓冲区本质上是一个内存块,您可以在其中写入数据,然后可以在以后再次读取。该内存块包装在NIO Buffer对象中,数据从通道读取到缓冲区,然后从缓冲区写入通道,该对象提供了一组方法,可以更轻松地使用该内存块。Java NIO 有以下Buffer类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

要获取Buffer对象,您必须首先分配它。每个Buffer类都有一个allocate()执行此操作的方法

ByteBuffer buf = ByteBuffer.allocate(1024);

2.1、使用Buffer来读取和写入数据通常遵循以下四个步骤:

  1. 将数据写入缓冲区
  2. 呼叫 buffer.flip()
  3. 从缓冲区读取数据
  4. 调用buffer.clear()或buffer.compact()清除数据

2.1.1 将数据写入缓冲区

数据写入缓冲区有两种方式:

int bytes = channel.read(buf); // 从通道读取数据写入缓冲池


buf.put("hello world".getByte());  // 在程序中向缓冲池添加数据

 

2.1.2 呼叫 buffer.flip()

buffer中的flip方法涉及到bufer中的Capacity,Position和Limit三个概念

  • 容量 (capacity)
  • 位置 (position)
  • 限制 (limit)

容量是在调用allocate(1024)时定义的容量,有兴趣可以看看源码:

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

// 传入的都是容量所以说开始的时候cap = lim
HeapByteBuffer(int cap, int lim) {
    super(-1, 0, lim, cap, new byte[cap], 0);
}

// 调用上级
ByteBuffer(int mark, int pos, int lim, int cap,   // package-private
             byte[] hb, int offset)
{
    super(mark, pos, lim, cap);
    this.hb = hb;
    this.offset = offset;
}

// 容量赋值position的开始位置赋值为0,标记设为-1,mark是起辅助判断作用。
Buffer(int mark, int pos, int lim, int cap) {       // package-private
    if (cap < 0)
        throw new IllegalArgumentException("Negative capacity: " + cap);
    this.capacity = cap;
    limit(lim);
    position(pos);
    if (mark >= 0) {
        if (mark > pos)
            throw new IllegalArgumentException("mark > position: ("
                                               + mark + " > " + pos + ")");
        this.mark = mark;
    }
}

// 最后会到这里来
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}

所以初始化之后的值:limit = cap = 1024(我传入的值) 而mark = -1 , position = 0 ;

读取过程(下面链接讲的挺清楚的):

https://blog.csdn.net/u013096088/article/details/78638245

读取结束(消息长15):position 位于15;

调用flip()方法之后:

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

limit = 15 ; positon 重置;也就是说调用flip之后,读写指针指到缓存头部,并且设置了最多只能读出之前写入的数据长度(而不是整个缓存的容量大小)

简答点描述就是:你将文件按一定的顺序叠起来,然后领导突然说把已经叠好的按顺序交给他,你只能把现在到了第几个文件记住,然后把已经整理的交给领导。大概就这个意思吧,可能例子不够恰当

2.1.3 从缓冲区读取数据

int bytes = channel.write(buf); // 从缓冲池中向通道写入数据 

 byte b = buffer.get(0);   // 从缓冲池中获取数据

2.1.4 调用buffer.clear()或buffer.compact()清除数据

读取所有数据后,需要清除缓冲区,以使其可以再次写入。您可以通过两种方式执行此操作:通过调用clear()或通过 compact()。该clear()方法清除整个缓冲区。该compact() 方法仅清除您已读取的数据。任何未读的数据都将移至缓冲区的开头,并且现在将在未读的数据之后将数据写入缓冲区。

3、selector 选择器

一个selector对象可以通过调用Selector.open()来创建,这个工厂方法会使用系统默认的selector provider来创建一个新的selector对象。或者我们还可以通过实现抽象类SelectorProvider自定义一个selector provider,然后调用它的openSelector()来创建,

例如:new SelectorProviderImpl().openSelector()

除非调用selector.close(),否则该selector将会一直保持打开状态。

参考:https://blog.csdn.net/u014730165/article/details/85089085?depth_1-utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromBaidu-7&utm_source=distribute.pc_relevant_right.none-task-blog-BlogCommendFromBaidu-7

通过channel的register方法,将channel注册到给定的selector中,并返回一个表示注册关系的SelectionKey 对象。

SelectionKey key = channel.register(selector,
 SelectionKey.OP_ACCEPT|SelectionKey.OP_READ|SelectionKey.OP_WRITE|SelectionKey.OP_CONNECT);
// 为了确保selector捕捉到信息(也就是有客户端的行为才响应)需要调用select方法
// 该方法属于一个阻塞方法,即没有内容不会往下执行,表示监听的端口没有人连接也没有人读写
selector.select();  

第一个表示选择器,参数二表示注册到选择器之后这个channel对什么事件感兴趣,SelectionKey抽象类里定义了4中,分别:是:

  • OP_ACCEPT: 接收连接进行事件,表示服务器监听到了客户连接
  • OP_READ: 读就绪事件,表示通道中已经有了可读的数据
  • OP_WRITE: 写就绪事件,表示已经可以向通道写数据了
  • OP_CONNECT:表示客户与服务器的连接已经建立成功

你可以获取selector中的所有selector中的selectionKeys的集合:

 selectionKeys = selector.selectedKeys();
 // 获取遍历selectionKeys的迭代器,你通过这个key值的遍历找出这个选择器中你感兴趣的事件进行业务操作。 
 iterator = selectionKeys.iterator();
 // 这个key你捕捉到了并且你处理了,那么就应该将这个key移除这个集合,避免重复处理相同事件
iterator.remove(selectionKey);

太深的内容我自己理解也不到位,希望将自己能理顺的知识做个总结,方便有需要的人参考;下面是我开发中用到的NIO例子;

 

完整的NIO 的Demo :

public class ServerCrawImpl implements IServerCraw {
    public ServerCrawImpl() {
    }

    @Override
    public void startup() {
        ServerSocketChannel serverChannel;
        Selector selector;
        try {
            // 开启管道
            serverChannel = ServerSocketChannel.open();
            // 获取服务端监听socket
            ServerSocket serverSocket = serverChannel.socket();
            //绑定端口
            serverSocket.bind(new InetSocketAddress(8603));
            // 设置socket非阻塞
            serverChannel.configureBlocking(false);
            //创建Selector复用器,选择器
            selector = Selector.open();
            //将多个Channel类型事件注册到多路复用器   实现Selector管理Channel,其中的事件可以多个
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (Exception e) {
            log.debug(ExceptionUtil.getStackMsg(e));
            return;
        }
        while (true) {
            try {
                //如果selector管理的管道没有客户端请求,客户端的读,客户端的写的交互,则阻塞,返回0
                int eventCount = selector.select();
                if (eventCount == 0) {
                    continue;
                }
            } catch (Exception e) {
                log.debug(ExceptionUtil.getStackMsg(e));
                break;
            }
            // 获取所有管道相关信息的keys集合
            Set<SelectionKey> keys = selector.selectedKeys();
            // 获取遍历keys集合的迭代器
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                // 获取当前的channel对应 key
                SelectionKey key = iterator.next();
                // 移除,防止重复处理
                iterator.remove();
                //处理业务
                try {
                    // key存在并且得到的是连接请求
                    if (key.isValid() && key.isAcceptable()) {
                        // 获取与客户端交互的channel
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        //接受客户端的请求
                        SocketChannel client = server.accept();
                        //客户端 服务端 全都设置为 非阻塞
                        client.configureBlocking(false);
                        client.register(key.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE, ByteBuffer.allocate(2048));
                    }
                    if (key.isValid() && key.isReadable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(3);
                        int res = 0;
                        try {
                            // 从缓存中读取数据到通道中返回给客户端,需要判断一下客户端收到的消息长度和自己发送的长度是否一致
                            res = client.read(buffer);
                        } catch (Exception e) {
                            client.close();
                            log.debug(ExceptionUtil.getStackMsg(e));
                            break;
                        }
                       //如果read()方法返回-1,说明客户端关闭了连接,那么客户端已经接收到了与自己发送字节数相等的数据,可以安全地关闭
                        if (res == -1) {
                            client.close();
                        } else if (res == 0) {
                            continue;
                        } else if (res > 0) {
                            continue;
                        }
                    }
                    if (key.isValid() && key.isWritable()) {
                            try {
                                // 从缓冲池中获取数据
                                // 业务操作
                            } catch (Exception e) {
                                log.debug("异常:" + ExceptionUtil.getStackMsg(e));
                            }
                        }
                    }
                } catch (Exception e) {
                    log.debug(ExceptionUtil.getStackMsg(e));
                }
            }
        }
    }
}

 

NIO 的弊端:客户端连接就绪之后都会有文件描述符的read阻塞监听,NIO 会重复调用read(),会曾加内核的压力;

我也仅限会用,太深的东西也没办法理解透彻,有不对的欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值