「Netty 01」从 BIO、NIO 聊到 Netty,最后还要实现个 RPC 框架!

你们好,我是 「后端技术进阶」 做者,一个热爱技术的少年。html

@[toc]java

以为不错的话,欢迎 star!ღ( ´・ᴗ・` )比心git

老套路,学习某一门技术或者框架的时候,第一步固然是要了解下面这几样东西。github

  1. 是什么?
  2. 有哪些特色?
  3. 有哪些应用场景?
  4. 有哪些成功使用的案例?
  5. .....

为了让你更好地了解 Netty 以及它诞生的缘由,先从传统的网络编程提及吧!编程

仍是要从 BIO 提及

传统的阻塞式通讯流程

早期的 Java 网络相关的 API(java.net包) 使用 Socket(套接字)进行网络通讯,不过只支持阻塞函数使用。后端

要经过互联网进行通讯,至少须要一对套接字:安全

  1. 运行于服务器端的 Server Socket。
  2. 运行于客户机端的 Client Socket

Socket 网络通讯过程以下图所示:服务器

img

https://www.javatpoint.com/so...微信

Socket 网络通讯过程简单来讲分为下面 4 步:网络

  1. 创建服务端而且监听客户端请求
  2. 客户端请求,服务端和客户端创建链接
  3. 两端之间能够传递数据
  4. 关闭资源

对应到服务端和客户端的话,是下面这样的。

服务器端:

  1. 建立 ServerSocket 对象而且绑定地址(ip)和端口号(port): server.bind(new InetSocketAddress(host, port))
  2. 经过 accept()方法监听客户端请求
  3. 链接创建后,经过输入流读取客户端发送的请求信息
  4. 经过输出流向客户端发送响应信息
  5. 关闭相关资源

客户端:

  1. 建立Socket 对象而且链接指定的服务器的地址(ip)和端口号(port):socket.connect(inetSocketAddress)
  2. 链接创建后,经过输出流向服务器端发送请求信息
  3. 经过输入流获取服务器响应的信息
  4. 关闭相关资源

一个简单的 demo

为了便于理解,我写了一个简单的代码帮助各位小伙伴理解。

服务端:

public class HelloServer {
    private static final Logger logger = LoggerFactory.getLogger(HelloServer.class);

    public void start(int port) {
        //1.建立 ServerSocket 对象而且绑定一个端口
        try (ServerSocket server = new ServerSocket(port);) {
            Socket socket;
            //2.经过 accept()方法监听客户端请求, 这个方法会一直阻塞到有一个链接创建
            while ((socket = server.accept()) != null) {
                logger.info("client connected");
                try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                     ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {
                   //3.经过输入流读取客户端发送的请求信息
                    Message message = (Message) objectInputStream.readObject();
                    logger.info("server receive message:" + message.getContent());
                    message.setContent("new content");
                    //4.经过输出流向客户端发送响应信息
                    objectOutputStream.writeObject(message);
                    objectOutputStream.flush();
                } catch (IOException | ClassNotFoundException e) {
                    logger.error("occur exception:", e);
                }
            }
        } catch (IOException e) {
            logger.error("occur IOException:", e);
        }
    }

    public static void main(String[] args) {
        HelloServer helloServer = new HelloServer();
        helloServer.start(6666);
    }
}

ServerSocketaccept() 方法是阻塞方法,也就是说 ServerSocket 在调用 accept()等待客户端的链接请求时会阻塞,直到收到客户端发送的链接请求才会继续往下执行代码,所以咱们须要要为每一个 Socket 链接开启一个线程(能够经过线程池来作)。

上述服务端的代码只是为了演示,并无考虑多个客户端链接并发的状况。

客户端:

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 16:56:00
 */
public class HelloClient {

    private static final Logger logger = LoggerFactory.getLogger(HelloClient.class);

    public Object send(Message message, String host, int port) {
        //1. 建立Socket对象而且指定服务器的地址和端口号
        try (Socket socket = new Socket(host, port)) {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            //2.经过输出流向服务器端发送请求信息
            objectOutputStream.writeObject(message);
            //3.经过输入流获取服务器响应的信息
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            return objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            logger.error("occur exception:", e);
        }
        return null;
    }

    public static void main(String[] args) {
        HelloClient helloClient = new HelloClient();
        helloClient.send(new Message("content from client"), "127.0.0.1", 6666);
        System.out.println("client receive message:" + message.getContent());
    }
}

发送的消息实体类

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 17:02:00
 */
@Data
@AllArgsConstructor
public class Message implements Serializable {

    private String content;
}

首先运行服务端,而后再运行客户端,控制台输出以下:

服务端:

[main] INFO github.javaguide.socket.HelloServer - client connected
[main] INFO github.javaguide.socket.HelloServer - server receive message:content from client

客户端:

client receive message:new content

资源消耗严重的问题

很明显,我上面演示的代码片断有一个很严重的问题:只能同时处理一个客户端的链接,若是须要管理多个客户端的话,就须要为咱们请求的客户端单首创建一个线程。 以下图所示:

img

对应的 Java 代码多是下面这样的:

new Thread(() -> {
   // 建立 socket 链接
}).start();

可是,这样会致使一个很严重的问题:资源浪费

咱们知道线程是很宝贵的资源,若是咱们为每一次链接都用一个线程处理的话,就会致使线程愈来愈好,最好达到了极限以后,就没法再建立线程处理请求了。处理的很差的话,甚至可能直接就宕机掉了。

不少人就会问了:那有没有改进的方法呢?

线程池虽能够改善,但终究未从根本解决问题

固然有! 比较简单而且实际的改进方法就是使用线程池。线程池还可让线程的建立和回收成本相对较低,而且咱们能够指定线程池的可建立线程的最大数量,这样就不会致使线程建立过多,机器资源被不合理消耗。

ThreadFactory threadFactory = Executors.defaultThreadFactory();
ExecutorService threadPool = new ThreadPoolExecutor(10, 100, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(100), threadFactory);
threadPool.execute(() -> {
     // 建立 socket 链接
 });

可是,即便你再怎么优化和改变。也改变不了它的底层仍然是同步阻塞的 BIO 模型的事实,所以没法从根本上解决问题。

为了解决上述的问题,Java 1.4 中引入了 NIO ,一种同步非阻塞的 I/O 模型。

再看 NIO

Netty 实际上就基于 Java NIO 技术封装完善以后获得一个高性能框架,熟悉 NIO 的基本概念对于学习和更好地理解 Netty 仍是颇有必要的!

初识 NIO

NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。

NIO 中的 N 能够理解为 Non-blocking,已经不在是 New 了(已经出来很长时间了)。

NIO 支持面向缓冲(Buffer)的,基于通道(Channel)的 I/O 操做方法。

NIO 提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不一样的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式:

  1. 阻塞模式 : 基本不会被使用到。使用起来就像传统的网络编程同样,比较简单,可是性能和可靠性都很差。对于低负载、低并发的应用程序,勉强能够用一下以提高开发速率和更好的维护性
  2. 非阻塞模式 : 与阻塞模式正好相反,非阻塞模式对于高负载、高并发的(网络)应用来讲很是友好,可是编程麻烦,这个是大部分人诟病的地方。因此, 也就致使了 Netty 的诞生。

NIO 核心组件解读

NIO 包含下面几个核心的组件:

  • Channel
  • Buffer
  • Selector
  • Selection Key

这些组件之间的关系是怎么的呢?

img

  1. NIO 使用 Channel(通道)和 Buffer(缓冲区)传输数据,数据老是从缓冲区写入通道,并从通道读取到缓冲区。在面向流的 I/O 中,能够将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,全部数据都是经过 Buffer(缓冲区)处理的。 Channel 能够看做是 Netty 的网络操做抽象类,对应于 JDK 底层的 Socket
  2. NIO 利用 Selector (选择器)来监视多个通道的对象,如数据到达,链接打开等。所以,单线程能够监视多个通道中的数据。
  3. 当咱们将 Channel 注册到 Selector 中的时候, 会返回一个 Selection Key 对象, Selection Key 则表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。经过 Selection Key 咱们能够获取哪些 IO 事件已经就绪了,而且能够经过其获取 Channel 并对其进行操做。

Selector(选择器,也能够理解为多路复用器)是 NIO(非阻塞 IO)实现的关键。它使用了事件通知相关的 API 来实现选择已经就绪也就是可以进行 I/O 相关的操做的任务的能力。

简单来讲,整个过程是这样的:

  1. 将 Channel 注册到 Selector 中。
  2. 调用 Selector 的 select() 方法,这个方法会阻塞;
  3. 到注册在 Selector 中的某个 Channel 有新的 TCP 链接或者可读写事件的话,这个 Channel 就会处于就绪状态,会被 Selector 轮询出来。
  4. 而后经过 SelectionKey 能够获取就绪 Channel 的集合,进行后续的 I/O 操做。

NIO 为啥更好?

相比于传统的 BIO 模型来讲, NIO 模型的最大改进是:

  1. 使用比较少的线程即可以管理多个客户端的链接,提升了并发量而且减小的资源消耗(减小了线程的上下文切换的开销)
  2. 在没有 I/O 操做相关的事情的时候,线程能够被安排在其余任务上面,以让线程资源获得充分利用。

使用 NIO 编写代码太难了

一个使用 NIO 编写的 Server 端以下,能够看出仍是总体仍是比较复杂的,而且代码读起来不是很直观,而且还可能因为 NIO 自己会存在 Bug。

不多使用 NIO,很大状况下也是由于使用 NIO 来建立正确而且安全的应用程序的开发成本和维护成本都比较大。因此,通常状况下咱们都会使用 Netty 这个比较成熟的高性能框架来作(Apace Mina 与之相似,可是 Netty 使用的更多一点)。

在这里插入图片描述

重要角色 Netty 登场

简单用 3 点归纳一下 Netty 吧!

  1. Netty 是一个基于 NIO 的 client-server(客户端服务器)框架,使用它能够快速简单地开发网络应用程序。
  2. 它极大地简化并简化了 TCP 和 UDP 套接字服务器等网络编程,而且性能以及安全性等不少方面甚至都要更好。
  3. 支持多种协议如 FTP,SMTP,HTTP 以及各类二进制和基于文本的传统协议。

用官方的总结就是:Netty 成功地找到了一种在不妥协可维护性和性能的状况下实现易于开发,性能,稳定性和灵活性的方法。

Netty 特色

根据官网的描述,咱们能够总结出下面一些特色:

  • 统一的 API,支持多种传输类型,阻塞和非阻塞的。
  • 简单而强大的线程模型。
  • 自带编解码器解决 TCP 粘包/拆包问题。
  • 自带各类协议栈。
  • 真正的无链接数据包套接字支持。
  • 比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
  • 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
  • 社区活跃
  • 成熟稳定,经历了大型项目的使用和考验,并且不少开源项目都使用到了 Netty 好比咱们常常接触的 Dubbo、RocketMQ 等等。
  • ......

使用 Netty 能作什么?

这个应该是老铁们最关心的一个问题了,凭借本身的了解,简单说一下,理论上 NIO 能够作的事情 ,使用 Netty 均可以作而且更好。Netty 主要用来作网络通讯 :

  1. 做为 RPC 框架的网络通讯工具 : 咱们在分布式系统中,不一样服务节点之间常常须要相互调用,这个时候就须要 RPC 框架了。不一样服务指点的通讯是如何作的呢?可使用 Netty 来作。好比我调用另一个节点的方法的话,至少是要让对方知道我调用的是哪一个类中的哪一个方法以及相关参数吧!
  2. 实现一个本身的 HTTP 服务器 :经过 Netty 咱们能够本身实现一个简单的 HTTP 服务器,这个你们应该不陌生。说到 HTTP 服务器的话,做为 Java 后端开发,咱们通常使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,好比 POST 请求、GET 请求等等。
  3. 实现一个即时通信系统 : 使用 Netty 咱们能够实现一个能够聊天相似微信的即时通信系统,这方面的开源项目还蛮多的,能够自行去 Github 找一找。
  4. 消息推送系统 :市面上有不少消息推送系统都是基于 Netty 来作的。
  5. ......

哪些开源项目用到了 Netty?

咱们日常常常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。

能够说大量的开源项目都用到了 Netty,因此掌握 Netty 有助于你更好的使用这些开源项目而且让你有能力对其进行二次开发。

实际上还有不少不少优秀的项目用到了 Netty,Netty 官方也作了统计,统计结果在这里:https://netty.io/wiki/related...

img

后记

RPC 框架源码已经开源了,地址:https://github.com/Snailclimb...

相关文章
相关标签/搜索