NIO

1. Channel

NIO中,基本所有的IO操作都是从Channel开始的,Channel通过Buffer(缓冲区)进行读写操作。

read()表示读取通道中数据到缓冲区,write()表示把缓冲区数据写入到通道。

Channel有好多实现类,这里有三个最常用:

  • SocketChannel:一个客户端发起TCP连接的Channel

  • ServerSocketChannel:一个服务端监听新连接的TCP Channel,对于每一个新的Client连接,都会建立一个对应的SocketChannel

  • FileChannel:从文件中读写数据

其中SocketChannelServerSocketChannel是网络编程中最常用的,一会在最后的示例代码中会有讲解到具体用法。


2. Buffer

Buffer也被成为内存缓冲区,本质上就是内存中的一块,我们可以将数据写入这块内存,之后从这块内存中读取数据。也可以将这块内存封装成NIO Buffer对象,并提供一组常用的方法,方便我们对该块内存进行读写操作。我们可以将Buffer理解为一个数组的封装,我们最常用的ByteBuffer对应的数据结构就是byte[]。

Buffer中有4个非常重要的属性:capacity、limit、position、mark

  • capacity属性:容量,Buffer能够容纳的数据元素的最大值,在Buffer初始化创建的时候被赋值,而且不能被修改。

上图中,初始化Buffer的容量为8(图中从0~7,共8个元素),所以capacity = 8

  • limit属性:代表Buffer可读可写的上限。

    • 写模式下:limit 代表能写入数据的上限位置,这个时候limit = capacity读模式下:在Buffer完成所有数据写入后,通过调用flip()方法,切换到读模式,此时limit等于Buffer中实际已经写入的数据大小。因为Buffer可能没有被写满,所以limit<=capacity

  • position属性:代表读取或者写入Buffer的位置。默认为0。

    • 写模式下:每往Buffer中写入一个值,position就会自动加1,代表下一次写入的位置。

    • 读模式下:每往Buffer中读取一个值,position就自动加1,代表下一次读取的位置。

从上图就能很清晰看出,读写模式下capacity、limit、position的关系了。

  • mark属性:代表标记,通过mark()方法,记录当前position值,将position值赋值给mark,在后续的写入或读取过程中,可以通过reset()方法恢复当前position为mark记录的值。

创建buffer

ByteBuffer buffer = ByteBuffer.allocate(1024);

ByteBuffer buffer = ByteBuffer.wrap("hello world".getBytes());

3. Selector

Selector是NIO中最为重要的组件之一,我们常常说的多路复用器就是指的Selector组件。Selector组件用于轮询一个或多个NIO Channel的状态是否处于可读、可写。通过轮询的机制就可以管理多个Channel,也就是说可以管理多个网络连接。

 

轮询机制

  1. 首先,需要将Channel注册到Selector上,这样Selector才知道需要管理哪些Channel

  2. 接着Selector会不断轮询其上注册的Channel,如果某个Channel发生了读或写的时间,这个Channel就会被Selector轮询出来,然后通过SelectionKey可以获取就绪的Channel集合,进行后续的IO操作。

1.创建Selector  

Selector selector = Selector.open();

2.注册Channel到Selector中

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

  • Connect事件 :连接完成事件( TCP 连接 ),仅适用于客户端,对应 SelectionKey.OP_CONNECT。

  • Accept事件 :接受新连接事件,仅适用于服务端,对应 SelectionKey.OP_ACCEPT 。

  • Read事件 :读事件,适用于两端,对应 SelectionKey.OP_READ ,表示 Buffer 可读。

  • Write事件 :写时间,适用于两端,对应 SelectionKey.OP_WRITE ,表示 Buffer 可写。

4.总结

服务端:

