UNIX网络编程5种I/O模型
-
I/O 复用模型(最大的优势是多路复用)
Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback
-
I/O 多路复用技术
- I/O 多路复用技术通过把多个I/O 的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求
-
目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在 Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll
-
支持一个进程打开的socket描述符(FD ) 不受限制(仅受限于操作系统的最大文
件句柄数)
- select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设
置,默认值是1024,选择修改这个宏需要重新编译内核且网络效率会下降
- cat /proc/sys/fs/file- max
-
I/O 效率不会随着FD数目的增加而线性下降
- 由于网络延时或者链路空闲,任一时刻只有少部分的socket是 “活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的socket进行操作
-
使用mmap加速内核与用户空间的消息传递
- 无论是select、poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必
要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存来实现的
- mmap-map files or devices into memory
- epoll的API更加简单
- 用来克服select/poll缺点的方法不只有epoll, epoll只是一种Linux的实现方案。在 freeBSD下有kqueue
-
从5种I/O模型来看,其实都涉及到两个阶段
- 等待数据准备就绪
-
数据从内核复制到用户空间
- 对于阻塞io,调用recvfrom,阻塞直到第二个阶段完成或者错误才返回
- 对于非阻塞io,调用recvfrom,如果缓冲区没有数据则直接返回错误,一般都对非阻塞I/O 模型进行轮询检査这个状态,看内核是不是有数据到来;数据准备后,第二个阶段也是阻塞的
-
对于I/O复用模型,第一个阶段进程阻塞在select调用,等待1个或多个套接字(多路)变为可读,而第二个阶段是阻塞的
- 这里进程是被select阻塞但不是被socket io阻塞
-
java nio实现
- 是否阻塞configureBlocking(boolean block)
- selector事件到来时(只是判断是否可读/可写)->具体的读写还是由阻塞和非阻塞决定->如阻塞模式下,如果输入流不足r字节则进入阻塞状态,而非阻塞模式下则奉行能读到多少就读到多少的原则->立即返回->
- 同理写也是一样->selector只是通知可写->但是能写多少数据也是有阻塞和非阻塞决定->如阻塞模式->如果底层网络的输出缓冲区不能容纳r个字节则会进入阻塞状态->而非阻塞模式下->奉行能输出多少就输出多少的原则->立即返回
- 对于accept->阻塞模式->没有client连接时,线程会一直阻塞下去->而非阻塞时->没有客户端连接->方法立刻返回null->
- 对于信号驱动I/O模型,应用进程建立SIGIO信号处理程序后立即返回,非阻塞,数据准备就绪时,生成SIGIO信号并通过信号回调应用程序通过recvfrom来读取数据,第二个阶段也是阻塞的
- 而对于异步I/O模型来说,第二个阶段的时候内核已经通知我们数据复制完成了
-
Java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现
-
Enhancements in JDK 6 Release
-
A new java.nio.channels.SelectorProvider implementation that is based on the Linux epoll event notification facility is included. The epoll facility is available in the Linux 2.6, and newer, kernels. The new epoll-based SelectorProvider implementation is more scalable than the traditional poll-based SelectorProvider implementation when there are thousands of SelectableChannels registered with a Selector. The new SelectorProvider implementation will be used by default when the 2.6 kernel is detected. The poll-based SelectorProvider will be used when a pre-2.6 kernel is detected.
- 即JDK6版本中默认的SelectorProvider即为epoll(Linux 2.6 kernal)
- macosx-sun.nio.ch.KQueueSelectorProvider
- solaris-sun.nio.ch.DevPollSelectorProvider
-
linux
- 2.6以上版本-sun.nio.ch.EPollSelectorProvider
- 以下版本-sun.nio.ch.PollSelectorProvider
- windows-sun.nio.ch.WindowsSelectorProvider
-
Oracle jdk会自动选择合适的Selector,如果想设置特定的Selector
- -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
-
Netty Native transports
- Since 4.0.16, Netty provides the native socket transport for Linux using JNI. This transport has higher performance and produces less garbage
-
Netty's epoll transport uses epoll edge-triggered while java's nio library uses level-triggered. Beside this the epoll transport expose configuration options that are not present with java's nio like TCPCORK, SOREUSEADDR and more.
- 即Netty的Linux原生传输层使用了epoll边缘触发
- 而jdk的nio类库使用的是epoll水平触发
-
epoll ET(Edge Triggered) vs LT(Level Triggered)
- 简单来说就是当边缘触发时,只有 fd 变成可读或可写的那一瞬间才会返回事件。当水平触发时,只要 fd 可读或可写,一直都会返回事件
- 简单地说,如果你有数据过来了,不去取LT会一直骚扰你,提醒你去取,而ET就告诉你一次,爱取不取,除非有新数据到来,否则不再提醒
- Nginx大部分event采用epoll EPOLLET(边沿触发)的方法来触发事件,只有listen端口的读事件是EPOLLLT(水平触发).对于边沿触发,如果出现了可读事件,必须及时处理,否则可能会出现读事件不再触发,连接饿死的情况
-
Java7 NIO2
-
implemented using IOCP on Windows
- WindowsAsynchronousSocketChannelImpl implements Iocp.OverlappedChannel
- 即nio2在windows的底层实现是iocp
-
Linux using epoll
- UnixAsynchronousSocketChannelImpl implements Port.PollableChannel
-
即nio2在linux 2.6后的底层实现还是epoll
- 通过epoll模拟异步
-
个人认为也许linux内核本身的aio实现方案其实并不是很完善,或多或少有这样或者那样的问题,即使用了aio,也没有明显的性能优势
- Not faster than NIO (epoll) on unix systems (which is true)
-
Reactor/Proactor
- 两种IO设计模式
-
Reactor-Dispatcher/Notifier
- Don't call us, we'll call you
- Proactor-异步io
- Reactor通过某种变形,可以将其改装为Proactor,在某些不支持异步I/O的系统上,也可以隐藏底层的实现,利于编写跨平台代码
-
参考
Java I/O类库的发展和改进
-
BIO(blocking)
-
采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁
- 该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1: 1 的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者值死,不能对外提供服务
- ServerSocket/Socket/输入输出流(阻塞)
-
伪异步I/O
- 为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型
- 采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架
-
当有新的客户端接入时,将客户端的Socket封装成一个Task,投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理
- 当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s 才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s, 在此期间,其他接入消息只能在消息队列中排队
- 当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size( 滑动窗口)不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息
-
NIO
- SocketChannel和ServerSocketChannel,支持阻塞和非阻塞两种模式
-
Buffer/Channel/Selector(多路复用器,可同时轮询多个Channel)
-
java.nio.ByteBuffer的几个常用方法
- flip、clear、compact、mark、rewind、hasRemaining、isDirect等
- 客户端发起的连接操作是异步的
- SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O 通信线程就可以处理其他的链路,不需要同步等待这个链路可用
- JDK的 Selector在 Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制
-
AIO
- AsynchronousServerSocketChannel、AsynchronousSocketChannel
-
CompletionHandler<V,A>
- V The result type of the I/O operation
- A The type of the object attached to the I/O operation
- 既然已经接收客户端成功了,为什么还要再次调用accept方法呢?原因是这样的:调用AsynchronousServerSocketChannel的accept方法后,如果有新的客户端连接接入,系统将回调我们传入的CompletionHandler实例的completed方法,表示新的客户端已经接入成功。因为一个AsynchronousServerSocketChannel可以接收成千上万个客户端,所以需要继续调用它的accept方法,接收其他的客户端连接,最终形成一个循环。每当接收一个客户读连接成功之后,再异步接收新的客户端连接
-
不选择Java原生NIO编程的原因
- N10的类库和API繁杂,使用麻烦
- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程
- 可靠性能力补齐,工作景和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题
- JDK NIO 的 BUG, 例如臭名昭著的epollbug, 它会导致Selector空轮询,最终导致CPU100%
-
为什么选择Netty
- 健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证
- API使用简单
- 预置了多种编解码功能,支持多种主流协议
- 可以通过ChannelHand丨er对通信框架进行灵活地扩展
- 性能高
- Netty修复了己经发现的所有JDKNIO BUG
- 社区活跃,版本迭代周期短
- 经历了大规模的商业应用考验,质量得到验证
Netty 入门
- ServerBootstrap、EventLoopGroup(boss)、EventLoopGroup(worker)、NioServerSocketChannel、ChannelOption、ChannelInitializer、ChannelPipeline、ChannelFuture、ChannelHandlerAdapter、ChannelHandlerContext
- Bootstrap、NioSocketChannel
- try/finally、shutdownGracefully(boss、worker)
- ChannelHandlerContext的 flush方法,它的作用是将消息发送队列中的消息写入SocketChannel中发送给对方.从性能角度考虑,为了防止频繁地唤醒Selector进行消息发送,Netty的 write方法并不直接将消息写入SocketChannel中,调用write方法只是把待发送的消息放到发送缓冲数组中,再通过调用flush方法,将发送缓冲区中的消息全部写到SocketChannel中
- 基于Netty开发的都是非Web的Java应用,它的打包形态非常简单,就是一个普通的.jar 包,通常可以使用Eclipse、Ant、Ivy、Gradle等进行构建
TCP 粘包/拆包问题的解决之道
- TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为 , 一个完 整的包可能会被 TCP拆分成多个包进行发送,也有可能把多个小的包封装成个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
-
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决
- 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
- 在包尾增加回车换行符进行分割,例如FTP协议
- 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度
- 没有考虑读半包问题,这在功能测试时往往没有问题,但是一旦压力上来,或者发送大报文之后,就会存在粘包/拆包问题,如循环发送100条消息,则可能会出现TCP粘包
-
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包
-
LineBasedFrameDecoder
- A decoder that splits the received {@link ByteBuf}s on line endings
-
StringDecoder
- A decoder that splits the received {@link ByteBuf}s on line endings
分隔符和定长解码器的应用
-
DelimiterBasedFrameDecoder
- A decoder that splits the received {@link ByteBuf}s by one or more delimiters
-
FixedLengthFrameDecoder
- A decoder that splits the received {@link ByteBuf}s by the fixed number of bytes
编解码技术
-
基于Java提供的对象输入/输出流ObjectlnputStream和 ObjectOutputStream,可以直接把Java对象作为可存储的字节数组写入文件 ,也可以传输到网络上,Java序列化的目的:
-
Java序列化的缺点
- 无法跨语言
-
序列化后的码流太大
-
对于字符串
- byte[] value = this.userName.getBytes();
- buffer.putInt(value.length);
- buffer.put(value);
- 序列化性能太低
-
业界主流的编解码框架
- Google的Protobuf
- Facebook的Thrift
-
JBoss Marshalling
- JBoss Marshalling是一个Java对象的序列化API包,修正了 JDK自带的序列化包的很多问题,但又保持跟java.io.Serializable接口的兼容
MessagePack编解码
-
MessagePack介绍
- It's like JSON. but fast and small
- MessagePack is an efficient binary serialization format. It lets you exchange data among multiple languages like JSON. But it's faster and smaller
- http://msgpack.org/
- 提供了对多语言的支持
-
API介绍
// Create serialize objects.
List<String> src = new ArrayList<String>();
src. add (,,msgpackw);
src.add("kumofs");
src.add("viver">;
MessagePack msgpack = new MessagePack();
// Serialize
byte[] raw = msgpack.write(src);
// Deserialize directly using a template
L±8t<String> dstl = msgpack. read (raw, Ten 5 >lates . tList (Ten^>lates. TString));
-
MessagePack编码器和解码器开发
- MessageToByteEncoder<I
- ByteToMessageDecoder、MessageToMessageDecoder<I
-
LengthFieldBasedFrameDecoder extends ByteToMessageDecoder
- A decoder that splits the received {@link ByteBuf}s dynamically by the value of the length field in the message
- public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip, boolean failFast)
- 功能很强大,可指定消息长度字段的偏移等,而不仅仅是消息头的第一个字段就是长度
-
LengthFieldPrepender extends MessageToMessageEncoder
- An encoder that prepends the length of the message.
- 可自动前面加上消息长度字段
Google Protobuf 编解码
-
主要使用了netty默认提供的关于protobuf的编解码器
-
ProtobufVarint32FrameDecoder extends ByteToMessageDecoder
- A decoder that splits the received {@link ByteBuf}s dynamically by the value of the Google Protocol Buffers
-
ProtobufVarint32LengthFieldPrepender extends MessageToByteEncoder
- An encoder that prepends the the Google Protocol Buffers
-
ProtobufEncoder extends MessageToMessageEncoder
- Encodes the requested Google Protocol Buffers Message And MessageLite into a {@link ByteBuf}
-
ProtobufDecoder extends MessageToMessageDecoder
- Decodes a received {@link ByteBuf} into a Google Protocol Buffers Message And MessageLite
- 注意其构造函数要传一个MessageLite对象,即协议类型,用来反序列化
BEFORE DECODE (302 bytes) AFTER DECODE (300 bytes)
+--------+---------------+ +---------------+
| Length | Protobuf Data |----->| Protobuf Data |
| 0xAC02 | (300 bytes) | | (300 bytes) |
+--------+---------------+ +---------------+
BEFORE ENCODE (300 bytes) AFTER ENCODE (302 bytes)
+---------------+ +--------+---------------+
| Protobuf Data |-------------->| Length | Protobuf Data |
| (300 bytes) | | 0xAC02 | (300 bytes) |
+---------------+ +--------+---------------+
-
Protobuf的使用注意事项
-
ProtobufDecoder仅仅负责解码,它不支持读半包。因此,在 ProtobufDecoder前面,
一定要有能够处理读半包的解码器
- 使用Netty提供的ProtobufVarint32FrameDecoder,它可以处理半包消息
- 继承Netty提供的通用半包解码器LengthFieldBasedFrameDecoder
- 继承ByteToMessageDecoder类,自己处理半包消息
JBoss Marshalling 编解码
- JBoss的Marshalling完全兼容JDK序列化
-
MarshallingDecoder extends LengthFieldBasedFrameDecoder
- Decoder which MUST be used with {@link MarshallingEncoder}
- 需要传入UnmarshallerProvider和maxObjectSize
-
MarshallingEncoder extends MessageToByteEncoder
posted on 2017-01-19 22:00
landon 阅读(3346)
评论(0) 编辑 收藏 所属分类:
Book