写入和读取都可能会被阻塞,比如Socket的read方法等消息接收完后也被阻塞(失去CPU的控制权,类似于多线程锁竞争失败被阻塞),一直等待新消息,可是访问量较大和性能要求较高时,当然可以用多个线程来维护收和发,不过在现在需要大量长连接的情况下,不可能保持这么多连接,而且线程开启的数量必然也是有限的。
这时候便需要使用NIO了。(NIO是New IO的意思,它可以设置为非阻塞IO,也可以为阻塞IO)
主要有仨部分,
1.缓冲区ByteBuffer等(除了boolean其他基本类型都有)
2.通道Channel等。(FileChannel, SocketChannel,ServerSocketChannel,DatagramChannel等)
3.选择器Selector(非阻塞式IO的情况下,用于轮询是否有数据进来,用户线程便不会被阻塞,只需要服务器这边监听就好了)
无论阻塞还是非阻塞,都需要创建通道与缓冲区,通道相当于修了条路,缓冲区相当于车,光有路没有车拉货(数据)也不行,两者都得有。
选择器就相当于快递站点,会在到货时做出相应通知,而不用一直在那儿傻等。
客户端的代码很简单,与一般的基本上无异,服务端稍微有点复杂
public void server() throws IOException {
SocketChannel socketChannel = null;
try(ServerSocketChannel ssChannel = ServerSocketChannel.open();
Selector selector = Selector.open()) {
//设置为非阻塞方式
ssChannel.configureBlocking(false);
ssChannel.bind(new InetSocketAddress(10086));
//注册监听事件
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
//获取选择器的迭代器
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey sk = it.next();
//是否为目标键
if (sk.isAcceptable()) {
socketChannel = ssChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (sk.isReadable()) {
socketChannel = (SocketChannel) sk.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
while (socketChannel.read(buf) != -1) {
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
}
it.remove();
}
}finally {
if(socketChannel!=null){
socketChannel.close();
}
}
}
这个程序将监听与处理放在了一个线程中,一般应用时并不这样处理,而是一个线程以阻塞式监听客户端的请求,,另一个线程专门负责处理请求,这个线程便会采用NIO非阻塞的方式。像Web服务器Tomcat和Jetty都是如此处理的。
可简单的看成一个基本类型的数组。
它有四个参数:
1、capacity:容量,不可变
2、limit:限制,表示缓冲区中可以操作数据的大小
3、position:位置
4.mark记录当前position的位置(默认为0),可通过reset()恢复到mark的位置
我觉得最有意思的地方是,它读写共用一套指针,所以写后读需要转换模式(更改position指针的位置,flip())
然后数据读完了,可使用claar"清空"之前的数据,或者读取接着写(相当于append),这个清空实际上只是将指针初始化,然后再写入时就会覆盖。
Buffer有两种创建方式,
1.用户内存中,就是在JVM的堆内存中,这种好处是不会堆外溢出,坏处是前文所述,操作磁盘,需要由操作系统完成,所以需要在用户态和核心态之间切换,这就会比较耗费资源与时间。(适用于并发量少于1000,I/O操作较少)
2.直接操作操作系统的缓冲区,通过内存页映射,直接访问I/O。不过每次调用会调用一次System.gc(),还可能造成内存泄露的问题。(适用于数据量大,生命周期较长)
1.Channel.tansferFrom.tansferTo
这个方法是是内存页内直接转移的,效率会比传统方式高。
public void testTransfor() throws IOException {
long start = System.currentTimeMillis();
try(FileChannel inChannel = FileChannel.open(Paths.get("src/main/resources/copy.png"),StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("src/main/resources/copy11.png"),StandardOpenOption.WRITE,StandardOpenOption.CREATE)){
inChannel.transferTo(0,inChannel.size(),outChannel);
}
System.out.println(System.currentTimeMillis()-start);
}
2.FileChannel.map
将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,省去了内核空间向用户空间复制的损耗。适合对大文件的只读性操作,如大文件的MD5校验。
@Test
public void testCopyD() throws IOException {
long start = System.currentTimeMillis();
try(FileChannel inChannel = FileChannel.open(Paths.get("src/main/resources/copy.png"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("src/main/resources/newCopy.png"),StandardOpenOption.WRITE,StandardOpenOption.READ)) {
MappedByteBuffer inMap = inChannel.map(FileChannel.MapMode.READ_ONLY,0,inChannel.size());
//注意由于这里只有读写模式,所以上面管道的设置中也需要添加上读
MappedByteBuffer outMap = outChannel.map(FileChannel.MapMode.READ_WRITE,0,outChannel.size());
byte[] dst = new byte[inMap.limit()];
inMap.get(dst);
outMap.put(dst);
}
System.out.println(System.currentTimeMillis()-start);
}
1.性能检测
Linux下可使用iostat命令查看io的情况
iowait参数不应该超过25%
还有一个参数就是IOPS,即每秒读写次数,应用程序所需的最低IOPS。
通常采用RAID(磁盘阵列)技术来优化,即不同的磁盘组合起来以提高I/O性能
计算公式为:
(磁盘数×每块磁盘的IOPS)/(磁盘读的吞吐量+RAID因子×磁盘写的吞吐量) = IOPS
2.提升I/O性能
通常的方法有:
磁盘阵列 | 说明 |
---|---|
RAID 0 | 数据被平均写到多个数据阵列中,写读并行,IOPS提升一倍 |
RAID 1 | 提升数据的安全性,将数据分别复制到多个磁盘阵列中,并不能提升IOPS。 |
RAID 5 | 将数据平均写到所有磁盘阵列总数减1的磁盘中,往另外一个磁盘中写入这份数据的奇偶校验信息。如果其中一个磁盘损坏,可以通过其他磁盘的数据和这个数据的奇偶校验信息来恢复 |
RAID 0+1 | 如名字一样,备份+分组读写 |
网络参数 | 说明 |
---|---|
echo “1024 65535”> sudo /proc/sys/net/ipv4/ip_local_port_range | 设置向外链接可用端口范围 |
echo 1>/proc/sys/net/ipv4/tcp_tw_reuse | 设置time_wait连接重用 |
echo 1>/proc/sys/net/ipv4/tcp_tw_recycle | 设置快速回收time_wait连接 |
echo 180000>/proc/sys/net/ipv4/tcp_max_tw_buckets | 设置最大time_wait连接长度 |
echo 0>/proc/sys/net/ipv4/tcp_timestamps | 表示是否启用以一种比超时重发更精确的方法来启用对RTT的计算 |
echo 1>/proc/sys/net/ipv4/tcp_windows_scaling | 设置TCP/IP滑动窗口是否可变 |
echo 20000>/proc/sys/net/ipv4/tcp_max_syn_backlog | 设置最大等待处于客户端还没有应答回来的连接数 |
echo 10000>/proc/sys/net/core/somexconn | 设置每一个处于监听状态的端口的监听队列的长度 |
echo 10000>/proc/sys/net/core/netdev_max_backlog | 设置最大等待CPU处理的包的数目 |
echo 10000>/proc/sys/fs/file-max | 设置最大打开文件数 |
echo 15>/proc/sys/net/ipv4/tcp_fin_timeout/ | 设置FIN-WAIT-2状态回收时间 |
echo 16777216>/proc/sys/net/core/rmem_max | 设置套接字数据接收缓冲大小 |
echo 262144>/proc/sys/net/core/rmem_default | 设置默认的系统套接字数据接收缓冲大小 |
echo 16777216>/proc/sys/net/core/wmem_max | 套接字发送最大缓冲 |
echo 262144>/proc/sys/net/core/wmem_default | 设置默认的系统套接字数据发送缓冲大小 |
echo “4096 87380 16777216”>/proc/sys/net/ipv4/tcp_rmem | 设置最大的TCP发送缓存大小,三个值分别对应,最小,默认,最大 |
echo “4096 87380 16777216”>/proc/sys/net/ipv4/tcp_wmem | 设置最大的TCP接收缓存大小,三个值分别对应,最小,默认,最大 |
以上设置均为临时性的,重启便会丢失。
还可用cat /proc/net/netstat
查看TCP的统计信息
cat /proc/net/snmp
查看当前系统的连接情况。
netstat -s
查看网络的统计信息。
优化存在以下原则
1.减少传输次数,具体可以设置缓存和合并传输。
2.减少网络传输的数据量的大小,通常是将数据压缩后再传输,还有尽可能使用简单的协议。
3.尽量减少编码,尽量提前将字符转换为字节的形式,将编码过程提前。
异步与同步的选择本质是可靠性与性能的平衡,不存在完美的选择
阻塞与非阻塞,阻塞会让被阻塞的线程的cpu停下去等待较慢操作完成,而非阻塞就不存在cpu利用率下降的问题,可是它会造成线程的频繁切换,视情况选择阻塞还是非阻塞
以上同步与否 和 阻塞与否 结合起来,就又可以分为四种情况
将一个类的接口转换为客户端所能接受的另一种接口,从而使两个接口不匹配的两个类能够一起工作。
在IO中就使用到了这种设计模式(字符流转换为字节流或者字节流转换为字符流)
以上的类图是字节流转换为字符流,由于需要解码,所以InputStreamReader以组合的形式纳入了一个StringDecoder对象,这里的源角色就是InputStream,目标就是Reader,自然适配器就是InputStreamReader啦。
将某个类再以类的层面再封装一下,使得类能够更强大。
而且使用上不应该与原类有啥不同。
详细可看本文
InputStream是抽象实体,定义了规则,FileInputStream是具体的实体,实现了所有的接口。
而FilterInputStream则是抽象装饰器,定义规则,而BufferInputStream则是具体的装饰器,实现规则,在不改变原来的情况下进行增强。