Netty源码分析-什么是Netty

前言

现在,越来越多的公司都在招聘要求中写上了熟悉javaIO,NIO编程, 这也NIO已经成为了java程序员所必须应该掌握的,平时的工作中或许不会用到NIO编程,但是掌握NIO的思想是很好的

IO编程:

在讲解NIO之前,首先要了解一下IO编程,思考一下:编写一个程序,客户端每2s给服务端发送消息,服务端接收到消息后打印出客户端的发送过来的消息

服务端代码

package test.java.test;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author zhtttylz
 */
public class ServerIO {

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

        // 创建serverSocket对象,监听8989端口
        ServerSocket serverSocket = new ServerSocket(8989);

        // 这里使用了内部类,方便在一个代码中进行展示
        new Thread(new Runnable() {
            @Override
            public void run() {

                while(true){

                    try {
                        // 如果有连接进入,就向下进行处理,否则一直在这里进行阻塞
                        Socket socket = serverSocket.accept();

                        // 对每一个新连接新建一个线程进行处理,这里可以使用线程池来进行优化
                        new Thread(new Runnable() {
                            @Override
                            public void run() {

                                byte[] b = new byte[1024];
                                try {
                                    // 从连接对象中获取输入流
                                    InputStream inputStream = socket.getInputStream();
                                    // 不停的检测这个连接是否有数据发送过来
                                    while (true){
                                            int len = 0;
                                        while ((len = inputStream.read(b)) != -1) {

                                            System.out.println(new String(b, 0, len));
                                        }
                                    }
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }).start();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

服务端监听了8989端口,每次有连接进入,就新创建一个线程来进行这个连接处理,使用while循环对每个连接进来的线程进行监控,看是否发送了数据,如果发送数据就从inputstream中读取1024个字节,然后打印到控制台

客户端代码

package test.java.test;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

/**
 * @author zhtttylz
 */
public class ClientIO {

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

        Socket socket = new Socket("127.0.0.1", 8989);

          for(int i = 0; i < 10; i++){

              int a = i;
              new Thread(new Runnable() {
                  @Override
                  public void run() {

                      // 通过socket获取outputStream,将内容写入,然后进行内容的传输
                      try {
                          OutputStream outputStream = socket.getOutputStream();
                          outputStream.write(("hello 你好啊! 我是客户端" + a).getBytes());
                          outputStream.flush();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                  }
              }).start();

              Thread.sleep(2000);
          }
    }
}

 客户端相对来说很简单,就是每隔2s给服务端发送消息

问题

从上面可以看出来,传统的IO必须每一个新连接都需要进行线程的创建,就算可以使用线程池,也只能缓解,无法再根本上解决问题,比如如果有10000个连接,这个时候就需要10000个线程来进行连接的维护,这样会带来如下几个问题:

  1. 线程资源受限:线程是操作系统中非常宝贵的资源,频繁的创建while阻塞线程是非常严重的资源浪费,操作系统耗不起
  2. 线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
  3. 除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高

 所以java在jdk1.4之后提出了NIO

JAVA NIO编程

相信NIO很多人听过并且熟悉它的概念,这里不对NIO进行深入的分析,只是阐述它相比较于传统的IO的区别,又或者可以这么说,它相比于传统的IO做了哪些优化

  • 传统的对每个进程都新建一个线程进行处理,每个线程内部使用while死循环来进行数据是否发送过来的监控,传统的IO模型如图

  • NIO对于每个连接不再新建线程进行处理,而是将某个连接直接绑定到一个固定的线程,以后这个线程就是这个连接的处理者,负责这个连接的读写操作 ,NIO的线程模型如图

 如图所示,如果每个线程连接进来,但是并不是一直在发送数据,只是偶尔发送一次,这时,传统的io中的每个线程中的死循环(用于检测是否有数据可读)就会造成大量的资源浪费,而在NIO中,它把每个线程的循环变成了一个循环,用于检测是否有数据发送过来,这是如何做到的呢.主要还是在于多路复用器Selector,每次新来一个连接,不是新建一个线程进行处理,而是将其绑定到selector上,selector可以将绑定在上面发送了数据的连接批量检测出来进行处理,这也就是NIO和传统IO最大的区别

小结

经过上面的图解可以看出NIO可以使用一个线程监视很多的连接通道,而传统的IO必须为每个连接新建一个线程进行管理 

例子

 场景:公司一共有100个人,到了下班时间,员工需要回家了

  • 总经理IO的处理方式:给每个员工配车,每个人开车回家,这种老板是每个员工都希望看到的,但是这种方式是很耗费公司财产的,如果员工多了,公司也很难承担的起每辆车的购买费用
  • 总经理NIO的处理方式:公司买一辆大巴车,所有的员工做大巴车,将每个员工送到他们的家,这样就节省了购买车所需要的成本,只需要买一辆车就可以了,如果员工多了,一辆大巴车不够,那么久再买大巴车

这就是NIO的处理方式,一个线程绑定多个连接,对这些链接进行监控,减少了线程的开支,由于NIO中线程的个数少了,所以线程切换效率也大大提高

IO是以字节流为单位进行读取的

IO是以字节流进行读取的,而NIO内部维护了一个缓冲区,每次可以从这个缓冲区中按块进行数据的读取,类似于抓鱼,IO使用鱼竿钓鱼,每次只能钓上来一条,NIO则使用渔网,一次捞上来一堆,效率得到了很大的提升

使用NIO来对之前的项目进行改造

了解了NIO和IO的大体区别,我们就将原本的项目进行改造,这里只对服务端进行改造,如果有兴趣可以自己把客户端进行改造

原本代码的改造(请作好心里准备) 

 NIO服务端

package test.java.test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author zhtttylz
 */
public class ServerNIO {

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

        // 通过Selector的open方法获取selector对象
        Selector selector = Selector.open();

        // 同样是创建一个线程进行连接的处理
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 获取一个通道
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                    // 将端口8989绑定到这个通道上
                    serverSocketChannel.socket().bind(new InetSocketAddress(8989));
                    // 将这个通道注册给selector,标记这个通道的状态是1<<4 也就是16(准备就绪)
                    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                    // 设置为非阻塞状态
                    serverSocketChannel.configureBlocking(false);

                    // 将selector进行轮询
                    while (true) {

                        // 设置阻塞时间是1ms
                        if (selector.select(1) > 0) {

                            // 获取通道中发生了变化的连接,可能是新连接,也有可能是旧的连接发送了数据
                            Set<SelectionKey> set = selector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();

                            while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();

                                if (key.isAcceptable()) {
                                    try {
                                        // 如果新进来的连接是准备状态,也就是新连接进来的连接,那么就将其设置为可读取状态,将其注册到轮询器中
                                        SocketChannel newChannel = ((ServerSocketChannel) key.channel()).accept();
                                        newChannel.configureBlocking(false);
                                        newChannel.register(selector, SelectionKey.OP_READ);
                                    } finally {
                                        // 注意:这里要将处理过的节点进行删除
                                        keyIterator.remove();
                                    }
                                }else if(key.isReadable()){ // 如果是可以发送数据的状态

                                    SocketChannel socketChannel = (SocketChannel)key.channel();
                                    // 建立一个缓冲区,进行数据的读取
                                    ByteBuffer buf = ByteBuffer.allocate(1024);
                                    socketChannel.read(buf);
                                    System.out.println(buf.toString());
                                }
                            }
                        }
                    }
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

 如果是没有进行过NIO方面学习的,估计对里面很多的类不是很熟悉,这个例子只是进行了连接和读操作的判断,一些其余的判断并未写出,如果有兴趣可以自行进行补充

个人感受

使用java原生的类库实现NIO竟然如此麻烦和复杂,并且对于初次接触这方面的人来说很不友好(我第一次是没看懂,被自己菜醒),接下来介绍一下NIO编程的核心思想(这里不详细对每个组件进行介绍)

 NIO编程的核心思想

  •  selector:用于轮询是否有新的连接,如果有新的连接,就要对其进行处理,那是不是可以使用两个selector,将新连接进入和旧连接发送消息分隔开呢,其实上面的服务端可以进行优化,创建两个selector,一个负责进行新连接的轮询,一个负责监控连接进来的连接是否要发送数据,在这里我们对服务端再次进行优化
package test.java.test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author zhtttylz
 */
public class ServerNIO {

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

        // 创建两个selector用来进行新连接的监控和旧连接发送消息的处理
        Selector serverSelector = Selector.open();
        Selector clientSelector = Selector.open();

        // 同样是创建一个线程进行连接的处理
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    // 获取一个通道
                    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                    // 将端口8989绑定到这个通道上
                    serverSocketChannel.socket().bind(new InetSocketAddress(8989));
                    // 将这个通道注册给selector,标记这个通道的状态是1<<4 也就是16(准备就绪)
                    serverSocketChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
                    // 设置为非阻塞状态
                    serverSocketChannel.configureBlocking(false);

                    // 将selector进行轮询
                    while (true) {

                        // 设置阻塞时间是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 {
                                        // 如果新进来的连接是准备状态,也就是新连接进来的连接,那么就将其设置为可读取状态,将其注册到轮询器中
                                        SocketChannel newChannel = ((ServerSocketChannel) key.channel()).accept();
                                        newChannel.configureBlocking(false);
                                        // 注意:这里要将已经进来的管道注册到clientSelector中中
                                        newChannel.register(clientSelector, SelectionKey.OP_READ);
                                    } finally {
                                        // 注意:这里要将处理过的节点进行删除
                                        keyIterator.remove();
                                    }
                                }
                            }
                        }
                    }
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {

                        // 设置阻塞时间是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 socketChannel = (SocketChannel)key.channel();
                                        // 建立一个缓冲区,进行数据的读取
                                        ByteBuffer buf = ByteBuffer.allocate(1024);
                                        socketChannel.read(buf);
                                        System.out.println(buf.toString());
                                    }finally {
                                        keyIterator.remove();
                                    }
                                }
                            }
                        }
                    }
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
  • 注意下面这一块:每次拿到新连接进来的channel,将其注册到专门处理已经连接进来的selector(clientSelector)中 ,这样就成功避免了传统IO的每一个线程就新建一个死循环进行发送数据的监控,从而节省了资源

