Netty怎么用更优

Netty是什么

定义

Netty是一种高性能,异步的,基于事件驱动的网络应用框架

核心架构

在这里插入图片描述

  • 核心:可扩展事件,统一的通信API,零拷贝机制和字节缓冲区
  • 传输服务:Socket,http通道,In-VM通道
  • 协议支持:http、SSL、Google protobuf、zlib/gzip、RTSP

Netty优势

  1. 基于NIO实现,统一封装了各种传输类型和协议实现的API。
  2. 简化开发,提高效率,开发人员只需关注业务即可
  3. 可定制线程模型
  4. 只依赖JDK底层API,低耦合。
  5. 减少了内存考虑,提高了性能。
  6. 快速迭代。

Netty的版本

Netty目前常用的3.X和4.X,而5.X不建议使用,因为性能没有大的提升,而是使得Netty的维护更加复杂。

为什么使用Netty而不直接使用NIO进行网络编程

  1. NIO的类库和API比较复杂
  2. 需要自己实现Reactor模型,开发需要学习额外的技能。
  3. 使用NIO进行网络编程,实现比较简单,但是可靠性较差,功能补齐工作量很大。
  4. JDK的NIO存在epoll(bug)。

业界通常使用Netty进行网络编程,最主要的原因是Netty的高性能,低延迟,而Netty为什么有更高的性能呢?主要原因是由于它的高性能设计。

Netty的高性能的原因

Netty的高性能源于它的设计模型。了解他的设计模型,需要先了解IO模型和Reactor模型。

IO模型

BIO

在JDK1.4 以前java的IO都是BIO(Blocking IO),即阻塞型IO。
在这里插入图片描述
BIO模型解读:

  1. 客户端的请求和后端线程数1:1,导致在高并发下,大量创建和销毁线程,开销非常大。甚至可能会发生OOM。
  2. 创建连接后,会创建一个线程,当改线程没有任何操作时候,该线程会一直阻塞,浪费资源。
NIO模型

在这里插入图片描述
NIO模型解读:
Buffer:缓冲区,底层通过数组实现,每一种java基本类型都有对应缓冲区。
channel:基于通道实现,和流不同的是,通道是双向的,因此,一个通道可以实现读写操作。
selector:多路复用器/选择器,NIO会启动一个单线程来运行Selector,selector会不断轮询channel,但channel中有事件的时候,就会被selector挑选出来,获取它的SelectionKey集合,SelectionKey中包含不同的事件类型,根据不同的事件类型(OP_ACCEPT, OP_READ, OP_WRITE)进行不同的操作。

NIO的优点:

  1. selector运行在单线程上,处理多个通道,避免了多线程上下文切换带来的系统开销。
  2. 一个channel并没有开启一个线程,而是channel中真正有事件的时候,才会开启线程进行读写操作,而BIO为每一个连接都开启了一个线程。

代码:

public class SelectorDemo {
	    /**
	     * 注册时间
	     */
	    private Selector getSelector() throws Exception {
	        // 获取Selector对象
	        Selector selector = Selector.open();
	
	        // 获取通道,设置非阻塞
	        ServerSocketChannel channel = ServerSocketChannel.open();
	        channel.configureBlocking(false);
	
	        // 获取服务端套接字,绑定端口号
	        ServerSocket socket = channel.socket();
	        socket.bind(new InetSocketAddress(5566));
	        
	        // 注册通道感兴趣的时间
	        channel.register(selector, SelectionKey.OP_ACCEPT);
	        
	        return selector;
	    }
	
	    /**
	     * 监听事件
	     */
	    public void listen() throws Exception {
	        Selector selector = this.getSelector();
	        while (true) {
	            selector.select(); // 多路复用器轮询通道,直到有一个通道有事件(阻塞)
	            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 将事件的SelectionKey都获取出来
	            Iterator<SelectionKey> iterator = selectionKeys.iterator();
	            while (iterator.hasNext()) {
	                SelectionKey selectionKey = iterator.next();
	                process(selectionKey, selector);
	                iterator.remove();
	            }
	
	        }
	    }
	
	    private void process(SelectionKey key, Selector selector) throws Exception {
	        if (key.isAcceptable()) { // 新连接请求
	            ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 获取服务端通道
	            SocketChannel channel = server.accept(); // 建立连接(阻塞)
	            channel.configureBlocking(false); // 设置通道非阻塞
	            channel.register(selector, SelectionKey.OP_READ); // 注册read事件
	        } else if(key.isReadable()) {
	            SocketChannel channel = (SocketChannel) key.channel(); // 获取通道
	            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 获取字节缓冲区
	            channel.read(byteBuffer); // 从通道中读取数据
	            System.out.println("from 客户端 " + new String(byteBuffer.array(), 0, byteBuffer.position()));
	        }
	    }
	
