java网络编程—NIO与Netty(二)

版权声明:坚持原创,坚持深入原理,未经博主允许不得转载!! https://blog.csdn.net/lemon89/article/details/76928005

相关文章
java网络编程—NIO与Netty(四)
java网络编程—NIO与Netty(三)
java网络编程—NIO与Netty(二)
java网络编程—NIO与Netty(一)
java网络编程—基石:五种IO模型及原理(多路复用\epoll)

继续接着总结NIO

Java NIO:transferFrom与transferTo

两个channel间的通信,不需要通过buffer直接进行数据交换。
示例:

/**
 * @author zhangsh
 */
public class NIOTransferData {

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

    public static void transferData() throws IOException {
        RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();

        RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        long position = 0;
        long count = fromChannel.size();

        toChannel.transferFrom(fromChannel, position, count);

        RandomAccessFile toFileEnd = new RandomAccessFile("toFileEnd.txt", "rw");
        FileChannel toChannelEnd = toFileEnd.getChannel();
        toChannel.transferTo(position, toChannel.size(), toChannelEnd);
    }
}

Java NIO:Selector

java NIO通过使用Selector可以单线程的管理多个(channel)通道的读写多个通道可以注册在一个Selector上,一个持有这个Selector的线程可以通过Selector去管理多个通道,每个通道都是某种类型的连接

Selector的创建

Selector selector = Selector.open();

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下:

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

与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式
注意register()方法的第二个参数。这是一个“interest集合——感兴趣的事件集合,也就是这个selector准备监听的集合。注意区分 感兴趣的时间 与 准备好的事件的区别。

SelectionKey

关键属性:

  • interest集合
    selectionKey.interestOps()
    interestOps()是你注册的感兴趣的事件,通过如下方法获取,并判断(详见代码见最后实例)
    如上边所说,register第二个参数是“interest set”,指定了channel监听的事件类型:

    Connect: SelectionKey.OP_CONNECT
    一个成功连接了Server的channel,注册为Connect

    Accept :SelectionKey.OP_ACCEPT
    一个接受连接请求的 serverSocketChannel,被注册为Accept状态(注意该事件只用于服务端

    Read :SelectionKey.OP_READ
    一个有数据并可被读取的channel,注册为Read状态

    Write SelectionKey.OP_WRITE
    一个可写入数据的channel,注册为Write状态

//如要你要注册多种事件,使用"|"操作
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

interestOps: channel上的关注的事件,通过&运算可以得到相应的判断

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet &
SelectionKey.OP_ACCEPT)!=0;
boolean isInterestedInConnect = (interestSet &
SelectionKey.OP_CONNECT)!=0;

boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ)!=0;

boolean isInterestedInWrite = (interestSet &
SelectionKey.OP_WRITE)!=0;

为什么这么判断?
比如,public static final int OP_WRITE = 1 << 2; 也就是说OP_WRITE =0010(2进制);
当你的interestSet 也是SelectionKey.OP_WRITE(1<<2)时,按位与操作后的结果还是本身 10,
否则其他的事件类型会得到0的结果。

  • ready集合
    与interest集合类似,但是这个ready集合中的事件表示客户端与服务端已经成功建立了连接。
    同样是4种类型:
    Connect: SelectionKey.OP_CONNECT
    一个成功连接了Server的channel,注册为Connect
    Accept :SelectionKey.OP_ACCEPT
    一个接受连接请求的 serverSocketChannel,被注册为Accept状态(注意该事件只用于服务端)
    Read :SelectionKey.OP_READ
    一个有数据并可被读取的channel,注册为Read状态
    Write SelectionKey.OP_WRITE
    一个可写入数据的channel,注册为Write状态
//源码也是使用位操作判断的
selectionKey.isAcceptable()//这个用在服务端
selectionKey.isConnectable()
selectionKey.isReadable()
selectionKey.isWritable()

代码实践,请仔细阅读注释:

 Java NIO 中所讲述的 Selector 的使用流程:

1 通过 Selector.open() 打开一个 Selector.
2 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)
3 不断重复:
    调用 select() 方法
    调用 selector.selectedKeys() 获取 selected keys
    迭代每个 selected key:
        1) 从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)
        2) 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 
           "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()"
           获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.
        3) 根据需要更改 selected key 的监听事件.
        4) 将已经处理过的 key 从 selected keys 集合中删除.

