nio
阻塞和非阻塞,同步和异步
阻塞和非阻塞
1.阻塞的意思是资源未就位,线程就一直等待就位,例如socket的读写操作,没有数据就一直卡在哪里。或者是我等待《庆余年》更新,但是现在电视还没有开始,那我就盯着电视,这期间我就不干别的了,对于我来说,我就是阻塞状态,这期间我不能做别的事情。
2.如果我一会打开爱奇艺看一下,没有播放,我写点bug,然后过几分钟我再看一下,对于我来说,我就是非阻塞的了。
3.但是这还是太依赖于我这个线程,而《庆余年》这个资源确实是太懒了。那么我就订阅一下,让他通知我,我就坐着等通知,开播了就给我发个短信,对于《庆余年》这个资源和我这个线程来说,就叫做异步阻塞了,我还是什么都干不了。
4.如果我写我的bug,打游戏,等电视开播了就通知我,就是异步非阻塞。我和他互不干扰。
1:同步阻塞,2:同步非阻塞,3:异步阻塞,4:异步非阻塞。
可以看到其实阻塞或者非阻塞是针对线程的,同步或者异步是针对资源的,而异步其实是需要回调或者叫做通知的。
nio
nio就是一个同步非阻塞,例如SocketChannel的read或者write,主要的进步就是如果对于socket套接字如果没有可读,可写的资源,就立刻返回一个错误的信息,我去干别的,一会再来看看,先去干别的。但是一个socket只能针对一个
###nio的内容(channel,buffer,selector)
buffer
buffer就是一个容器,用来装数据,nio数据的读写其实都实在buffer里来操作的,buffer里有三个重要的变量,position,limit, capacity,读写和时候会一直用到他们三个,下边的图介绍了他么的关系。
在设置读模式的时候,position就是这个容器的开始,limit是当前读取的位置,capacity是容器的大小,一般就是最大。从图中可以看出,表示buffer里有五个数字,0,1,2,3,4,limit表示一共有5个可以读的数据,position从0开始读。
在写模式的时候position就是当前写的位置,limit和capacity一样,都是表示容器最大可以写到哪里。如图所示,已经写了0,1,2,那么接下来就从索引3开始写,position就是3。
在nio中buffer是一个抽象类。平时我们用的是他的实现类,用到的大概有8种,
- ByteBuffer
- CharBuffer
- LongBuffer
- IntBuffer
- DoubleBuffer
- FloatBuffer
- ShortBuffer
- MappedByteBuffer
前7中是我们平时常用的八种基本数据类型的7中,没有boolean的buffer,而MappedByteBuffer有些特别,是用来专门读取文件的。接下来会有几个小demo依次讲解怎么用。
buffer的通用方法基本操作的都是position,limit,capacity,所以接下来我们讲解一下他们的常用方法,然后在写代码。
从最基本的开始
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.limit();
buffer.position();
buffer.capacity();
从语法上就能看出来他们的返回值就是前边说的三大金刚。
写转读-flip()方法
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
看源码和最开始的图片对比一下可以发现,其实他是把buffer从写状态变成了读模式。写和读模式的转化其实就是position和limit的转化。
从buffer中获取数据-get()方法
从buffer中读取数据可以直接调用buffer的get方法,这样就会position++,然后返回上一个读取的信息。当然position要小于等于limit。
重新读-rewind()
把position设置成0,其他的不变。
剩余未读数 - remaining()
返回limit和position的差值。
public final int remaining() {
return limit - position;
}
是否有可读数据hasRemain()
public final boolean hasRemaining() {
return position < limit;
}
重新开始写 clear()
public final Buffer clear() {
position = 0;
limit = capacity;
return this;
}
position设置成0,limit = capacity,只是恢复到了写最开始的状态,并没有清除数据,但是可以知道是从哪里写了。
保留部分数据 compact()
如果我们读取了上图中的0,1,2,想要保留3和4,就需要用你compact,他会保留3,4,防止到buffer的最开始。源码如下
public ByteBuffer compact() {
int pos = position();
int lim = limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
unsafe.copyMemory(ix(pos), ix(0), (long)rem << 0);
position(rem);
limit(capacity());
discardMark();
return this;
}
int rem = (pos <= lim ? lim - pos : 0);
这一步就是要计算要保留的数据的大小。copyMemory我不知道底层这么实现的,但是看方法名大概就知道了,然后重新这是三大金刚。
比较大小 compareTo
比较里的元素是不是一样。看源码如下:
public int compareTo(ByteBuffer that) {
int n = this.position() + Math.min(this.remaining(), that.remaining());
for (int i = this.position(), j = that.position(); i < n; i++, j++) {
int cmp = compare(this.get(i), that.get(j));
if (cmp != 0)
return cmp;
}
return this.remaining() - that.remaining();
}
一眼就能看出他们是循环比较大小的,如果没有必要,就不要比较了。
当然还有最特殊的MappedByteBuffer,可以看下班的channel部分,会专门说为什么要有他。
#####代码实例
读写转化
ByteBuffer buffer = ByteBuffer.allocate(256);
System.out.println("put ----------");
buffer.put((byte) 1).put((byte) 2).put((byte) 3);
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("flip ----------");
buffer.flip();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("get ----------");
byte b = buffer.get();
System.out.println("读取数据" + b);
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("compact ----------");
buffer.compact();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("rewind ----------");
buffer.rewind();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("clear ----------");
buffer.clear();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
具体可以看一下执行结果。
channel
channel包含FileChannek,DatagramChannel,SocketChannel,ServerSocketChannel.
- FIleChannel可以读取文件的数据
- DatagramChannel读取UDP的数据
- SocketChannel读取tcp的数据
- ServerSocketChanne监听网络,对于每一个新进来的链接都会创建一个SocketChannel
他就是一个管道,用来进行数据的传输,至于数据的操作,其实是在buffer里的。下边就是范例,表示读取数据的代码。
FileChannel
try {
ByteBuffer buffer = ByteBuffer.allocate(1024);
RandomAccessFile file = new RandomAccessFile("/Users/menghaibin/Desktop/test.sql","r");
FileChannel channel = file.getChannel();
//获取数据到到buffer
int read = channel.read(buffer);
while (read != -1){
//如果数据没有读完,则打印数据
while (buffer.hasRemaining()){
System.out.print((char)buffer.get());
}
//将position设置成0,limit=capacity,表示可以重新写入
buffer.clear();
//接着读取数据到buffer
read = channel.read(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
DatagramChannel
UDP是一个无连接的协议,所以不能像其它通道那样读取和写入,他是发送和接受数据包。接受数据是receive方法来接收数据。发送数据是send方法。链接到制定地址为connect,发送消息和接受消息代码如下:
客户端
package com.mhb.nio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Scanner;
public class TestDatagradChannelSend {
public static void main(String[] args) throws IOException {
final DatagramChannel channel = DatagramChannel.open();
new Thread(new Runnable() {
@Override
public void run() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] bytes = null;
while (true){
buffer.clear();
SocketAddress socketAddress = null;
try {
socketAddress = channel.receive(buffer);
} catch (IOException e) {
e.printStackTrace();
}
if (socketAddress != null){
int position = buffer.position();
bytes = new byte[position];
buffer.flip();
for (int i = 0; i < position; i++) {
bytes[i] = buffer.get();
}
}
try {
System.out.println("receive remote " + socketAddress.toString() + ":" + new String(bytes, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
}).start();
while (true){
Scanner sc = new Scanner(System.in);
String next = sc.next();
try {
sendMessage(channel, next);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void sendMessage(DatagramChannel channel, String mes) throws IOException {
if (mes == null || mes.isEmpty()) {
return;
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put(mes.getBytes("UTF-8"));
buffer.flip();
System.out.println("send msg:" + mes);
int send = channel.send(buffer, new InetSocketAddress("localhost",8989));
}
}
服务端
package com.mhb.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
public class TestDatagramChannelReceive {
public static void main(String[] args) throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.bind(new InetSocketAddress(8989));
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] b;
while (true){
buffer.clear();
SocketAddress socketAddress = channel.receive(buffer);
if (socketAddress != null){
int position = buffer.position();
b = new byte[position];
buffer.flip();
for (int i = 0; i < position; i++) {
b[i] = buffer.get();
}
System.out.println("receive remote" + socketAddress.toString() + ":" + new String(b, "UTF-8"));
sendReback(socketAddress, channel);
}
}
}
public static void sendReback(SocketAddress socketAddress, DatagramChannel channel) throws IOException {
String message ="我收到了消息";
ByteBuffer allocate = ByteBuffer.allocate(1024);
allocate.put(message.getBytes());
allocate.flip();
channel.send(allocate, socketAddress);
}
}
ServerSocketChannel
ServerSocketChannel用open来创建一个channel,然后通过socket().bind方法绑定端口,accept()来坚挺请求,通过configureBlocking来设置是否阻塞
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
}
selector
selector就是java中的多路复用器,用来检测nio中的channle是否可读,可写状态。也可以管理网络连接
如上图,他能用更少的线程完成更多的事情,而不是像传统的socket,如果要多个连接,我们只能使用多线程来创建channel,channel是多个的,但是管理还是靠一个selector来创建的。
判断selector注册了那些时间,可以用is***方法来判断,例如isAcceptable, isConnectable, isReadable, isWritable方法,示例如下:
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey register = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println(SelectionKey.OP_ACCEPT);
int i = register.interestOps();
boolean acceptable = register.isAcceptable();
register.isConnectable();
System.out.println(i);
}
总结
简单来说,nio用到的就是buffer,channel,selector,buffer就是我们就是数组,channel用来传输数据,selector就是一个容器,用来关注channel,有关注的channel,就可以把channel的数据buffer做交换处理了