	    public static void main(String[] args) throws Exception {
	        new SelectorDemo().listen();
	    }
	}
AIO模型

AIO是真正的异步IO,它将BIO中阻塞的步骤优化成了非阻塞的,但是目前AIO模型存在不足,它依赖于操作系统,window下的AIO性能很高,但是AIO在linux系统下却不完善。
在这里插入图片描述

Reactor模型

Reactor线程模型是一种并发编程思想,而不是一种技术,不是一种语言专有的。
Reactor模型中的三个角色:

  1. Reactor:负责监听和分配事件,将事件分配给对应的Handler。
  2. Acceptor:处理客户端的新连接,并分派请求到处理器链中。
  3. Handler:将自身与事件绑定,执行非阻塞的读写,完成channel中的读写操作。

常见的三种Reactor线程模型

  • 单Reactor单线程模型:

在这里插入图片描述
模型解读:
Reactor充当selector,监听多路连接的请求
如果是新连接通过Acceptor完成,其他的交由Handler完成
Handler完成业务逻辑的处理:Read -》 业务处理 -》Send。

优点: 结构简单
缺点: 性能低,高并发下消息堆积,可靠性差。

  • 单Reactor多线程模型
    在这里插入图片描述
    优点:Handler不会处理业务,而是交由其他线程完成,降低Reactor性能开销。
    缺点:高并发下涉及共享数据的互斥和保护,Reactor承担事件的监听和响应,只在主线程中运行,还是存在性能问题。

  • 主从Reactor多线程模型
    在这里插入图片描述
    模型解读:
    Main Reactor负责监听,用来处理新连接的建立,然后将建立的channel注册给Sub Reactor。
    Sub Reactor完成数据交互和业务逻辑的处理。
    相当于Main负责接收任务, sub负责真正处理任务。

优点:响应快,可扩展性强,复用性高。

Netty模型

Netty在Reactor模型的基础上,进行优化,对于三种Reactor线程模型都有很好的支持。
在这里插入图片描述
BossGroup:相当于Main Reactor,负责处理新连接。
NioEventLoop:表示不断循环处理任务的线程,它监听了绑定在它上面的的读写事件。
Pipeline(管道):真正执行业务逻辑的处理,Pipeline中会有多个channelHandler。

Netty快速入门

服务端代码

依赖

<dependencies>
	        <dependency>
	            <groupId>io.netty</groupId>
	            <artifactId>netty-all</artifactId>
	            <version>4.1.50.Final</version>
	        </dependency>
	        <dependency>
	            <groupId>junit</groupId>
	            <artifactId>junit</artifactId>
	            <version>4.12</version>
	        </dependency>
	    </dependencies>
	    <build>
	        <plugins>
	            <!-- java编译插件 -->
	            <plugin>
	                <groupId>org.apache.maven.plugins</groupId>
	                <artifactId>maven-compiler-plugin</artifactId>
	                <version>3.2</version>
	                <configuration>
	                    <source>1.8</source>
	                    <target>1.8</target>
	                    <encoding>UTF-8</encoding>
	                </configuration>
	            </plugin>
	        </plugins>
	    </build>

MyRpcServer

public class MyRpcServer {
	
	    public void start(int port) throws Exception {
	        // BossGroup: 只接受客户端请求
	        EventLoopGroup boss = new NioEventLoopGroup(1);
	
	        // workerGroup
	        EventLoopGroup worker = new NioEventLoopGroup(2);
	
	        // 服务启动类
	        try {
	            ServerBootstrap serverBootstrap = new ServerBootstrap();
	
	            serverBootstrap.group(boss, worker) // 设置线程组
	                    .channel(NioServerSocketChannel.class) // 配置server通道
	                    .childHandler(new MyChannelInitializer()); // worker线程处理器
	
	            // 通道绑定端口
	            ChannelFuture future = serverBootstrap.bind(port).sync();
	            System.out.println("服务器启动完成, 端口号:" + port);
	
	            // 等待服务端监听端口关闭
	            future.channel().closeFuture().sync();
	        } finally {
	            // 关闭线程组
	            boss.shutdownGracefully();
	            worker.shutdownGracefully();
	        }
	    }
	}

