NIO-Buffer、Channel、Selector

Buffer

Buffer本质是内存的一块,可以写入或者获取数据。

java.nio定义了CharBuffer\ShortBuffer\IntBuffer\LongBuffer\FloatBuffer\DoubleBuffer\ByteBuffer->MappedByteBuffer的实现,核心是ByteBuffer。可以对应理解为相应基本类型的数组。

Buffer中的重要属性-position、limit、capacity

  • capacity-缓冲区(数组)的容量,一旦buffer的容量达到capacity,需要清空buffer才能重新写入值。
  • position-初始值是0,Buffer中每写入一个值,position就+1,读操作的时候也是每读一个值,position就+1。
  • Limit-写操作模式下代表最大能写入的数据,此时Limit=capacity。读操作模式下Limit=Buffer中实际数据大小。

flip()方法-仅用于将buffer从写入模式切换到读取模式,而且在读取时必须调用,否则读不出数据。

注意,通常在说 NIO 的操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作。

public final Buffer flip() {
    limit = position; // 将 limit 设置为实际写入的数据数量
    position = 0; // 重置 position 为 0
    mark = -1; 
    return this;
}

Buffer的初始化

allocate(int capacity)可以实例化一个Buffer,另外wrap方法亦可以初始化Buffer,它接收一个byte[] 参数。

Buffer的填充

使用put方法填充Buffer。可以接收byte、int(指定index位置)和byte、byte[]参数,需要控制Buffer大小不能超过capacity.

或者将来自channel的数据填充到Buffer中,依照前文所述,在系统层面上称之为NIO的操作。

int num = channel.read(buffer);

返回从channel中读入到Buffer的数据大小。

读取Buffer

首先切换模式,使用flip()方法,即切换position和limit。对应一系列put方法, 也有一系列get方法,如根据position获取数据的byte get()、获取指定位置数据的byte get(int index)、将buffer中的数据写入到数组中的ByteBuffer get(byte[] dest)。

更加常用的是写操作,将Buffer中的值通过各种channel写入到对应的位置。

int num = channel.write(buffer);

mark() reset()

buffer的Mark属性主要是为了临时保存Position的值,提供mark()方法将mark的值设置为当前的position。后续有需要的时候调用reset()方法,可以回到Position为mark的地方。

rewind() clear() compact()

rewind():重置position为0,将mark设置为-1,可以用于从头读写buffer。

clear(): 重新初始化buffer的主要属性,相当于重新实例化buffer,一般在重新填充buffer之前调用clear()。

clear()并不会清空buffer中的数据,只是初始化了buffer中的主要属性。所以后续写入的数据,会在position=0的位置重新写入,相当于清空了数据。

compact():也是准备在buffer写入新数据之前调用。但是它会将position到limit之间的数据移到左边,在这个基础上再开始写入。此时limit等于capacity,position指向原来数据的右面。

具体的一个compact()实现:

//整体是将position到limit之间的数据移到左面。
public LongBuffer compact() {

        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());//将position设置为limit-position
        limit(capacity());//将limit设置为capacity
        discardMark();//将mark设置为-1
        return this;
}

Channel

channel是数据来源或数据写入的目的地,java.nio包中实现的有FileChannel\SocketChannel\DatagramChannel\ServerSocketChannel.

其中:

  • FileChannel用于文件的读写(少用,不是nio包的主要关注目标,不支持非阻塞)
  • DatagramChannel用于UDP的连接和发送
  • SocketChannel用于TCP连接,可以理解为TCP客户端
  • ServerSocketChannel用于TCP对应的服务端,监听某个端口进来的请求。

重点关注SocketChannelServerSocketChannel.

Channel类似于IO中的流,读操作的时候把channel中的数据填充到buffer中,写操作时将Buffer中的数据写入到channel中。

再次说明,NIO层面上的“读”操作和“写”操作是相对于Channel而言,即“读”是从Channel中读到buffer中(对应channe的读取),“写”是从buffer写到channel中(对应channel的写入)。而对应buffer的操作正相反,“读”对应buffer的“写入”,“写”对应buffer的“读取”。

