java 自己动手实现一个分布式系统 之 Netty篇 入门 - Go语言中文社区

java 自己动手实现一个分布式系统 之 Netty篇 入门


什么是netty?

  • Netty是一个NIO网络编程框架,快速开发高性能、高可靠性的网络服务器/客户端程序。 极大地简化了TCP和UDP等网络编程,是一个异步事件驱动的网络编程框架
  • 重点是NIO、快速、高性能、异步、事件驱动。

哪些中间件用到了netty?

  • RPC(pigeon、dubbo、HSF)Hadoop、SparkMQ(swallow、RocketMQ)Zookeeper等

最核心的三大组件 缓冲(Buffer)通道(Channel)事件模型(Event Model)

netty的前身可以当成是IO编程模型,当时IO编程模型出现了各种问题,

  • 线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起
  • 线程切换效率低下:单机cpu核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。
  • 除了以上两个问题,IO编程中,我们看到数据读写是以字节流为单位,效率不高。

当时为了解决这些问题,jdk1.4之后便提出了 NIO,由于原生的java NIO编程复杂、效率低等缺点,就泛生出了netty这个编程模型,netty就是对NIO的一种封装。整体架构的变化如下图所示:

也就是下面这样,当selectot的线程池为1时,netty模型就相当于reactor中的多线程模型;假如为多个时,就是一个netty模型就相当于reactor中的主从多线程模型。

非阻塞的socket

netty的线程模型可以参照Reactor设计模式中的三个,单线程Reactor模式、多线程Reactor模式、多Reactor模式,架构图如下图所示:

单线程Reactor模式

多线程Reactor模式

多Reactor模式

 

netty对原生NIO封装后,

  •  使用多路复用技术,提高处理连接的并发性
  •  零拷贝:
  •  Netty的接收和发送数据采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝
  •  Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象进行一次操作
  •  Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题
  •  内存池:为了减少堆外直接内存的分配和回收产生的资源损耗问题,Netty提供了基于内存池的缓冲区重用机制
  •  使用主从Reactor多线程模型,提高并发性
  •  采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降
  •  默认使用Protobuf的序列化框架
  •  灵活的TCP参数配置

netty的组件

  • Bootstrap:netty的辅助启动器,netty客户端和服务器的入口,Bootstrap是创建客户端连接的启动器,ServerBootstrap是监听服务端端口的启动器,跟tomcat的Bootstrap类似,程序的入口。
  • Channel:关联jdk原生socket的组件,常用的是NioServerSocketChannel和NioSocketChannel,NioServerSocketChannel负责监听一个tcp端口,有连接进来通过boss reactor创建一个NioSocketChannel将其绑定到worker reactor,然后worker reactor负责这个NioSocketChannel的读写等io事件。
  • EventLoop:netty最核心的几大组件之一,就是我们常说的reactor,人为划分为boss reactor和worker reactor。通过EventLoopGroup(Bootstrap启动时会设置EventLoopGroup)生成,最常用的是nio的NioEventLoop,就如同EventLoop的名字,EventLoop内部有一个无限循环,维护了一个selector,处理所有注册到selector上的io操作,在这里实现了一个线程维护多条连接的工作。
  • ChannelPipeline:netty最核心的几大组件之一,ChannelHandler的容器,netty处理io操作的通道,与ChannelHandler组成责任链。write、read、connect等所有的io操作都会通过这个ChannelPipeline,依次通过ChannelPipeline上面的ChannelHandler处理,这就是netty事件模型的核心。ChannelPipeline内部有两个节点,head和tail,分别对应着ChannelHandler链的头和尾。
  • ChannelHandler:netty最核心的几大组件之一,netty处理io事件真正的处理单元,开发者可以创建自己的ChannelHandler来处理自己的逻辑,完全控制事件的处理方式。ChannelHandler和ChannelPipeline组成责任链,使得一组ChannelHandler像一条链一样执行下去。ChannelHandler分为inBound和outBound,分别对应io的read和write的执行链。ChannelHandler用ChannelHandlerContext包裹着,有prev和next节点,可以获取前后ChannelHandler,read时从ChannelPipeline的head执行到tail,write时从tail执行到head,所以head既是read事件的起点也是write事件的终点,与io交互最紧密。
  • Unsafe:顾名思义这个类就是不安全的意思,但并不是说这个类本身不安全,而是不要在应用程序里面直接使用Unsafe以及他的衍生类对象,实际上Unsafe操作都是在reactor线程中被执行。Unsafe是Channel的内部类,并且是protected修饰的,所以在类的设计上已经保证了不被用户代码调用。Unsafe的操作都是和jdk底层相关。EventLoop轮询到read或accept事件时,会调用unsafe.read(),unsafe再调用ChannelPipeline去处理事件;当发生write事件时,所有写事件都会放在EventLoop的task中,然后从ChannelPipeline的tail传播到head,通过Unsafe写到网络中。
  • Callback:Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfaceChannelHandler 的实现处理。当一个新的连接已经被建立时,ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息。
  • Future:Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty 提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。
  • Event:Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:- 记录日志- 数据转换- 流控制- 应用程序逻辑
    Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:- 连接已被激活或者连接失活- 数据读取- 用户事件- 错误事件
    出站事件是未来将会触发的某个动作的操作结果,这些动作包括:- 打开或者关闭到远程节点的连接- 将数据写到或者冲刷到套接字。