MyChannelInitializer

public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
	    /**
	     * 将处理操作的Handler添加到管道中
	     *
	     * @param socketChannel
	     * @throws Exception
	     */
	    @Override
	    protected void initChannel(SocketChannel socketChannel) throws Exception {
	        socketChannel.pipeline().addLast(new MyChannelHandler());
	    }
	}

MyChannelHandler

public class MyChannelHandler extends ChannelInboundHandlerAdapter {
	    /**
	     * 获取客户端发来的数据
	     *
	     * @param ctx
	     * @param msg
	     * @throws Exception
	     */
	    @Override
	    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
	        // 读取客户端发来的数据
	        ByteBuf buf = (ByteBuf) msg; // ByteBuffer不好用, netty自己实现了ByteBuf
	        String msgStr = buf.toString(CharsetUtil.UTF_8);
	        System.out.println("客户端发来数据:" + msgStr);
	
	        // 向客户端发送响应
	        ctx.writeAndFlush(Unpooled.copiedBuffer("ok" , CharsetUtil.UTF_8));
	    }
	
	    /**
	     * 异常处理
	     *
	     * @param ctx
	     * @param cause
	     * @throws Exception
	     */
	    @Override
	    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
	        ctx.fireExceptionCaught(cause);
	    }
	}

测试

public class TestServer {
	
	    @Test
	    public void testServer() throws Exception {
	        MyRpcServer server = new MyRpcServer();
	        server.start(5566);
	    }
	}

测试结果

服务器启动完成, 端口号:5566
客户端发来数据:123

客户端发来数据:456

客户端代码

MyRpcClient
public class MyRpcClient {
	    public void start(String host, int port) throws Exception {
	        // 客户端只需要发送请求, 因此, 只需要工作线程组
	        EventLoopGroup worker = new NioEventLoopGroup();
	
	        try {
	            Bootstrap bootstrap = new Bootstrap();
	
	            bootstrap.group(worker) // 设置线程组
	                .channel(NioSocketChannel.class) // 配置客户端通道
	                .handler(new MyClientChannelHandler());
	
	            // 开启连接
	            ChannelFuture future = bootstrap.connect(host, port).sync();
	
	            // 关闭
	            future.channel().closeFuture().sync();
	        } finally {
	            worker.shutdownGracefully();
	        }
	    }
	}

MyClientChannelHandler

public class MyClientChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
	    /**
	     * 读取服务端的数据
	     *
	     * @param channelHandlerContext
	     * @param byteBuf
	     * @throws Exception
	     */
	    @Override
	    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
	        System.out.println("接收到服务端数据:" + byteBuf.toString(CharsetUtil.UTF_8));
	    }
	
	    /**
	     * 向服务端发送数据
	     *
	     * @param ctx
	     * @throws Exception
	     */
	    @Override
	    public void channelActive(ChannelHandlerContext ctx) throws Exception {
	        String msg = "test";
	        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
	    }
	
	    /**
	     * 异常处理
	     *
	     * @param ctx
	     * @param cause
	     * @throws Exception
	     */
	    @Override
	    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
	        cause.printStackTrace();
	        ctx.close();
	    }
	}

测试

public class TestDemo {
	
	    @Test
	    public void testServer() throws Exception {
	        MyRpcServer server = new MyRpcServer();
	        server.start(5566);
	    }
	
	    @Test
	    public void testClient() throws Exception {
	        MyRpcClient client = new MyRpcClient();
	        client.start("127.0.0.1", 5566);
	    }
	}

测试结果

服务端:

服务器启动完成, 端口号:5566
客户端发来数据:test

客户端:

接收到服务端数据:ok

Netty核心组件

Channel

Channel理解为socket理解,Netty的Channel接口提供了很多的API,降低了直接使用socket的不便。

  • NioSocketChannel,NIO的客户端 TCP Socket 连接。
  • NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
  • NioDatagramChannel, UDP 连接。
  • NioSctpChannel,客户端 Sctp 连接。
  • NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP ⽹络 IO 以及⽂件IO。

EventLoop,EventLoopGroup

在这里插入图片描述
说明:
入站:服务器接收消息
出站:服务器发出消息
在 Netty 中每个 Channel 都会被分配到⼀个 EventLoop。⼀个 EventLoop 可以服务于多个 Channel,每个 EventLoop 会占⽤⼀个 Thread。
EventLoopGroup 是⽤来⽣成 EventLoop 的。

