开篇题
新入职一家公司,工作节奏较快,没有过多属于自己的时间,时间管理方面必须将时间颗粒度划分的更细点,努力挤出点属于自己的时间,学习和巩固下相关知识。netty目前计划规划三遍,基础篇,核心理论篇,实战篇,现在开始netty学习之旅。
1.什么是netty?
Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。
2.为什么Netty受欢迎?
如第一部分所述,netty是一款受到大公司青睐的框架,在我看来,netty能够受到青睐的原因有三:
- 并发高
- 传输快
- 封装好
想要学好netty,首先要懂点网络通信原理,下节开始网络通信原理学习
3.网络通信原理
计算机与计算机之间要有统一连接标准才能够完成相互通信,这个标准被称为互联网协议,而网络就是物理链接介质+互联网协议。按照功能不同,人们将互联网协议从不同维度分为OSI七层、TCP/IP五层或者TCP/IP四层。

应用层(Application Layer)提供为应用软件而设的接口,以设置与另一应用软件之间的通信。例如: HTTP,HTTPS,FTP,TELNET,SSH,SMTP,POP3等。
表示层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式。
会话层(Session Layer)负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接。
传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。例如:传输控制协议(TCP,UDP)等。
网络层(Network Layer)决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络数据。例如:互联网协议(IP)等。
数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成帧。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。例如以太网、无线局域网(Wi-Fi)和通用分组无线服务(GPRS)等。
分为两个子层:逻辑链路控制(logic link control,LLC)子层和介质访问控制(media access control,MAC)子层。
物理层(Physical Layer):主要功能为定义了网络的物理结构,传输的电磁标准,Bit流的编码及网络的时间原则,如分时复用及分频复用。决定了网络连接类型(端到端或多端连接)及物理拓扑结构。说的通俗一些,这一层主要负责实际的信号传输。

4.什么是I/O?
我们都知道在UNIX世界一切皆文件,而文件是什么呢?文件就是一串二进制流而已,其中不管是Socket,队列,管道,终端。对于计算机来说一切都是文件,一切都是流。在信息交换过程中,计算机都是对这些流进行数据的收发操作,简称I/O操作(Input and Output).
我们的程序I/O交互流程分为两大块:
用户空间和内核空间

5.I/O通信模型
1. 同步阻塞IO
主要体现在两个阻塞点
-
服务端接收客户端连接时的阻塞。
-
客户端和服务端的IO通信时,数据未就绪的情况下的阻塞。

2. 非阻塞IO
从前面的分析发现,服务端在处理一次请求时,会处于阻塞状态无法处理后续请求,那是否能够让被阻塞的地方优化成不阻塞呢?于是就有了非阻塞IO(NIO)
非阻塞IO,就是客户端向服务端发起请求时,如果服务端的数据未就绪的情况下, 客户端请求不会被阻塞,而是直接返回。但是有可能服务端的数据还未准备好的时候,客户端收到的返回是一个空的, 那客户端怎么拿到最终的数据呢?
如图所示,客户端只能通过轮询的方式来获得请求结果。NIO相比BIO来说,少了阻塞的过程在性能和连接数上都会有明显提高。

3. I/O多路复用
I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。
常见的IO多路复用方式有【select、poll、epoll】,都是Linux API提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型
-
select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
-
由于他能够同时监听多个文件描述符,假如说有1000个,这个时候如果其中一个fd 处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
-
同时,select在单个进程中能打开的fd是有限制的,默认是1024,对于那些需要支持单机上万的TCP连接来说确实有点少
-
-
epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于1024
【由于epoll能够通过事件告知应用进程哪个fd是可读的,所以我们也称这种IO为异步非阻塞IO,当然它是伪异步的,因为它还需要去把数据从内核同步复制到用户空间中,真正的异步非阻塞,应该是数据已经完全准备好了,我只需要从用户空间读就行】
I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图下图。
客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。

