1、概括一下netty
(1)、Netty是一个基于Nio的clinet-server框架,使用它可以快速简单地开发网络应用程序
(2)、它极大地简化并优化了TCP和UDP套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好
(3)、支持多种协议如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议
用官方的总结就是:Netty成功地找到了一种在不妥协可维护性和性能的情况下实现易于开发,性能,稳定性和灵活性的方法。
2、为什么要使用netty
(1)、统一的API,支持多种传输类型,阻塞和非阻塞的。
(2)、简单而强大的线程模型
(3)、自带编解码器解决TCP粘包/拆包问题
(4)、真正的无连接数据包套接字支持
(5)、比直接使用Java核心API有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制
(6)、安全性不错,有完整的SSL/TLS以及StartTLS支持
(7)、社区活跃
3、Netty的应用场景
(1)、作为RPC框架的网络通信工具:我们在分布式系统中,不同服务节点之间经常需要相互调用,这时候就需要RPC框架了。不同服务节点之间的通信是如何做的呢?可以使用netty来做。比如我调用另外一个节点的方法的话,至少是要让对方知道我调用的是哪个类中的哪个方法以及相关参数。
(2)、实现一个自己的HTTP服务器
(3)、实现一个即使通讯系统:类似于微信那种
(4)、实现消息推送系统:比如dubbo、RocketMQ等等
4、Netty的核心组件有哪些?分别有什么作用?
1、Channel
Channel接口是Netty对网络操作抽象类,它除了包括基本的I/O操作,如bind()、connect()、read()、write()等。比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端)。Netty的Channel接口所提供的API,大大的降低了直接使用Scoket类的复杂性。
2、EventLoop
EventLoop的主要作用实际就是负责监听网络事件并调用事件处理器进行相关的I/O操作的处理
Channel为Netty网络操作的抽象类,EventLoop负责处理注册到其上的Channel处理I/O操作。两者配合参与I/O操作。
3、ChannelFuture
Netty是异步非阻塞的,所有的I/O操作都是异步的
因此,我们不能立刻得到操作是否执行成功,但是,你可以通过ChannelFuture接口的addListener()方法注册一个ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
还可以通过ChannelFuture接口的sync()方法让异步的操作变成同步的。
4、ChannelHandler 和 ChannelPipeline
ChannelHandler是消息的具体处理器,它负责处理读写操作、客户端连接等事情
ChannelPipeline为ChannelHandler的链,提供了一个容器并定义了用于沿着链传播入站和出栈事件流的API。当Channel被创建时,它会被自动地分配到它专属地ChannelPipeline
ChannelPipeline可以通过addLast()方法添加一个或多个ChannelHandler,因为一个数据或者事件可能会被多个Handle处理。当一个ChannelHandle处理完后就降数据交给下一个ChannelHandler。
5、EventLoopGroup,还有它和EventLoop什么关系
EventLoopGroup包含多个EventLoop(每个EventLoop通常内部包含一个线程),EventLoop的主要作用实际就是负责监听网络事件并调用事件处理器进行相关的I/O操作
并且EventLoop处理I/O事件都将在它专有的Thread上被处理,即Thread和EventLoop属于1:1的关系,从而保证线程安全。
当客户端通过connect方法连接服务端时,bossGroup处理客户端连接请求。当客户端处理完成之后,会将这个连接提交给workerGroup来处理,然后workerGroup负责处理其IO相关操作。
6、Bootstrap和ServerBootstrap
Bootstrap是客户端地启动引导类/辅助类,具体使用方法如下
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建客户端启动引导/辅助类:Bootstrap
Bootstrap b = new Bootstrap();
//指定线程模型
b.group(group).
......
// 尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
f.channel().closeFuture().sync();
} finally {
// 优雅关闭相关线程组资源
group.shutdownGracefully();
}
ServerBootstrap服务端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup).
......
// 6.绑定端口
ChannelFuture f = b.bind(port).sync();
// 等待连接关闭
f.channel().closeFuture().sync();
} finally {
//7.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
从上面的示例中、我们可以看出:
(1)、Bootstrap通常使用connect()方法连接到远程的主机和端口,作为一个Netty TCO协议通信中的客户端。另外,Bootstrap也可以通过bind()方法绑定本地的一个端口,作为UDP协议通信中的一端。
(2)、ServerBootstrap通常使用bind()方法绑定本地的端口上,然后等待客户端的连接。
(3)、Bootstrap只需要配置一个线程组------EventLoopGroup,而ServerBootstrap需要配置两个线程租----EvenetLoopGroup,一个用于接收连接,一个用于具体的处理。
7、Netty的线程模型
大部分网络框架都是基于Reactor模式设计开发的
“Reactor模式基于事件驱动,采用多路复用将事件分发给相应的Handle处理,非常适合处理海量IO的场景”
Reactor 模式是一种设计模式,用于处理并发 I/O 操作。它主要解决了多个并发客户端请求的事件分发和处理问题,是一种高效的事件驱动编程模式。下面详细描述 Reactor 模式的组成要素和工作原理
主要组成元素
-
Reactor(反应器):负责监听事件并分发给对应的处理程序。在Reactor模式中,Reactor是核心角色,它通过一个或多个事件循环(Event Loop)来监听并分发事件。
-
Handlers(处理程序):也称为处理器或者事件处理器,用于处理特定类型的事件。在Reactor模式中,可以有多个Handlers,每个Handler负责处理特定的事件类型,如连接事件、读取事件、写入事件等。
-
Dispatcher(分发器):负责将事件分发给对应的处理程序。Dispatcher可以根据事件的类型将事件分发给相应的处理程序,实现事件与处理程序之间的映射关系。
工作原理
-
注册事件:客户端通过系统调用(如select、poll、epoll等)将事件注册到Reactor中,例如注册对某个文件描述符(socket)的读取事件
-
事件循环:Reactor通过事件循环不断地检测注册的事件,一旦有事情发生,Reactor就会触发相关的处理程序进行处理。这种方式避免了传统同步I/O的阻塞等待,提高了系统的并发性能。
-
事件分发:当事件发生时,Reactor根据事件类型选择合适的处理程序来处理事件。如果是连接事件,则交给连接处理程序;如果是读取事件,则交给读取处理程序等
-
事件处理:处理程序根据事件类型执行相应的操作,可能包括数据读取、数据处理和返回结果等。
-
响应处理:处理程序处理完事件后,将结果返回给客户端,或者注册新事件到Reactor,形成一个循环
优势
-
高并发性:Reactor模式可以有效地处理大量并发的客户端请求,每个请求都可以在事件发生时被及时处理
-
可扩展性:由于Reactor模式采用了事件驱动的方式,因此它对系统的扩展性良好,可以方便地添加新的事件类型和处理程序
-
非阻塞I/O:Reactor模式通过事件循环实现了非阻塞地I/O操作,提高了系统的I/O效率。
在Netty主要靠NioEventLoopGroup线程池来实现具体的线程模型
我们实现服务端的时候,一般会初始化两个线程组
1、bossGroup:接收连接
2、workGroup:负责具体的处理,交由对应的Handle处理
(1)单线程模型
一个线程模型需要执行处理所有的accept、read、decode、process、encode、send事件。对于高负载、高并发,并且对性能要求比较高的场景不适用。
对应到netty代码是下面这样的
//1.eventGroup既用于处理客户端连接,又负责具体的处理。
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
boobtstrap.group(eventGroup, eventGroup)
//......
(2)多线程模型
一个Acceptor线程只负责监听客户端的连接,一个NIO线程池负责具体处理:accept、read、decode、process、encode、send事件。满足绝大部分应用场景,并发连接不大的时候没啥问题,但是遇到并发连接大的时候就可能会出现问题,成为性能瓶颈。
对应到netty代码是下面这样的
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
//......
(3)主从线程模型
从一个主线程NIO线程池中选择一个线程作为Acceptor线程,绑定监听端口,接收客户端连接的连接,其他线程负责后续的接入认证等工作。连接建立完成后,Sub NIO线程池负责具体处理I/O读写。如果多线程模型无法满足你的需求的时候,可以考虑使用主从多线程模型。
8、Netty服务端和客户端的启动过程
(1)服务端
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
//2.创建服务端启动引导/辅助类:ServerBootstrap
ServerBootstrap b = new ServerBootstrap();
//3.给引导类配置两大线程组,确定了线程模型
b.group(bossGroup, workerGroup)
// (非必备)打印日志
.handler(new LoggingHandler(LogLevel.INFO))
// 4.指定 IO 模型
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
//5.可以自定义客户端消息的业务处理逻辑
p.addLast(new HelloServerHandler());
}
});
// 6.绑定端口,调用 sync 方法阻塞知道绑定完成
ChannelFuture f = b.bind(port).sync();
// 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法)
f.channel().closeFuture().sync();
} finally {
//8.优雅关闭相关线程组资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
-
首先创建两个NioEventLoopGroup对象实例:bossGroup和workerGroup。bossGroup用于处理客户端的TCP连接请求,workerGroup负责每一条的具体读写数据的处理逻辑,真正负责I/O读写操作,交由对应的Handler处理。从源码来看,使用NioEventLoopGroup类的无参构造函数设置的线程数量的默认值就是CPU核心数*2
-
创建一个服务端引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作
-
通过.group()方法给引导类ServerBootstrap配置两大线程组,确定了线程模型
-
通过channel()方法给引导类ServerBootstrap指定了服务端的IO模型为NIO,NioServerSocketChannel指定服务端的IO模型为NIO,与BIO编程模型中的ServerSocket对应。NioSocketChannel指定客户端的IO模型为NIO,与BIO编程模型的Socket对应
-
通过.childHandler()给引导类创建一个ChannelInitializer,然后制定了服务端消息的业务处理逻辑HelloServerHandler对象
-
调用ServerBootstrap类的bind()方法绑定端口
(2)客户端
//1.创建一个 NioEventLoopGroup 对象实例
EventLoopGroup group = new NioEventLoopGroup();
try {
//2.创建客户端启动引导/辅助类:Bootstrap
Bootstrap b = new Bootstrap();
//3.指定线程组
b.group(group)
//4.指定 IO 模型
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
// 5.这里可以自定义消息的业务处理逻辑
p.addLast(new HelloClientHandler(message));
}
});
// 6.尝试建立连接
ChannelFuture f = b.connect(host, port).sync();
// 7.等待连接关闭(阻塞,直到Channel关闭)
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
-
创建一个NioEventLoopGroup对象实例
-
创建一个客户端启动引导类Bootstrap
-
通过.group()方法给引导类Bootstrap配置一个线程组
-
通过channel()方法给引导类Bootstrap指定了IO模型为NIO
-
通过childHandler()给引导类创建了一个ChannelInitializer,然后制定了客户端消息的业务处理逻辑HelloClinetHandler对象
-
调用Bootstrap类的connect()方法进行连接,这个方法需要指定两个参数inetHost(IP地址),inetPort(端口号)