上图关系为:

  • ⼀个 EventLoopGroup 包含⼀个或者多个 EventLoop;
  • ⼀个 EventLoop 在它的⽣命周期内只和⼀个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • ⼀个 Channel 在它的⽣命周期内只注册于⼀个 EventLoop;
  • ⼀个 EventLoop 可能会被分配给⼀个或多个 Channel。

ChannelPipeline

在这里插入图片描述
数据传输过程中,往往有很多业务逻辑要处理,比如:编解码,读写操作等等。
每种操作都需要有对应的channelHandler处理。因此,每个Channel都维护了一个ChannelPipeline,一个ChannelPipeline中维护了一个ChannelHandler链表。

Bootstrap

Bootstrap是引导的意思,它的作⽤是配置整个Netty程序,将各个组件都串起来,最后绑定端⼝、启动
Netty服务。
Netty中提供了2种类型的引导类,⼀种⽤于客户端(Bootstrap),⽽另⼀种(ServerBootstrap)⽤于服务
器。
区别:

  • ServerBootstrap 将绑定到⼀个端⼝,⽽ Bootstrap 则是由想要连接到远程节点的客户端应⽤程序所使⽤的。
  • 引导⼀个客户端只需要⼀个EventLoopGroup,但是⼀个ServerBootstrap则需要两个。因为服务器需要两组不同的 Channel。
    • 第⼀组将只包含⼀个 ServerChannel,代表服务器⾃身的已绑定到某个本地端⼝的正在监听
      的套接字。
    • 第⼆组将包含所有已创建的⽤来处理传⼊客户端连接。

Future

Future提供了⼀种在操作完成时通知应⽤程序的⽅式。Netty提供了它自己的实现ChannelFuture。

在这里插入图片描述

Netty的字节缓冲区ByteBuf

在这里插入图片描述
ByteBuf结构说明:
Netty的ByteBuf替代了JDK NIO中的ByteBuffer,原因是ByteBuffer的使用过于负责,不利于开发。
ByteBuf中存在两个索引,readerIndex和writerIndex。

  • 当从ByteBuf中读取数据时候,readerIndex 会随着读取的字节数的增加而递增。
  • 当将数据写入ByteBuf中时,writerIndex会随着写入的字节数的增加而递增。
  • 当readerIndex > writerIndex,会抛出IndexOutOfBoundsException。

基本使用

读取操作
public class TestBuf {
	
	    @Test
	    public void testRead() {
	        //构造
	        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
	
	        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
	
	        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
	
	        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
	
	        while (byteBuf.isReadable()){ // 法 :内部通过移动readerIndex进 读取
	            System.out.println((char)byteBuf.readByte());
	        }
	
	        // 法 :通过下标直接读取
	//        for (int i = 0; i < byteBuf.readableBytes(); i++) {
	//            System.out.println((char)byteBuf.getByte(i));
	//        }
	
	        // 法三:转化为byte[]进 读取
	//        byte[] bytes = byteBuf.array();
	//        for (byte b : bytes) {
	//            System.out.println((char)b);
	//
	//        }
	    }
	}
写入操作

@Test
public void testWrite() {
//构造空的字节缓冲区,初始⼤⼩为10,最⼤为20
ByteBuf byteBuf = Unpooled.buffer(10,20);
System.out.println(“byteBuf的容量为:” + byteBuf.capacity());
System.out.println(“byteBuf的可读容量为:” + byteBuf.readableBytes());
System.out.println(“byteBuf的可写容量为:” + byteBuf.writableBytes());
for (int i = 0; i < 5; i++) {
byteBuf.writeInt(i); //写⼊int类型,⼀个int占4个字节
}
System.out.println(“ok”);
System.out.println(“byteBuf的容量为:” + byteBuf.capacity());
System.out.println(“byteBuf的可读容量为:” + byteBuf.readableBytes());
System.out.println(“byteBuf的可写容量为:” + byteBuf.writableBytes());
while (byteBuf.isReadable()){
System.out.println(byteBuf.readInt());
}
}

丢弃已读字节
 @Test
	    public void testDiscardReadByte() {
	        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world",
	                CharsetUtil.UTF_8);
	        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
	        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
	        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
	        byteBuf.clear(); //重置readerIndex 、 writerIndex 为0
	        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
	        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
	        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());
	    }

使用模式

