• Java NIO通信的基础,基于TCP C/S例子介绍


    为了更好的理解Netty异步事件驱动网络通信框架,有必要先了解一点Java NIO原生的通信理论,下面将结合基于TCP的例子程序,含客户端和服务端的源码,实现了Echo流程。

    Java NIO的核心概念有三个:ChannelSelectorByteBuffer

    而这当中,Channel的比重最大,NIO的功能主要基于Channel来实现,进行业务逻辑操作。Selector主要是IO事件选择器,当一个Channel创建并配置好后,注册到Selector上,与Selector相关的重要概念是SelectionKey,这个上面绑定了IO事件相关的Channel。在获取到Channel后,进行数据的读写操作,Channel的数据读写是不能直接操作数据的,必须基于ByteBuffer进行,然而,Java NIO原生的ByteBuffer操作比较繁琐,要flip和clear操作。

    1. 而我们在业务逻辑操作中,用到的channel,主要有ServerSocketChannel,SocketChannel,DataGramChannel。下面,用一个图,来简要的描述下Channel到这三个具体之类之间的继承/实现关系(该图来自网络,若有不妥,请告知,谢谢)。

    2. Selector,是事件选择器,创建Selector后,在调用select之前,在注册Channel到这个Selector上时,必须指定关注的事件类型(interestOps)。通过这个类的select函数,可以获取选择上监听到的IO事件。一旦select函数检测到事件,就可以从Selector上获取到具体有哪些IO事件,这些事件通过SelectionKey承载,SelectionKey上标记出该事件的类型,比如是OP_CONNECT,OP_ACCEPT还是OP_READ等。另外,SelectionKey还记录了对应该IO事件发生的Channel,可以通过SelectionKey得到该Channel。

    3. ByteBuffer。 因为字节操作,是操作系统与IO设备之间进行通信的基本数据单元,在Java NIO中,各通道Channel之间进行数据通信时,指定必须是基于ByteBuffer的。 ByteBuffer有两个重要的函数,flip和clear。当Channel调用read函数,将数据读到ByteBuffer中后,ByteBuffer的数据长度指针将会移动到数据长度所在的位置,这个位置是小于等于ByteBuffer容量capacity值的。当业务逻辑操作读取到的数据前,需要对ByteBuffer做一下flip操作,就是将limit指针指向当前数据指针position的位置,然后,将position指针指向0的位置。数据逻辑结束后,一般要恢复ByteBuffer,即调用clear函数。

    这三个重要的概念,做了一番解释和描述后,就以一个demo程序,基于Java NIO的TCP C/S源码,代码中带有了重要逻辑的注释,后续不再单独解释。

    A. TCP Server:

    /**
     * @author "shihuc"
     * @date   2017年3月16日
     */
    package javaSocket.tcp.server;
    
    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.nio.charset.Charset;
    import java.util.Iterator;
    import java.util.Set;
    
    import javaSocket.tcp.Constants;
    
    /**
     * @author chengsh05
     *
     */
    public class TcpServer {
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            try {
                startServer(Constants.SERVER_PORT);
            } catch (IOException e) {            
                e.printStackTrace();
            }
        }
        
        public static void startServer(int port) throws IOException{
            /*
             *开启一个服务channel, 
             *A selectable channel for stream-oriented listening sockets.
             */
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            serverChannel.bind(new InetSocketAddress(port));
    
            /*
             * 创建一个selector
             */
            Selector selector = Selector.open();    
            /*
             * 将创建的serverChannel注册到selector选择器上,指定这个channel只关心OP_ACCEPT事件
             */
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
    
            while (true) {
                /*
                 * select()操作,默认是阻塞模式的,即,当没有accept或者read时间到来时,将一直阻塞不往下面继续执行。
                 */
                int readyChannels = selector.select();  
                if (readyChannels <= 0) {
                    continue;
                }
    
                /*
                 * 从selector上获取到了IO事件,可能是accept,也有可能是read
                 */
                Set<SelectionKey> SelectonKeySet = selector.selectedKeys(); 
                Iterator<SelectionKey> iterator = SelectonKeySet.iterator();
    
                /*
                 * 循环遍历SelectionKeySet中的所有的SelectionKey
                 */
                while (iterator.hasNext()) {    
                    SelectionKey key = iterator.next();                
                    if (key.isAcceptable()) {          //处理OP_ACCEPT事件 
                        SocketChannel socketChannel = serverChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {  //处理OP_READ事件
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        StringBuilder sb = new StringBuilder();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        
                        int readBytes = 0;
                        int ret = 0;
                        /*
                         * 注意读数据的时候,ByteBuffer的操作,需要flip,clear进行指针位置的调整
                         */
                        while ((ret = socketChannel.read(byteBuffer)) > 0) {
                            readBytes += ret;
                            byteBuffer.flip();                        
                            sb.append(Charset.forName("UTF-8").decode(byteBuffer).toString());                        
                            byteBuffer.clear();
                        }
                        
                        if (readBytes == 0) {
                            System.err.println("handle opposite close Exception");
                            socketChannel.close();
                        }
                        
                        String message = sb.toString();
                        System.out.println("Message from client: " + message);
                        if (Constants.CLIENT_CLOSE.equalsIgnoreCase(message.toString().trim())) {
                            System.out.println("Client is going to shutdown!");                        
                            socketChannel.close();
                        } else if (Constants.SERVER_CLOSE.equalsIgnoreCase(message.trim())) {
                            System.out.println("Server is going to shutdown!");                        
                            socketChannel.close();
                            serverChannel.close();
                            selector.close();
                            System.exit(0);
                        } else {
                            String outMessage = "Server response:" + message;
                            socketChannel.write(Charset.forName("UTF-8").encode(outMessage));
                        }
                    }
                    /*
                     * 将selector上当前已经监听到的且已经处理了的事件标记清除掉。
                     */
                    iterator.remove();
                }
            }
        }
    }

    B. TCP Client

    /**
     * @author "shihuc"
     * @date   2017年3月16日
     */
    package javaSocket.tcp.client;
    
    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.Charset;
    import java.util.Scanner;
    
    import javaSocket.tcp.Constants;
    
    /**
     * @author chengsh05
     *
     */
    public class TcpClient {
    
        /**
         * @param args
         */
        public static void main(String[] args) {
            try {
                startClient(Constants.SERVER_IP, Constants.SERVER_PORT);
            } catch (IOException e) {        
                e.printStackTrace();
            }
        }
        
        public static void startClient(String serverIp, int serverPort) throws IOException{
            /*
             * 创建一个SocketChannel,指定为非阻塞模式
             * A selectable channel for stream-oriented connecting sockets. 
             */
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            
            /*
             * 连接到指定的服务地址
             */
            socketChannel.connect(new InetSocketAddress(serverIp, serverPort));
    
            /*
             * 创建一个事件选择器Selector
             */
            Selector selector = Selector.open();
            
            /*
             * 将创建的SocketChannel注册到指定的Selector上,并指定关注的事件类型为OP_CONNECT
             */
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
    
            /*
             * 从系统输入终端读取数据,作为客户端信息输入源
             */
            Scanner sc = new Scanner(System.in);
            String cont = null;        
            while(true){
                if(socketChannel.isConnected()){
                    cont = sc.nextLine();
                    socketChannel.write(Charset.forName("UTF-8").encode(cont));
                    if(cont == null || cont.equalsIgnoreCase(Constants.CLIENT_CLOSE)){
                        socketChannel.close();
                        selector.close();
                        sc.close();
                        System.out.println("See you, 客户端退出系统了");
                        System.exit(0);
                    }
                }
                /*
                 * 设置1sec的超时时间,进行IO事件选择操作
                 */
                int nSelectedKeys = selector.select(5000);
                if(nSelectedKeys > 0){
                    for(SelectionKey skey: selector.selectedKeys()){
                        /*
                         * 判断检测到的channel是不是可连接的,将对应的channel注册到选择器上,指定关心的事件类型为OP_READ
                         */
                        if(skey.isConnectable()){
                            SocketChannel connChannel = (SocketChannel) skey.channel();
                            connChannel.configureBlocking(false);
                            connChannel.register(selector, SelectionKey.OP_READ);
                            connChannel.finishConnect();
                        }
                        /*
                         * 若检测到的IO事件是读事件,则处理相关数据的读相关的业务逻辑
                         */
                        else if(skey.isReadable()){
                            SocketChannel readChannel = (SocketChannel) skey.channel();
                            StringBuilder sb = new StringBuilder();
                            /*
                             * 定义一个ByteBuffer的容器,容量为1k
                             */
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                           
                            int readBytes = 0;
                            int ret = 0;
                            /*
                             * 注意,对ByteBuffer的操作,需要关心的是flip,clear等。
                             */
                            while ((ret = readChannel.read(byteBuffer)) > 0) {
                                readBytes += ret;
                                byteBuffer.flip();                            
                                sb.append(Charset.forName("UTF-8").decode(byteBuffer).toString());                            
                                byteBuffer.clear();
                            }
                            
                            if (readBytes == 0) {
                                System.err.println("handle opposite close Exception");
                                readChannel.close();
                            }
                        }                                        
                    }
                    /*
                     * 一次监听的事件处理完毕后,需要将已经记录的事件清除掉,准备下一轮的事件标记
                     */
                    selector.selectedKeys().clear();
                }else{
                    System.err.println("handle select timeout Exception");
                    socketChannel.close();
                }
            }        
        }
    }

    阅读上述代码时,请注意,server和client的实现风格不太一样,主要是针对SelectionKeySet的遍历,一次select操作获取到的所有的SelectionKey处理完后的扫尾工作,体现出Selector的工作逻辑,若写过C程序实现过TCP server/client程序,对事件选择的过程应该就更清楚了。

    最后,总结一下Java NIO TCP协议下的C/S结构程序流程图,为彻底理解Java NIO服务。

    基于这个例子引出的Java NIO的逻辑过程和思想,再去研读Netty的代码,相信会容易理解Netty的核心reactor模型工作原理。

  • 相关阅读:
    LLVM 笔记(二)—— PHI node
    Ubuntu,sublime快速启动
    ubuntu14.04安装Thinkphp
    ubuntu,scrapy安装
    ubuntu,sublime text 3中文输入的问题
    ubuntu安装beatifulsoup,pip,creepy
    ubuntu,系统设置无法打开
    Ubuntu14.04安装完成的基本配置
    在Sublime Text 3中添加snippet
    领航工作室启用新域名啦!
  • 原文地址:https://www.cnblogs.com/shihuc/p/6559025.html
Copyright © 2020-2023  润新知