组件之间,比如Channel、EventLoop与EventLoopGroup之间有如下关系:

netty简单实现一个RPC Server的结构是下图这样的,整体结构也是由上面的几个组件组合而成,代码就放ServerbootStrap的部分代码了,注释比较详细,仅供理解。

  • ServerBootStrap代码:
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 *功能还是实现了,RPC 的目的就是允许像调用本地服务一样调用远程服务,
 *需要对使用者透明,于是我们使用了动态代理。并使用 Netty 的 handler 发送数据和响应数据,
 *完成了一次简单的 RPC 调用。

 * 一个EventLoop在它的生命周期内只和一个Thread绑定,所有有EventLoop处理的IO事件都将在它专有的Thread上被处理。一个channel在它的生命周期内只注册一个
 * EventLoop,每一个EventLoop负责处理一个或者多个Channel。
 * 
 * @author chain
 *
 */
public class ServerBootStrap {

	private static void startServer0(String hostName, int port) {

//		 NioEventLoopGroup的bossGroup可以设置数量的,默认为this(0);

//	      bossGroup继承自线程池,维护了NioServerSocketChannel的实例;负责连接,开销小,连接成功后将这个SocketChannel转发给workerGroup。workerGroup会选出一个eventLoop
//        去维护这个socketChannel并注册到其维护的selector中,然后对其进行后续的IO事件处理。

//		当bossGroup的数量设置为1时,netty模型就相当于reactor中的多线程模型
//		当bossGroup的数量设置为多个时(比如4个),netty模型就相当于reactor中的主从多线程模型

		EventLoopGroup bossGroup = new NioEventLoopGroup();
		EventLoopGroup workerGroup = new NioEventLoopGroup();
		try {
			ServerBootstrap bootstrap = new ServerBootstrap();

//			当设置一个EventLoopGroup时,看源码仍然是绑定了bossGroup和workerGroup,只不过是同一个EventLoopGroup而已,
//			这就相当于Reactor的单线程模型

			bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
//		          channelPipeline中每一个channelhandler都是通过它的eventLoop来传递给他的事件的,至关重要的是不要阻塞这个线程。
//		          有的可以在添加pipline的时候也指定EventExecutorGroup去处理,channelHandler中所有的方法都会在这个指定的EventExecutorGroup中运行
//		          p.addLast(workerGroup, "handler", new StringEncoder());

					.childHandler(new ChannelInitializer<SocketChannel>() {
						@Override
						protected void initChannel(SocketChannel ch) throws Exception {
//							执行顺序为注册的逆序
							ChannelPipeline p = ch.pipeline();
							p.addLast(new StringDecoder());
							p.addLast(new StringEncoder());

							p.addLast(new DealMesHandler());
							p.addLast(new HelloServerHandler());

						}
					});

			ChannelFuture future = bootstrap.bind(hostName, port).sync();
			future.channel().closeFuture().sync();

		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

	public static void main(String[] args) {
		ServerBootStrap netBootStrap = new ServerBootStrap();
		netBootStrap.startServer0("localhost", 8081);
	}
}

补:忘了放一张比较经典的图:

这里写图片描述

最后,简介部分还有些面试题,可参考博文:netty面试常见问题

 

参考:

https://blog.csdn.net/wozniakzhang/article/details/92174287

https://blog.csdn.net/eric_sunah/article/details/80424344

https://blog.csdn.net/yx0628/article/details/78684828

https://www.cnblogs.com/lxyit/p/10430939.html

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/mojir/article/details/103105631
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-04-18 23:25:52
  • 阅读 ( 1340 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