  •  NIO的每次数据的读写都会以块为单位,也就是bytebuffer

小结(对NIO的吐槽)

  • JDK的NIO编程需要了解很多的概念,编程复杂,对NIO入门非常不友好,编程模型不友好,ByteBuffer的api简直反人类,.
  • JDK的NIO底层由epoll实现,该实现饱受诟病的空轮训bug会导致cpu飙升100%(说是修好了,但是并没有)
  • 项目庞大之后,需要非常多的维护,如果成员变动,新成员根本无法很快的熟悉代码,而且自己造轮子很容易出现BUG(都是前人的血泪史)

基于以上几点,netty都对其进行了很好的解决

我们为什么要使用netty

  • netty的API使用简单,开发门槛低于NIO
  • 功能强大,支持各种协议的开发,如webSocket(后期会有基于netty的网页视频聊天室的讲解,敬请期待)
  • 经历了大规模的应用验证。在互联网、大数据、网络游戏、企业应用、电信软件得到成功,很多著名的框架通信底层就用了Netty,比如Dubb
  • 稳定,修复了NIO出现的所有Bug。比如javaNIO的空轮询bug
  • Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑

Netty编程

首先需要进行maven依赖的引入,这里使用的是netty4,官方的netty5已经废弃(说是模型有问题,笔者这里并没有深入研究为什么,有兴趣的可以研究一下共同交流)

maven依赖:

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.25.Final</version>
</dependency>

