Netty框架

一、Netty基本介绍

Netty是由JBOSS提供的一个java开源框架,Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
简单来说,Netty其实就是一个基于NIO的客户、服务器端的网络通信编程框架,使用Netty可以快速简单的开发出一个稳定的网络应用,Netty要做的事其实就是网络通信(协议)这块的内容,比如基于TCP或UDP的socket服务开发。

为什么需要netty框架,直接用jdk原生的nio进行网络通信不可以吗?
其实这个问题就好比,你用servlet也可以开发java应用,那为什么还要用Spring框架?很显然,jdk提供的那套nio的源码,确实可以用来做客户端和服务端的通信,但在之前的nio编程代码里可以看到,使用原生nio实现通信是非常麻烦的,我们只是写了个很简单的通信,客户端和服务端就要写那么多代码(注册事件,开启线程监听select等)。
而且网络通信是一件很复杂的事,牵扯到数据的传输,加密解密,压缩解压,还有各种协议不说,且如果你想要自定义一个应用层协议,那自己手写实现是非常麻烦且可能会出现各种各样的bug的,所以Netty框架就出现了,你可以简单理解为netty框架就是对jdk的原生nio的一个封装,使得我们在开发时不仅简化了代码,而且还可以稳定高效不出bug,我们需要做的只是简单的去做一些需要我们自定义的一些代码即可。

网络通信在我们日常开发的项目中很少用到(因为我们开发的项目是部署到服务器上的,通信是服务器要考虑的事情),Netty其实在一些RPC框架或中间件的底层代码中要用到,因rpc是远程调用,那必然要用到远程通信,而用到通信那不用想就是netty框架来实现无疑是最方便的,比如dubbo框架底层就用到了netty,而一些中间件,比如redis、rocketmq这些,也是需要用到通信的,最典型的redis就是单线程多路复用,redis底层正是用netty来实现通信的。且netty 支持自定义协议,某些框架或中间件可以根据自身的需求来自定义自己的应用层通信协议,像dubbo有自己的应用层通信协议dubbo协议,redis有自己的redis协议,而使用netty来自定义协议无疑也是非常方便的。

学习Netty并不是说在我们开发的过程中一定会用到,而是因为netty在很多的软件中都有用到,是一个非常优秀的网络通信框架,所以需要掌握netty是如何使用的。当然如果你要自己开发一款类似服务器或中间件的程序,那肯定要用到netty来完成通信相关的功能。

二、Netty框架的使用及其组件分析

在学习netty之前,还是先来了解一下netty有哪些优势

  1. API 使用简单,开发门槛低;
  2. 功能强大,预置了多种编解码功能,支持多种主流协议
  3. 定制能力强,可以通过 ChannelHandler 对通信框架进行灵活地扩展;
  4. 性能高,通过与其他业界主流的 NIO 框架对比,Netty 的综合性能最优;
  5. 成熟、稳定,Netty 修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为NIO 的 BUG 而烦恼;
  6. 社区活跃,版本迭代周期短,发现的 BUG 可以被及时修复,同时,更多的新功能会加入;
  7. 经历了大规模的商业应用考验,质量得到验证。

这是网上的一些总结,其实netty的优势就是首先它对nio进行了封装,所以使用起来非常简单,且netty不仅支持现有的http、ftp等这些应用层的协议,netty的扩展性很好还支持自定义协议,且netty性能高,传输速度快,基本上没有bug。

既然Netty有这么多的优势,那下面就来看一下如何使用netty,先从一个简单的demo案例开始:

  1. 首先要导入netty依赖
<dependency>
	 <groupId>io.netty</groupId>
	 <artifactId>netty-all</artifactId
	 <version>4.1.42.Final </version>
	 <scope>compile</scope>
</dependency>

在Netty中有一些组件,需要先进行个介绍,然后才能使用,每个框架都有自己定义好的一些规则,比如spring需要有容器,配置类、bean、相关的配置文件等,在Netty框架中也有类似的一些组件,不同的组件负责不同的功能,想使用netty就要先把这些组件的功能弄清楚才可。