分类
  • 堆缓冲区(HeapByteBuf)
    内存分配和回收速度比较快,可以被GC回收,但是如果进行socket的IO读写,需要额外多做一次 内存拷贝,性能有所下降

  • 直接缓冲区(DirectByteBuf)
    非堆内存,内存分配和回收的速度会慢一些,而且需要手动回收,但是进行socket的IO的读写,会减少一次内存考虑,性能会有所提升。

  • 复合缓冲区
    两类缓冲区聚合在⼀起。Netty 提供了⼀个 CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在⼀起,让使⽤更加⽅便。

Netty默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,需要设置参数;

System.setProperty("io.netty.noUnsafe", "true"); 
serverBootstrap.childOption(ChannelOption.ALLOCATOR,UnpooledByteBufAllocator.DEFAULT);

ByteBuf的内存分配

PooledByteBufAllocator :ByteBuf的对象的池化,可以提高性能,减少内存碎片
UnpooledByteBufAllocator:没有实现对象的池化,每次都会创建对象的实例。
Netty模式使用了PooledByteBufAllocator 。

//可以在引导类中设置⾮池化模式
serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);

ByteBuf的释放

  • 手动释放:ReferenceCountUtil.release(byteBuf); (不推荐)
  • 自动释放:
    1. 入站的TailHandler释放:ctx.fireChannelRead(msg); //将接受的ByteBuf向下传递
    2. 继承SimpleChannelInboundHandler
    3. HeadHandler的出站释放: 在出站流程开始的时候,通过调⽤ ctx.writeAndFlush(msg)

小结:
1. 入站流程中,如果对元消息不处理,调用ctx.fireChannelReal(msg); 由TaliHandler完成自动释放
2. 如果在入站流程中,截断了,可以继承SimpleChannelInboundHandler,完成释放
3. 入站处理中,如果将原消息转化为新的消息并调用ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
4. 入站处理中, 如果已经不再调⽤ ctx.fireChannelRead(msg) 传递任何消息,也没有继承SimpleChannelInboundHandler 完成⾃动释放,那更要把原消息release掉;
5. 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成⾃动释放。

Netty编解码器

什么是编解码器

网络传输过程中,以字节流方式传递。客户端将数据转化为字节,称之为编码。服务端获取字节流将字节转化为原来的数据格式,称之为解码。编码器负责出站数据操作,解码器负责入站数据操作。

解码器

Netty中提供了ByteToMessageDecoder的抽象实现,⾃定义解码器只需要继承该类,实现decode()即
可。常见的Netty提供的解码器:

  • RedisDecoder 基于Redis协议的解码器
  • XmlDecoder 基于XML格式的解码器
  • JsonObjectDecoder 基于json数据格式的解码器
  • HttpObjectDecoder 基于http协议的解码器

Netty也提供了MessageToMessageDecoder,将⼀种格式转化为另⼀种格式的解码器,也提供了⼀些实现:

  • StringDecoder 将接收到ByteBuf转化为字符串
  • ByteArrayDecoder 将接收到ByteBuf转化字节数组
  • Base64Decoder 将由ByteBuf或US-ASCII字符串编码的Base64解码为ByteBuf。

编码器

Netty提供了MessageToByteEncoder的抽象实现,它实现了ChannelOutboundHandler接⼝,本质上也是ChannelHandler,常见的Netty的编码器:

  • ObjectEncoder 将对象(需要实现Serializable接⼝)编码为字节流
  • SocksMessageEncoder 将SocksMessage编码为字节流
  • HAProxyMessageEncoder 将HAProxyMessage编码成字节流

Netty也提供了MessageToMessageEncoder,将⼀种格式转化为另⼀种格式的编码器,也提供了⼀些
实现:

  • RedisEncoder 将Redis协议的对象进⾏编码
  • StringEncoder 将字符串进⾏编码操作
  • Base64Encoder 将Base64字符串进⾏编码操作

Demo(Http服务器)

Server
public class MyRpcServer {