服务端:

public class NIOServer {

    /* 标识数字 */
    private int flag = 0;
    /* 缓冲区大小 */
    private int BLOCK = 4096;
    /* 接受数据缓冲区 */
    private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);// 4KB
    /* 发送数据缓冲区 */
    private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);// 4KB
    private Selector selector;
    ServerSocketChannel serverSocketChannelTemp;

    public NIOServer(int port) throws IOException {
        // ServerSocketChannel用来在服务端监听Socket连接
        // 在这个ServerSocketChannel建立之后(open静态方法建立),创建ServerSocket相应TCP请求
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 在这个ServerSocketChannel
        // 创建ServerSocket相应TCP请求。一个ServerSocketChannel上的ServerSocket是单例对象
        System.out.println("serverSocketChannel" + serverSocketChannel);
        ServerSocket serverSocket = serverSocketChannel.socket();
        // 然后在这个ServerSocket上绑定ip+port
        serverSocket.bind(new InetSocketAddress(port));
        serverSocketChannelTemp = serverSocketChannel;

        // 最后,把那个ServerSocketChannel对象注册到selector上
        // -----------------------------------------------------------Selector--------------------------------------------------------------
        // 可以从selector中获取多个注册了的channel,一个selector可以管理多个channel,
        // 因此消除了创建多个线程去处理多个请求的做法。
        // 另外,selector中对channel的管理都是非阻塞的,所以FileChannel这种阻塞的channel不能使用selector

        selector = Selector.open();

        // register第二个参数是“interest set”,指定了channel监听的事件类型
        /**
         * <pre>
         * -Connect SelectionKey.OP_CONNECT 一个成功连接了Server的channel,注册为Connect
         *
         * -Accept SelectionKey.OP_ACCEPT 一个接受连接请求的 serverSocketChannel,被注册为Accept状态
         * 
         * -Read SelectionKey.OP_READ 一个有数据并可被读取的channel,注册为Read状态
         * 
         * -Write SelectionKey.OP_WRITE 一个可写入数据的channel,注册为Write状态
         * 
         * 如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来 
         * int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
         * </pre>
         */
        // 每次在一个selector上注册一个channel,就会产生一个SelectionKey对象,
        // 需要说一下,SelectionKey对象的如下属性
        //
        /**
         * 1)interestOps: channel上的关注的事件,通过&运算可以得到相应的判断
         * 
         * <pre>
         * int interestSet = selectionKey.interestOps();
         * 
         * boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT;
         * 
         * boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT)==SelectionKey.OP_CONNECT;
         * 
         * boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ)==SelectionKey.OP_READ;
         *
         * boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE)==SelectionKey.OP_WRITE;
         * 
         * 2)readyOps 是channel准备好了的事件类型;注意与interestOps并不一样! 
         * Selector.select()就是检查是否有注册的兴趣事件中已经准备好了的事件!
         * 可以通过如下方式判断: 
         * selectionKey.isAcceptable(); 一个server socket channel准备好接收新进入的连接
         * selectionKey.isConnectable();  某个channel成功连接到另一个服务器
         * selectionKey.isReadable(); 一个有数据可读的通道
         * selectionKey.isWritable();等待写数据的通道
         * </pre>
         */
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT) " + selectionKey);
        System.out.println("Server Start----:" + port + "   selector    " + selector);
    }

    // 监听
    private void listen() throws IOException {
        while (true) {
            /**
             * <pre>
             * select 返回在这个selector上注册过的兴趣事件 (interestsSet)对应的channel.
             * 比如你在这个selector上注册过,Accept事件,
             * 那么select的含义就是去选择已经准备好的,accept事件对应的channel。
             * 
             * int select():阻塞方法,直到至少返回一个你注册过的兴趣事件对应的channel.
             * int select(long timeout):与select()类似,不同之处在于设定了阻塞超时时间
             * int selectNow():与select()类似,只是不会产生阻塞,立即返回
             * 
             * </pre>
             */
            // It returns only after at least one channel is selected, this
            // selector's wakeup method is invoked, or the current thread is
            // interrupted, whichever comes first.
            if (selector.select() <= 0) {// Selector.select()就是检查是否有注册的兴趣事件中已经准备好了的事件.
            //注意.这里返回的是处于ready状态的事件对应的channel数量
                continue;
            }

            // 执行完selector.select(),会暗示你是否有准备好的channel,
            // 接着执行 Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历获取准备好的channel
            // 每次在一个selector上注册一个channel,就会产生一个SelectionKey对象
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            int i = 0;
            while (iterator.hasNext()) {
                ++i;
                SelectionKey selectionKey = iterator.next();
                iterator.remove();// 必须手动remove这个使用过的key
                handleKey(selectionKey);
            }
            System.out.println("iterator size" + i);
        }
    }
    /**
     * 通过selectKey可以获取到对应的channel和selector (selectionKey.selector())
     * @param selectionKey
     * @throws IOException
     */
    private void handleKey(SelectionKey selectionKey) throws IOException {
        // 接受请求
        ServerSocketChannel server = null;
        SocketChannel client = null;
        String receiveText;
        String sendText;
        int count = 0;
        // 测试此键的通道是否已准备好接新的Socket connection。
        if (selectionKey.isAcceptable()) {
            server = (ServerSocketChannel) selectionKey.channel();
            // 可阻塞模式:若为阻塞方法,只用于ServerSocketChannel,去监听建立的连接。
            // 如果有连接过来,就返回这个新连接的channel
            // 如非阻塞模式:若没有连接建立,会返回null
            System.out.println("serverSocketChannel:" + server);
            System.err.println(serverSocketChannelTemp == server);// 可以看到还是server端之前自己注册的那个serverSocketChannel

            // 通过 ServerSocketChannel.accept() 方法监听新进来的连接。当
            // accept()方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。
            // 通常不会仅仅只监听一个连接,在while循环中调用 accept()方法
            client = server.accept();
            client.configureBlocking(false);
            // 配置为非阻塞
            System.out.println("clientSocketChannel:" + client);
            // 注册到selector,等待连接
            client.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isConnectable()) {
            // a connection was established with a remote server.
            System.out.println(" this is connectable !");
        } else if (selectionKey.isReadable()) {
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            System.err.println(client.toString());// 可以看到还是server端之前自己注册的那个serverSocketChannel

            // 将缓冲区清空以备下次读取
            receivebuffer.clear();
            // 读取服务器发送来的数据到缓冲区中
            count = client.read(receivebuffer);
            if (count > 0) {
                receiveText = new String(receivebuffer.array(), 0, count);
                System.out.println("服务器端接受客户端数据--:" + receiveText);
                client.register(selector, SelectionKey.OP_WRITE);// 客户端消息获取后,读取掉。接着注册一个可写事件,用来向客户端发送消息
            }
        } else if (selectionKey.isWritable()) {
            // 将缓冲区清空以备下次写入
            sendbuffer.clear();
            // 返回为之创建此键的通道。
            client = (SocketChannel) selectionKey.channel();
            sendText = "message from server--" + flag++;
            // 向缓冲区中输入数据
            sendbuffer.put(sendText.getBytes());
            // 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
            sendbuffer.flip();
            // 输出到通道
            client.write(sendbuffer);
            System.out.println("服务器端向客户端发送数据--:" + sendText);
            client.register(selector, SelectionKey.OP_READ);// 向客户端发送消息后,注册一个可读事件,当客户端再次发送消息时,这个事件将ready
        }
    }
    public static void main(String[] args) throws IOException {
        int port = 8989;
        NIOServer server = new NIOServer(port);
        server.listen();
    }
}

