美文网首页
【015期】JavaSE面试题(十五):网络IO流

【015期】JavaSE面试题(十五):网络IO流

作者: 我可能是个假开发 | 来源:发表于2020-07-16 10:45 被阅读0次

    开篇介绍

    大家好,我是Java最全面试题库的提裤姐,今天这篇是JavaSE系列的第十五篇,主要总结了Java中的IO流的问题,IO流分为两篇来讲,这篇是第二篇,主要是网络IO流,在后续,会沿着第一篇开篇的知识线路一直总结下去,做到日更!如果我能做到百日百更,希望你也可以跟着百日百刷,一百天养成一个好习惯。

    什么是bio

    同步阻塞式IO,服务端创建一个ServerSocket,然后客户端用一个Socket去连接那个ServerSocket,然后ServerSocket接收到一个Socket的连接请求就创建一个Socket和一个线程去跟那个Socket进行通信。

    public class BioServer {  
        public static void main(String[] args) {
            // 服务端开启一个端口进行监听
            int port = 8080;
            ServerSocket serverSocket = null;   //服务端
            Socket socket;  //客户端
            InputStream in = null;
            OutputStream out = null;
            try {
    
                serverSocket = new ServerSocket(port);  //通过构造函数创建ServerSocket,指定监听端口,如果端口合法且空闲,服务器就会监听成功
                // 通过无限循环监听客户端连接,如果没有客户端接入,则会阻塞在accept操作
                while (true) {
                    System.out.println("Waiting for a new Socket to establish" + " ," + new Date().toString());
                    socket = serverSocket.accept();//阻塞  三次握手
                    in = socket.getInputStream();
                    byte[] buffer = new byte[1024];
                    int length = 0;
                    while ((length = in.read(buffer)) > 0) {//阻塞
                        System.out.println("input is:" + new String(buffer, 0, length) + " ," + new Date().toString());
                        out = socket.getOutputStream();
                        out.write("success".getBytes());
                        System.out.println("Server end" + " ," + new Date().toString());
                    }
    
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 必要的清理活动
                if (serverSocket != null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    什么是nio

    同步非阻塞
    包括Selector,这是多路复用器,selector会不断轮询注册的channel,如果某个channel上发生了读写事件,selector就会将这些channel获取出来,我们通过SelectionKey获取有读写事件的channel,就可以进行IO操作。一个Selector就通过一个线程,就可以轮询成千上万的channel,这就意味着你的服务端可以接入成千上万的客户端。

    public class NioDemo implements Runnable {
        public int id = 100001;
        public int bufferSize = 2048;
    
        @Override
        public void run() {
            init();
        }
    
        public void init() {
            try {
                // 创建通道和选择器
                ServerSocketChannel socketChannel = ServerSocketChannel.open();
                Selector selector = Selector.open();
                InetSocketAddress inetSocketAddress = new InetSocketAddress(
                        InetAddress.getLocalHost(), 4700);
                socketChannel.socket().bind(inetSocketAddress);
                // 设置通道非阻塞 绑定选择器
                socketChannel.configureBlocking(false);
                socketChannel.register(selector, SelectionKey.OP_ACCEPT).attach(
                        id++);
                System.out.println("Server started .... port:4700");
                listener(selector);
    
            } catch (Exception e) {
    
            }
        }
    
        public void listener(Selector in_selector) {
            try {
                while (true) {
                    Thread.sleep(1 * 1000);
                    in_selector.select(); // 阻塞 直到有就绪事件为止
                    Set<SelectionKey> readySelectionKey = in_selector
                            .selectedKeys();
                    Iterator<SelectionKey> it = readySelectionKey.iterator();
                    while (it.hasNext()) {
                        SelectionKey selectionKey = it.next();
                        // 判断是哪个事件
                        if (selectionKey.isAcceptable()) {// 客户请求连接
                            System.out.println(selectionKey.attachment()
                                    + " - 接受请求事件");
                            // 获取通道 接受连接,
                            // 设置非阻塞模式(必须),同时需要注册 读写数据的事件,这样有消息触发时才能捕获
                            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey
                                    .channel();
                            serverSocketChannel
                                    .accept()
                                    .configureBlocking(false)
                                    .register(
                                            in_selector,
                                            SelectionKey.OP_READ
                                                    | SelectionKey.OP_WRITE).attach(id++);
                            System.out
                                    .println(selectionKey.attachment() + " - 已连接");
    
                            // 下面这种写法是有问题的 不应该在serverSocketChannel上面注册
                            /*
                             * serverSocketChannel.configureBlocking(false);
                             * serverSocketChannel.register(in_selector,
                             * SelectionKey.OP_READ);
                             * serverSocketChannel.register(in_selector,
                             * SelectionKey.OP_WRITE);
                             */
                        }
                        if (selectionKey.isReadable()) {// 读数据
                            System.out.println(selectionKey.attachment()
                                    + " - 读数据事件");
                            SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                            ByteBuffer receiveBuf = ByteBuffer.allocate(bufferSize);
                            clientChannel.read(receiveBuf);
                            System.out.println(selectionKey.attachment()
                                    + " - 读取数据:" + getString(receiveBuf));
                        }
                        if (selectionKey.isWritable()) {// 写数据
                            System.out.println(selectionKey.attachment()
                                    + " - 写数据事件");
                            SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                            ByteBuffer sendBuf = ByteBuffer.allocate(bufferSize);
                            String sendText = "hello\n";
                            sendBuf.put(sendText.getBytes());
                            sendBuf.flip();        //写完数据后调用此方法
                            clientChannel.write(sendBuf);
                        }
                        if (selectionKey.isConnectable()) {
                            System.out.println(selectionKey.attachment()
                                    + " - 连接事件");
                        }
                        // 必须removed 否则会继续存在,下一次循环还会进来,
                        // 注意removed 的位置,针对一个.next() remove一次
                        it.remove();
                    }
                }
            } catch (Exception e) {
                System.out.println("Error - " + e.getMessage());
                e.printStackTrace();
            }
    
        }
    
        /**
         * ByteBuffer 转换 String
         *
         * @param buffer
         * @return
         */
        public static String getString(ByteBuffer buffer) {
            String string = "";
            try {
                for (int i = 0; i < buffer.position(); i++) {
                    string += (char) buffer.get(i);
                }
                return string;
            } catch (Exception ex) {
                ex.printStackTrace();
                return "";
            }
        }
    }
    

    什么是aio

    异步非阻塞
    每个连接发送过来的请求,都会绑定一个buffer,然后通知操作系统去异步完成读,此时你的程序是会去干别的事儿的,等操作系统完成数据读取之后,就会回调你的接口,给你操作系统异步读完的数据。

    public class AIOServer {
    
        public final static int PORT = 9888;
        private AsynchronousServerSocketChannel server;
    
        public AIOServer() throws IOException {
            server = AsynchronousServerSocketChannel.open().bind(
                    new InetSocketAddress(PORT));
        }
    
        public void startWithFuture() throws InterruptedException,
                ExecutionException, TimeoutException {
            while (true) {// 循环接收客户端请求
                Future<AsynchronousSocketChannel> future = server.accept();
                AsynchronousSocketChannel socket = future.get();// get() 是为了确保 accept 到一个连接
                handleWithFuture(socket);
            }
        }
    
        public void handleWithFuture(AsynchronousSocketChannel channel) throws InterruptedException, ExecutionException, TimeoutException {
            ByteBuffer readBuf = ByteBuffer.allocate(2);
            readBuf.clear();
    
            while (true) {// 一次可能读不完
                //get 是为了确保 read 完成,超时时间可以有效避免DOS攻击,如果客户端一直不发送数据,则进行超时处理
                Integer integer = channel.read(readBuf).get(10, TimeUnit.SECONDS);
                System.out.println("read: " + integer);
                if (integer == -1) {
                    break;
                }
                readBuf.flip();
                System.out.println("received: " + Charset.forName("UTF-8").decode(readBuf));
                readBuf.clear();
            }
        }
    
        public void startWithCompletionHandler() throws InterruptedException,
                ExecutionException, TimeoutException {
            server.accept(null,
                    new CompletionHandler<AsynchronousSocketChannel, Object>() {
                        public void completed(AsynchronousSocketChannel result, Object attachment) {
                            server.accept(null, this);// 再此接收客户端连接
                            handleWithCompletionHandler(result);
                        }
    
                        @Override
                        public void failed(Throwable exc, Object attachment) {
                            exc.printStackTrace();
                        }
                    });
        }
    
        public void handleWithCompletionHandler(final AsynchronousSocketChannel channel) {
            try {
                final ByteBuffer buffer = ByteBuffer.allocate(4);
                final long timeout = 10L;
                channel.read(buffer, timeout, TimeUnit.SECONDS, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(Integer result, Object attachment) {
                        System.out.println("read:" + result);
                        if (result == -1) {
                            try {
                                channel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            return;
                        }
                        buffer.flip();
                        System.out.println("received message:" + Charset.forName("UTF-8").decode(buffer));
                        buffer.clear();
                        channel.read(buffer, timeout, TimeUnit.SECONDS, null, this);
                    }
    
                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        exc.printStackTrace();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String args[]) throws Exception {
    //        new AIOServer().startWithFuture();
            new AIOServer().startWithCompletionHandler();
            Thread.sleep(100000);
        }
    
    }
    
    

    什么是epoll

    一种多路复用的技术,可以解决之前poll和select大量并发连接情况下cpu利用率过高,以及需要遍历整个被侦听的描述符集的问题。epoll只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

    什么是mmap技术

    把一个磁盘文件映射到内存里来,然后把映射到内存里来的数据通过socket发送出去
    有一种mmap技术,也就是内存映射,直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA引擎拷贝的,同时用户缓 冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了
    光是这一点,就可以避免一次拷贝了,但是这个过程中还是会用户态切换到内核态去进行映射拷贝,接着再次从内核态切换到用户态, 建立用户缓冲区和内核缓冲区的映射
    接着把数据通过Socket发送出去,还是要再次切换到内核态
    接着直接把内核缓冲区里的数据拷贝到Socket缓冲区里去,然后再拷贝到网络协议引擎里,发送出去就可以了,最后切换回用户态
    减少一次拷贝,但是并不减少切换次数,一共是4次切换,3次拷贝

    什么是零拷贝技术

    linux提供了sendfile,也就是零拷贝技术
    这个零拷贝技术,就是先从用户态切换到内核态,在内核态的状态下,把磁盘上的数据拷贝到内核缓冲区,同时从内核缓冲区拷贝一些 offset和length到Socket缓冲区;接着从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去
    同时从Socket缓冲区里拷贝一些offset和length到网络协议引擎里去,但是这个offset和length的量很少,几乎可以忽略
    只要2次切换,2次拷贝,就可以了

    说一下select,poll,epoll的区别?

    selectpoll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。
    epoll也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

    selectpoll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

    网络IO模型有哪些?

    阻塞IO(bloking IO)

    应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。

    非阻塞IO(non-blocking IO)

    应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。

    多路复用IO(multiplexing IO)

    轮询多个IO任务的状态,只要有任何一个任务完成,就去处理它。这就是IO多路复用

    信号驱动式IO(signal-driven IO)
    应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

    异步IO(asynchronous IO)

    应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。

    相关文章

      网友评论

          本文标题:【015期】JavaSE面试题(十五):网络IO流

          本文链接:https://www.haomeiwen.com/subject/sgtphktx.html