使用Netty实现服务端与客户端之间的通信

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。

在Netty官网上是如上定义Netty的。Netty利用了Java的高级网络能力,提供了一个比JDK原生API更容易使用且功能更强大的客户端/服务器框架。

Netty与Tomcat的区别

提到服务器,大家可能更多的想到的会是Tomcat。Netty与Tomcat最大的区别就在于通信协议,Tomcat是基于HTTP协议的,它的实质是一个基于HTTP协议的web容器,但是Netty不一样,他能通过codec来自己编码/解码字节流,即Netty可以自定义各种通信协议,这就是Netty与Tomcat最大的不同。

Netty的简单应用

依然是用时间服务器来呈现Netty的应用,Netty版本使用的是4.1.46.Final。

首先是服务器端,NettyServerTest类,用于构建Netty相关的组件,并进行初始化:

package com.test.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServerTest {
	
	public void bind(int port) throws InterruptedException {
		// NioEventLoopGroup 是用来处理I/O操作的多线程事件循环器
		// 第一个经常被叫做‘boss’,用来接收进来的连接
		EventLoopGroup bossGroup = new NioEventLoopGroup();
		// 第二个经常被叫做‘worker’,用来处理已经被接收的连接,一旦‘boss’接收到连接,就会把连接信息注册到‘worker’上
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			// ServerBootstrap 是一个启动 NIO 服务的辅助启动类
			ServerBootstrap b = new ServerBootstrap();
			b.group(bossGroup, workerGroup)
				.channel(NioServerSocketChannel.class)
				// 设置 socket 的参数选项
				.option(ChannelOption.SO_BACKLOG, 1024)
				.childHandler(new childChannelHandler());
			// 绑定端口,同步等待成功
			ChannelFuture f = b.bind(port).sync();
			
			// 等待服务端监听端口关闭
			f.channel().closeFuture().sync();
			
		}finally {
			// 优雅退出,释放线程资源
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}
	
	/**
	 * ChannelInitializer 是一个特殊的处理类,他的目的是帮助使用者配置一个新的 Channel
	 * @author 
	 *
	 */
	private class childChannelHandler extends ChannelInitializer<SocketChannel>{

		@Override
		protected void initChannel(SocketChannel arg0) throws Exception {
			arg0.pipeline().addLast(new ServerTestHandler());
		}
		
	}

	public static void main(String[] args) throws InterruptedException {
		int port = 8888;
		new NettyServerTest().bind(port);
	}

}

然后我们还需要构建一个用于处理服务器端接收到的客户端连接的Handle类,读取客户端消息和向客户端发送response:

package com.test.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerTestHandler extends ChannelInboundHandlerAdapter  {
	/**
	 * 每当从客户端收到新的数据时,channelRead()方法会在收到消息时被调用
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf) msg;
		byte[] req = new byte[buf.readableBytes()];
		buf.readBytes(req);
		String body = new String(req, "UTF-8");
		System.out.println("The time server receive order : " + body);
		String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
				System.currentTimeMillis()).toString() : "BAD ORDER";
		ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
		ctx.write(resp);
	}
	
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
		// 将消息发送发送队列中的消息写入到SocketChannel中发送给对方
		ctx.flush();
	}
	
	/**
	 * exceptionCaught() 事件处理方法是当出现 Throwable 对象才会被调用,
	 * 即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		ctx.close();
	}
}

客户端NettyClientTest类:

package com.test.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClientTest {

	public void connect(int port, String host) throws Exception {
		// 配置客户端NIO线程组
		EventLoopGroup group = new NioEventLoopGroup();
		try {
			Bootstrap b = new Bootstrap();
			b.group(group).channel(NioSocketChannel.class)
				.option(ChannelOption.TCP_NODELAY, true)
				.handler(new ChannelInitializer<SocketChannel>() {

				@Override
				protected void initChannel(SocketChannel ch) throws Exception {
					// TODO Auto-generated method stub
					ch.pipeline().addLast(new ClientTestHandler());
				}
				
			});
			// 发起异步连接操作
			ChannelFuture f = b.connect(host, port).sync();
			// 等待客户端链路关闭
			f.channel().closeFuture().sync();
		} finally {
			// 优雅退出,释放NIO线程组
			group.shutdownGracefully();
		}
	}
	
	public static void main(String[] args) throws Exception {
		int port = 8888;
		new NettyClientTest().connect(port, "127.0.0.1");
	}

}

客户端ClientTestHandler 类:

package com.test.netty;

import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ClientTestHandler extends ChannelInboundHandlerAdapter{
	
	private static final Logger logger = Logger.getLogger(ClientTestHandler.class.getName());
	
	private ByteBuf firstMessage;
	
	public ClientTestHandler() {
		byte[] req = "QUERY TIME ORDER".getBytes();
		firstMessage = Unpooled.buffer(req.length);
		firstMessage.writeBytes(req);
	}
	
	@Override
	public void channelActive(ChannelHandlerContext ctx) {
		ctx.writeAndFlush(firstMessage);
	}
	
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		ByteBuf buf = (ByteBuf)msg;
		byte[] req = new byte[buf.readableBytes()];
		buf.readBytes(req);
		String body = new String(req, "UTF-8");
		System.out.println("Now is : " + body);
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		// 释放资源
		logger.warning("Unexpected exception from downstream : " + cause.getMessage());
		ctx.close();
	}
}

运行结果:

在这里插入图片描述

依然是客户端发送请求到服务器端,服务器端解析客户端请求并返回响应(当前的时间)。

Netty的逻辑架构

Netty的逻辑架构主要可以分为三层:Reactor通信调度层、职责链ChannelPipeline和业务逻辑编排层(Service ChannelHandler)。

Reactor通信调度层

Reactor通信调度层主要由一系列辅助类完成,包括Reactor线程NioEventLoop及其父类,NioSocketChannel/NioServerSocketChannel及其父类、ByteBuffer以及由其衍生出来的各种Buffer、Unsafe以及其衍生出来的各种内部类。

该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等,将这些事件触发到PipeLine中,由PipeLine管理的职责链来进行后续的处理。

职责链ChannelPipeline

它负责事件在职责链中的有序传播,同时负责动态地编排职责链。职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件。不同应用的Handler节点的功能也不同,通常情况下,往往会开发编解码Hanlder用于消息的编解码,它可以将外部的协议消息转换成内部的POJO对象,这样上层业务则只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。

业务逻辑编排层(Service ChannelHandler)

业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。例如CMPP协议,用于管理和中国移动短信系统的对接。

架构的不同层面,需要关心和处理的对象都不同,通常情况下,对于业务开发者,只需要关心职责链的拦截和业务Handler的编排,因为应用层协议栈往往是开发一次,到处运行,实际上对于业务开发者来说,只需要关心服务层的业务逻辑开发即可。各种应用协议以插件的形式提供,只有协议开发人员需要关注协议插件,对于其他业务开发人员来说,只需关心业务逻辑定制即可。这种分层的架构设计理念实现了NIO框架各层之间的解藕,便于上层业务协议栈的开发和业务逻辑的定制。

Netty主要组件

ByteBuf

当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。

实际上,7种基础类型(Boolean除外)都有自己的缓冲区实现,对于NIO编程而言,我们主要使用的是ByteBuffer。但是ByteBuffer存在一定的局限性:

(1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常;
(2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致程序处理失败;
(3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。

为了弥补这些不足,Netty提供了自己的ByteBuffer实现——ByteBuf。

ByteBuf依然是个Byte数组的缓冲区,但netty的ByteBuf有两个位置指针用于处理读写操作,相比JDK的ByteBuffer更强大、方便。

JDK ByteBuffer由于只有一个位置指针(position)用于处理读写操作,因此每次读写的时候都需要额外调用flip()和clear()等方法,否则可能会出错。

在调用flip()方法前,ByteBuffer的数组缓冲区是:
在这里插入图片描述

当执行flip()方法后,它的limit被设置为position,position设置为0,capacity不变,由于读取的内容是从position到limit之间,因此,它能够正确的读取到之前写入缓冲区的内容。

在这里插入图片描述

JDK中ByteBuffer的flip()方法的源码为:

在这里插入图片描述

ByteBuffer的典型用法如下:

package com.test;

import java.nio.ByteBuffer;

public class Test {

	public static void main(String args[]) {
		ByteBuffer buffer = ByteBuffer.allocate(88);
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		String value = "Netty学习";
		buffer.put(value.getBytes());
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		buffer.flip();
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		byte[] vArray = new byte[buffer.remaining()];
		buffer.get(vArray);
		String decodeValue = new String(vArray);
		System.out.println(decodeValue);
	}

}

ByteBuffer对象buffer在执行完put后执行flip操作设置position和limit后能正确的读到缓冲区的内容,程序执行结果如下:

在这里插入图片描述

ByteBuffer刚创建的时候,position是0,limit等于capacity的值88;当执行put操作后,position会往后移动put内容相应大小的数值,limit也还是等于capacity的值88;执行完flip()操作后,position重新置为了0,limit变成了position的值9,capacity不变,这样在执行get操作的时候才能正确地读到position到limit直接的内容。

如果在put操作后不进行flip()操作,则get操作读取的是position=9到limit=88之间的空白内容,如:

package com.test;

import java.nio.ByteBuffer;

public class Test {

	public static void main(String args[]) {
		ByteBuffer buffer = ByteBuffer.allocate(88);
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		String value = "Netty学习";
		buffer.put(value.getBytes());
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		//buffer.flip();
		System.out.println("position = " + buffer.position());
		System.out.println("limit = " + buffer.limit());
		byte[] vArray = new byte[buffer.remaining()];
		buffer.get(vArray);
		String decodeValue = new String(vArray);
		System.out.println(decodeValue);
	}

}

执行结果如下,没有打印出“Netty学习”的内容:

在这里插入图片描述

Netty中的ByteBuf采用了两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。

readerIndex和writerIndex一开始都是0,随着数据的写入writerIndex会增加,读数据会使readerIndex增加,但是它不会超过writerIndex。在读取之后,0 ~ readerIndex的就被视为discard,调用discardReadBytes方法,可以释放这部分空间,它的作用类似于ByteBuffer的compact方法。

ReaderIndex和writerIndex之间的数据是可读取的,等价于ByteBuffer position和limit之间的数据。Writerlndex和capacity之间的空间是可写的,等价于ByteBuffer limit和capacity之间的可用空间。

初始分配的ByteBuf,如下图所示:

在这里插入图片描述

写入了N个字节之后的ByteBuf如下图所示:

在这里插入图片描述

读取M个字节之后的ByteBuf,如下图所示:

在这里插入图片描述

调用discardableReadBytes操作之后的ByteBuf如图所示:

在这里插入图片描述

调用clear操作之后的ByteBuf如图所示:

在这里插入图片描述

当需要写入的字节数大于可写的字节数时,肯定就不能够将所需写入的内容全部写入,在JDK的ByteBuffer中,当我们对ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOverflowException异常。

在这里插入图片描述

为了避免这种情况,我们都通常在ByteBuffer的每次put操作前对剩余可用空间进行校验,如果剩余可用空间不足,需要重新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放旧的ByteBuffer。但是这导致了代码冗余,Netty的ByteBuf为了解决这个问题,对write操作进行了封装,由ByteBuf的write操作负责进行可用空间的校验,如果可用空间不足,ByteBuf会自动进行动态扩展。

在这里插入图片描述

从AbstracByteBuf中的writeBytes的源码可以看到,在它的方法体中第一行调用了ensureWritable方法,从方法名称可以看出它是确定“可写性”。ensureWritable的源码如下:

在这里插入图片描述

在ensureWritable中,调用的是ensureWritable0方法,并且传入的参数是checkPositiveOrZero的返回值。checkPositiveOrZero方法的作用是检查需要写入的字节长度是否大于0,小于等于0则抛出异常,否则返回需要写入的字节数。

ensureWritable0方法的源码解析如下:

final void ensureWritable0(int minWritableBytes) {
	// 获取缓冲区的当前writerIndex位置索引
    final int writerIndex = writerIndex();
	// 计算写入字节所需的目标容量
    final int targetCapacity = writerIndex + minWritableBytes;
    // 如果目标容量小于等缓冲区容量则不需要动态扩容
	if (targetCapacity <= capacity()) {
		// 每种尝试访问缓冲区内容的方法都应调用此方法,以检查缓冲区是否之前已释放。
		ensureAccessible();
        return;
    }
	// 如果目标容量大于缓冲区最大容量
    if (checkBounds && targetCapacity > maxCapacity) {
        ensureAccessible();
		// 抛出IndexOutOfBoundsException异常
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }

    // Normalize the target capacity to the power of 2.
	// 返回不涉及内部重新分配和数据拷贝情况下的最大可写入字节数
    final int fastWritable = maxFastWritableBytes();
	// 计算新容量
    int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
            : alloc().calculateNewCapacity(targetCapacity, maxCapacity);

    // Adjust to the new capacity.
    capacity(newCapacity);
}

ByteBuf主要功能类的继承关系如下图:

在这里插入图片描述

从内存分配的角度看,ByteBuf可以分为两类:
(1)堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动回收;缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
(2)直接内存(DirectByteBuf)字节缓冲区:非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。

Netty提供了多种ByteBuf供开发者使用,经验表明,ByteBuf的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。

从内存回收角度看,ByteBuf也分为两类:基于对象池的ByteBuf和普通ByteBuf。两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表明使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。

Channel

Io.netty.channel.Channel是Netty网络操作的抽象类,它聚合了一组功能,包括但不限于网络的读、写、客户端发起连接、主动关闭连接,链路关闭,获取通信双方的链路地址等。它也包含了Netty框架相关的一些功能,包括获取该Channel的Eventloop,获取缓冲分配器ByteBufAllocator和pipeline等。

其实JDK已经提供了原生的Channel,但还是存在一些缺陷,所以Netty要重新提供Channel的实现:
(1)JDK的SocketChannel和ServerSocketChannel没有统一的Channel接口供业务开发者使用;
(2)JDK的SocketChannel和ServerSocketChannel的主要职责就是网络I/O操作,由于它们是SPI类接口,由具体的虚拟机厂家提供,所以通过继承来拓展其功能的难度很大;
(3)Netty的Channel需要能够跟Netty的整体架构融合在一起;
(4)自定义Channel,功能实现更灵活。

基于以上的原因,Netty重新设计了Channel接口,并且给予了很多不同的实现。它的设计原理比较简单,但是功能却比较繁杂,主要设计理念如下:
(1)在Channel接口层,采用Facade模式进行统一封装,将网络I/O操作、网络I/O相关联的其他操作封装起来,统一对外提供。
(2)Channel接口的定义尽量大而全,为SocketChannel和ServerSocketChannel提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度上实现功能和接口的重用。

从使用者的角度来看,我们主要使用的是NioServerSocketChannel和NioSocketChannel。顾名思义,NioServerSocketChannel主要用于服务端,NioSocketChannel主要用于客户端。

NioServerSocketChannel的主要继承关系如下:

在这里插入图片描述

NioSocketChannel的主要继承关系如下:

在这里插入图片描述

从上面的继承关系,我们可以看到,Channel是netty中channel体系的顶级接口,紧接着是两个抽象类的对接口中的一些通用功能进行了实现,然后根据不同的功能分支,分成服务端的channel和客户端的channel。

Netty中的channel相比于JDK中的channel增加了一些组件:
id 标识唯一身份信息
可能存在的parent Channel
管道 pepiline
用于数据读写的unsafe内部类
关联上相伴终生的NioEventLoop
Netty中的Channel是需要注册到EventLoop的多路复用器上,用于处理I/O事件,通过eventLoop()可以获取到Channel注册的EventLoop。EventLoop本质上就是处理网络读写事件的Reactor线程。
Channel接口中定义的parent方法可以获取到channel的父母channel,对于服务端Channel而言,它的父Channel为空;对于客户端Channel,它的父Channel就是创建它的ServerSocketChannel。
在Channel中真正执行I/O操作的是它的内部类Unsafe。

ChannelPipeline和ChannelHandler

Netty基于事件驱动,我们也可以理解为当Chnanel进行I/O操作时会产生对应的I/O事件,然后驱动事件在ChannelPipeline中传播,由对应的ChannelHandler对事件进行拦截和处理,不关心的事件可以直接忽略。采用事件驱动的方式可以非常轻松地通过事件定义来划分事件拦截切面,方便业务的定制和功能扩展,相比AOP,其性能更高,但是功能却基本等价。

Netty的Channel过滤器实现原理与Servlet Filter机制一致,它将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便地通过新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

ChannelPipeline是ChannelHandler的容器,它负责ChannelHandler的管理和事件拦截与调度。

消息的读取与发送流程如下:
(1)底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead事件,由I/O线程NioEventLoop调用ChannelPipeline的fireChannelRead(Object msg)方法,将消息(ByteBuf)传输到ChannelPipeline中;
(2)消息依次被HeadHandler、ChannelHandler1、ChannelHandler2……TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递;
(3)调用ChannelHandlerContext的write方法发送消息,消息从TailHandler开始,途经ChannelHandlerN……ChannelHandler1、HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流程,构造异常的Future返回。

在这里插入图片描述

inbound和outbound是Netty自身根据事件在pipeline中的流向抽象出来的术语,在其他NIO框架中并没有这个概念。inbound事件通常由I/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等;由用户线程或者代码发起的I/O操作被称为outbound事件。

ChannelHandler用于处理Channel对应的事件,我们除了使用Netty提供的ChannelHandler之外,我们还可以实现它的子接口ChannelInboundHandler和ChannelOutboundHandler,为了便利,框架提供了ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter适配类,在使用的时候只需要实现你关注的方法即可。

ChannelInboundHandlerAdapter提供的方法及回调机制如下:

在这里插入图片描述
在这里插入图片描述

ChannelOutboundHandlerAdapter提供的方法及回调机制如下:

在这里插入图片描述

在这里插入图片描述

创建了自定义的ChannelHandler之后,我们就需要将自定义的ChannelHandler加入到pipeline中。Pipeline是不需要自己创建的,因为使用ServerBootstrap或者Bootstrap启动服务端或者客户端时,Netty会为每个Channel连接创建一个独立的pipeline,我们只需要将自定义的拦截器加入到pipeline中即可。

在这里插入图片描述

ChannelPipeline是ChannelHandler的管理容器,负责ChannelHandler的查询、添加、替换和删除。

EventLoop和EventLoopGroup

EventLoop和EventLoopGroup主要涉及的是Netty框架中的线程模型,Netty框架中的主要线程就是I/O线程。

在Nio线程模型中,最经典的就是Reactor单线程模型了。

在这里插入图片描述

Reactor单线程模型,是指所有的I/O操作都在同一个NIO线程上面完成。NIO线程
的职责如下。
作为NIO服务端,接收客户端的TCP连接;
作为NIO客户端,向服务端发起TCP连接;
读取通信对端的请求或者应答消息;
向通信对端发送消息请求或者应答消息。

由于Reactor线程模式使用的是异步非阻塞模式I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关操作。在一些小容量场景下,可以使用单线程模型。但是对于高负载、高并发的应用场景却不适合。

后面又演进了Reactor多线程模型,Reactor多线程模型与单线程模型最大的区别就是有一组NIO线程来处理IO 操作。

在这里插入图片描述

Reactor多线程有一个专门的NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求。而网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码和发送。

在绝大多数情况下,Reactor多线程模型可以满足性能要求,但是在个别特殊场景中,只有一个NIO线程来监听和处理所有的客户端连接可能会存在性能问题。为了提升性能,又演进了“主从多线程模型”。

主从多线程模型的特点是接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。

在这里插入图片描述

利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方demo中,推荐使用该线程模型。

通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。下图中的启动参数配置的是主从多线程模型。

在这里插入图片描述

服务端启动的时候,创建了两个NioEventLoopGroup,它们实际是两个独立的Reactor线程池。一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。

在这里插入图片描述

Netty用于接收客户端的请求的线程池接收到客户端TCP连接后,初始化Channel参数,将Channel“注册”到负责I/O操作的NioEventLoop,并将链路状态变更事件通知给ChannelPipeline。

处理I/O操作的NioEventLoop首先负责轮询注册的Channel,读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg)。只要不主动切换线程,一直都是由NioEventLoop调用Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。

在这里插入图片描述

Netty的多线程编程最佳实践如下。
(1)创建两个NioEventLoopGroup,用于逻辑隔离NIOAcceptor和NIOI/O线程。
(2)尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
(3)解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
(4)如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
(5)如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。推荐的线程数量计算公式有以下两种。
公式一:线程数量=(线程总时间/瓶颈资源时间)×瓶颈资源的线程并行数;
公式二:QPS=1000/线程总时间x线程数。

Future

在Netty中,所有的I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而不是像传统BIO那样同步等待操作完成。异步操作会带来一个问题:调用者如何获取异步操作的结果?

Netty中引入了JDK中Future的概念,它用于代表异步操作的结果;ChannelFuture就是代表异步I/O操作的结果。

在这里插入图片描述

与java.util.concurrent.Future一样,Netty的Future也可以通过get方法获取操作结果,isDone方法判断时候判断操作是否完成,cancel方法尝试 取消异步操作。

Netty强烈建议通过添加监听器(GenericFutureListener)的方式获取I/O操作的结果而不是get方法,原因是:当我们进行异步I/O操作时,完成的时间是无法预测的,如果不设置超时时间,它会导致调用线程长时间被阻塞,甚至挂死。如果设置超时时间,时间又无法精确预测。

ChannelFuture可以同时设置一个或多个GenericFutureListener,也可以通过remove方法删除GenericFutureListener。

当I/O操作完成之后,I/O线程会回调ChannelFuture中GenericFutureListener的operationComplete方法,并把ChannelFuture对象当作方法的入参。

但是要注意不要在ChannelHandler中调用ChannelFuture的await()方法,这会导致死锁。发起I/O操作之后,由I/O线程负责异步通知发起I/O操作的用户线程,如果用户线程和I/O线程是同一个线程的时候,就会导致I/O线程自己等待自己通知自己操作完成。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页