    public void start(int port) throws Exception {
        // BossGroup: 只接受客户端请求
        EventLoopGroup boss = new NioEventLoopGroup(1);

        // workerGroup
        EventLoopGroup worker = new NioEventLoopGroup(2);

        // 服务启动类
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(boss, worker) // 设置线程组
                    .channel(NioServerSocketChannel.class) // 配置server通道
                    .childHandler(new MyChannelInitializer()); // worker线程处理器

            // 通道绑定端口
            ChannelFuture future = serverBootstrap.bind(port).sync();
            System.out.println("服务器启动完成, 端口号:" + port);

            // 等待服务端监听端口关闭
            future.channel().closeFuture().sync();
        } finally {
            // 关闭线程组
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    /**
     * 将处理操作的Handler添加到管道中
     *
     * @param socketChannel
     * @throws Exception
     */
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast(new HttpRequestDecoder()) // 添加http解码器
                // 将http请求中的uri以及请求体聚合成⼀个完整的FullHttpRequest对象
                .addLast(new HttpObjectAggregator(1024 * 128))
                .addLast(new HttpResponseEncoder()) // http服务器需要给客户端响应,所以添加http响应编码器
                .addLast(new ChunkedWriteHandler()) // ⽀持异步的⼤⽂件传输,防⽌内存溢出
                .addLast(new MyChannelHandler());
    }
}
MyChannelHandler
public class MyChannelHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
	
	    @Override
	    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
	        // 解析request获取参数
	        Map<String, String> param = new RequestParser(request).parse(); // 解析请求获取request中的参数
	        String name = param.get("name");
	        // 构造响应
	        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
	        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=utf-8");
	        String responseStr = "<h1>" + "你好," + name + "</h1>";
	        response.content().writeBytes(Unpooled.copiedBuffer(responseStr, CharsetUtil.UTF_8));
	        ctx.writeAndFlush(response) // 给客户端响应
	                .addListener(ChannelFutureListener.CLOSE); // 因为http是一个短连接,所以在操作完成后需要将channel关闭
	    }
	
	    /**
	     * 异常处理
	     *
	     * @param ctx
	     * @param cause
	     * @throws Exception
	     */
	    @Override
	    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
	        ctx.fireExceptionCaught(cause);
	    }
	}
RequestParser
public class RequestParser {
    private FullHttpRequest fullReq;
    /**
     * 构造⼀个解析器
     * @param req
     */
    public RequestParser(FullHttpRequest req) {
        this.fullReq = req;
    }

    /**
     * 解析请求参数
     * @return 包含所有请求参数的键值对, 如果没有参数, 则返回空Map
     *
     * @throws Exception
     */
    public Map<String, String> parse() throws Exception {
        HttpMethod method = fullReq.method();
        Map<String, String> paramMap = new HashMap<>();
        if (HttpMethod.GET == method) {
            // 是GET请求
            QueryStringDecoder decoder = new QueryStringDecoder(fullReq.uri());
            decoder.parameters().entrySet().forEach( entry -> {
                // entry.getValue()是⼀个List, 只取第⼀个元素
                paramMap.put(entry.getKey(), entry.getValue().get(0));
            });
        } else if (HttpMethod.POST == method) {
            // 是POST请求
            HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(fullReq);
            decoder.offer(fullReq);
            List<InterfaceHttpData> paramList = decoder.getBodyHttpDatas();
            for (InterfaceHttpData param : paramList) {
                Attribute data = (Attribute) param;
                paramMap.put(data.getName(), data.getValue());
            }
        } else {
            // 不⽀持其它⽅法
            throw new RuntimeException("不⽀持其它⽅法"); // 这是个⾃定义的异常, 可删掉这⼀⾏
        }
        return paramMap;
    }
}
测试

在这里插入图片描述

Demo(对象的编解码)

我们在实际开发过程中,最常用的是将一个对象转换成一个字节流进行传输,Netty也支持Object对象的编解码,只需要java bean实现java.io.Serializable接⼝。

