Java网络编程--IO与NIO(二)

作者: 无剑_君 | 来源:发表于2019-08-03 11:00 被阅读32次

    一、IO编程

    场景:客户端每隔两秒发送一个带有时间戳的"hello world"给服务端,服务端收到之后打印。


    IO编程
    1. 服务端类
    /**
     * server端首先创建了一个serverSocket来监听8000端口,
     * 然后创建一个线程,线程里面不断调用阻塞方法 serversocket.accept();
     * 获取新的连接,当获取到新的连接之后,
     * 给每条连接创建一个新的线程,这个线程负责从该连接中读取数据,
     * 然后读取数据是以字节流的方式。
     */
    public class IOServer {
        public static void main(String[] args) throws Exception {
            ServerSocket serverSocket = new ServerSocket(8000);
            // TODO (1) 接收新连接线程
            new Thread(() -> {
                while (true) {
                    try {
                        // TODO (1) 阻塞方法获取新的连接
                        Socket socket = serverSocket.accept();
                        // TODO (2) 每一个新的连接都创建一个线程,负责读取数据
                        new Thread(() -> {
                            try {
                                byte[] data = new byte[1024];
                                InputStream inputStream = socket.getInputStream();
                                while (true) {
                                    int len;
                                    // TODO (3) 按字节流方式读取数据
                                    while ((len = inputStream.read(data)) != -1) {
                                        System.out.println(new String(data, 0, len));
                                    }
                                }
                            } catch (IOException e) {
                            }
                        }).start();
    
                    } catch (IOException e) {
                    }
                }
            }).start();
        }
    }
    
    1. 客户端
    /**
     * 客户端的代码,连接上服务端8000端口之后,
     * 每隔2秒,我们向服务端写一个带有时间戳的 "hello world"。
     */
    public class IOClient {
    
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    Socket socket = new Socket("127.0.0.1", 8000);
                    while (true) {
                        try {
                            socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                            socket.getOutputStream().flush();
                            Thread.sleep(2000);
                        } catch (Exception e) {
                        }
                    }
                } catch (IOException e) {
                }
            }).start();
        }
    }
    

    IO编程模型在客户端较少的情况下运行良好,但是对于客户端比较多的业务来说,单机服务端可能需要支撑成千上万的连接,IO模型可能就不太合适了。
    从服务端代码中我们可以看到,在传统的IO模型中,每个连接创建成功之后都需要一个线程来维护,每个线程包含一个while死循环,那么1w个连接对应1w个线程,继而1w个while死循环,这就带来如下几个问题:
    线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
    线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
    除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。

    二、NIO编程

    线程资源受限
    NIO编程模型中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接所有的读写都由这个线程来负责。
    线程切换效率低下
    由于NIO模型中线程数量大大降低,线程切换效率因此也大幅度提高
    IO读写以字节为单位
    NIO解决这个问题的方式是数据读写不再以字节为单位,而是以字节块为单位。IO模型中,每次都是从操作系统底层一个字节一个字节地读取数据,而NIO维护一个缓冲区,每次可以从这个缓冲区里面读取一块的数据。

    1. 服务端NIOServer.java
    public class NIOServer {
        public static void main(String[] args) throws IOException {
            // 服务端Selector
            Selector serverSelector = Selector.open();
            // 客户端Selector
            Selector clientSelector = Selector.open();
    
            new Thread(() -> {
                try {
                    // 对应IO编程中服务端启动
                    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                    // 绑定IP与端口号
                    listenerChannel.socket().bind(new InetSocketAddress(8000));
                    listenerChannel.configureBlocking(false);
                    listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
    
                    while (true) {
                        // 监测是否有新的连接,这里的1指的是阻塞的时间为1ms
                        if (serverSelector.select(1) > 0) {
                            Set<SelectionKey> set = serverSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();
                            while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();
                                if (key.isAcceptable()) {
                                    try {
                                        // TODO (1) 每来一个新连接,不需要创建一个线程,而是直接注册到clientSelector
                                        SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                        clientChannel.configureBlocking(false);
                                        // 注册到客户端通道
                                        clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                    } finally {
                                        keyIterator.remove();
                                    }
                                }
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
            }).start();
    
            new Thread(() -> {
                try {
                    while (true) {
                        // TODO (2) 批量轮询是否有哪些连接有数据可读,这里的1指的是阻塞的时间为1ms
                        if (clientSelector.select(1) > 0) {
                            Set<SelectionKey> set = clientSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();
    
                            while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();
    
                                if (key.isReadable()) {
                                    try {
                                        SocketChannel clientChannel = (SocketChannel) key.channel();
                                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                        // TODO (3) 读取数据以块为单位批量读取
                                        clientChannel.read(byteBuffer);
                                        byteBuffer.flip();
                                        System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                                .toString());
                                    } finally {
                                        keyIterator.remove();
                                        key.interestOps(SelectionKey.OP_READ);
                                    }
                                }
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
            }).start();
        }
    }
    
    1. 客户端NIOClient.java
    public class NIOClient {
        public static void main(String[] args) {
            new Thread(() -> {
                try {
                    Socket socket = new Socket("127.0.0.1", 8000);
                    while (true) {
                        try {
                            socket.getOutputStream().write((new Date() + ": hello world").getBytes());
                            socket.getOutputStream().flush();
                            Thread.sleep(2000);
                        } catch (Exception e) {
                        }
                    }
                } catch (IOException e) {
                }
            }).start();
        }
    }
    

    相关文章

      网友评论

        本文标题:Java网络编程--IO与NIO(二)

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