jdk1.4提供了java.nio包,为从根本上改善I/O的性能提供了可能,但是nio要比以前的I/O要复杂,提供了更底层的操作和更细的api。学起来并不是那么快就上手,有专门一本书
介绍nio的。我希望通过总结更好的梳理整个nio框架各个类之间的关系,从而能够灵活的使用nio包。
nio通常需要涉及到三个对象:
1、数据源:从文件中获得的FileInputStream/FileOutputStream、从socket中获得的输入输出流等
2、缓冲区对象(buffer):nio定义了一系列buffer对象,最基本的是ByteBuffer,其他还有各种基本数据类型除了boolean类型外,所对应的buffer类型。使用这些buffer一般的过程是:从数据源中读到ByteBuffer中,然后使用ByteBuffer.asXxxBuffer()转换成特定的基本类型,然后对这个基本类型的buffer进行操作;把基本类型转换成ByteBuffer类型,然后写入数据源中。
3、通道对象(channel):他提供了对数据源的一个连接,可以从文件中获得FileChannel,从Socket中获得SocketChannel,它是一个中介。提供了数据源与缓冲区之间的读写操作。
我们可以同过一句话来解释他们三者之间的关系:
channel对象提供了数据源与缓冲区之间的读写操作,是数据源与缓冲区的一个桥梁,通过这个桥梁,数据在两者之间流动。
首先我们看看这个中介Channel类的整个结构:
Channel < Closable WritableByteChannel < Channel InterruptibleChannel < Channel ReadableByteChannel < Channel GetherByteChannel < WritableByteChannel ByteChannel < WritableByteChannel,ReadableByteChannel ScatteringByteChannel < ReadableByteChannel
Closeable: void close()//关闭源或目的地,并释放资源 Channel: void close()//关闭通道 void boolean isOpen()//判断通道是否打开 ReadableByteChannel: int read(ByteBuffer input) //将字节从通道读入input缓冲区中,返回读取的字节数,到达流结尾时,返回-1 WritableByteChannel: int write(ByteBuffer output) //将字节从实参output缓冲区写入通道,返回写的字节数 ByteChannel 仅仅继承了ReadableByteChannel和WritableByteChannel的方法 ScatteringByteChannel: int read(ByteBuffer[] inputs) //将字节从通道读入inputs缓冲区数组中,返回读取的字节,到达流末尾是,返回-1 int read(ByteBuffer[] inputs,int offset,int length) //将字节从通道读inputs[offset]---inputs[offeset+length-1]缓冲区中 GatherByteChannel: int write(ByteBuffer[] outputs); //把outputs缓存区数组写入通道中 int write(ByteBuffer[] outputs,int offset,int length); 把从outputs[offset]-->outputs[offset+length-1]缓冲区写入通道中
一、各种缓冲区:
所有的缓冲区都继承了Buffer,Buffer类定义了所有缓冲区共有的基本特征,缓冲区存储了制定类型的元素序列有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
我们需要知道这些基本类型占用存储空间与Byte的对应关系:
char 2 bytes,int 4 bytes,double 8 bytes
二、我们看看Buffer三个重要的概念以及他们之间的关系:
capacity 所能存方特定类型值得最大数量
limit 第一个不能读取或写入的索引位置
position 下一个要读取或写入的缓冲区索引位置
理解这三者的关系是对缓冲区操作的关键:
新创建的一个缓冲区,position=0,limit=capacity
我们可以通过position(int newPosition)和limit(int newLimit)来调整position和limit的值。显然0 =< position =< limit =< capacity,所以我们在设置这个位置
需要小心,以满足这个条件,一般如果同时设置limit和position的时候,下面代码是一个
安全的方法:
buf.position(0).limit(newLimit).position(newPosition);
position和capacity存在的理由不需要解释。
为什么需要limit呢?这是理解缓冲区操作的关键所在。
一段数据怎么去标示出来呢?两个点决定了一个段啊,
而position就是起点,limit就是终点。但这个起点和终点在不同时候,他
表示的含义不同:
当把数据写入buffer时,poision到limit就是可以写的空间
当从buffer读数据时,position到limit就是可读取的数据
所以我们经常做的事情就是:
把数据写入Buffer,,然后使用buf.flip()操作,最后把Buffer写入File中。这个过程发生了什么?
我们把数据写入Buffer后,position为写入的最后一个数据的位置,而buf.flip()是
buf.limit(buf.position()).position(0);的简写形式。这样position到limit就
标示了可以写到文件的数据
另外还有一个mark()方法,他记录了上一次position和limit的位置,通过reset()可以
恢复到这个状态。
三、下面我们看看如何创建缓冲区的:
缓冲区类没有共有的构造方法,但提供了静态工厂方法allocate来创造缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024); FloatBuffer buf = FloatBuffer.allocate(100); //...
我们可以通过wrap来把字节数组包装成ByteBuffer
String str = "Hi,just a test"; byte[] arr = str.getBytes(); ByteBuffer buf = ByteBuffer.wrap(arr); //ByteBuffer buf = ByteBuffer.wrap(arr,offset,len);
四、视图缓存区:
从名字我们可以知道这种缓冲区是为了提供方便操作的视图而存在的,从ByteBuffer可以
得到各种基本类型(boolean除外)对应的缓冲区,这个过程是通过asXxBuffer()方法得到的。
有了视图缓冲区我们就可以方便的对缓冲区进行操作了,所做的操作直接影响后备缓冲区(ByteBuffer)的数据,但不影响后备缓冲区的limit和position.所以我们如果这时候
在想把后备缓冲区写入文件的时候,需要手工调整limit值。
五、缓冲区数据的传送:
1、将数据送到缓冲区:
ByteBuffer有两种put操作(相对的和决对的),用来传送数据到缓冲区:
put(byte b); put(int index,byte b); put(byte[] array,int offset,int len); put(ByteBuffer src);
例子:
String text = "Value of e"; ByteBuffer buf = ByteBuffer.allocate(text.length() + sizeof(Math.E)); buf.put(text.getBytes()).putDouble(Math.E);
这个例子会用本地的编码获的字节数组。
而如果要想以原来Unicode字符码的形式传递的话,可以这么写:
char[] array = text.toCharArray(); ByteBuffer buf = ByteBuffer.allocate(50); for(char ch : array) buf.putChar(ch); buf.putDouble(Math.E);
六、写文件
我们把数据写入缓冲区和上次我们总结的用Channel把缓冲区的数据写入文件合起来就可以写文件了:
String text = "Value of e"; ByteBuffer buf = ByteBuffer.allocate(text.length() + sizeof(Math.E)); buf.put(text.getBytes()).putDouble(Math.E); buf.flip(); File file = new File("D:/test.txt"); FileOutputStream fos = new FileOutputStream(file); FileChannle outputChannel = fos.getChannel(); outputChannel.write(buf); fos.close();
六、使用视图缓冲区:
前面我们介绍了视图缓冲区,我们看看使用视图缓冲区写文件的例子:
String greeting = "Hi,nice to meet you!"; ByteBuffer buf = ByteBuffer.allocate(1024); CharBuffer charBuf = buf.asCharBuffer();//得到试图缓冲区 charBuf.put(greeting);//把字符串转换成Buffer,操作视图缓冲区 buf.limit(2*charBuffer.position()); FileChannel outputChannel = fos.getChannel(); fos.write(buf); fos.close();
七、集中写操作:
文件通道有两个方法可以执行集中写操作:
write(ByteBuffer[] buffers);
write(ByteBuffer[] buffers,int offset,int length);
把分散在多个buffer的数据集中写入文件
我们看一个例子:
关键字: java nio
int read(ByteBuffer buf); 从文件中读取buf.remaining()[即limit-position]个字节到缓冲区buf,从缓冲区的当前位置开始存取 int read(ByteBuffer[] buffers); 从文件中读到多个缓冲区中 int read(ByteBuffer[] buffers,int offset,int length); 从文件中读到buffers[offset]--buffers[offset+length-1]缓冲区中
File file = new File("D:/text.txt"); FileInputStream fis = new FileInputStream(file); FileChannel inputChannel = fis.getChannel(); ByteBuffer buf = ByteBuffer.allocate(48); while(inChannel.read(buf) != -1){ //....从buf中提取出数据;for example: System.out.println(buf.filp().asCharBuffer().toString()); buf.clear(); }
File fromFile = new File("D:/from.txt"); File toFile = new File("D:/to.txt"); FileInputStream fis = new FileInputStream(fromFile); FileOutputStream fos = new FileOutputStream(toFile); FileChannel inChannel = fis.getChannel(); FileChannel outChannel = fos.getChannel(); int byteWritten = 0; long byteCount = inChannel.size(); while(byteWritten < byteCount){ byteWritten += inChannel.transferTo(bytesWritten,byteCount- bytesWritten,outChannel); } fis.close(); fos.close();
File file = new File("D:/test.txt"); RandomAccessFile raf = new RandomAccessFile(file,"rw"); FileChannel ioChannel = rad.getChannel();
File file = new File("D:/test.txt"); RandomAccessFile raf = new RandomAccessFile(file,"rw"); FileChannel ioChannel = rad.getChannel(); MappedByteBuffer buf = ioChannel.map(READ_WRITE.0L,ioChannel.size()).load();
写入基本类型:
读入基本类型:
将刚写入的数据读出:
Java nio(四)
关键字: java nio, socket channel
并不那么容易.看《Java nio》那本书,差点没看崩溃,洋洋洒洒写了近三百大页介绍java nio,写的太散太细,反而觉得很乱,理不清思路了。那本书用起来作为参考手册还行,想很快上手这本书还真不适合。
今天打算总结和Socket异步通信相关的内容。
传统的Socket是阻塞,像ServerSocket在调用accept方法后便处于阻塞状态等待Client端的连接,所以一般会在Server端使用许多线程,对每一个Socket连接分配一个线程。充分利用并发特性来提高性能。
但这样会带来许多问题:
1、Server端创建了许多线程来处理Socket连接,而这些线程大部分的时间都在等待连接,
也就是说这些线程占了资源,真正做事情的时间却不多。也就是说资源利用率比较低,这就会
直接导致一个问题,可伸缩性比较差,当接受1000个连接还可以,但增加到10000个或更多时性能会很快下降。像Web服务器Jetty和Tomcat6.x都是用了异步通信模式,来接受客户端的连接,从而很大程度上提高了系统的伸缩性,提高了性能。
2、由于使用多线程,就会使问题变得复杂,事实如果你敢说精通并发编程,说明你太乐观了,并发编程很容易出错,并且你很难发现问题。并且需要互斥访问一些资源,这往往是个瓶颈,会降低并发性。
异步的Socket可以解决上面的问题,异步的Socket它是非阻塞的,它尝试去连接,但不管是否能够立即建立连接,它都会立即返回,返回之后它便可以做其他的事情了,但连接真正建立成功,就会有相应的事件来通知,这时候你去做连接成功之后的读写操作了.这个过程,整个线程都是处于忙碌状态,所以只需要单个或者很少几个线程,就可以达到阻塞方式的成百上千的线程的性能.
类似异步Socket功能应运而生,但Java在jdk1.4才引入这个功能,考虑以前Socket的已提供的功能,而且接口很难一致,Java并没有单独设计异步Socket,而是在java nio中引入了SocketChannel之类的通道来处理这个问题,我们会发现这些通道和对应的Socket提供的接口有很大的相似之处,但这些Channel不是关于Socket的抽象,它们并不处理TCP/UDP协议,而是委托给对Socket来处理。
这种新的Channel可以在非阻塞的模式下操作。通过注册Selector,使用一种类似观察者模式。其实是Selector不断的轮询连接的端口,我们可以通过Selector的select()方法,这个方法是阻塞的,他会更新就绪操作集的键的数目,并作为返回值返回。我们会通常在判断这个返回值如果不为零,则可能有我们感兴趣的事件发生,然后我们可以通过
来得到键的迭代器,这样我们就可以通过遍历这个集合来判断有没有我们感兴趣的事情发生,如果有我们就做一些处理,没有我们可以把这个键remove掉:
一、下面我们介绍一下与Socket相关的各种Channel
与几种Socket对应的提供了一下几种Channel:
ServerSocketChannel,SocketChannel,DatagramChannel.
1、ServerSocketChannel:
我们发现这个Channel根本不支持读写操作,叫Channel有点名不副实了,但Java中Channel本身的抽象也不一定支持读写操作的,需要实现WriteableChannel和ReadableChannnel接口才能支持。其实ServerSocketChannel本身也不是用于读写
操作的,它通常通过socket() 方法获取相关的ServerSocket,然后通过ServerSocket 方法bind来绑定端口。ServerSocketChannel提供了open()静态工厂方法来创建ServerSocketChannel对象。同时ServerSocketChannel从AbstractSelectableChannel
继承了:
这两个方法是最常用的了。通过configureBlocking来设置通道的是否为阻塞模式,
通过register向给定的选择器注册此通道。
从上面的过程我们用代码总结一下一般的流程:
2、SocketChannel:
SocketChannel通常作为客户端,建立一个对Server端的连接,任何一个SocketChannel
都是和对应的Socket关联的,但一个Socket并不一定有SocketChannel可以获得。
同样的SocketChannel也适用静态工厂方法open()来实例化SocketChannel.
下面来看看最常用的操作:
public abstract boolean connect(SocketAddress remote)
throws IOException连接此通道的套接字。
如果此通道处于非阻塞模式,则调用此方法会发起一个非阻塞连接操作。如果立即建立连接(使用本地连接时就是如此),则此方法返回 true。否则此方法返回 false,并且必须在以后通过调用 finishConnect 方法来完成该连接操作。
我们可以通过
来建立连接,这个过程是异步的,他会立即返回,如果立即建立连接成功则返回true,否则
返回false.随后可以通过finishConnect来完成连接的建立。
另外可通过:
等价于:
下面我们演示一下整个的使用过程:
3、DatagramChannel
这个Channel与DatagramSocket相对应,提供了基于UDP协议的数据包的套接字的通道。
UDP协议是无连接的,DatagramChannelt既可以作为Server端,也可以作为Client端,如果想新创建一个DatagramChannel作为Server端来监听,那么需要绑定到特定的端口或地址和端口的组合,一般过程如下:
但是一个没有绑定特定端口的DatagramChannel仍然是可以接收数据的,事实上会有一个
动态生成的端口分配给他。不管DatagramChannel是否绑定到了一个端口,任何一个包的发送都会包含它的地址,下面是DatagramChannel提供的发送和接收数据的方法:
DatagramChannel并不能保证数据能够发送到目的端,因为UDP协议本身就是不可靠的。
另外我们再看看DatagramChannel提供了以下下几个方法:
从名字看起来很让人迷惑,因为DatagramChannel就是基于无连接的,为什么还会有
connect之类的方法呢?
其实这个Connect的语义和基于流的Socket是不一样的,这里的连接只是制定了远程的
地址,这样就可以忽略其他地址发来的数据包了。一但使用完connect方法,就不能像其
他的地址send数据了。但这里的connect和SockectChannel的connect不同,它是可以
随时disconnect,然后去connect其他的地址的。但使用了connect方法之后,就可以
像FileChannel那样read,write数据,而不用指名地址了。
来自:http://fuliang.javaeye.com/blog/