package cn.demo.netty_chat.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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {


    private static Selector selector;

    public static void main(String[] args) {
        init();
        listen();
    }

    private static void init() {
        ServerSocketChannel serverSocketChannel = null;

        try {
            //1.得到一个selector对象
            selector = Selector.open();
            //2.获取ServerSocketChannel对象,老大
            serverSocketChannel = ServerSocketChannel.open();
            //3.设置非阻塞方式
            serverSocketChannel.configureBlocking(false);
            //4.绑定端口号
            serverSocketChannel.socket().bind(new InetSocketAddress(9000));
            //5.把serverSocketChannel注册给selector,监控客户端连接
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("NioServer 启动完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void listen() {
        //6.监听
        while (true) {
            try {
                //6.1 监控客户端
                if (selector.select(2000) == 0) { //NIO的非阻塞的优势
                    System.out.println("server没有别的客户端连接,在监控客户端的同时,还能做别的事情");
                    continue;
                }
                //6.2得到SelectedKey,判断通道里的事件
                Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
                while (keysIterator.hasNext()) {
                    SelectionKey key = keysIterator.next();
                    //6.3防止重复处理
                    keysIterator.remove();
                    //6.4处理请求事件
                    handleRequest(key);
                }
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }
    }

    private static void handleRequest(SelectionKey key) throws IOException {
        SocketChannel channel = null;
        try {
            if (key.isAcceptable()) {
                ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                channel = serverSocketChannel.accept();
                channel.configureBlocking(false);
                System.out.println("服务端接收新的连接");
                channel.register(selector, SelectionKey.OP_READ);
            }

            if (key.isReadable()) {
                channel = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int count = channel.read(buffer);
                if (count > 0) {
                    System.out.println("服务端接收请求:" + new String(buffer.array(), 0, count));
                    channel.register(selector, SelectionKey.OP_WRITE);
                }
            }

            if (key.isWritable()) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                buffer.put("服务器收到".getBytes());
                buffer.flip();
                channel = (SocketChannel) key.channel();
                channel.write(buffer);
                channel.register(selector, SelectionKey.OP_READ);
            }
        } catch (Throwable t) {
            t.printStackTrace();
            if (channel != null) {
                channel.close();
            }
        }
    }
}

客户端:

package cn.demo.netty_chat.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.util.Iterator;

public class NIOClient {
    public static void main(String[] args) {
        new Worker().start();
    }

    static class Worker extends Thread {
        @Override
        public void run() {
            SocketChannel channel = null;
            Selector selector = null;
            try {
                channel = SocketChannel.open();
                channel.configureBlocking(false);

                selector = Selector.open();
                channel.register(selector, SelectionKey.OP_CONNECT);
                channel.connect(new InetSocketAddress("127.0.0.1",9000));
                while (true) {
                    if (selector.select(2000) == 0) { //NIO的非阻塞的优势
                        System.out.println("client连接服务端的同时,做了别的事情");
                        continue;
                    }
                    Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
                    while (keysIterator.hasNext()) {
                        SelectionKey key = keysIterator.next();
                        keysIterator.remove();

                        if (key.isConnectable()) {
                            System.out.println();
                            channel = (SocketChannel) key.channel();

                            if (channel.isConnectionPending()) {
                                channel.finishConnect();

                                ByteBuffer buffer = ByteBuffer.allocate(1024);
                                buffer.put("你好".getBytes());
                                buffer.flip();
                                channel.write(buffer);
                            }

                            channel.register(selector, SelectionKey.OP_READ);
                        }

                        if (key.isReadable()) {
                            channel = (SocketChannel) key.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            int len = channel.read(buffer);

                            if (len > 0) {
                                System.out.println("[" + Thread.currentThread().getName()
                                        + "]收到响应:" + new String(buffer.array(), 0, len));
                                Thread.sleep(5000);
                                channel.register(selector, SelectionKey.OP_WRITE);
                            }
                        }

                        if (key.isWritable()) {
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            buffer.put("你好".getBytes());
                            buffer.flip();

                            channel = (SocketChannel) key.channel();
                            channel.write(buffer);
                            channel.register(selector, SelectionKey.OP_READ);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (channel != null) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (selector != null) {
                    try {
                        selector.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

回顾一下使用 NIO 开发服务端程序的步骤:

  1. 创建 ServerSocketChannel 和业务处理线程池。

  2. 绑定监听端口,并配置为非阻塞模式。

  3. 创建 Selector,将之前创建的 ServerSocketChannel 注册到 Selector 上,监听 SelectionKey.OP_ACCEPT

  4. 循环执行 Selector.select() 方法,轮询就绪的Channel`。

  5. 轮询就绪的 Channel 时,如果是处于 OP_ACCEPT 状态,说明是新的客户端接入,调用 ServerSocketChannel.accept 接收新的客户端。

  6. 设置新接入的 SocketChannel 为非阻塞模式,并注册到 Selector 上,监听 OP_READ

  7. 如果轮询的 Channel 状态是 OP_READ,说明有新的就绪数据包需要读取,则构造 ByteBuffer 对象,读取数据。

NIO 原生 API 的弊端 :

① NIO 组件复杂 : 使用原生 NIO 开发服务器端与客户端 , 需要涉及到 服务器套接字通道 ( ServerSocketChannel ) , 套接字通道 ( SocketChannel ) , 选择器 ( Selector ) , 缓冲区 ( ByteBuffer ) 等组件 , 这些组件的原理 和API 都要熟悉 , 才能进行 NIO 的开发与调试 , 之后还需要针对应用进行调试优化

② NIO 开发基础 : NIO 门槛略高 , 需要开发者掌握多线程、网络编程等才能开发并且优化 NIO 网络通信的应用程序

③ 原生 API 开发网络通信模块的基本的传输处理 : 网络传输不光是实现服务器端和客户端的数据传输功能 , 还要处理各种异常情况 , 如 连接断开重连机制 , 网络堵塞处理 , 异常处理 , 粘包处理 , 拆包处理 , 缓存机制 等方面的问题 , 这是所有成熟的网络应用程序都要具有的功能 , 否则只能说是入门级的 Demo

④ NIO BUG : NIO 本身存在一些 BUG , 如 Epoll , 导致 选择器 ( Selector ) 空轮询 , 在 JDK 1.7 中还没有解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值