客户端:

public class NIOClient {

    /* 标识数字 */
    private static int flag = 0;
    /* 缓冲区大小 */
    private static int BLOCK = 4096;
    /* 接受数据缓冲区 */
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);// 4KB;
    /* 发送数据缓冲区 */
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);

    /* 服务器端地址 */
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress("localhost", 8989);

    public static void main(String[] args) throws IOException {
        /**
         * SocketChannel创建的两种方式:
         * 
         * 1.客户端 : SocketChannel.open(); socketChannel.connect(SERVER_ADDRESS);
         * 
         * 
         * 2.服务端: SocketChannel client =serverSocketChannel.accept();
         * 当有连接建立,accept方法返回建立连接的SocketChannel
         */
        SocketChannel socketChannel = SocketChannel.open();
        // You can set a SocketChannel into non-blocking mode. When you do so,
        // you can call connect(), read() and write() in asynchronous mode.
        socketChannel.configureBlocking(false);
        // 打开选择器
        Selector selector = Selector.open();
        // 注册连接服务端socket动作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 异步连接操作,如 connect() read() write()
        // 即使没有建立连接也会立刻返回,使用socketChannel.finishConnect()检查连接建立是否成功,未成功会抛出异常
        socketChannel.connect(SERVER_ADDRESS);
        // 分配缓冲区大小内存

        Set<SelectionKey> selectionKeys;
        Iterator<SelectionKey> iterator;
        SelectionKey selectionKey;
        SocketChannel client;
        String receiveText;
        String sendText;
        int count = 0;

        while (true) {
            // 选择一组键,其相应的通道已为 I/O 操作准备就绪。
            // 此方法执行处于阻塞模式的选择操作。

            // This method performs a blocking selection operation. It returns
            // only after at least one channel is selected, this selector's
            // wakeup method is invoked, or the current thread is interrupted,
            // whichever comes first.
            System.out.println(selector.select());
            // 返回此选择器的已选择键集。
            selectionKeys = selector.selectedKeys();
            // System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                selectionKey = iterator.next();
                if (selectionKey.isConnectable()) {
                    System.out.println(" this is connectable !");
                    //通过selectKey可以获取到对应的channel和selector
                   //selectionKey.selector()
                    client = (SocketChannel) selectionKey.channel();

                    System.err.println(client == socketChannel);
                    // 异步连接操作,如 connect() read() write()
                    // 即使没有建立连接也会立刻返回,使用socketChannel.finishConnect()检查连接建立是否成功,未成功会抛出异常
                    if (client.isConnectionPending() && client.finishConnect()) {
                        System.out.println("完成连接!");
                        sendbuffer.clear();
                        sendbuffer.put("Hello,Server".getBytes());
                        sendbuffer.flip();
                        client.write(sendbuffer);
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    // 将缓冲区清空以备下次读取
                    receivebuffer.clear();
                    // 异步连接操作 read(),即使socketChannel中没有可读内容,也会立刻返回
                    count = client.read(receivebuffer);
                    if (count > 0) {
                        receiveText = new String(receivebuffer.array(), 0, count);
                        System.out.println("客户端接受服务器端数据--:" + receiveText);
                        client.register(selector, SelectionKey.OP_WRITE);// 读取后接着注册一个可写事件,为了向服务端发消息
                    }

                } else if (selectionKey.isWritable()) {
                    sendbuffer.clear();
                    client = (SocketChannel) selectionKey.channel();
                    sendText = "message from client--" + (flag++);
                    sendbuffer.put(sendText.getBytes());
                    // 将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    sendbuffer.flip();
                    while (sendbuffer.hasRemaining()) {// 无法保证一次全部写完,所以使用循环方式
                        client.write(sendbuffer); // 异步连接操作
                                                  // write(),什么都没写入也会返回,所以循环使用
                    }
                    System.out.println("客户端向服务器端发送数据--:" + sendText);
                    client.register(selector, SelectionKey.OP_READ);// 向服务端发送消息后,注册一个可读事件,当服务端再次返回消息时,这个事件将ready
                }
            }
            selectionKeys.clear();
        }
    }
}

SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:

  • 打开一个SocketChannel并连接到互联网上的某台服务器。上边示例中,Client使用的方式。

  • 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。上边示例中,Server使用的方式,ServerSocketChannel.accept()

非阻塞模式

阻塞模式中,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

就是说channel可以使用异步的方式调用connect()、write()、read()等方法

connect()

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}

write()

非阻塞模式下,write()方法在尚未写出(指数据未到达tcp写缓冲区)任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。

read()

非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。

serverSocketChannel.accept()
非阻塞的监听连接到当前服务器上的Socket连接。如果没有则返回null.

为什么说JAVA NIO提供了基于Selector的异步网络I/O?

第一节介绍了NIO是同步非阻塞的。那么我们为什么经常说的”NIO异步网络模型“

NIO异步网络模型指的编程模型上的异步,通过reactor模型将具体IO操作放入线程池异步化

下一节开始介绍Netty。

猜你喜欢

转载自blog.csdn.net/lemon89/article/details/76928005