Netty入门 图文并茂 B站课程(尚硅谷)学习总结

前文 Java-I/O模型

1. 什么是Netty

Netty官网 netty.io

异步的基于事件驱动的网络应用框架,用于快速开发高性能的协议服务和客户端。
image.png

Netty 是一个 NIO 客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化和流线了网络编程,例如 TCP 和 UDP 套接字服务器。
“快速和简单”并不意味着生成的应用程序会受到可维护性或性能问题的影响。Netty 是根据从实现许多协议(如 FTP、SMTP、HTTP 以及各种二进制和基于文本的旧协议)中获得的经验精心设计的。因此,Netty 成功地找到了一种方法,可以在不妥协的情况下实现易于开发、性能、稳定性和灵活性。

  • JBoss 提供的一个Java开源框架,Github独立项目:https://github.com/netty/netty
  • 异步的,基于事件驱动的网络应用框架,用以快速开发高性能,高可靠的网络IO程序
  • Netty主要针对在TCP协议下,面向Client端的高并发应用,或者Peer-to-Peer场景下的阿亮数据连续传输的应用
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
    • TCP/IP -> JDK IO -> NIO -> Netty

1.1 Netty的应用场景

互联网行业

  • 分布式系统中,作为各个节点间通信的高性能**RPC框架中的基础通信组件,**例如dubbo

游戏行业

  • Netty作为高性能的基础通信组件,提供TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登陆服务器
  • 作为地图服务器的通信组件

大数据领域

  • 经典的Hadoop的高性能通信和序列化组件的RPC框架,默认采用Netty进行跨节点通信

1.2 Netty设计初衷

解决NIO的已有问题,为NIO应用提供一套易用可靠的框架

原生NIO存在的问题

  • NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector,ServerSocketChannle, SocketChannel,ByteBuffer等
  • 需要具备其他的额外技能,要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
  • 开发工作量和开发难度都非常大,例如客户端面临断线重连,网络闪断,半包读写,失败缓存,网络拥塞和异常流的处理等等。
  • JDK NIO的bug:例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU100%。知道JDK1.7版本该问题依然存在,没有被根本解决。

2. Netty 架构设计

2.1 Netty线程模型

2.1.1 阻塞I/O服务模型

阻塞IO获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。
问题:

  • 当并发数很大,就会创建大量的线程,占用很大系统资源。
  • 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费

2.1.2 Reactor模式

针对传统阻塞IO服务模型的2个缺点,解决方案

  • 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

image.png å

  • 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

image.png

  • Reactor
    • 监听和分发事件,分发给适当的Handle处理相应的IO事件。
  • Handlers
    • 处理程序执行IO事件要完成的实际事件。

Reactor 模式分类:

  • 单Reactor - 单线程
    • image.png
    • image.png
    • image.png
  • 单Reactor - 多线程
    • image.png
    • image.png
    • image.png
  • 主从Reactor - 多线程 Netty
    • 主Reactor是老鸨,子Reactor负责接客。
    • image.png
    • image.png
    • image.png
    • image.png

Reactor 模式小结
image.png

2.2 Netty模型

预定:accept
接待:非accept事件(读写 )

image.png
酒店总管 -> BossGroup -> 接受预定并分派给一个酒店前台主管

  • 酒店前台主管 -> WorkGroup -> 管理预定客人的服务事项
    • 酒店服务员 -> Handler -> 接待服务客人

image.png

2.3 Netty快速入门实例

channel 是水龙头管进管出,pipeline是水管,数据在管道中被处理。

/**
 * @author kern
 */
public class ExampleNettyHelloServer {

    public static void main(String[] args) throws InterruptedException {
        //默认子线程是CPU核心数 * 2,每个子线程即一个NioEventLoop

        /*
         * 主事件循环组(boosGroup)负责客户端的连接处理
         * 子事件循环组(workGroup)负责具体的读写处理
         */
        EventLoopGroup boosGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(boosGroup, workGroup)
                    //设置通道类型
                    .channel(NioServerSocketChannel.class)
                    //bossGroup的线程队列的连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    //保持连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //设置处理器
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new ExampleNettyHelloServerHandler());
                        }
            });
            System.out.println("Server is ready!");
            ChannelFuture channelFuture = bootstrap.bind(6668).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            boosGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

