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

来源:互联网 发布:重生君颜网络剧在线看 编辑:程序博客网 时间:2024/06/06 17:48

相关文章
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。

原创粉丝点击