4.异步IO
异步IO和多路复用机制,最大的区别在于:当数据就绪后,客户端不需要发送内核指令从内核空间读取数据,而是系统会异步把这个数据直接拷贝到用户空间,应用程序只需要直接使用该数据即可。

异步IO
所以Netty出现了,Netty的I/O模型是基于非阻塞IO实现的,底层依赖的是JDK NIO框架的多路复用器Selector来实现。
一个多路复用器Selector可以同时轮询多个Channel,采用epoll模式后,只需要一个线程负责Selector的轮询,就可以接入成千上万个客户端连接。
5.Reactior模型
http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf
了解了NIO多路复用后,就有必要再和大家说一下Reactor多路复用高性能I/O设计模式,Reactor本质上就是基于NIO多路复用机制提出的一个高性能IO设计模式,它的核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理。
Reactor模型有三个重要的组件:
-
Reactor :将I/O事件发派给对应的Handler
-
Acceptor :处理客户端连接请求
-
Handlers :执行非阻塞读/写

Reactor模型
这是最基本的单Reactor单线程模型(整体的I/O操作是由同一个线程完成的)。
其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。
Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于racotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,有Reactor分发)
Reactor 模式本质上指的是使用
I/O 多路复用(I/O multiplexing) + 非阻塞 I/O(non-blocking I/O)的模式。
6.多线程单Reactot模型
单线程Reactor这种实现方式有存在着缺点,从实例代码中可以看出,handler的执行是串行的,如果其中一个handler处理线程阻塞将导致其他的业务处理阻塞。由于handler和reactor在同一个线程中的执行,这也将导致新的无法接收新的请求,我们做一个小实验:
-
在上述Reactor代码的DispatchHandler的run方法中,增加一个Thread.sleep()。
-
打开多个客户端窗口连接到Reactor Server端,其中一个窗口发送一个信息后被阻塞,另外一个窗口再发信息时由于前面的请求阻塞导致后续请求无法被处理。
为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将reactor和handler在不同的线程来执行

7.多线程多Reactor模型
在多线程单Reactor模型中,我们发现所有的I/O操作是由一个Reactor来完成,而Reactor运行在单个线程中,它需要处理包括Accept()/read()/write/connect操作,对于小容量的场景,影响不大。但是对于高负载、大并发或大数据量的应用场景时,容易成为瓶颈,主要原因如下:
-
一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
-
当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
所以,我们还可以更进一步优化,引入多Reactor多线程模式,如图所示,Main Reactor负责接收客户端的连接请求,然后把接收到的请求传递给SubReactor(其中subReactor可以有多个),具体的业务IO处理由SubReactor完成。
Multiple Reactors 模式通常也可以等同于 Master-Workers 模式,比如 Nginx 和 Memcached 等就是采用这种多线程模型,虽然不同的项目实现细节略有区别,但总体来说模式是一致的。

