Netty介绍及入门示例

目录

 

1. 什么是netty

2. 为什么需要netty

2.1 Netty为什么并发高

2.2 Netty为什么传输快

2.3 为什么说Netty封装好?

3. Netty基本概念

3.1 Channel

3.2 ByteBuf

3.3 Codec

4. 示例:

4.1 server:

4.2 ServerHandler

4.3 Client:

4.4 ClientHandler

5. 存在的问题:


1. 什么是netty

引用官方的话就是 "Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients."

即 Netty是一个提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty利用了JAVA的高级网络能力,隐藏了起背后的复杂性,提供一个易于使用的API的客户端/服务器框架。

 

2. 为什么需要netty

Netty是目前比较流行的NIO框架,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。它已经得到成百上千的商业/商用项目验证,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等。

 

而Netty 受到大公司青睐的原因主要有三:

  • 并发高
  • 传输快
  • 封装好

 

2.1 Netty为什么并发高

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:

阻塞IO的通信方式

 

非阻塞IO的通信方式

 

从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector。

当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。

在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。

而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端——这个过程是阻塞的,这样就能让一个Thread处理更多的请求了。

下面两张图是基于BIO的处理流程和netty的处理流程,辅助你理解两种方式的差别:

BIO的处理流程

 

NIO的处理流程

 

2.2 Netty为什么传输快

Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。

Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。

下两图就介绍了两种拷贝方式的区别,摘自Linux 中的零拷贝技术,第 1 部分

传统数据拷贝

零拷贝

上文介绍的ByteBuf是Netty的一个重要概念,他是netty数据处理的容器,也是Netty封装好的一个重要体现,将在下一部分做详细介绍。

 

2.3 为什么说Netty封装好?

传统的Socket编程步骤繁杂,而Netty编程非常友好,代码如下:

public class NettyOioServer {

    public void server(int port) throws Exception {
        final ByteBuf buf = Unpooled.unreleasableBuffer(
                Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
        EventLoopGroup group = new OioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();        //1

            b.group(group)                                    //2
             .channel(OioServerSocketChannel.class)
             .localAddress(new InetSocketAddress(port))
             .childHandler(new ChannelInitializer<SocketChannel>() {//3
                 @Override
                 public void initChannel(SocketChannel ch) 
                     throws Exception {
                     ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {            //4
                         @Override
                         public void channelActive(ChannelHandlerContext ctx) throws Exception {
                             ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
                         }
                     });
                 }
             });
            ChannelFuture f = b.bind().sync();  //6
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();        //7
        }
    }
}

3. Netty基本概念

3.1 Channel

数据传输流,与channel相关的概念有以下四个,上一张图让你了解netty里面的Channel。

 

Channel一览

    • Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。
    • ChannelHandler,核心处理业务就在这里,用于处理业务请求。
    • ChannelHandlerContext,用于传输业务数据。
    • ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。

3.2 ByteBuf

ByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示:

ByteBuf数据结构

 

他有三种使用模式:

  1. Heap Buffer 堆缓冲区

堆缓冲区是ByteBuf最常用的模式,他将数据存储在堆空间。

  1. Direct Buffer 直接缓冲区

直接缓冲区是ByteBuf的另外一种常用模式,他的内存分配都不发生在堆,jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处

    • 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此”无能为力”,也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
  1. Composite Buffer 复合缓冲区

复合缓冲区相当于多个不同ByteBuf的视图,这是netty提供的,jdk不提供这样的功能。

除此之外,他还提供一大堆api方便你使用,在这里我就不一一列出了,具体参见ByteBuf字节缓存

3.3 Codec

Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,从而达到自定义协议的目的。

在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了,前者负责对收到的请求解码,后者负责对发送的响应编码。

 

先了解一下 HTTP GET/POST request/response 的构成

 

request 构成

 

 

response 构成

