Netty核心技术

  • 一、Netty介绍和应用场景
  • 1.介绍
  • 2. Netty的应用场景
  • 二、JAVA的BIO
  • 1.介绍
  • 2.应用示例
  • 3.代码追踪
  • 4.BIO优劣分析
  • 三、Java的NIO
  • 1.介绍
  • 2. 应用示例
  • 3.代码追踪
  • 4.NIO优劣分析


一、Netty介绍和应用场景

1.介绍

官网:“Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器“”

android使用Netty的应用场景_android使用Netty的应用场景

  • Netty主要针对在TCP协议下,面对Client的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输应用
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景

    综上:要彻底理解Netty,需要先搞懂NIO

2. Netty的应用场景

  • 互联网行业:互联网行业: 在分布式系统中, 各个节点之间需要远程服务调用, 高性能的 RPC 框架必不可少, Netty 作为异步高性能的通信框架, 往往作为基础通信组件被这些 RPC 框架使用。
  • 游戏行业:无论是手游服务端、还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信。
  • 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架, 默认采用 Netty 进行跨界点通信。
  • 企业软件:企业和 IT 集成需要 ESB,Netty 对多协议支持、私有协议定制的简洁性和高性能是 ESB RPC 框架的首选通信组件。事实上,很多企业总线厂商会选择 Netty 作为基础通信组件,用于企业的 IT 集成。
  • 通信行业:Netty 的异步高性能、高可靠性和高成熟度的优点,使它在通信行业得到了大量的应用

二、JAVA的BIO

1.介绍

  • java BIO:java block IO,即java阻塞式IO,到底什么是阻塞式IO,哪里阻塞了,请看下图JAVA的BIO模型图及对比代码即可了解

2.应用示例

  • 使用BIO模型编写一个服务器端,监听9091端口,当有客户端连接时,就启动一个线程与之通讯。
  • 要求使用线程池机制改善,可以连接多个客户端.
  • 服务器端可以接收客户端发送的数据(通过命令:“nc IP地址 端口”)。
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {

    public static void main(String[] args) throws IOException {
        //1、创建一个线程池,若服务端收到一个客户端连接就启动一个线程与客户端通讯
        ExecutorService pool = Executors.newCachedThreadPool();

        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9091);//服务端Socket,绑定到9091端口,用于接收客户端连接请求
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("server started");

        while (true) {
            /**
             * 服务端接收客户端连接,这个地方会阻塞,
             */
            final Socket client = serverSocket.accept();//
            System.out.println("connect a client");
            pool.execute(new Runnable() {
                public void run() {
                    handler(client);
                }
            });

        }
    }

    public static void handler(Socket socket) {
        byte[] bytes = new byte[1024];
        try {
            /**
             * 读取客户端发送过来的数据,这个地方会阻塞
             */
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.print(Thread.currentThread().getId() + " : " + Thread.currentThread().getName()+":  ");
                    System.out.println(new String(bytes, 0, read));
                } else {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }


    }
}

3.代码追踪

  1. 服务端启动步骤
    服务端三部曲 创建socket绑定端口进行端口监听
    启动BIO代码追踪(启动后会在目录下生成许多out.*文件,一般文件大小最大的为主线程的执行步骤)
  2. android使用Netty的应用场景_netty_02


  3. android使用Netty的应用场景_socket_03

  4. 查看out.1929文件,在启动过程中,linux内核主要会执行以下操作
    ①创建socket(会返回一个数字5的文件描述符,相当于java代码中xxxRef的一个引用的对象,这里的5相当于服务端socket的一个引用)
  5. android使用Netty的应用场景_websocket_04

  6. 对应的java代码
  7. android使用Netty的应用场景_netty_05

②绑定端口(服务端socket绑定端口9091,对应到代码中new ServerSocket(9091);)

android使用Netty的应用场景_android使用Netty的应用场景_06


对应的java代码

android使用Netty的应用场景_netty_07

③启动监听

android使用Netty的应用场景_android使用Netty的应用场景_08


对应的java代码

android使用Netty的应用场景_网络_09

  1. 客户端与服务端通信
    ①客户端连接9091端口,并发送消息( nc 192.168.56.101 9091),
  2. android使用Netty的应用场景_websocket_10

  3. ②服务端收到客户端连接请求,并打印发送过来的数据(线程名为: pool-1-thread-1)
  4. android使用Netty的应用场景_socket_11

  5. ③再来一个客户端,并发送数据
  6. android使用Netty的应用场景_netty_12

  7. ④服务端收到客户端请求,并打印发送过来的数据(线程名为: pool-1-thread-2)
  8. android使用Netty的应用场景_netty_13

  9. 从系统调用角度理解BIO
    服务端启动后一直处于等待客户端连接状态,当一个客户端连接过来,accept指令就会收到该客户端的文件描述符(6,可以理解为这个客户端的引用),当服务端没收到客户端请求时,一直会处于accpept阻塞状态,不会执行后续步骤,收到一个客户端请求后,返回一个该客户端的文件描述符,并执行后续步骤
    主线程所对应的out.1729文件
  10. android使用Netty的应用场景_websocket_14

  11. 主线程收到一个客户端连接,起一个线程(clone指令,相当于起一个线程),返回1767,对应一个out.1767文件
  12. android使用Netty的应用场景_netty_15

  13. 新起的线程的执行步骤会在out.1767文件中可以看到
  14. android使用Netty的应用场景_websocket_16

  15. 线程池开辟一个线程后,用于接收文件描述符为6的客户端发送过来的数据,若没发送数据过来,就会一直处于recv状态。这就解释了为啥读写是阻塞的问题(读写数据为新起的线程,所以读写操作在out.1767文件中看到)
  16. android使用Netty的应用场景_网络_17

  17. 客户端发送数据过来(aaaaaa)
  18. android使用Netty的应用场景_网络_18

  19. 线程收到数据后,执行write操作,当数据写完之后,又处于recv状态,等待客户端发送数据过来
  20. android使用Netty的应用场景_android使用Netty的应用场景_19