 然后我们是用netty进行服务端的改造(这里只对服务端进行改造,有兴趣的话可以对客户端进行同样的改造)

netty的服务端改造

这里直接上代码

package chapter1;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author zhtttylz
 */

public class NettyServer {

    public static void main(String[] args) {

        // 创建两个线程组,分别用来对应传统NIO中的serverSelector和clientSelector
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss, worker)
                    // 着这里绑定一个NIOSocketChannel,用于处理新连接进来的客户端的通道,类似于NIO中的ServerSocketChannel
                    .channel(NioServerSocketChannel.class)
                    // 在这里添加我们自定义的拦截器,这里为了方便观看,我直接使用内部类进行书写
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        protected void initChannel(NioSocketChannel ch) {

                            // 在这里绑定编解码器
                            ch.pipeline().addLast(new StringDecoder());
                            ch.pipeline().addLast(new StringEncoder());

                            // 在这里添加我们的服务端逻辑,为了方便观看,同样使用的内部类
                            ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                                    System.out.println(msg);
                                }
                            });
                        }
                    });

            // 绑定服务端的端口
            ChannelFuture f = b.bind(8989).sync();

            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {

            // 优雅的退出,释放线程资源
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

 这些代码实现了我们之前所写的所有的功能,整个代码逻辑很清晰,服务端启动,添加通道,绑定编解码器,进行业务处理(这里为了方便看,我都是使用的内部类,真实场景可以将每个内部类抽取为单独的业务代码块),到最后的关闭,是不是相对于之前的NIO感觉简洁了很多

注意:代码中的boss对应的就是我们在NIO中的serverSelector(用于监控新连接的多路复用器),works对应的自然就是clientSelector(监控已经连接进来的连接)了,相信从NIO过渡到Netty并不是一件很困难的事情

结语

如果你没有学习过netty,我相信本系列会是你从0开始的最佳资料,,如果你在工作中需要接触到网络编程,那么掌握netty是应该是属于你的刚需,我会将每一章的代码分包放在github上,如果有需要的朋友可以自行下载,同时也欢迎大家一起进行学习交流

github地址  : https://github.com/zhtttylz/Netty_code_demo      (偷偷的求个赞或者star^ˇ^)

猜你喜欢

转载自blog.csdn.net/zhttly/article/details/82685986