Client
MyClientInitializer
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast(new ObjectEncoder())
                .addLast(new MyClientChannelHandler());
    }
}
MyClientChannelHandler
public class MyClientChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {
    /**
     * 读取服务端的数据
     *
     * @param channelHandlerContext
     * @param byteBuf
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("接收到服务端数据:" + byteBuf.toString(CharsetUtil.UTF_8));
    }

    /**
     * 向服务端发送数据
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        User user = new User("张三", 18, "男");
        ctx.writeAndFlush(user);
    }

    /**
     * 异常处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
Server
MyChannelInitializer
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    /**
     * 将处理操作的Handler添加到管道中
     *
     * @param socketChannel
     * @throws Exception
     */
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast(new ObjectDecoder(ClassResolvers.weakCachingResolver(this.getClass().getClassLoader())))
                .addLast(new MyChannelHandler());
    }
}
MyChannelHandler
public class MyChannelHandler extends SimpleChannelInboundHandler<User> {
    /**
     * 获取客户端发来的数据
     *
     * @param ctx
     * @param user
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, User user) throws Exception {
        //获取到user对象
        System.out.println(user);
        ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8));
    }

    /**
     * 异常处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}
User
public class User implements Serializable {
    private static final long serialVersionUID = -89217070354741790L;

    private String name;

    private Integer age;

    private String sex;

    public User(String name, Integer age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                '}';
    }
}
测试结果

在这里插入图片描述
虽然JDK序列化使用比较方便,但是性能较差,所以我们通常会采用第三方序列化框架,例如:Hessian。

TCP粘包/拆包问题

什么是TCP粘包/拆包问题

TCP传递的是流,一个没有界限的数据。服务端在接收到客户端发出的数据的时候,不知道这是一条数据,还是多条数据。因此,服务端和客户端需要约定好拆包的规则,客户端按照此规则粘包,服务端按照此规则拆包。

TCP粘包/拆包问题的解决方法

  • 数据包中添加头,头中存储了数据的大小。服务端根据投中数据的大小,来拆包。
  • 客户端发送定长的数据,不足的补0.
  • 数据包之间设置边界,如在边界处添加特殊字符,服务端根据特殊字符来确定边界,进行拆包。

以上三种方法中,最常用的是第一种。

Demo

编解码器

编码器

public class MyProtocolEncoder extends MessageToByteEncoder<MyProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MyProtocol myProtocol, ByteBuf byteBuf) throws Exception {
        byteBuf.writeInt(myProtocol.getLength()); // 将数据长度写入
        byteBuf.writeBytes(myProtocol.getBody()); // 发送数据
    }
}

解码器

public class MyProtocolDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> list) throws Exception {
        int length = byteBuf.readInt(); // 读取头中的数据的长度
        byte[] body = new byte[length];
        byteBuf.readBytes(body); // 读取长度为length的字节数据

        MyProtocol myProtocol = new MyProtocol();
        myProtocol.setBody(body);
        myProtocol.setLength(length);
        list.add(myProtocol);
    }
}
自定义规则
public class MyProtocol {
    private int length;

    private byte[] body;

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public byte[] getBody() {
        return body;
    }

    public void setBody(byte[] body) {
        this.body = body;
    }
}
服务端

MyChannelHandler

public class MyChannelHandler extends SimpleChannelInboundHandler<MyProtocol> {
    private int count;

    /**
     * 获取客户端发来的数据
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
        System.out.println("服务端接收到消息:" + new String(msg.getBody(),
                CharsetUtil.UTF_8));
        System.out.println("服务端接收到消息数量:" + (++count));

        //获取到user对象
        byte[] data = "ok".getBytes(CharsetUtil.UTF_8);
        MyProtocol myProtocol = new MyProtocol();
        myProtocol.setLength(data.length);
        myProtocol.setBody(data);
        ctx.writeAndFlush(myProtocol);
    }

    /**
     * 异常处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}	

MyChannelInitializer

public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    /**
     * 将处理操作的Handler添加到管道中
     *
     * @param socketChannel
     * @throws Exception
     */
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast(new MyProtocolDecoder())
                .addLast(new MyProtocolEncoder())
                .addLast(new MyChannelHandler());
    }
}
客户端

MyClientChannelHandler

