您的位置:首页 > Web前端

Java NIO (2)Channel和Buffer

2017-04-18 17:18 369 查看

buffer缓冲器

Buffer的基本用法

使用Buffer读写数据一般遵循以下四个步骤:

 1)写入数据到Buffer

 2)调用flip()方法

 3)从Buffer中读取数据

 4)调用clear()方法或者compact()方法

当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。

有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

public class NIO {
public static void main(String[] args)throws Exception{
//使文件连接Channel
RandomAccessFile aFile = new RandomAccessFile("src/1.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);  //将文件内容读入缓冲区

while (bytesRead != -1) {

buf.flip();  //反转buffer,使其变成向外读的状态
while(buf.hasRemaining()){
System.out.print((char) buf.get()); //每次向外读一个字节
}

buf.clear(); //清空数据准备好再次写入
bytesRead = inChannel.read(buf);
}
aFile.close();
}
}


思考:为什么clear完还要加一个 bytesRead = inChannel.read(buf);呢?

因为缓冲区大小只有48,如果读入的文件大于48,就要clear完缓冲区继续从channel中读入,直到读取到-1 退出循环。

Buffer的属性

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

为了理解Buffer的工作原理,需要熟悉它的三个属性:

  capacity

  position

  limit

1)capacity

作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

2)position

当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.

当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

3)limit

在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。

当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

Buffer的类型

Java NIO 有以下Buffer类型

  ByteBuffer

  MappedByteBuffer

  CharBuffer

  DoubleBuffer

  FloatBuffer

  IntBuffer

  LongBuffer

  ShortBuffer

如你所见,这些Buffer类型代表了不同的数据类型。换句话说,就是可以通过char,short,int,long,float 或 double类型来操作缓冲区中的字节。

Buffer的分配

要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个allocate方法。下面是一个分配48字节capacity的ByteBuffer的例子。

ByteBuffer buf = ByteBuffer.allocate(48);


这是分配一个可存储1024个字符的CharBuffer:

CharBuffer buf = CharBuffer.allocate(1024);


向Buffer中写数据

写数据到Buffer有两种方式:

1)从Channel写到Buffer。

2)通过Buffer的put()方法写到Buffer里。

从Channel写到Buffer的例子

int bytesRead = inChannel.read(buf); //read into buffer.


通过put方法写Buffer的例子:

buf.put(127);


put方法有很多版本,允许你以不同的方式把数据写入到Buffer中。例如, 写到一个指定的位置,或者把一个字节数组写入到Buffer。

flip()方法

flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。

换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等 —— 现在能读取多少个byte、char等。

从Buffer中读取数据

从Buffer中读取数据有两种方式:

1)从Buffer读取数据到Channel。

2)使用get()方法从Buffer中读取数据。

从Buffer读取数据到Channel的例子:

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);


使用get()方法从Buffer中读取数据的例子

byte aByte = buf.get();


get方法有很多版本,允许你以不同的方式从Buffer中读取数据。

rewind()方法

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。

clear()与compact()方法

一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。

如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。

Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。

如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。

如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用compact()方法。

compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。

mark()与reset()方法

通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:

buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset();  //set position back to mark.


Channel 通道

一个通道代表了一个通向某实体的连接,这些实体可能是一个硬件设备,一个文件,一个网络套接字,或者是一个程序组件,它们具有执行某种或多种独立的IO操作的能力,例如读或写



1)通道是一种高效传输数据的管道,

2)通道的一端(接收端或发送端)必须是字节缓冲区,

3)另一端则是拥有IO能力的实体,

4)通道本身不能存储数据,

5)且往往通过流或套接字来创建,

6)一旦创建,则通道与之形成一一对应的依赖关系。

Channel的实现

这些是Java NIO中最重要的通道的实现:

 FileChannel

 DatagramChannel

 SocketChannel

 ServerSocketChannel

FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

【1】文件通道FileChannel

通道主要分为文件通道和Socket通道,由于文件通道只能处于阻塞模式,较为简单,因此先介绍文件通道。

文件通道的创建

再说一遍,文件通道总是处于阻塞模式。创建文件通道最常用的三个类是FileInputStream、FileOutputStream和RandomAccessFile,它们均提供了一个getChannel()方法,用来获取与之关联的通道。

对于文件通道来说,FileInputStream创建的通道只能读,FileOutputStream创建的通道只能写,而RandomAccessFile可以创建同时具有读写功能的通道(使用“rw”参数创建)。

1)打开FileChannel

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();


2)从FileChannel读取数据

调用多个read()方法之一从FileChannel中读取数据。如:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);


首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。

然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

3)向FileChannel写数据

使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer。如:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);

buf.clear();
buf.put(newData.getBytes());

//flip方法 转换buffer的读写状态
buf.flip();

while(buf.hasRemaining()) {
channel.write(buf);
}


注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

4)关闭FileChannel

用完FileChannel后必须将其关闭。如:

channel.close();


【2】数据报通道DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

1)打开 DatagramChannel

下面是 DatagramChannel 的打开方式:

DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
//这个例子打开的 DatagramChannel可以在UDP端口9999上接收数据包。


2)接收数据

通过receive()方法从DatagramChannel接收数据,如:

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);


receive()方法会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。

3)发送数据

通过send()方法从DatagramChannel发送数据,如:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));


这个例子发送一串字符到”jenkov.com”服务器的UDP端口80。 因为服务端并没有监控这个端口,所以什么也不会发生。也不会通知你发出的数据包是否已收到,因为UDP在数据传送方面没有任何保证。

4)连接到特定的地址

可以将DatagramChannel“连接”到网络中的特定地址的。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。

这里有个例子:

channel.connect(new InetSocketAddress("jenkov.com", 80));


当连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证。这里有几个例子:

int bytesRead = channel.read(buf);
int bytesWritten = channel.write(buf);


【3】Socket通道SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过以下2种方式创建SocketChannel:

(1)打开一个SocketChannel并连接到互联网上的某台服务器。

(2)一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。

1)打开 SocketChannel

下面是SocketChannel的打开方式:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));


2)关闭 SocketChannel

当用完SocketChannel之后调用SocketChannel.close()关闭SocketChannel:

socketChannel.close();


3)从 SocketChannel 读取数据

要从SocketChannel中读取数据,调用一个read()的方法之一。以下是例子:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);


首先,分配一个Buffer。从SocketChannel读取到的数据将会放到这个Buffer中。

然后,调用SocketChannel.read()。该方法将数据从SocketChannel 读到Buffer中。read()方法返回的int值表示读了多少字节进Buffer里。如果返回的是-1,表示已经读到了流的末尾(连接关闭了)。

写入 SocketChannel

写数据到SocketChannel用的是SocketChannel.write()方法,该方法以一个Buffer作为参数。示例如下:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
channel.write(buf);
}


注意SocketChannel.write()方法的调用是在一个while循环中的。Write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。

非阻塞模式

可以设置 SocketChannel 为非阻塞模式(non-blocking mode).设置之后,就可以在异步模式下调用connect(), read() 和write()了。

1) connect()

如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。像这样:

socketChannel.configureBlocking(false);

socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

while(! socketChannel.finishConnect() ){
//wait, or do something else...
}


2) write()

非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。前面已经有例子了,这里就不赘述了。

3)read()

非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。

【4】ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 并发 buffer nio 数据