Bootstrap:netty框架的启动类和主入口类(和springboot中的启动类比较类似),又分为客户端启动类Bootstrap服务端启动类ServerBootstrap两种。

Channel:channel是java nio的一个基本构造,可以当做数据的载体。可以简单理解为就是网络通信里的一个socket

EventLoop:事件循环,在前面nio的编程里对于接收连接、读写数据这些事件,我们是开辟了一个线程,在while方法里循环调用select方法获取就绪事件,然后处理事件,这就是事件循环。而在Netty中,使用EventLoop来表示事件循环,一个EventLoop由一个永远都不会改变的线程 Thread来驱动,所以我们可以近似理解为EventLoop就是一个线程,我们nio编程里开辟的那个线程,用来循环处理一个个socket(channel)发生的连接、读写这些就绪事件的。

通过上面对channel和EventLoop的介绍,不难发现,channel是负责数据传输的类似socket,而EventLoop是用来处理chanel的读写等事件的,也就是说EventLoop和channel之间是互相对应的,且一个EventLoop可以用来处理多个channel,也就是一对多的意思,这也就是nio的体现,一个线程可以处理很多个连接。

EventLoopGroup:EventLoopGroup的作用负责为每个Channel分配一个EventLoop,也就是说EventLoopGroup管理着EventLoop,当有channel创建时,这个channel要哪个EventLoop来处理,把channel和EventLoop对应起来,这就是EventLoopGroup要做的事。

在这里插入图片描述
ChannelFuture:Netty的IO操作都是异步的,异步那必然要有一种获取结果的方式,ChannelFuture其实和并发编程里的Future类似,Future是通过get方法来获取异步执行结果,ChannelFuture也是Netty用来获取异步IO执行结果的一种方式。

事件、ChannelHandler、ChannelPipeline介绍:
在这里插入图片描述
如图,在Netty中,一个网络请求过来需要经过入站处理(解密、解压、权限校验)一系列的处理到达业务逻辑层,然后经过一系列的出站事件(加密、压缩),发送到网络。

事件:Netty里的事件不仅仅只传输的数据,还有像读写这些动作。

ChannelHandler:这些传输数据的事件,可以分发给一个个的ChannelHandler来处理,ChannelHandler里可以写我们自己想定义的逻辑,ChannelHandler分为入站和出站两类,比如入站的Handler我们可以对数据进行比如解密解压的处理,出站的Hadnler里可以对数据进行加密压缩的处理。
当然Netty也为我们提供了很多开箱即用的写好的ChannelHandler用于处理各种协议如(如 HTTP 和 SSL/TLS),所以我们可以直接用Netty提供的Handler也可以自定义Handler,视情况而定。

ChannelPipeline:所有的ChannelHandler都要放在ChannelPipeline中进行统一管理,因为ChannelHandler可以有很多,且是要有执行顺序的,这里就是个责任链模式的体现,就像我们自己开发的项目中的拦截器过滤器类似,写多个那也有要顺序。Netty就是通过ChannelPipeline来管理所有的ChannelHandler。一个Channel事件过来,在ChannelPipeline中进行流动,被一个或多个ChannelHandler处理。

在这里插入图片描述
如图,在ChannelPipeline中的所有CahnnelHandler分为入站和出站两类,同一个方向的Handler之间是有顺序的,每个CahnnelHandler在创建时都会有其对应的一个ChannelHandlerContext,ChannelHandlerContext内部维护了一个双向链表,各个Handlner之间的顺序就是通过这个双向链表来维护的,而不同方向的Handler是可以没有顺序的。

CahnnelHandler是一个接口,因为上面说了有出站和入站两种类型的Handler,所以为:

  1. ChannelInboundHandler是用于接收入站事件和数据的Handler,
  2. ChannelOutboundHandler用于处理出站的事件和数据。
    但因为我们如果直接定义这些接口的实现类来编写一个Handler,那实现类要实现接口里的所有方法,比较麻烦,所以Netty提供了这两个接口对应适配器类,我们写的Handler只需要继承这个适配类就可以快速的开发Handler了
    在这里插入图片描述