public class MyClientChannelHandler extends SimpleChannelInboundHandler<MyProtocol> {
    private int count;
    /**
     * 读取服务端的数据
     *
     * @param channelHandlerContext
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, MyProtocol msg) throws Exception {
        System.out.println("接收到服务端数据:" + new String(msg.getBody(), CharsetUtil.UTF_8));
        System.out.println("接收到服务端的消息数量:" + (++count));

    }

    /**
     * 向服务端发送数据
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        for (int i = 0; i < 10; i++) {
            byte[] body = "from client message!".getBytes(CharsetUtil.UTF_8);
            MyProtocol protocol = new MyProtocol();
            protocol.setLength(body.length);
            protocol.setBody(body);
            ctx.writeAndFlush(protocol);
        }
    }

    /**
     * 异常处理
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

MyClientInitializer

public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline()
                .addLast(new MyProtocolDecoder())
                .addLast(new MyProtocolEncoder())
                .addLast(new MyClientChannelHandler());
    }
}

注意:

TCP是基于流的,只保证接收到数据包分⽚顺序,⽽不保证接收到的数据包每个分⽚⼤⼩。因此在使⽤
ReplayingDecoder时,即使不存在多线程,同⼀个线程也可能多次调⽤decode()⽅法。在decode中修
改ReplayingDecoder的类变量时必须⼩⼼谨慎。

//这是⼀个错误的例⼦:
//消息中包含了2个integer,代码中decode⽅法会被调⽤两次,此时队列size不等于2,这段代码达不到期望结果。
public class MyDecoder extends ReplayingDecoder<Void> {
	private final Queue<Integer> values = new LinkedList<Integer>();
	
	@Override
	public void decode(ByteBuf buf, List<Object> out) throws Exception {
		// A message contains 2 integers.
		values.offer(buf.readInt());
		values.offer(buf.readInt());
		assert values.size() == 2;
		out.add(values.poll() + values.poll());
	}
}
//正确的做法:
public class MyDecoder extends ReplayingDecoder<Void> {
	private final Queue<Integer> values = new LinkedList<Integer>();
	@Override
	public void decode(ByteBuf buf, List<Object> out) throws Exception {
		// Revert the state of the variable that might have been changed
		// since the last partial decode.
		values.clear();
		// A message contains 2 integers.
		values.offer(buf.readInt());
		values.offer(buf.readInt());
		// Now we know this assertion will never fail.
		assert values.size() == 2;
		out.add(values.poll() + values.poll());
	}
}

Netty源码解析

我们自己在分析Netty的源码的时候主要看服务端的启动过程和连接请求的过程。

服务端启动过程

创建服务端Channel

  • 查看服务端的源码,入口在ServerBootstrap的bind()方法。
  • AbstractBootstrap中的initAndRegister()进⾏创建Channel,创建Channel的⼯作由ReflectiveChannelFactory反射类中的newChannel()⽅法完成。
  • NioServerSocketChannel中的构造⽅法中,通过jdk nio底层的SelectorProvider打开ServerSocketChannel。
  • 在AbstractNioChannel的构造⽅法中,设置channel为⾮阻塞:ch.configureBlocking(false);
  • 通过的AbstractChannel的构造⽅法,创建了id、unsafe、pipeline内容。
  • 通过NioServerSocketChannelConfig获取tcp底层的⼀些参数

Netty的优化建议

1. 零拷贝

Netty的零拷贝体现在三个方面

  • ByteBuf默认使用的池化的DirectByteBuf(堆外内存)
  • CompositeByteBuf将多个ByteBuf封装成一个ByteBuf,在添加ByteBuf不需要进程拷贝
  • 文件传输类DefaultFileRegion的transferTo方法将文件送给目标channel,不需要进行循环拷贝。

2. 使用EventLoop的任务调度

在EventLoop的⽀持线程外使⽤channel:

channel.eventLoop().execute(new Runnable() {
	@Override
	public void run() {
	channel.writeAndFlush(data)
	}
});

⽽不是直接使⽤channel.writeAndFlush(data);
前者会直接放⼊channel所对应的EventLoop的执⾏队列,⽽后者会导致线程的切换。

3. 减少ChannelHandler的创建

如果channelhandler是⽆状态的(即不需要保存任何状态参数),那么使⽤Sharable注解,并在
bootstrap时只创建⼀个实例,减少GC。否则每次连接都会new出handler对象。

@ChannelHandler.Sharable
public class StatelessHandler extends ChannelInboundHandlerAdapter {
	@Override
	public void channelActive(ChannelHandlerContext ctx) {}
}

public class MyInitializer extends ChannelInitializer<Channel> {
	private static final ChannelHandler INSTANCE = new StatelessHandler();
	
	@Override
	public void initChannel(Channel ch) {
		ch.pipeline().addLast(INSTANCE);
	}

4. 减少ChannelPipline的调⽤⻓度

5. 一些参数的配置

ServerBootstrap启动时,通常bossGroup只需要设置为1即可,因为ServerSocketChannel在初始化阶
段,只会注册到某⼀个eventLoop上,⽽这个eventLoop只会有⼀个线程在运⾏,所以没有必要设置为
多线程。⽽ IO 线程,为了充分利⽤ CPU,同时考虑减少线上下⽂切换的开销,通常设置为 CPU 核数的
两倍,这也是 Netty 提供的默认值。
在对于响应时间有⾼要求的场景,使⽤.childOption(ChannelOption.TCP_NODELAY, true)
和.option(ChannelOption.TCP_NODELAY, true)来禁⽤nagle算法,不等待,⽴即发送。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值