-
Acceptor,请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。
-
Main Reactor,主 Reactor 线程组,主要负责连接事件,并将IO读写请求转发到 SubReactor 线程池。
-
Sub Reactor,Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通道的读(OP_READ)、写事件(OP_WRITE)。
6.Netty的基本使用
需要说明一下,我们讲解的Netty版本是4.x版本,之前有一段时间netty发布了一个5.x版本,但是被官方舍弃了,原因是:使用ForkJoinPool增加了复杂性,并且没有显示出明显的性能优势。同时保持所有的分支同步是相当多的工作,没有必要。
添加jar包依赖
使用4.1.66版本
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
创建Netty Server服务
大部分场景中,我们使用的主从多线程Reactor模型,Boss线程是主Reactor,Worker是从Reactor。他们分别使用不同的NioEventLoopGroup
主Reactor负责处理Accept,然后把Channel注册到从Reactor,从Reactor主要负责Channel生命周期内的所有I/O事件。
public class NettyBasicServerExample {
public void bind(int port){
// 我们要创建两个EventLoopGroup,
// 一个是boss专门用来接收连接,可以理解为处理accept事件,
// 另一个是worker,可以关注除了accept之外的其它事件,处理子任务。
//上面注意,boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景,
// worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。
EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//服务端要启动,需要创建ServerBootStrap,
// 在这里面netty把nio的模板式的代码都给封装好了
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup) //配置boss和worker线程
//配置Server的通道,相当于NIO中的ServerSocketChannel
.channel(NioServerSocketChannel.class)
//childHandler表示给worker那些线程配置了一个处理器,
// 配置初始化channel,也就是给worker线程配置对应的handler,当收到客户端的请求时,分配给指定的handler处理
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NormalMessageHandler()); //添加handler,也就是具体的IO事件处理器
}
});
//由于默认情况下是NIO异步非阻塞,所以绑定端口后,通过sync()方法阻塞直到连接建立
//绑定端口并同步等待客户端连接(sync方法会阻塞,直到整个启动过程完成)
ChannelFuture channelFuture=bootstrap.bind(port).sync();
System.out.println("Netty Server Started,Listening on :"+port);
//等待服务端监听端口关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放线程资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
new NettyBasicServerExample().bind(8080);
}
}
创建Netty client服务
public class ClientNetty {
// 要请求的服务器的ip地址
private String ip;
// 服务器的端口
private int port;
public ClientNetty(String ip, int port){
this.ip = ip;
this.port = port;
}
// 请求端主题
private void action() throws InterruptedException, UnsupportedEncodingException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
Bootstrap bs = new Bootstrap();
bs.group(bossGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 序列化对象的解码
// socketChannel.pipeline().addLast(MarshallingCodefactory.buildDecoder());
// 序列化对象的编码
// socketChannel.pipeline().addLast(MarshallingCodefactory.buildEncoder());
// 处理来自服务端的响应
socketChannel.pipeline().addLast(new ClientHandler());
}
});
// 客户端
ChannelFuture cf = bs.connect(ip, port).sync();
String reqStr = "我是客户端请求!";
// 发送客户端的请求了
cf.channel().writeAndFlush(Unpooled.copiedBuffer(reqStr.getBytes(Constant.charset)));
// Thread.sleep(3000);
// cf.channel().writeAndFlush(Unpooled.copiedBuffer("我是客户端请求!!".getBytes(Constant.charset)));
// Thread.sleep(3000);
// cf.channel().writeAndFlush(Unpooled.copiedBuffer("我是客户端请求!!!".getBytes(Constant.charset)));
// Student student = new Student();
// student.setId(1);
// student.setName("李四");
// cf.channel().writeAndFlush(student);
// 等待直到连接中断了
cf.channel().closeFuture().sync();
}
public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException {
new ClientNetty("127.0.0.1", Constant.serverSocketPort).action();
}
}
public class ClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
ByteBuf bb = (ByteBuf)msg;
byte[] respByte = new byte[bb.readableBytes()];
bb.readBytes(respByte);
String respStr = new String(respByte, Constant.charset);
System.err.println("client--收到响应:" + respStr);
// 直接转成对象
// handlerObject(ctx, msg);
} finally{
// 必须释放msg数据
ReferenceCountUtil.release(msg);
}
}
private void handlerObject(ChannelHandlerContext ctx, Object msg) {
Student student = (Student)msg;
System.err.println("server 获取信息:"+student.getId()+student.getName());
}
// 数据读取完毕的处理
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.err.println("客户端读取数据完毕");
}
// 出现异常的处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.err.println("client 读取数据出现异常");
ctx.close();
}
}
(完 ^_^)
引用文献:
本文介绍了新入职员工如何通过学习Netty来提升时间管理和技能,包括Netty的定义、为何受欢迎,网络通信原理,I/O与多路复用模型(同步阻塞、非阻塞、I/O多路复用和异步IO),以及Netty在多线程Reactor模式中的应用。重点讲解了Netty的使用,如服务器和客户端的创建,以及不同模型在性能优化中的角色。
1万+

被折叠的 条评论
为什么被折叠?