理解了上面Netty中的组件的作用,下面就可以看一下一个基于Netty框架的demo案例:


服务端代码
public class EchoServer  {
    private static final Logger LOG = LoggerFactory.getLogger(EchoServer.class);

    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 9999;
        EchoServer echoServer = new EchoServer(port);
        LOG.info("服务器即将启动");
        echoServer.start();
        LOG.info("服务器关闭");
    }

    public void start() throws InterruptedException {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        /*线程组*/
        EventLoopGroup group  = new NioEventLoopGroup();
        try {
            /*服务端启动必备*/
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
            .channel(NioServerSocketChannel.class)/*指定使用NIO的通信模式*/
            .localAddress(new InetSocketAddress(port))/*指定监听端口*/
                    //.childOption()
            //.handler();
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(serverHandler);
                }
            });
            ChannelFuture f = b.bind().sync();/*异步绑定到服务器,sync()会阻塞到完成*/
            f.channel().closeFuture().sync();/*阻塞当前线程,直到服务器的ServerChannel被关闭*/
        } finally {
            group.shutdownGracefully().sync();

        }


    }


}

服务端的ChannelHandler
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        System.out.println("Server accept: "+in.toString(CharsetUtil.UTF_8));
//
        ctx.writeAndFlush(in);
        //ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

客户端代码
public class EchoClient {

    private final int port;
    private final String host;

    public EchoClient(int port, String host) {
        this.port = port;
        this.host = host;
    }

    public void start() throws InterruptedException {

        /*线程组*/
        EventLoopGroup group  = new NioEventLoopGroup();
        try {
            /*客户端启动必备*/
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)/*指定使用NIO的通信模式*/
                    .remoteAddress(new InetSocketAddress(host,port))/*指定服务器的IP地址和端口*/
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());

                        }
                    });
            ChannelFuture f = b.connect().sync();/*异步连接到服务器,sync()会阻塞到完成*/
            f.channel().closeFuture().sync();/*阻塞当前线程,直到客户端的Channel被关闭*/
        } finally {
            group.shutdownGracefully().sync();

        }
    }

    public static void main(String[] args) throws InterruptedException {
        new EchoClient(9999,"127.0.0.1").start();
    }
}

客户端Handler
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /*读取到网络数据后进行业务处理*/
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("client Accept"+msg.toString(CharsetUtil.UTF_8));
        //ctx.close();
        ctx.close();

    }

    /*channel活跃后,做业务处理*/
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer(
                "Hello,Netty",CharsetUtil.UTF_8));
    }
}

可以看到,使用Netty框架来实现一个nio的通信编程,比jdk原生的nio API来实现要简单了很多。

Handler的共享和并发安全性:从上面的介绍中可以了解到,一个socketChannel有自己的channelPipeline,pipeline中存放了很多的handler,这些handler按理来说是属于一个pipeline即一个socketChannel的,但有些场景下需要handler共享,比如统计服务器收到和发出的报文总数等,这就需要一个共享的handler横跨所有socketChannel。ChannelHandlerAdapter 还提供了实用方法 isSharable()用来共享,但共享就会有可能发生线程安全问题,所以尽量少用全局变量或者改用Atomic类来保证线程安全。

引导类Bootstrap
在网络编程里有客户端与服务端两个角色,netty中的引导类也分为两种,一种是用于客户端的引导类Bootstrap,另一种是用于服务器的引导类ServerBootstrap。
在这里插入图片描述
上图第二个区别,客户端引导类Bootstrap需要一个EventLoopGroup,而服务端引导类ServerBootstrap则需要两个,这是因为为服务器需要两组不同的 Channel。第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的 Channel。