4.BIO优劣分析

优势:

  1. 容易理解并应用
  2. 一个线程一个连接、可以建立很多连接
  • 问题
    客户端连接多的话,会有很多线程开启,浪费资源
  • 解决方案:使用NIO模型

三、Java的NIO

1.介绍

  • Java NIO 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java
    提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的
    NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
  • NIO是 面向缓冲区 ,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
    非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

2. 应用示例

NIO代码展示

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;

public class NIOServer {
    public static void main(String[] args) throws IOException, InterruptedException {
        LinkedList<SocketChannel> clients = new LinkedList<SocketChannel>();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(9091));
        ssc.configureBlocking(false);//设置非阻塞

        while (true) {
            Thread.sleep(1000);
            SocketChannel client = ssc.accept();//这里不会阻塞,要么返回null,要么有值,BIO这块是阻塞的
            if (client == null) {
                String time = LocalDateTime.now().format(DateTimeFormatter.ISO_TIME).toString();
                System.out.println("current time" +time);
            } else {
                client.configureBlocking(false);
                int port = client.socket().getPort();
                String time = LocalDateTime.now().format(DateTimeFormatter.ISO_TIME).toString();
                System.out.println("current time is " + time + " client port :" + client.socket().hashCode() + " : " + port);
                clients.add(client);//若有新的客户单会将此客户端添加到客户端列表中
            }
            ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
            for (SocketChannel c : clients) {//遍历所有客户端,输出客户端数据
                int num = c.read(buffer);//这里也不会阻塞,要么读取到数据返回非0,要么读不到数据,BIO这一块是阻塞的
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);
                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }else {
                    System.out.println("client "+c.socket().getLocalAddress()+"connected ,but no send data to server");
                }
            }
        }


    }
}

代码对应流程图

android使用Netty的应用场景_socket_20

3.代码追踪

  • 执行命令:strace -ff -o out java NIOServer
  • 每隔1s会获取一次客户端连接,若没有客户端连接则打印null,但不会阻塞
  • android使用Netty的应用场景_socket_21

  • 客户端连接了但没发送数据,又进行下一次获取客户端操作,也没有发生阻塞
  • android使用Netty的应用场景_android使用Netty的应用场景_22

  • 客户端发送数据过来了(注意,若有某个或某几个客户端发送大量数据过来,服务端多单线程处理打印客户端发送过来的数据,这个地方是同步执行的)
  • android使用Netty的应用场景_android使用Netty的应用场景_23

系统调用追踪

server启动三部曲:创建socket、绑定端口、执行监听-

android使用Netty的应用场景_android使用Netty的应用场景_24

android使用Netty的应用场景_socket_25


android使用Netty的应用场景_网络_26

  • 启动server后每隔1s,accept一次,没有客户端连接返回-1,不会阻塞(若阻塞就直指是accpet状态等,处于等待返回值状态)
  • 客户端一旦建立连接,accpet返回值变为5(这个5就是客户端的一个文件描述符,相当于java中的xxxRef)
  • 客户端连接后没发送数据,服务端又接收下一个客户端的连接,没收到客户端连接,返回值为-1
  • 客户端发送数据到服务端之前,read的返回值一直是-1(若为BIO则这里会阻塞(参考上文RecvFrom),等待客户端发送数据过来,),发送数据过来口read返回值为72了,读取数据完了之后,又进行下一轮循环,read返回值为-1.

    说到这里,应该对NIO有了深刻的认识,那相对于BIO,NIO有什么优势和不足呢??

4.NIO优劣分析

优势:一个线程可以同时接受客户端请求和处理客户端发送过来的数据,避免了多线程资源浪费
问题:如下所示

观察如下代码:每秒循环一次,会遍历所有的client

android使用Netty的应用场景_网络_27


那么!!问题来了,假如我有100000个客户端连接,只有100个客户端处于活跃状态(即会向服务端发送数据),每秒进行一次while循环,会遍历100000个客户端,拿到有用的客户端才100个,代价是不是太大了。

怎么解决呢,想象一下,要是有什么办法能够直接获取到这活跃的100个客户端,那么每秒进行一次while循环,只需要获取到这100个活跃客户端的连接不就行了。