前言

作为Netty的入门介绍篇,本篇主要通过Java IO网络编程相关概念引出Netty。Netty主要解决了什么问题,为什么网络通讯我们首选Netty呢。

Java网络编程

首先看下Java原生提供的网络编程组件,包括了最早JDK1.0版本就支持的Socket这种BIO(`阻塞`)编程模型;及JDK1.4实现的NIO(`非阻塞`)编程模型。它们有什么优缺点呢?

Java BIO

早起操作系统只支持本地系统套接字库提供的阻塞函数。这种数据传输模型使用的叫BIO编程模型,普通示例如下:

服务端:

/**
 * 单线程的服务器
 */
@Slf4j
public class SocketServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(82);

        try {
            while (true) {
                // 阻塞接收连接
                Socket socket = serverSocket.accept();

                // 接收信息
                InputStream inputStream = socket.getInputStream();
                OutputStream outputStream = socket.getOutputStream();
                int port = socket.getPort();
                int maxLen = 2048;
                byte[] context = new byte[maxLen];

                // 阻塞获取数据
                int readLen = inputStream.read(context, 0, maxLen);
                String message = new String(context, 0, readLen);

                // 打印
                log.info("接收来自端口:{} 的信息:{}", port, message);

                // 发送数据
                outputStream.write("回应客户端请求".getBytes(StandardCharsets.UTF_8));

                //关闭
                outputStream.close();
                inputStream.close();
                socket.close();
            }
        } catch (Exception e) {
            log.error("服务端错误", e);
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }

}

客户端:

/**
 * 客户端socket线程
 */
@Slf4j
public class SocketClient {

    public static void main(String[] args) {
        Socket socket;
        OutputStream out = null;
        InputStream in = null;
        try {
            socket = new Socket("localhost", 82);
            out = socket.getOutputStream();
            in = socket.getInputStream();

            out.write(("客户端请求").getBytes(StandardCharsets.UTF_8));
            out.flush();

            // 等待服务器返回
            log.info("客户端请求发送完成, 等待返回");
            int maxLen = 1024;
            byte[] context = new byte[maxLen];
            int realLen;
            String message = "";

            // 开始读取
            while ((realLen = in.read(context, 0, maxLen))  != -1) {
                message += new String(context, 0, realLen);
            }
            log.info("客户端收到的信息是:" + message);
        } catch (Exception e) {
            log.error("客户端异常:", e);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                log.error("客户端异常:", e);
            }
        }
    }

}

运行:先启动服务端;再启动客户端。

该方式在接受连接请求和读写IO时都会导致阻塞。服务器在同一时间只能处理一个连接,如果需要同时处理多个连接,则需要使用多线程。

所以,它只适合并发量少的场景,对于并发量高的场景,线程和内存资源是有限的,该方案性能太差。

Java NIO

Java NIO是在JDK1.4引入的,源于操作系统支持非阻塞函数系统调用和事件通知API(对应着IO多路复用模型),具体大佬的文章参考>。普通示例如下:

服务器:

/**
 * nio服务端
 */
@Slf4j
public class NIOServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        // server的socketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        // 注册selector,对OP_ACCEPT事件感兴趣
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // serverSocket
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 8888));

        while (true) {
            // 阻塞获取事件
            int select = selector.select();
            log.info("select count:{}", select);
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 连接事件
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel ssChannel = (ServerSocketChannel)selectionKey.channel();

                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel.accept();
                    sChannel.configureBlocking(false);

                    // 这个新连接主要用于从客户端读取数据,对OP_READ事件感兴趣
                    sChannel.register(selector, SelectionKey.OP_READ);
                    log.info("acceptChannel:{} hashCode:{}", sChannel, sChannel.hashCode());
                } else if (selectionKey.isReadable()) {
                    // 可读
                    SocketChannel sChannel = (SocketChannel) selectionKey.channel();
                    log.info("readableChannel:{} hashCode:{}", sChannel, sChannel.hashCode());
                    String msg = readDataFromSocketChannel(sChannel);
                    log.info("msg:{}", msg);
                    // 不能close,否则无法再收到数据
                    // sChannel.close();
                }
                // 重要,这里移除的是selector内部的集合中的SelectionKey实例,对应已经处理过的io事件。
                // 不移除的话,如果此次的SelectionKey出现,与之前的SelectionKey集合不全相同,会出错;但如果是全部相同的SelectionKey再次出现,会判重相等而不会报错
                iterator.remove();
            }
        }
    }

    /**
     * 读取通道的数据
     * 正常需要数据解码,不然客户端发送多次,被一次读取完
     *
     * @param sChannel:socket通道
     */
    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder sb = new StringBuilder();

        buffer.clear();

        int read = sChannel.read(buffer);
        if (read <= 0) {
            return "";
        }

        // 读取buffer
        buffer.flip();
        // 可读大小
        int limit = buffer.limit();
        char[] dst = new char[limit];
        for (int i= 0; i < limit; i++) {
            dst[i] = (char)buffer.get(i);
        }
        sb.append(dst);
        buffer.clear();

        return sb.toString();
    }

}

客户端

/**
 * 客户端,未使用Nio,直接Socket连接写入数据
 */
@Slf4j
public class NIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream outputStream = socket.getOutputStream();

        // 通过consult获取输入
        Scanner in = new Scanner(System.in);
        while(true){
            if(in.hasNextLine()) {
                String line = in.nextLine();
                outputStream.write(line.getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
            }
        }
    }
}

运行:先启动服务器代码;再启动客户端代码,在客户端输入控制台输入内容回车。

NIO引入了核心的组件包括:

  • 通道 Channel

  • 缓冲区 ByteBuffer

  • 多路复用选择器 Selector

Java NIO解决了BIO的问题,可以使用较少的线程,同时监听多个IO事件;更少的线程也意味着更少的上下文的切换。

但是,在高负载下可靠及高效地处理和调度IO操作仍是一项繁琐且容易出错的任务。

为什么选择Netty

首先Netty被定义为:

是一款异步事情驱动的网络应用程序框架,支持快速地开发可维护的高性能面向协议的服务器和客户端。

Netty所支持的特性,包括如下:

  • 设计方面

对NIO进行了抽象,简化了网络编程的复杂性。

  • 使用方面

提供了简单的API和大量的示例集;也提供了很多超出标准的Java NIO的功能,如多种协议(Http、WebSocket)的支持、常用的编解码器支持。

  • 性能方面

拥有更高的吞吐量及更低的延迟,得益于池化和复用,更少的内存复制;异步和事件驱动模型。

  • 稳定性方面

强大且活跃的社区,很多大型公司在使用Netty,Facebook、Google等。

  • 安全性

完整的SSL/TLS以及StartTLS的支持。

总结

Netty是一个高性能的网络通讯框架,对Java NIO进行抽象,基于异步和事件驱动模型,实现了更高的性能、更方便使用的API等等一系列特性。