TCP的粘包与半包问题
在这里插入图片描述
TCP的粘包:实际上就是TCP每次发送的数据包大小是基本固定的,而应用层传来的发送的数据大小不是固定的,但传输层TCP这里是不知道你业务上的数据是怎么分的,所以在TCP进程传输数据包时,若本次发送的网络数据包太小则本身会启用Nagle算法将较小的数据包进行合并然后再发送,这个合并也就是粘包的发生
而对于UDP来说是没有粘包发生的,因为UDP不会对小数据包进行合并,每个数据包都是完整的所以也就没有粘包这一说。

TCP的拆包:应用程序写入数据的字节大小大于socket发送缓存区的大小,也就是数据包太大了,此时要拆包进行发送

TCP的粘包与半包这种问题,如果我们不解决,就会导致服务端接收到的数据包信息是杂乱的,因为包不是合并了就是拆开了,也就导致服务端接收到的包数据无法正常的解析,那服务端处理起来就非常麻烦或者发生错误,所以我们应该想办法去解决粘包与半包的问题。

解决TCP的粘包与半包
前面说了,因为底层的TCP是无法理解业务上的数据内容的,所以底层TCP这里是无法保证数据不拆包与粘包的,所以我们解决粘包与版本的思路就只能通过上层来解决:

  1. 在包尾增加分隔符,比如回车换行符进行分割,这样服务端在接收到数据包后就可以根据该分隔符来分割出来客户端要发送的每一个包。
  2. 消息定长,例如每个报文的大小长度固定为200字节,如果不够,空位补上。这种方式虽然能一定程度上解决,但会浪费空间,且若数据内容过大还是会产生拆包。
  3. 将消息分为消息头和消息体,消息头中包含消息总长度的这么一个字段,这样服务端在读取数据是读到消息头就可以知道该消息的长度,从而拿到完整的一个消息内容。

三、http与https的区别及SSL/TLS加密知识总结

因为netty里提供了对SSL/TLS这种安全协议的支持,再加上面试经常会问到http与https的协议的区别,所以这里对http与https做一个详细的梳理总结,参考文章:HTTP与HTTPS的区别以及SSL/TLS加密知识总结

Http(Hyper Text Transfer Protocol):超文本传输协议,是一个应用层协议,是web上进行数据交换的基础,简单来说就是用来规范客户端与服务器之间通信行为的(规定了请求头、请求方法post/get、响应码200、404、500这一类的东西)。

Https(Hyper Text Transfer Protocol Secure),:安全的超文本传输协议,https多的这个s就是Secure安全的意思,在http的基础上使用了SSL/TLS协议将传递的数据进行了加密和安全认证,简单来说可以理解为https = http + SSL/TLS。默认端口443。

http与https的区别:

  1. http采用未加密的明文进行数据传输,不安全。https则是具有安全的SSL/TLS加密传输协议。
  2. http默认端口80,https默认端口443
  3. http是无状态的,即不记录每次请求直接的联系,只能通过cookie来进行身份验证。https是可提供数据加密与身份认证的一种网络协议,比http要安全。

其实http与https最大的区别就是https对数据进行加密传输,用到的是SSL/TLS协议进行的数据加密,那下面就来看一下SSL/TLS是什么

SSL(Secure Sockets Layer)/TLS(Transport Layer Security)是一种安全协议,可以理解为它是介于应用层协议与传输层协议之间的一种安全协议,在数据传输之前对应用层的数据进行加密后再进行传输
TLS是SSL的升级后的版本,两者区别不是很大,现在用的一般都是TLS协议,不用过于纠结两者的区别,了解即可。

SSL/TLS这种安全协议对数据加密采用了非对称加密与对称加密两种方式相结合的方式进行加密,具体的加密流程可以看上面的链接文章,这里将对称加密与非对称加密进行一个简单介绍:

在这里插入图片描述

在这里插入图片描述