/**
 * @author kern
 */
public class ExampleNettyHelloServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("ctx active :" + ctx);
        System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
        super.channelActive(ctx);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("ctx : " + ctx);
        System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
        System.out.println("客户端发送消息 : " +  ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.out.println("读取完毕,服务端响应消息");
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 客户端", StandardCharsets.UTF_8));
        ctx.fireChannelReadComplete();
    }

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


/**
 * @author kern
 */
public class ExampleNettyHelloClient {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel ch) throws Exception {
                            ch.pipeline()
                                    .addLast(new ExampleNettyHelloClientHandler());
                        }
                    });
            System.out.println("Client is ready");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}


/**
 * @author kern
 */
public class ExampleNettyHelloClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("ctx active " + ctx);
        System.out.println("客户端发送消息");
        ChannelFuture channelFuture = ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 服务端", StandardCharsets.UTF_8));
        System.out.println(channelFuture.channel().remoteAddress());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("ctx : " + ctx);
        System.out.println("服务端IP地址 : " + ctx.channel().remoteAddress());
        System.out.println("服务端发送消息 : " +  ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        Thread.sleep(1000);
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 服务端", StandardCharsets.UTF_8));
        ctx.fireChannelReadComplete();
    }

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

2.3.1 Netty 常用 ChannelOptions

ChannelOption的各种属性在套接字选项中都有对应。
下面简单的总结一下ChannelOption的含义已及使用的场景。

2.3.1.1 ChannelOption.SO_BACKLOG

ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数。函数listen(int socketfd, int backlog)用来初始化服务端可连接队列。
服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。

2.3.1.2 ChannelOption.SO_REUSEADDR

ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口。
比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用。
比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。

2.3.1.3 ChannelOption.SO_KEEPALIVE

Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。
当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

2.3.1.4 ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF

ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作发送缓冲区大小和接受缓冲区大小。
接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。

2.3.1.5 ChannelOption.SO_LINGER

ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发送剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送。

2.3.1.6 ChannelOption.TCP_NODELAY

ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。
Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到来,组装成大的数据包进行发送,虽然该算法有效提高了网络的有效负载,但是却造成了延时。
而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输。和TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

2.3.2 Netty异步模型-任务队列

image.png

  • NioEventLoopGroup下包含多个NioEventLoop
  • 每个NioEventLoop 中包含有一个 Selector,一个taskQueue(任务队列),一个schemaTaskQueue(定时任务队列)
  • 每个NioEventLoop 中的Selector上可以注册监听多个NioChannel
  • 每个NioChannel只会绑定在唯一的NioEventLoop上
  • 每个NioChannel都绑定有一个自己的ChannelPipeline
2.3.2.1 用户自定义异步任务

多个异步任务使用同一个线程,将添加到同一个任务队列中,由一个线程先后执行

	@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.channel().eventLoop().execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ctx.writeAndFlush(Unpooled.copiedBuffer("服务端异步输出", StandardCharsets.UTF_8));
        });
        System.out.println("ctx : " + ctx);
        System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
        System.out.println("客户端发送消息 : " +  ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
    }
2.3.2.2 用户自定义异步定时任务
 	@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.channel().eventLoop().schedule(() -> {
            ctx.writeAndFlush(Unpooled.copiedBuffer("服务端定时任务异步输出", StandardCharsets.UTF_8));
        }, 5, TimeUnit.SECONDS);
        System.out.println("ctx : " + ctx);
        System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
        System.out.println("客户端发送消息 : " +  ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
    }
2.3.2.3 管理channel自定义异步任务

/**
 * @author kern
 */
public class ExampleNettyHelloServer {

    public static List<NioSocketChannel> SOCKET_CHANNELS = new LinkedList<>();
    public static void main(String[] args) throws InterruptedException {
        //默认子线程是CPU核心数 * 2,每个子线程即一个NioEventLoop

        /*
         * 主事件循环组(boosGroup)负责客户端的连接处理
         * 子事件循环组(workGroup)负责具体的读写处理
         */
        EventLoopGroup boosGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(boosGroup, workGroup)
                    //设置通道类型
                    .channel(NioServerSocketChannel.class)
                    //bossGroup的线程队列的连接个数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    //保持连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //设置处理器
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel socketChannel) throws Exception {
                            //使用容器管理channel,并在必要的时候通过容器获取channel执行io操作
                            SOCKET_CHANNELS.add(socketChannel);
                            socketChannel.pipeline().addLast(new ExampleNettyHelloServerHandler());
                        }
            });
            System.out.println("Server is ready!");
            ChannelFuture channelFuture = bootstrap.bind(6668).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            boosGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}