HTTP 响应和 HTTP 请求非常相似,HTTP 响应包含三个部分:status line、header、massage body。其中 status line 包含 protocol version、状态码(status code)、reason phrase 三部分。状态码用于描述 HTTP 响应的状态,例如 200 表示成功,404 表示资源未找到,500 表示服务器出错。在上面的 HTTP 响应中,Header 中的 Content-Length 同样用于表示 message body 的字节数。Content-Type 表示 message body 的类型,通常浏览网页其类型是HTML,当然还会有其他类型,比如图片、视频等。

 

而request在Netty中是这样的:

  1. HTTP Request 第一部分是包含的头信息
  2. HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
  3. LastHttpContent 标记是 HTTP request 的结束,同时可能包含头的尾部信息
  4. 完整的 HTTP request,由1,2,3组成

 

 

response则是这样:

  1. HTTP response 第一部分是包含的头信息
  2. HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
  3. LastHttpContent 标记是 HTTP response 的结束,同时可能包含头的尾部信息
  4. 完整的 HTTP response,由1,2,3组成

 

要注意构建HttpRequest或者HttpResponse时,需要设置Content-Length(),否则Server端/Client端收不到任何数据,例如 request.headers().set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(bbuf.readableBytes()));

 

在Netty中使用编码解码器(Server端)

public void initChannel(SocketChannel ch) throws Exception {
     // server端发送的是httpResponse,所以要使用HttpResponseEncoder进行编码
     ch.pipeline().addLast(new HttpResponseEncoder());
     // server端接收到的是httpRequest,所以要使用HttpRequestDecoder进行解码
     ch.pipeline().addLast(new HttpRequestDecoder());
     ch.pipeline().addLast(new MyHttpServerInboundHandler());
}  

也可以使用聚合的编码解码器:

public void initChannel(SocketChannel ch) throws Exception {
     // 相当于HttpResponseEncoder 和 HttpRequestDecoder的结合
     ch.pipeline().addLast(new HttpServerCodec());
     // 消息聚合器,将Request或者response聚合成FullHttpRequest或者FullHttpResponse,长度不超过512k
     ch.pipeline().addLast(new HttpObjectAggregator(512*1024));
     ch.pipeline().addLast(new MyHttpServerInboundHandler());
}

4. 示例:

4.1 server:

 

public class Server {  
  
    private int port;  
  
    public Server(int port) {  
        this.port = port;  
    }  
  
    public void run() {  
        EventLoopGroup bossGroup = new NioEventLoopGroup(); //用于处理服务器端接收客户端连接  
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //进行网络通信(读写)  
        try {  
            ServerBootstrap bootstrap = new ServerBootstrap(); //辅助工具类,用于服务器通道的一系列配置  
            bootstrap.group(bossGroup, workerGroup) //绑定两个线程组  
                    .channel(NioServerSocketChannel.class) //指定NIO的模式  
                    .childHandler(new ChannelInitializer<SocketChannel>() { //配置具体的数据处理方式  
                        @Override  
                        protected void initChannel(SocketChannel socketChannel) throws Exception {  
                            socketChannel.pipeline().addLast(new ServerHandler());  
                        }  
                    })  
                    /** 
                     * 对于ChannelOption.SO_BACKLOG的解释: 
                     * 服务器端TCP内核维护有两个队列,我们称之为A、B队列。客户端向服务器端connect时,会发送带有SYN标志的包(第一次握手),服务器端 
                     * 接收到客户端发送的SYN时,向客户端发送SYN ACK确认(第二次握手),此时TCP内核模块把客户端连接加入到A队列中,然后服务器接收到 
                     * 客户端发送的ACK时(第三次握手),TCP内核模块把客户端连接从A队列移动到B队列,连接完成,应用程序的accept会返回。也就是说accept 
                     * 从B队列中取出完成了三次握手的连接。 
                     * A队列和B队列的长度之和就是backlog。当A、B队列的长度之和大于ChannelOption.SO_BACKLOG时,新的连接将会被TCP内核拒绝。 
                     * 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户端无法连接。要注意的是,backlog对程序支持的 
                     * 连接数并无影响,backlog影响的只是还没有被accept取出的连接 
                     */  
                    .option(ChannelOption.SO_BACKLOG, 128) //设置TCP缓冲区  
                    .option(ChannelOption.SO_SNDBUF, 32 * 1024) //设置发送数据缓冲大小  
                    .option(ChannelOption.SO_RCVBUF, 32 * 1024) //设置接受数据缓冲大小  
                    .childOption(ChannelOption.SO_KEEPALIVE, true); //保持连接  
            ChannelFuture future = bootstrap.bind(port).sync();  
            future.channel().closeFuture().sync();  
        } catch (Exception e) {  
            e.printStackTrace();  
        } finally {  
            workerGroup.shutdownGracefully();  
            bossGroup.shutdownGracefully();  
        }  
    }  
  
