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对应的服务端,监听某个端口进来的请求。
重点关注SocketChannel和ServerSocketChannel.
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()方法获取通道信息。
回顾步骤:
- 开启selector
- 注册channel
- 调用select()
Selector 的其他方法:
- select() - 将准备好的channel对应的SelectionKey复制到selected set中,如果没有channel准备好,该方法会阻塞,直到至少有一个channel准备好。指上次select之后准备好的channel
- selectNow() - 和上述方法的区别是,如果没有准备好的channel,该方法会立即返回0.
- select(long timeout) - 如果没有通道准备好,该方法会等待超时后阻塞,实际select()方法调用的就是select(0L).
- 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();
}
}