2.3.3 Netty异步模型-Future-Listener机制

image.png

ChannelFuture channelFuture = bootstrap.bind(6668).sync();
channelFuture.addListener(cf -> {
    if (cf.isSuccess()) {
        System.out.println("监听端口6668成功!");
    } else {
        System.out.println("监听端口6668失败!");
    }
});
//监听端口6668成功!

2.3.4 Netty快速入门实例-HTTP服务

package cn.kern.demo.netty.http;

import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
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.http.*;
import io.netty.handler.codec.http2.Http2ConnectionHandler;

import java.nio.charset.StandardCharsets;

/**
 * @author kern
 */
public class ExampleNettyHttpServer {

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup work = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(boss, work)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline()
                            	//Netty提供的Http编解码器
                                .addLast(new HttpServerCodec())
                            	//自定义处理器
                                .addLast(new SimpleChannelInboundHandler<HttpObject>() {
                                    @Override
                                    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
                                        if (msg instanceof HttpRequest) {
                                            System.out.println("ctx: "+ ctx.hashCode());
                                            System.out.println("channel: " + ctx.channel().id().asLongText());
                                            HttpRequest request = (HttpRequest) msg;
                                            System.out.println("uri: " + request.uri());
                                            ByteBuf byteBuf = Unpooled.copiedBuffer("Hello 浏览器,这里是服务器", StandardCharsets.UTF_8);
                                            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
                                            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
                                            response.headers().set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
                                            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
                                            ctx.writeAndFlush(response);
                                        }
                                    }
                                })
                        ;
                    }
                });
        ChannelFuture channelFuture = bootstrap.bind(8080).sync();
        channelFuture.addListener(cf -> {
            if (cf.isSuccess()) {
                System.out.println("绑定端口8080成功");
            } else {
                System.out.println("绑定端口8080失败");
            }
        });
        channelFuture.channel().closeFuture().sync();
    }
}

2.4 Netty Inbound & Outbound 理解

Netty pipline实际是一个双向链表,存储了ChannelHandler。

protected void initChannel(NioSocketChannel ch) throws Exception {
    ChannelPipeline pipeline = ch.pipeline();
    pipeline.addFirst(new HttpServerEncoder())
        .addLast(new HttpServerDecoder())
        .addLast(new MyHttpMsgHandler());
}

image.png

当客户端请求netty时,此时触发Inbound事件, 将从第一个InboundHandler 开始执行到最后一个InboundHandler, 这里使用了责任链模式, 消息经由InboundHandler链的每一个环节进行处理,是否将消息继续传递取决于链路中的InboundHandler。

// For example : Read Message

public void channelRead(ChannelHandlerContext ctx, Object msg) {

	//Handing message
    if(msg instanceof String) {
    	System.out.print("Read Message :" + (String)msg);
        //Do not continue message delivery
    } else {
        //Pass the message to the next InboundHandler
    	ctx.fireChannelRead(msg);
    }

}

这是Inbound事件的执行逻辑,而如果是Outbound事件,例如你执行了如下代码 ctx.writeAndFlush(msg); 可以简单理解,读取(客户端 -> 管道 -> Netty服务)的行为就是Inbound, 写入(Netty服务 -> 管道 -> 客户端)的行为就是Outbound,应时刻记住Netty服务和客户端之间是经由管道通信的。

言归正传, Outbound 与 Inbound 正好相反, 他将有 某个 Outbound 开始逆序执行到第一个OutboundHandler, 非常值得注意的是, 无论你在InboundHandler 链中的任何一环触发了Outbound, 他将由当前Handler链开始逆序执行OutboundHandler,而排在它之后的OutboundHandler将不起作用。

image.png

综上, 基于常规的读取流程 decode解码 -> handler处理 应该把负责解码的inboundhandler 放在负责处理的 inoundhandler之后
基于常规的写入流程, handler处理 -> encode编码 应该把编码器放在所有handler的前面,以确保所有写入的数据在传递给客户端时有正确的格式。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值