在IO执行有两个阶段很重要:
- 等待数据准备
- 将数据从内核复制到进程中
BIO(Blocking I/O)阻塞I/O模型
当用户进程调用了recvfrom这个系统调用,内核就开始了io的第一个阶段:等待数据准备。如果数据还没准备好(比如还没有收到一个完整的udp包),这时候内核要等待足够的数据到来,而在用户进程这边,进程会被阻塞。当内核等到数据准备好,进程将数据从内核中拷贝到用户空间,然后内核返回结果,用户进程才解除block状态,重新运行起来。
BIO在IO执行的两个阶段都被阻塞了。
示例代码:
public static void server(){
ServerSocket serverSocket = null;
InputStream in = null;
try
{
serverSocket = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
Socket clntSocket = serverSocket.accept();
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
System.out.println("Handling client at "+clientAddress);
in = clntSocket.getInputStream();
while((recvMsgSize=in.read(recvBuf))!=-1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println(new String(temp));
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally{
try{
if(serverSocket!=null){
serverSocket.close();
}
if(in!=null){
in.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
NIO(Non-Blocking I/O)非阻塞I/O模型
NIO是一种同步非阻塞的IO模型。同步指线程不断轮询IO事件是否就绪,非阻塞是指线程在等待IO的时候,可以做其他任务。同步的核心是Selector,Selector代替了线程本身轮询IO事件,避免了阻塞同时减少了不必要的线程消耗;非阻塞的核心是通道和缓冲区,当IO事件就绪时,可以通过写入缓冲区,保证IO的成功,无需线程阻塞式地等待。
当用户进程调用recvfrom时,系统不会阻塞用户进程,如果数据还没准备好,系统立刻返回一个ewouldblock错误给进程,用户进程知道数据还没准备好,用户进程就可以去做其他事了。进程轮询内核查看数据是否准备好。直到数据准备好了,并且收到用户进程的system call,进程复制数据报,然后返回。
NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(多路复用器)
Channel
基本上,所有的IO在NIO中都从一个Channel开始。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel。
Buffer
Buffer通过以下几个变量来保存这个数据的当前位置状态:
capacity:缓冲区数组的总长度
position:下一个要操作的数据元素的位置
limit:缓冲区数组中不可操作的下一个元素的位置
mark:用于记录当前position的前一个位置或者默认是-1
0 <= mark <= position <= limit <= capacity开始时Buffer的position为0,limit为capacity,程序写入数据到缓冲区,position往后移。当Buffer写入数据结束之后,调用flip()方法之后(此时limit=position,position=0),Buffer为输出数据做好准备;当Buffer输出数据结束之后,调用clear()方法(此时limit=capacity,position=0),clear()方法不是清空Buffer的数据,它仅仅将position置为0,将limit置为capacity,为再次向Buffer装入数据做准备;
Buffer的使用:
- 分配空间:ByteBuffer buf = ByteBuffer.allocate(1024);
- 写入数据到Buffer:int bytesRead = fileChannel.read(buf);
- 调用flip()方法:buf.flip();
- 从Buffer中读取数据:buf.get();
- 调用clear()方法或compact()方法
Buffer一些重要方法:
- flip():把limit设置为position,position设置为0,为输出数据做好准备
- clear():把position设置为0,limit设置为capacity,为再次向Buffer装入数据做准备
- compact():将所有未读的数据拷贝到Buffer起始处,把position设置到最后一个未读元素的正后面,limit设置为capacity
- mark():可以标记Buffer中的一个特定的position,之后可以通过reset()恢复到position的位置
- rewind():将position设回为0,limit保持不变,这样可以重读Buffer中的所有数据。
示例代码:
public static void client(){
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = null;
try
{
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));
if(socketChannel.finishConnect())
{
int i=0;
while(true)
{
TimeUnit.SECONDS.sleep(1);
String info = "I'm "+i+++"-th information from client";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while(buffer.hasRemaining()){
System.out.println(buffer);
socketChannel.write(buffer);
}
}
}
}
catch (IOException | InterruptedException e)
{
e.printStackTrace();
}
finally{
try{
if(socketChannel!=null){
socketChannel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
Selector
Channel和Selector配合使用,必须先将Channel注册到Selector上,通过register()方法来实现。Channel.register()方法会返回一个SelectionKey对象。这个对象代表了注册到该Selector的通道。
一旦向Selector注册了一个或多个通道,就可以调用select()方法。
select()方法返回的int值表示有多少通道已经就绪。
参考资料:疯狂Java讲义