读操作:channel.read(buffer); 将数据从channel读到buffer中,进行后续处理。

写操作:channel.write(buffer); 将数据从Buffer写入到channel中。

※ 这两个方法都是channel实例方法。
※ 所有channel都和buffer做交互。

SocketChannel

打开一个TCP连接:

SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("http://www.baidu.com", 80));

//读取
sc.read(buffer);
//写入
while(buffer.hasRemaining()) {
	sc.write(buffer);
}

后续部分在ServerSocketChannel中再看。

ServerSocketChannel

用于监听机器端口,管理从这个端口进来的TCP连接。

ServerSocketChannel ssc = ServerSocketChannel.open();
//监听8080
ssc.socket().bind(new InetSocketAddress(8080));

while(true) { 
	//一旦有TCP连接进入,对应创建一个socketChannel处理。
	SocketChannel sc = ssc.accept();
}

DatagramChannel

DatagramChannel一个类处理服务端和客户端。

//监听端口
DatagramChannel dc = DatagramChannel.open();
dc.socket().bind(new InetSocketAddress(9090));

ByteBuffer byf = ByteBuffer.allocate(48);
ddc.receive(buf);

//发送数据
String data = "new Data";
ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(data.getBytes());
buf.flip();//准备切换到buffer的读取
int byteSent = dc.send(buf, new InetSocketAddress("anyuri"), 80);

Selector

NIO中的多路复用器,用于一个线程管理多个channel.Selector建立在非阻塞的基础上(FileChannel不能使用).

使用方式(基本的接口操作):

//开启Selector
Selector selector = Selector.open();
//注册Channel到Selector上
//必须开启非阻塞模式
SocketChannel channel = SocketChannel.open(new InetSocketAddress("anyuri", 80));
channel.configureBlocking(false);
//注册
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

register(Selector s, int ops)中第二个int参数,代表需要监听的类型。SelectionKey中共定义了4种类型常量:

OP_READ = 1 << 0; (00000001)
OP_WRITE = 1 << 2; (00000100)
OP_CONNECT = 1 << 3; (00001000)
OP_ACCEPT = 1 << 4; (00010000)

可以同时监听多个事件,如要同时监听read和accept事件,指定ops为OP_READ+OP_ACCEPT即可。

channel注册该方法返回一个selectionKey实例。

接下来调用select()方法获取通道信息。

回顾步骤:

  1. 开启selector
  2. 注册channel
  3. 调用select()

Selector 的其他方法:

  1. select() - 将准备好的channel对应的SelectionKey复制到selected set中,如果没有channel准备好,该方法会阻塞,直到至少有一个channel准备好。指上次select之后准备好的channel
  2. selectNow() - 和上述方法的区别是,如果没有准备好的channel,该方法会立即返回0.
  3. select(long timeout) - 如果没有通道准备好,该方法会等待超时后阻塞,实际select()方法调用的就是select(0L).
  4. wakeup() - 唤醒等待在select()和select(long timeout)上的线程,如果wakeup()先被调用,此时没有线程在select上阻塞,那么之后的一个select()或select(long timeout) 会立即返回,而不会阻塞,该方法只会作用一次。

Selector操作的简单示例:

SocketChannel sc = SocketChannel.open(new InetSocketAddress("anyuri", 80));
sc.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey key = sc.register(selector, SelectionKey.OP_READ);

while(true) {
	int readyChannelNum = selector.select();
	if (readyChannelNum == 0) continue;

	Set<SelectionKey> selectedKeys = selector.selectedKeys();
	Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
	while(keyIterator.hasNext()) {
		if (key.isAcceptable()) {
			//某连接处于accept状态
		} else if (key.isConnectable()) {
			//某连接被远程服务器建立,处于可连接状态
		} else if (key.isReadable()) {
			//某连接处于可读状态
		} else if (key.isWritable()) {
			//某连接处于可写状态
		}
		//移除该key
		keyIterator.remove();
	}
}

你可能感兴趣的:(并发)