一起来研究[Netty的OIO和NIO]与[不使用OIO和NIO的传输]

337 阅读3分钟

这是我参与8月更文挑战的第21天,活动详情查看:8月更文挑战

一、不通过Netty使用OIO和NIO

  下面我们使用JDK API的应用程序的阻塞(OIO)版本和异步(NIO)版本。以下代码展示了其阻塞版本的实现

public class PlainOioServer {
    public void server(int port) throws IOException {
        // 将服务器绑定到指定端口
        final ServerSocket socket = new ServerSocket(port);
        try {
            for (;;) {
                // 接受连接
                final Socket clientSocket = socket.accept();
                System.out.println("Accepted connection from " + clientSocket);
                // 创建一个新的线程来处理该连接
                new Thread(()->{
                    OutputStream out;
                    try {
                        out = clientSocket.getOutputStream();
                        // 将消息写给已连接的客户端
                        out.write("Hi\r\n".getBytes(Charset.forName("UTF-8")));
                        out.flush();
                        // 关闭连接
                        clientSocket.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            clientSocket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  以下代码完全可以处理中等数量的并发客户端。但是随着应用程序变得流行起来,你会发现它并不能很好地伸缩到支撑成千上万的并发。当你决定改用异步网络编程,你会发现异步API是完全不同的,以至于你不得不重写程序。

以下代码是未使用Netty的异步网络代码清单:

public class PlainNioServer {
    public void server(int port) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        ServerSocket socket = serverSocketChannel.socket();
        InetSocketAddress address = new InetSocketAddress(port);
        // 将服务器绑定到选定的端口
        socket.bind(address);
        // 打开Selector来处理Channel
        Selector selector = Selector.open();
        // 将ServerSocketChannel注册到Selector以接受连接
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
        for (;;) {
            try {
                // 等待需要处理的新事件;阻塞将一直持续到下一个传入事件
                selector.select();
            } catch (IOException e) {
                e.printStackTrace();
                break;
            }
            // 获取所有接收事件的SelectionKey实例
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                try {
                    // 检查事件是否是一个新的已经就绪可以被接受的连接
                    if (key.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        // 接受客户端并将它注册到选择器
                        client.register(selector, 
                                SelectionKey.OP_READ|SelectionKey.OP_WRITE, msg.duplicate());
                        System.out.println("Accepted connection from " + client);
                    }
                    // 检查套接字是否已经准备好写数据
                    if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        while (buffer.hasRemaining()) {
                            // 将数据写到已连接的客户端
                            if (client.write(buffer) == 0) {
                                break;
                            }
                        }
                        // 关闭连接
                        client.close();
                    }
                } catch (IOException e) {
                    key.cancel();
                    key.channel().close();
                }
            }
        }
    }
}

  虽然这段代码所做的事情与上一版本完全相同,但是代码却截然不同。虽然代码复杂性增加了,但效率提高了。

二、通过Netty使用OIO和NIO

  以下代码清单,是使用Netty框架编写的阻塞网络编程

public class NettyOioServer {
    public void server(int port) throws Exception {
        final ByteBuf buf = Unpooled.unreleasableBuffer(
                Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
        // netty官方不建议使用,推荐使用NIO / EPOLL / KQUEUE transport
        EventLoopGroup group = new OioEventLoopGroup();
        try {
            // 创建ServerBootstrap
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    // 使用OioServerSocketChannel以允许阻塞模式(旧的I/O)
                    .channel(OioServerSocketChannel.class)
                    .localAddress(new InetSocketAddress(port))
                    // 指定ChannelInitializer,对于每个已接受的连接都调用它
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(
                                    // 添加一个ChannelInboundHandlerAdapter以拦截和处理事件
                                    new ChannelInboundHandlerAdapter() {
                                        @Override
                                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                            ctx.writeAndFlush(buf.duplicate())
                                                    // 将消息写到客户端,并添加ChannelFutureListener,以便消息一被写完就关闭连接
                                                    .addListener(ChannelFutureListener.CLOSE);
                                        }
                                    }
                            );
                        }
                    });
            // 绑定服务器以接受连接
            ChannelFuture future = bootstrap.bind().sync();
            future.channel().closeFuture().sync();
        } finally {
            // 释放所有的资源
            group.shutdownGracefully().sync();
        }
    }
}

  非阻塞的Netty,只需要改动以下两处代码就能实现Netty的异步网络编程,第一处把EventLoopGroup group = new OioEventLoopGroup()改成EventLoopGroup group = new NioEventLoopGroup(),还一处是.channel(OioServerSocketChannel.class)改成.channel(NioServerSocketChannel.class),把这两处代码一改就成了Netty异步网络编程

  因为Netty为每种传输的实现都暴露了相同的API,所以无论选用哪一种传输的实现,代码都仍然不受影响。在所有的情况下,传输的实现都依赖于interface Channel、ChannelPipeline和ChannelHandler。