    public static void main(String[] args) {  
        new Server(8379).run();  
    }  
}

关键字及其含义:

1、NioEventLoopGroup 是用来处理I/O操作的线程池,Netty对 EventLoopGroup 接口针对不同的传输协议提供了不同的实现。在本例子中,需要实例化两个NioEventLoopGroup,通常第一个称为“boss”,用来accept客户端连接,另一个称为“worker”,处理客户端数据的读写操作。

2、ServerBootstrap 是启动服务的辅助类,有关socket的参数可以通过ServerBootstrap进行设置。

3、这里指定NioServerSocketChannel类初始化channel用来接受客户端请求。

4、通常会为新SocketChannel通过添加一些handler,来设置ChannelPipeline。ChannelInitializer 是一个特殊的handler,其中initChannel方法可以为SocketChannel 的pipeline添加指定handler。

 

4.2 ServerHandler

 

public class ServerHandler  extends ChannelHandlerAdapter {  
  
    @Override  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
      
            //do something msg  
            ByteBuf buf = (ByteBuf)msg;  
            byte[] data = new byte[buf.readableBytes()];  
            buf.readBytes(data);  
            String request = new String(data, "utf-8");  
            System.out.println("Server: " + request);  
            //写给客户端  
            String response = "我是反馈的信息";  
            ctx.writeAndFlush(Unpooled.copiedBuffer("888".getBytes()));  
            //.addListener(ChannelFutureListener.CLOSE);  
              
  
    }  
  
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
        cause.printStackTrace();  
        ctx.close();  
    }  
  
}  

4.3 Client:

 

public class Client {  
  
    public static void main(String[] args) throws InterruptedException {  
        EventLoopGroup workerGroup = new NioEventLoopGroup();  
        Bootstrap bootstrap = new Bootstrap();  
        bootstrap.group(workerGroup)  
                .channel(NioSocketChannel.class)  
                .handler(new ChannelInitializer<SocketChannel>() {  
                    @Override  
                    protected void initChannel(SocketChannel socketChannel) throws Exception {  
                        socketChannel.pipeline().addLast(new ClientHandler());  
                    }  
                });  
        ChannelFuture future = bootstrap.connect("127.0.0.1", 8379).sync();  
        future.channel().writeAndFlush(Unpooled.copiedBuffer("777".getBytes()));  
        future.channel().closeFuture().sync();  
        workerGroup.shutdownGracefully();  
    }  
  
}

4.4 ClientHandler

public class ClientHandler extends ChannelHandlerAdapter {  
  
    @Override  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {  
        try {  
            ByteBuf buf = (ByteBuf) msg;  
            byte[] data = new byte[buf.readableBytes()];  
            buf.readBytes(data);  
            System.out.println("Client:" + new String(data).trim());  
        } finally {  
            ReferenceCountUtil.release(msg);  
        }  
    }  
  
  
    @Override  
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {  
        cause.printStackTrace();  
        ctx.close();  
    }  
  
}

运行结果:

5. 存在的问题:

内存泄漏问题

继承 SimpleChannelHandler,它在所有接收消息的地方都调用了 ReferenceCountUtil.release(msg)。

 

 

参考:

https://www.jianshu.com/p/b9f3f6a16911

https://www.jianshu.com/p/ed0177a9b2e3

https://www.cnblogs.com/cyfonly/p/5616493.html

https://blog.csdn.net/haoyuyang/article/details/53243785

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值