Java NIO基础 -我们到底能走多远系列(17)
我们到底能走多远系列(17)
扯淡:
长期接触所谓web框架的企业级应用的开发者们,不知道你们有没有这样的感受,几年之后,发现:路越走越窄,学的东西大多是表层的,编程的技巧没有太大的改变,接触大量的所谓框架也写不出一个核心的模块,学习了框架的原理后也不会很好的设计,大量调用别人的库函数感觉看得懂业务的人都可以写。
我觉得作为从事编码行业的我们,埋头苦干是必备的素质,但是抬头思考却是核心的竞争力,因为前者是进步的一个条件,而后者则是进步的原动力。
这里我斗胆提出一个自己想法:假如你没有进入一个类似金饭碗的公司,那么在你工作5年内至少以1年的频率进行面试经历,我不是指一年平率的跳槽,是面试,面试可以让你知道目前其他公司使用的技术方向,让你知道你的水平,以及你期望的岗位距离有多远,总之残酷的面试可以让你认清自己,从而不断重构自己,不断更新自己,让今天的自己优于昨天的自己。
假如还有学习的动力,这样的方式是不错的选择,还有一个前提是:你必须是一个有勇气推翻重来的人。
主题:
关于NIO的基础知识博客园的文章:直达车
1,关于缓冲区:
NIO (No-blocking I/O)从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞I/O操作的API被引入。
为什么说是基于缓冲区呢?因为NIO中使用的数据类型都是有缓冲功能的。
数据类型如下:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
不知道有没有朋友对缓冲这个有点疑惑呢?所谓的缓冲的就是:比如一百个箱子要从A搬到B只能一个个般,缓冲区就相当于来了辆三轮车,从A搬10个盒子到三轮车,运到B,在把10个盒子搬给B。就是这么个过程。如果你接触过BufferedInputStream的源码,那么就比较直观的看到所谓的Buffer,就是一个new出来的byte数组,了解详细请 猛击
上面7个数据类型其实就是对各自的基本类型数组的封装,比如ByteBuffer
就是对Byte[]的封装,CharBuffer
就是Char
[]的封装,封装后对外提供才做他们的方法。
ByteBuffer
为例,来看下它的源码:
我们通过allocate方法取得ByteBuffer的:
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); //调用HeapByteBuffer的构造函数 return new HeapByteBuffer(capacity, capacity); }
HeapByteBuffer的构造函数:
HeapByteBuffer(int cap, int lim) { // package-private // HeapByteBuffer的super又是ByteBuffer // new byte[] 在这呢,这就所谓开辟的缓存区啦 super(-1, 0, lim, cap, new byte[cap], 0); }
ByteBuffer的构造函数:
ByteBuffer(int mark, int pos, int lim, int cap, // package-private byte[] hb, int offset) { // ByteBuffer的super是Buffer类 super(mark, pos, lim, cap); // hb就是new出来的byte数组,从此 ByteBuffer就由维护这个数组了 this.hb = hb; this.offset = offset; }
Buffer类的构造函数做什么呢?
这个构造函数的作用是初始化Buffer的状态变量(后面会了解到),因为上面7个基本类型的Buffer数据类型都继承Buffer类,他们的状态变量统一归Buffer类管理,继承带来的好处啊。
Buffer(int mark, int pos, int lim, int cap) { // package-private if (cap < 0) throw new IllegalArgumentException(); // 设置capacity this.capacity = cap; //设置limit limit(lim); //设置position position(pos); if (mark >= 0) { if (mark > pos) throw new IllegalArgumentException(); this.mark = mark; } }
至此,我想我们应该对所谓的缓冲区没有什么疑惑了吧。
2,阻塞IO和非阻塞IO
网上比较好解释如下:
Java 传统的IO操作都是阻塞式的(blocking I/O),如果有socket的编程基础,你会接触过堵塞socket和非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,一般来说,如果使用堵塞socket,通常我们通常开一个线程accept socket,当读完这次socket请求的时候,开一个单独的线程处理这个socket请求;如果使用非堵塞socket,通常是只有一个线程,一开始是select状,当有信号的时候可以通过 可以通过多路复用(Multiplexing)技术传递给一个指定的线程池来处理请求,然后原来的线程继续select状态。 最简单的多路复用技术可以通过java管道(Pipe)来实现。换句话说,如果客户端的并发请求很大的时候,我们可以使用少于客户端并发请求的线程数来处理这些请求,而这些来不及立即处理的请求会被阻塞在java管道或者队列里面,等待线程池的处理。请求 听起来很复杂,在这个架构当道的java 世界里,现在已经有很多优秀的NIO的架构方便开发者使用,比如Grizzly,Apache Mina等等,如果你对如何编写高性能的网络服务器有兴趣,你可以研读这些源代码。
3,结合tomcat使用NIO来理解
在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可伸缩性。
从Tomcat6.0以后, Java开发者很容易就可以是用NIO的技术来提升tomcat的并发处理能力。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
修改成:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443" />
然后启动服务器,你会看到org.apache.coyote.http11.Http11NioProtocol start的信息,表示NIO已经启动
从tomcat使用NIO这一点来看,如果我们从事java服务器的开发,肯定是离不开NIO的。
4,状态变量
状态变量的解释在最前面推荐的博文中已经有非常好的解释了,这里是学习下源码:
三个值指定缓冲区的状态:
position 它指定了下一个字节将放到数组的哪一个元素中。
limit 表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的层数组的容量。
函数源码:flip()
public final Buffer flip() { // limit 置成position位置 limit = position; // position 置0 position = 0; mark = -1; return this; }
flip()
后的图:执行
clear()源码:
// 注意这里的妙处是我们对数组的处理知识改变状态变量,而没有真正去clear数组中的内容, // 因为真的去删数组中的内容,效率会很慢,而且也没有必要这样做 public final Buffer clear() { // position置为0 position = 0; // limit调成最大 limit = capacity; mark = -1; return this; }
我们可以通过下面的方式取得或设置状态变量:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
buffer.position();
buffer.limit();
buffer.capacity();
buffer.mark();
buffer.position( 3 );
buffer.limit( 7 );
5,基础例子
1,copy文件例子:
package code.stu.nio; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; // 拷贝文件例子 public class CopyFileByNio { public static void main(String[] args) throws IOException { FileInputStream in = new FileInputStream(""); FileOutputStream out = new FileOutputStream(""); // 得到缓冲区 ByteBuffer bb = ByteBuffer.allocate(1024); // 通道 FileChannel fi = in.getChannel(); FileChannel fo = out.getChannel(); while(fi.read(bb) != -1){ //System.out.println(bb.position()); // 写之前先调flip bb.flip(); fo.write(bb); // 写之后再调clear bb.clear(); } in.close(); out.close(); } }
2,网上传输文件的例子:
a,服务器
package code.stu.nio.socket; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class MyServer { public static void main(String[] args) { Selector selector = null; ServerSocketChannel serverSocketChannel = null; try { // Selector for incoming time requests selector = Selector.open(); // Create a new server socket and set to non blocking mode serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); // Bind the server socket to the local host and port serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.socket().bind(new InetSocketAddress(10000)); // Register accepts on the server socket with the selector. This // step tells the selector that the socket wants to be put on the // ready list when accept operations occur, so allowing multiplexed // non-blocking I/O to take place. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // Here's where everything happens. The select method will // return when any operations registered above have occurred, the // thread has been interrupted, etc. while (selector.select() > 0) { // Someone is ready for I/O, get the ready keys Iterator<SelectionKey> it = selector.selectedKeys().iterator(); // Walk through the ready keys collection and process date requests. while (it.hasNext()) { SelectionKey readyKey = it.next(); it.remove(); // The key indexes into the selector so you // can retrieve the socket that's ready for I/O doit((ServerSocketChannel) readyKey.channel()); } } } catch (ClosedChannelException ex) { System.out.println(ex); } catch (IOException ex) { System.out.println(ex); } finally { try { selector.close(); } catch(Exception ex) {} try { serverSocketChannel.close(); } catch(Exception ex) {} } } private static void doit(final ServerSocketChannel serverSocketChannel) throws IOException { SocketChannel socketChannel = null; try { socketChannel = serverSocketChannel.accept(); // 做一个接文件的操作 receiveFile(socketChannel, new File("E:\\test\\test.txt")); } finally { try { socketChannel.close(); } catch(Exception ex) {} } } private static void receiveFile(SocketChannel socketChannel, File file) throws IOException { FileOutputStream fos = null; FileChannel channel = null; try { // 用于把数据从管道上拿下来变成文件 fos = new FileOutputStream(file); channel = fos.getChannel(); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); int size = 0; while ((size = socketChannel.read(buffer)) != -1) { buffer.flip(); if (size > 0) { buffer.limit(size); channel.write(buffer); buffer.clear(); } } } finally { try { channel.close(); } catch(Exception ex) {} try { fos.close(); } catch(Exception ex) {} } } }
客户端:
package code.stu.nio.socket; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.SocketChannel; public class MyClient { public static void main(String[] args) throws Exception { new Thread(new MyRunnable()).start(); // 建立个线程来处理 } private static final class MyRunnable implements Runnable { public void run() { SocketChannel socketChannel = null; try { socketChannel = SocketChannel.open(); SocketAddress socketAddress = new InetSocketAddress("localhost", 10000); socketChannel.connect(socketAddress); // 发送数据 sendFile(socketChannel, new File("D:\\我的文档\\test.txt")); } catch (Exception ex) { System.out.println(ex); } finally { try { socketChannel.close(); } catch(Exception ex) {} } } private void sendFile(SocketChannel socketChannel, File file) throws IOException { FileInputStream fis = null; FileChannel channel = null; try { // 把文件编程流再方法通道上去 fis = new FileInputStream(file); channel = fis.getChannel(); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); int size = 0; while ((size = channel.read(buffer)) != -1) { buffer.rewind(); buffer.limit(size); socketChannel.write(buffer); buffer.clear(); } socketChannel.socket().shutdownOutput(); } finally { try { channel.close(); } catch(Exception ex) {} try { fis.close(); } catch(Exception ex) {} } } } }
总结:
1,就目前来看原IO的操作已经可以被NIO代替,在java服务器端的开发中使用多线程技术结合NIO,是一个比较好的解决方案。
2,大家也看到了在处理new出来的缓存是,不移动删除内容,只需用标记的方式来实现读写操作,大大提高了效率,印象深刻。
让我们继续前行
----------------------------------------------------------------------
努力不一定成功,但不努力肯定不会成功。
共勉。