java中的javax.net.ssl 包提供了对SSL/TLS的支持,在Netty中同样通过一个 名为SslHandler的channelHandler实现对ssl的支持,这个SslHandler其实就是利用了javax.net.ssl 包下提供的API实现对SSL协议的支持,大多数情况下,SslHandler应该是Pipeline中的第一个handler,因为数据传输过来的第一步肯定是要先进行加密或解密的。所以在Netty中如果想使用https,只需将sslhandler加入到ChannelPipeline中即可

四、使用Netty实现一个自己的通信框架

上面大致介绍了netty的基本使用,下面主要讲解使用netty来手写一个通信框架,既然是通信,必然是要有客户端和服务器两个角色,一个通信框架要考虑的问题有很多,这里简单介绍几个要关注的点:
在这里插入图片描述

使用netty来实现通信框架,需要我们重点关注的就是这些channelhandler,每一个handler都负责了通信中的一些内容,比如处理TCP粘包半包的handler,序列化相关的、处理心跳机制相关的handler等。这里不做过多的介绍,可以直接看代码,比较复杂,这里简单先理解使用netty要做什么事。

netty相关面试题分析:

Netty 是如何解决 JDK 中的 Selector BUG 的?
在jdk的nio编程里,可能会出现操作系统linux的epoll告诉selector有事件来了,但jdk并没有拿到事件,也就是说发生的事件并不是那4种连接、接收连接、读、写),然后jdk的select会一直循环去拿事件,但因为事件又拿不到,所以就会一直空转,从而导致CPU100%

netty的解决方法是对selector的空转进行记录,当空转到达一定次数后,重建selector,并将之前的selector关闭,以解决空转问题。

DNS 域名解析的过程
我们浏览器输入一个网址,例如www.baidu.com,而这个域名对应的应该有一个ip,我们需要去DNS服务器中,找到www.baidu.com对应的IP,然后通过ip方法到具体的百度的某一台服务器,打开百度的页面。

而DNS服务器并不是我们传统理解的全世界就13台根DNS服务器,这是一个错误的理解。DNS是一个分层的服务系统,大致来说有3种类型的DNS服务器:根DNS服务器、顶级域DNS服务器、权威DNS服务器,每一类服务器分别负责解析的内容不同,分工合作。

截止到目前大概全世界有1500台根DNS服务器,根DNS服务器负责解析 com、cn、gov等这些域名的尾部,一个请求如www.baidu.com过来会先打到根DNS服务器上,根DNS服务器发现是com,则转发到 负责解析com类型域名的顶级域 DNS服务器,顶级域DNS服务器拿到此域名后,再转发到权威DNS服务器上,权威DNS服务器根据此域名找到对应的ip地址。

也就是一个域名的解析要经历这3种DNS服务器,一步一步解析完成,但也有有域名缓存这些类似的功能,并不说每次方法都要这么麻烦。

五、Netty总结

上面这几点主要讲解了Netty框架的基本构造,以及网络通信中可能发生的一些问题(如TCP粘包拆包、数据加密等)。我们这里再回过头来看Netty框架到底帮我们解决了什么问题?

网络通信其实是一个非常复杂的问题,因为这里涉及到的点非常的多,一个较为完整的网络通信框架应该具备基础的功能,比如nio多路复用、数据加密解密、序列化、粘包拆包问题的解决、授权、协议的处理等等,所以说想要手写实现一个较为完善的具备网络通信的中间件是非常麻烦的。

而Netty是一个网络通信框架,它的出现就是为了解决网络通信中可能遇到的各种各样的问题,比如它里面的Channel和EventLoop就是用来实现nio多路复用的功能模型,再比如通过ChannelPipeline管理所有的ChannelHandler,让每个ChannelHandler有序的执行对应的步骤,且Netty还提供了很多定义好的ChannelHandler 如SslHandler解决网络通信数据加密功能,且支持自定义ChannelHandler根据你的需求而自行设定,等等很多功能,可以说Netty框架将网络通信的这一整套复杂的内容进行了封装,我们通过Netty框架来实现一个通信框架变的很简单,相比于使用原生的nio编程代码要更方便且功能更全面更强大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值