摘自:killme2008的pdf
IO划分为两个阶段:
1 等待数据就绪
2 从内核缓冲区copy到进程缓冲区(从socket通过socketChannel复制到ByteBuffer)
non-direct ByteBuffer: HeapByteBuffer,创建开销小
direct ByteBuffer:通过操作系统native代码,创建开销大
基于block的传输通常比基于流的传输更高效
使用NIO做网络编程容易,但离散的事件驱动模型编程困难,而且陷阱重重
Reactor模式:经典的NIO网络框架
核心组件:
1 Synchronous Event Demultiplexer : Event loop + 事件分离
2 Dispatcher:事件派发,可以多线程
3 Request Handler:事件处理,业务代码
理想的NIO框架:
1 优雅地隔离IO代码和业务代码
2 易于扩展
3 易于配置,包括框架自身参数和协议参数
4 提供良好的codec框架,方便marshall/unmarshall
5 透明性,内置良好的日志记录和数据统计
6 高性能
NIO框架性能的关键因素
1 数据的copy
2 上下文切换(context switch)
3 内存管理
4 TCP选项,高级IO函数
5 框架设计
减少数据copy:
ByteBuffer的选择
View ByteBuffer
FileChannel.transferTo/transferFrom
FileChannel.map/MappedByteBuffer
ByteBuffer的选择:
不知道用哪种buffer时,用Non-Direct
没有参与IO操作,用Non-Direct
中小规模应用(<1K并发连接),用Non-Direct
长生命周期,较大的缓冲区,用Direct
测试证明Direct比Non-Direct更快,用Direct
进程间数据共享(JNI),用Direct
一个Buffer发给多个Client,考虑使用view ByteBuffer共享数据,buffer.slice()
HeapByteBuffer缓存
使用ByteBuffer.slice()创建view ByteBuffer:
ByteBuffer buffer2 = buffer1.slice();
则buffer2的内容和buffer1的从position到limit的数据内容完全共享
但是buffer2的position,limit是独立于buffer1的
传输文件的传统方式:
byte[] buf = new byte[8192];
while(in.read(buf)>0){
out.write(buf);
}
使用NIO后:
FileChannel in = ...
WriteableByteChannel out = ...
in.transferTo(0,fsize,out);
性能会有60%的提升
FileChannel.map
将文件映射为内存区域——MappedByteBuffer
提供快速的文件随机读写能力
平台相关
适合大文件,只读型操作,如大文件的MD5校验等
没有unmap方法,什么时候被回收取决于GC
减少上下文切换
时间缓存
Selector.wakeup
提高IO读写效率
线程模型
时间缓存:
1网络服务器通常需要频繁获取系统时间:定时器,协议时间戳,缓存过期等
2 System.currentTimeMillis
a linux调用gettimeofday需要切换到内核态
b 普通机器上,1000万次调用需要12秒,平均一次1.3毫秒
c 大部分应用不需要特别高的精度
3 SystemTimer.currentTimeMillis(自己创建)
a 独立线程定期更新时间缓存
b currentTimeMillis直接返回缓存值
c 精度取决于定期间隔
d 1000万次调用降低到59毫秒
Selector.wakeup() 主要作用:
解除阻塞在Selector.select()上的线程,立即返回
两次成功的select()之间多次调用wakeup等价于一次调用
如果当前没有阻塞在select()上,则本次wakeup将作用在下次select()上
什么时候wakeup() ?
注册了新的Channel或者事件
Channel关闭,取消注册
优先级更高的事件触发(如定时器事件),希望及时处理
wakeup的原理:
1 linux上利用pipe调用创建一个管道
2 windows上是一个loopback的tcp连接,因为win32的管道无法加入select的fd set
3 将管道或者tcp连接加入selected fd set
4 wakeup向管道或者连接写入一个字节
5 阻塞的select()因为有IO时间就绪,立即返回
可见wakeup的调用开销不可忽视
减少wakeup调用:
1 仅在有需要时才调用。如往连接发送数据,通常是缓存在一个消息队列,当且仅当队列为空时注册write并wakeup
booleanneedsWakeup=false;
synchronized(queue){
if(queue.isEmpty()) needsWakeup=true;
queue.add(session);
}
if(needsWakeup){
registerOPWrite();
selector.wakeup();
}
2 记录调用状态,避免重复调用,例如Netty的优化
读到或者写入0个字节:
不代表连接关闭
高负载或者慢速网络下很常见的情况
通常的处理方法是返回并继续注册read/write,等待下次处理,缺点是系统调用开销和线程切换开销
其他解决办法:循环一定次数写入(如Mina)或者yield一定次数
启用临时选择器Temporary Selector在当前线程注册并poll,例如Girzzy中
在当前线程写入:
当发送缓冲队列为空的时候,可以直接往channel写数据,而不是放入缓冲队列,interest了write等待IO线程写入,可以提高发送效率
优点是可以减少系统调用和线程切换
缺点是当前线程中断会引起channel关闭
线程模型
selector的三个主要事件:read,write,accept,都可以运行在不同的线程上
通常Reactor实现为一个线程,内部维护一个selector
1 Boss Thread + worker Thread
boss thread处理accept,connect
worker thread处理read,write
Reactor线程数目:
1 Netty 1 + 2 * cpu
2 Mina 1 + cpu + 1
3 Grizzly 1 + 1
常见线程模型:
1 read和accept都运行在reactor线程上
2 accept运行在reactor线程上,read运行在单独线程
3 read和accept都运行在单独线程
4 read运行在reactor线程上,accept运行在单独线程
选择适当的线程模型:
类echo应用,unmashall和业务处理的开销非常低,选择模型1
模型2,模型3,模型4的accept处理开销很低
最佳选择:模型2。unmashall一般是cpu-bound,而业务逻辑代码一般比较耗时,不要在reactor线程处理
内存管理
1 java能做的事情非常有限
2 缓冲区的管理
a 池化。ThreadLocal cache,环形缓冲区
b 扩展。putString,getString等高级API,缓冲区自动扩展和伸缩,处理不定长度字节
c 字节顺序。跨语言通讯需要注意,默认字节顺序Big-Endian,java的IO库和class文件
数据结构的选择
1 使用简单的数据结构:链表,队列,数组,散列表
2 使用j.u.c框架引入的并发集合类,lock-free,spin lock
3 任何数据结构都要注意容量限制,OutOfMemoryError
4 适当选择数据结构的初始容量,降低GC带来的影响
定时器的实现
1 定时器在网络程序中频繁使用
a 周期事件的触发
b 异步超时的通知和移除
c 延迟事件的触发
2 三个时间复杂度
a 插入定时器
b 删除定时器
c PerTickBookkeeping,一次tick内系统需要执行的操作
3 Tick的方式
Selector.select(timeout)
Thread.sleep(timeout)
定时器的实现:链表
将定时器组织成链表结构
插入定时器,加入链表尾部
删除定时器
插入定时器,找到合适的位置插入
删除定时器
插入定时器
删除定时器
指针按照一定周期旋转,一个tick跳动一个槽位
定时器根据延时时间和当前指针位置插入到特定槽位
连接IDLE的判断
1 连接处于IDLE状态:一段时间没有IO读写事件发生
2 实现方式:
a 每次IO读写都记录IO读和写的时间戳
b 定时扫描所有连接,判断当前时间和上一次读或写的时间差是否超过设定阀值,超过即认为连接处于IDLE状态,通知业务处理器
c 定时的方式:基于select(timeout)或者定时器。Mina:select(timeout);Netty:HashWheelTimer
合理设置TCP/IP选项,有时会起到显著效果,需要根据应用类型、协议设计、网络环境、OS平台等因素做考量,以测试结果为准
Socket缓冲区设置选项:SO_RCVBUF 和 SO_SNDBUF
Socket.setReceiveBufferSize/setSendBufferSize 仅仅是对底层平台的提示,是否有效取决于底层平台。因此get返回的不是真实的结果。
设置原则:
1 以太网上,4k通常是不够的,增加到16k,吞吐量增加了40%
2 Socket缓冲区大小至少应该是连接的MSS的三倍,MSS=MTU+40,一般以太网卡的MTU=1500字节。
MSS:最大分段大小
MTU:最大传输单元
3 send buffer最好与对端的receive buffer尺寸一致
4 对于一次性发送大量数据的应用,增加缓冲区到48k、64k可能是唯一最有效的提高性能的方式。
为了最大化性能,send buffer至少要跟BDP(带宽延迟乘积)一样大。
5 同样,对于大量接收数据的应用,提高接收缓冲区,能减少发送端的阻塞
6 如果应用既发送大量数据,又接收大量数据,则send buffer和receive buffer应该同时增加
7 如果设置的ServerSocket的receive buffer超过RFC1323定义的64k,那么必须在绑定端口前设置,以后accept产生的socket将继承这一设置
8 无论缓冲区大小多少,你都应该尽可能地帮助TCP至少以那样大小的块写入
BDP = 带宽 * RTT
Nagle算法:SO_TCPNODELAY
通过将缓冲区内的小包自动相连组成大包,阻止发送大量小包阻塞网络,提高网络应用效率对于实时性要求较高的应用(telnet、网游),需要关闭此算法
Socket.setTcpNoDelay(true) 关闭算法
SO_LINGER选项,控制socket关闭后的行为
Socket.setSoLinger(boolean linger,int timeout)
1 linger=false,timeout=-1
当socket主动close,调用的线程会马上返回,不会阻塞,然后进入CLOSING状态,残留在缓冲区中的数据将继续发送给对端,并且与对端进行FIN-ACK协议交换,最后进入TIME_WAIT状态
4 慎重使用此选项,TIME_WAIT状态的价值:
可靠实现TCP连接终止
允许老的分节在网络中流失,防止发给新的连接
持续时间=2*MSL(MSL为最大分节生命周期,一般为30秒到2分钟)
SO_REUSEADDR:重用端口
Socket.setReuseAddress(boolean) 默认false
适用场景:
1 当一个使用本地地址和端口的socket1处于TIME_WAIT状态时,你启动的socket2要占用该地址和端口,就要用到此选项
2 SO_REUSEADDR允许同一端口上启动一个服务的多个实例(多个进程),但每个实例绑定的地址是不能相同的
3 SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不适用TCP
SO_REUSEPORT
listen做四元组,多进程同一地址同一端口做accept,适合大量短连接的web server
Freebsd独有
其他选项:
Socket.setPerformancePreferences(connectionTime, latency, bandwidth) 设置连接时间、延迟、带宽的相对重要性
Socket.setKeepAlive(boolean) 这是TCP层的keep-alive概念,非HTTP协议的。用于TCP连接保活,默认间隔2小时,建议在应用层做心跳
Socket.sendUrgentData(data) 带外数据
技巧:
1 读写公平
Mina限制一次写入的字节数不超过最大的读缓冲区的1.5倍
2 针对FileChannel.transferTo的bug
Mina判断异常,如果是temporarily unavailable的IOException,则认为传输字节数为0
3 发送消息,通常是放入一个缓冲区队列注册write,等待IO线程去写
线程切换,系统调用
如果队列为空,直接在当前线程channel.write,隐患是当前线程的中断会引起连接关闭
4 事件处理优先级
ACE框架推荐:accept > write > read (推荐)
Mina 和 Netty:read > write
5 处理事件注册的顺序
在select()之前
在select()之后,处理wakeup竞争条件
Java Socket实现在不同平台上的差异
由于各种OS平台的socket实现不尽相同,都会影响到socket的实现
需要考虑性能和健壮性
1 定时器在网络程序中频繁使用
a 周期事件的触发
b 异步超时的通知和移除
c 延迟事件的触发
2 三个时间复杂度
a 插入定时器
b 删除定时器
c PerTickBookkeeping,一次tick内系统需要执行的操作
3 Tick的方式
Selector.select(timeout)
Thread.sleep(timeout)
定时器的实现:链表
将定时器组织成链表结构
插入定时器,加入链表尾部
删除定时器
PerTickBookkeeping,遍历链表查找expire事件
定时器的实现:排序链表
将定时器组织成有序链表结构,按照expire截止时间升序排序插入定时器,找到合适的位置插入
删除定时器
PerTickBookkeeping,直接从表头找起
定时器的实现:优先队列
将定时器组织成优先队列,按照expire截止时间作为优先级,优先队列一般采用最小堆实现插入定时器
删除定时器
PerTickBookkeeping,直接取root判断
定时器的实现:Hash wheel timer
将定时器组织成时间轮指针按照一定周期旋转,一个tick跳动一个槽位
定时器根据延时时间和当前指针位置插入到特定槽位
插入定时器
删除定时器
删除定时器
PerTickBookkeeping
槽位和tick决定了精度和延时
Hours Wheel,Minutes Wheel,Seconds Wheel定时器的实现:Hierarchical Timing
连接IDLE的判断
1 连接处于IDLE状态:一段时间没有IO读写事件发生
2 实现方式:
a 每次IO读写都记录IO读和写的时间戳
b 定时扫描所有连接,判断当前时间和上一次读或写的时间差是否超过设定阀值,超过即认为连接处于IDLE状态,通知业务处理器
c 定时的方式:基于select(timeout)或者定时器。Mina:select(timeout);Netty:HashWheelTimer
合理设置TCP/IP选项,有时会起到显著效果,需要根据应用类型、协议设计、网络环境、OS平台等因素做考量,以测试结果为准
Socket缓冲区设置选项:SO_RCVBUF 和 SO_SNDBUF
Socket.setReceiveBufferSize/setSendBufferSize 仅仅是对底层平台的提示,是否有效取决于底层平台。因此get返回的不是真实的结果。
设置原则:
1 以太网上,4k通常是不够的,增加到16k,吞吐量增加了40%
2 Socket缓冲区大小至少应该是连接的MSS的三倍,MSS=MTU+40,一般以太网卡的MTU=1500字节。
MSS:最大分段大小
MTU:最大传输单元
3 send buffer最好与对端的receive buffer尺寸一致
4 对于一次性发送大量数据的应用,增加缓冲区到48k、64k可能是唯一最有效的提高性能的方式。
为了最大化性能,send buffer至少要跟BDP(带宽延迟乘积)一样大。
5 同样,对于大量接收数据的应用,提高接收缓冲区,能减少发送端的阻塞
6 如果应用既发送大量数据,又接收大量数据,则send buffer和receive buffer应该同时增加
7 如果设置的ServerSocket的receive buffer超过RFC1323定义的64k,那么必须在绑定端口前设置,以后accept产生的socket将继承这一设置
8 无论缓冲区大小多少,你都应该尽可能地帮助TCP至少以那样大小的块写入
BDP(带宽延迟乘积)
为了优化TCP吞吐量,发送端应该发送足够的数据包以填满发送端和接收端之间的逻辑通道BDP = 带宽 * RTT
Nagle算法:SO_TCPNODELAY
通过将缓冲区内的小包自动相连组成大包,阻止发送大量小包阻塞网络,提高网络应用效率对于实时性要求较高的应用(telnet、网游),需要关闭此算法
Socket.setTcpNoDelay(true) 关闭算法
Socket.setTcpNoDelay(false)
打开算法,默认SO_LINGER选项,控制socket关闭后的行为
Socket.setSoLinger(boolean linger,int timeout)
1 linger=false,timeout=-1
当socket主动close,调用的线程会马上返回,不会阻塞,然后进入CLOSING状态,残留在缓冲区中的数据将继续发送给对端,并且与对端进行FIN-ACK协议交换,最后进入TIME_WAIT状态
2 linger=true,timeout>0
调用close的线程将阻塞,发生两种可能的情况:一是剩余的数据继续发送,进行关闭协议交换,二是超时过期,剩余数据将被删除,进行FIN-ACK协议交换
3 linger=true,timeout=0
进行所谓“hard-close”,任何剩余的数据将被丢弃,并且FIN-ACK交换也不会发生,替代产生RST,让对端抛出“connection reset”的SocketException4 慎重使用此选项,TIME_WAIT状态的价值:
可靠实现TCP连接终止
允许老的分节在网络中流失,防止发给新的连接
持续时间=2*MSL(MSL为最大分节生命周期,一般为30秒到2分钟)
SO_REUSEADDR:重用端口
Socket.setReuseAddress(boolean) 默认false
适用场景:
1 当一个使用本地地址和端口的socket1处于TIME_WAIT状态时,你启动的socket2要占用该地址和端口,就要用到此选项
2 SO_REUSEADDR允许同一端口上启动一个服务的多个实例(多个进程),但每个实例绑定的地址是不能相同的
3 SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不适用TCP
SO_REUSEPORT
listen做四元组,多进程同一地址同一端口做accept,适合大量短连接的web server
Freebsd独有
其他选项:
Socket.setPerformancePreferences(connectionTime, latency, bandwidth) 设置连接时间、延迟、带宽的相对重要性
Socket.setKeepAlive(boolean) 这是TCP层的keep-alive概念,非HTTP协议的。用于TCP连接保活,默认间隔2小时,建议在应用层做心跳
Socket.sendUrgentData(data) 带外数据
技巧:
1 读写公平
Mina限制一次写入的字节数不超过最大的读缓冲区的1.5倍
2 针对FileChannel.transferTo的bug
Mina判断异常,如果是temporarily unavailable的IOException,则认为传输字节数为0
3 发送消息,通常是放入一个缓冲区队列注册write,等待IO线程去写
线程切换,系统调用
如果队列为空,直接在当前线程channel.write,隐患是当前线程的中断会引起连接关闭
4 事件处理优先级
ACE框架推荐:accept > write > read (推荐)
Mina 和 Netty:read > write
5 处理事件注册的顺序
在select()之前
在select()之后,处理wakeup竞争条件
Java Socket实现在不同平台上的差异
由于各种OS平台的socket实现不尽相同,都会影响到socket的实现
需要考虑性能和健壮性
相关推荐
5. NIO和BIO的区别:BIO是阻塞的,一个线程只能处理一个连接,当连接数增多时,线程数也随之增加,可能导致资源耗尽。而NIO是非阻塞的,通过选择器,一个线程可以处理多个连接,提高了系统资源利用率。 6. NIO在...
2. **选择器(Selector)**:选择器是NIO中的核心组件,它允许单个线程监控多个通道的事件,如读、写、连接完成等。当某个通道准备好进行I/O操作时,选择器会通知我们,这样可以有效地减少了线程的使用,提高了...
- 缓冲区在写入和读取数据时,标记位置会随之改变。 3. **选择器(Selector)**: - 选择器允许单个线程可以监视多个输入通道。 - 使用选择器可以实现单线程管理多个网络连接,提高了性能。 4. **文件通道...
这种方式在连接数量较少时是可行的,但当并发连接增加时,线程数量也随之增加,可能导致服务器资源耗尽。 Java NIO则引入了非阻塞的概念。在NIO中,通道代表了一个打开的I/O流,它可以是文件、套接字或其他类型的流...
Java NIO(New Input/Output),即新的输入/输出库,是随 JDK 1.4 一同引入的重要更新。它提供了一种高效、面向块的数据处理方式,相较于传统的 Java IO 库,NIO 在数据读写性能上有了显著提升。 #### 二、Java NIO...
在BIO模型中,每个连接都会创建一个线程进行处理,当连接数量增大时,线程数量也随之增加,可能导致服务器资源耗尽。而NIO则通过选择器(Selector)和通道(Channel)来解决这个问题。选择器允许单个线程轮询多个...
1. **非阻塞操作**:NIO中的Channel支持非阻塞模式,这意味着当没有数据可读或没有空间可写时,不会阻塞当前线程,而是立即返回。 2. **多路复用**:Selector能够监控多个Channel的状态变化,通过单个线程就能处理...
- **limit**:限制了可以读写的数据范围,初始值等于capacity,当数据被写入或读出后,limit会随之变化。 - **position**:当前读写位置,初始值为0,每次读写操作后,position会递增。 - **mark**:记录position...
- **文件观察者**: 利用`java.nio.file.WatchService`可以监听文件系统的变更事件。 - **并发访问**: 处理大量文件时,可以考虑使用多线程或多进程技术提高效率。 通过以上内容的学习,我们可以了解到在Java中如何...
- **IO与NIO**:传统IO与非阻塞IO的区别和应用场景。 - **反射机制**:运行时动态获取类信息和调用方法。 - **注解(Annotation)**:自定义注解及其在编译、运行时的应用。 10. **Java EE基础** - **Servlet**...
8. NIO.2的进展和预期发布信息:NIO.2的开发始于2006年,预计随Java 7发布。由于技术原因文章部分内容的OCR扫描可能存在识别错误,但整体上强调了Java NIO的更新与发展。 以上知识点是对Java IO特别是Java NIO和NIO...
《精通Java核心技术全书随书源码实例》是一份丰富的Java学习资源,涵盖了Java编程的多个核心领域。这个压缩包包含了一系列与Java基础知识相关的实践代码示例,旨在帮助初学者和有经验的开发者深入理解Java语言的核心...
5. **文件系统和NIO**:Java 7引入的新I/O API,包括Channel、Buffer、Selector等。 6. **枚举和泛型**:枚举类型的使用,泛型的优势,以及通配符和类型擦除。 7. **JVM内存模型**:了解堆、栈、方法区、本地方法...
10. **Java标准库**:书中详细讲解了Java SE库中的各种类和接口,如日期和时间API、集合框架的改进、NIO.2、Swing GUI组件等。源码会涵盖这些库的实际应用。 通过阅读《Java核心技术(第八版)》的随书源码,读者...
随书附带的光盘包含了源代码、习题解答以及实验工具,为学习者提供了丰富的实践资源,使得理论与实践相结合,从而更有效地掌握Java编程技能。 Java是全球广泛使用的编程语言,尤其在企业级应用开发领域占据主导地位...
5. **IO流与NIO**:书中会讲解Java的输入/输出流系统,包括字符流、字节流,以及后来引入的非阻塞I/O(New IO,NIO)框架,用于高效的数据读写。 6. **多线程**:Java对多线程支持良好,书中会涵盖线程的创建、同步...
NIO(非阻塞I/O)的引入提供了更高效的I/O操作方式,适用于大数据传输和高并发场景。 反射机制是Java的高级特性,它允许程序在运行时动态访问类、接口、字段和方法的信息,增强了程序的灵活性和动态性。 最后,JVM...
1. **异步事件驱动**:Netty 使用非阻塞 I/O 模型,基于 Java NIO(非阻塞输入/输出)库,提高了并发性能和系统资源利用率。 2. **Channel**:Netty 中的 Channel 是连接的抽象,它可以读取和写入数据。每个 ...
5. **IO流**:理解输入输出流的概念,学会读写文件、网络通信,以及使用NIO(New IO)进行高效的数据传输。 6. **多线程**:学习线程的创建与管理,同步机制(如synchronized关键字、Lock接口),以及并发工具类。 ...