1. 背景
1.1 基础知识
1.1.1 BIO 与 IO 多路复用
- 同步阻塞 BIO(图例为多线程模式对应的 Acceptor 模型)
IO 多路复用
- select、poll、epoll 详见
Socket 与 TCP/IP 协议模型的关系
- Socket 通信原理(左服务器端,右客户端)
2. 发展过程
2.1 传统 Apache 服务器
- 传统服务器根据编码解码协议不同可分为不同类型,如: 协议是 redis ,则为 redis 服务器
- 传统的 HTTP 服务器原理 :
- 创建一个 ServerSocket,监听并绑定一个端口;
- 一系列客户端来请求这个端口;
- 服务器使用 Accept,获得一个来自客户端的Socket连接对象;
- 启动一个新线程处理连接:
- 读Socket,得到字节流;
- 解码协议,得到Http请求对象 ;
- 处理Http请求,得到一个结果,封装成一个HttpResponse对象 ;
- 编码协议,将结果序列化字节流 写Socket,将字节流发给客户端 ;
- 继续循环步骤 3
- 优点:具有很高的响应速度,控制简单;
- 缺点:传统的多线程服务器为 BlockingIO 模式,占用操作系统的调度资源,且连接数较多会造成资源浪费;
-
产生背景
- 高并发环境下,线程数量可能会创建太多,操作系统的任务调度压力大,系统负载也会比较高;
特点:同步非阻塞,使用一个线程把Accept、读写操作、请求处理的逻辑全部实现,伪码如下:
while true {
events = takeEvents(fds) // 获取事件,如果没有事件,线程就休眠
for event in events {
if event.isAcceptable {
doAccept() // 新链接来了
} elif event.isReadable {
request = doRead() // 读消息
if request.isComplete() {
doProcess()
}
} elif event.isWriteable {
doWrite() // 写消息
}
}
}
优点
- 充分利用多核
- 当 IO 阻塞系统,但是 CPU 空闲时,可利用多线程使用 CPU 资源;
缺点
- Reactor 多线程模型,在处理链部分采用多线程(线程池),后端程序常用模型
- Reactor 主从模型,多个 Acceptor 的 NIO 线程池用于接受客户端的连接;
2.2.3 buffer 与 channel
- Channel 是 NIO 中的数据通道,与流类似,区别在于 Channel 既可从中读取数据,又可以从写数据到通道中,但是流的读写通常是单向的;
- Channel 可以异步的读写;
- Channel 类型包括:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel;
Channel 中的数据总是要先读到一个 Buffer 中,或者从缓冲区中将数据写到通道中;
2. Netty
2.1 概要介绍
定义
- Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架;
- 本质:基于原生Java NIO技术封装的一套处理 socket 远程通信的框架,是用 JBoss 做的一个Jar包,有完整的 ioc 容器支持;
- 优点
- 框架设计优雅,底层模型随意切换适应不同的网络协议要求;
- 提供很多标准的协议、安全、编码解码的支持;
- 解决了很多 NIO 不易用的问题;
- 社区更为活跃,在很多开源框架中使用,如 Dubbo、RocketMQ、Spark 等;
- 支持自定义协议服务器开发;
- 作为异步高性能的通信框架,往往作为基础通信组件被其他 RPC 框架使用;
- 支持的功能及特性
- 设计统一的API,适用于不同的协议(阻塞和非阻塞),基于灵活、可扩展的事件驱动模型(三种 Reactor 模型)高度可定制的线程模型可靠的无连接数据Socket支持(UDP);
- 性能更好的吞吐量,低延迟更省资源尽量减少不必要的内存拷贝;
- 安全完整的 SSL/TLS和STARTTLS 的支持,能在 Applet 与 Android 的限制环境运行良好;
- 健壮性,不再因过快、过慢或超负载连接导致 OutOfMemoryError ,不再有在高速网络环境下 NIO 读写频率不一致的问题;
- 易用完善的 JavaDoc,用户指南和样例简洁简单仅信赖于 JDK1.5;
- 实现原理
- 在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理。Accept连接和读写操作也可以使用同一个线程池来进行处理。而请求处理逻辑既可以使用单独的线程池进行处理,也可以跟放在读写线程一块处理。线程池中的每一个线程都是NIO线程。用户可以根据实际情况进行组装,构造出满足系统需求的并发模型。
- 应用
- 阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。它的架构图如下:
- 淘宝的消息中间件 RocketMQ 的消息生产者和消息消费者之间,也采用 Netty 进行高性能、异步通信;
- 经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨节点通信,它的 Netty Service 基于 Netty 框架二次封装实现;
2.2 具体实现
2.2.1 主要的组件:
- Bootstrap:netty的组件容器,用于把其他各个部分连接起来;如果是TCP的Server端,则为ServerBootstrap.
- Channel:代表一个Socket的连接
- EventLoopGroup:一个Group包含多个EventLoop,可以理解为线程池
- EventLoop:处理具体的Channel,一个EventLoop可以处理多个Channel
- ChannelPipeline:每个Channel绑定一个pipeline,在上面注册处理逻辑handler
- Handler:具体的对消息或连接的处理,有两种类型,Inbound和Outbound。分别代表消息接收的处理和消息发送的处理,当接收消息的时候,会从链表的表头开始遍历,如果是inbound就调用对应的方法;如果发送消息则从链表的尾巴开始遍历;
-
2.2.2 解决TCP 粘包与拆包
产生原因:ByteBuf 为 单位来发送数据,server按照Bytebuf读取,但是到了底层操作系统仍然是按照字节流发送数据,因此,数据到了服务端,也是按照字节流的方式读入,然后到了 Netty 应用层面,重新拼装成 ByteBuf,而这里的 ByteBuf 与客户端按顺序发送的 ByteBuf 可能是不对等的
解决:
2.2.3 netty 零拷贝
传统意义的拷贝
是在发送数据的时候,传统的实现方式是:
1. File.read(bytes)
2. Socket.send(bytes)
这种方式需要四次数据拷贝和四次上下文切换:
1. 数据从磁盘读取到内核的read buffer
2. 数据从内核缓冲区拷贝到用户缓冲区
3. 数据从用户缓冲区拷贝到内核的socket buffer
4. 数据从内核的socket buffer拷贝到网卡接口(硬件)的缓冲区
零拷贝的概念
明显上面的第二步和第三步是没有必要的,通过java的FileChannel.transferTo方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)
1. 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
2. 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上面的两次操作都不需要CPU参与,所以就达到了零拷贝。
Netty中的零拷贝
主要体现在三个方面:
1、bytebuffer
Netty发送和接收消息主要使用bytebuffer,bytebuffer使用对外内存(DirectMemory)直接进行Socket读写。
原因:如果使用传统的堆内存进行Socket读写,JVM会将堆内存buffer拷贝一份到直接内存中然后再写入socket,多了一次缓冲区的内存拷贝。DirectMemory中可以直接通过DMA发送到网卡接口
2、Composite Buffers
传统的ByteBuffer,如果需要将两个ByteBuffer中的数据组合到一起,我们需要首先创建一个size=size1+size2大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用Netty提供的组合ByteBuf,就可以避免这样的操作,因为CompositeByteBuf并没有真正将多个Buffer组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。
3、对于FileChannel.transferTo的使用
Netty中使用了FileChannel的transferTo方法,该方法依赖于操作系统实现零拷贝。
2.2.4 Netty 内部执行流程
服务端:
1、创建ServerBootStrap实例
2、设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
3、设置并绑定服务端的channel
4、5、创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证
6、绑定并启动监听端口
7、当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler
客户端