1、NIO概念
1.1 Unix定义了五种 I/O 模型
- 阻塞 I/O
- 非阻塞 I/O
- I/O 复用
- 信号驱动 I/O
- 异步 I/O
1.1.1 阻塞(Block)和非阻塞(NonBlock)
阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式:
阻塞:需要等待缓冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。
非阻塞:进程访问数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。
1.1.2 同步与异步
一个进程的IO调用步骤大致如下:
1、进程向操作系统请求数据
2、操作系统把外部数据加载到内核的缓冲区中
3、操作系统把内核的缓冲区拷贝到进程的缓冲区
4、进程获得数据完成自己的功能
当操作系统在把外部数据放到进程缓冲区的这段时间(即上述的第二,三步),如果应用进程是挂起等待的,那么就是同步IO,反之,就是异步IO
Java中IO分为:
BIO (Blocking IO)
NIO (Non-blocking IO/New IO)
AIO (Asynchronous IO/NIO2)
1.2 NIO模型图
2、 NIO与传统IO的区别
IO | NIO |
---|---|
面向流 | 面向缓冲 |
阻塞IO | 非阻塞IO |
无 | selector |
2.1 面向流与缓冲
IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性
2.2 阻塞与非阻塞
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
2.3 选择器(Selector)
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
3、NIO构成组件
Java NIO Channel通道和流非常相似,主要有以下几点区别:
a.通道可以读也可以写,多功能高速通道,流一般来说是单向的(只能读或者写,如输入流与输出流);
b.通道可以异步读写;
c.通道总是基于缓冲区Buffer来读写;
正如上面提到的,我们可以从通道中读取数据,写入到buffer,也可以中buffer内读数据,写入到通道中;下面有个示意图:
Channel的实现:
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
FileChannel用于文件的数据读写。
DatagramChannel用于UDP的数据读写。
SocketChannel用于TCP的数据读写。
ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel.
Buffer用于和Channel交互。我们从channel中读取数据到buffers里,从buffer把数据写入到channel,Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
3.1 Buffer基本用法
利用Buffer读写数据,通常遵循四个步骤:
- 把数据写入buffer;
- 调用flip;
- 从Buffer中读取数据;
- 调用buffer.clear()或者buffer.compact()
当写入数据到buffer中时,buffer会记录已经写入的数据大小。当需要读数据时,通过flip()方法把buffer从写模式调整为读模式;在读模式下,可以读取所有已经写入的数据。
当读取完数据后,需要清空buffer,以满足后续写入操作。清空buffer有两种方式:调用clear()或compact()方法。clear会清空整个buffer,compact则只清空已读取的数据,未被读取的数据会被移动到buffer的开始位置,写入位置则近跟着未读数据之后。
Buffer有三个属性:
容量,位置,上限(capacity, position , limit)
buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操作这块内存。
3.2 Java NIO Selector
Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。因此此可以实现单线程管理多个channels,也就是可以管理多个网络连接。
1.创建
要使用必须先创建一个:
创建一个selector可以通过selector.open()方法
2.注册
如果要监测channel,必须先把channel注册到Selector上,这个操作使用channel.register(),
3.监测
我们关注的channel状态,有四种基础类型可供监听:(观察客人发来的需求)
Connect, Accept, Read ,Write 通过selectedKeys(),可以获取所有channel的相关信息,包括channel本身,以及channel的状态等信息,知道了channel的状态,我们就可以对channel进行相应的操作。
4、基于NIO实现的多人聊天
4.1 服务端
public class NIOSelectorDemo {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel
ServerSocketChannel server = ServerSocketChannel.open();
//绑定9090端口
server.bind(new InetSocketAddress(9090));
//设置为非阻塞
server.configureBlocking(false);
//创建Selector
Selector selector = Selector.open();
//当serversocket有连接请求时,监控这个事件
server.register(selector, SelectionKey.OP_ACCEPT);
//创建SocketChannel的ArrayList用于管理客户端与服务端的连接
ArrayList<SocketChannel> clients = new ArrayList<SocketChannel>();
while(true)
{
//阻塞,当至少有一个channel上有事件发生时,返回
//selector.select()返回了,意味着channel发生可处理事件
selector.select();
//获得可处理的事件selectionKeys(包含目标channel)
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while( it.hasNext() )
{
SelectionKey key = it.next();
it.remove();
//表示可接收请求
if(key.isAcceptable())
{
ServerSocketChannel socket = (ServerSocketChannel)key.channel();
//接收到客户端请求时生成SocketChannel对象,用于和客户的数据传输
SocketChannel client = socket.accept();
//设置为非阻塞
client.configureBlocking(false);
//注册进Selector
client.register(selector, SelectionKey.OP_READ);
//向客户端发送消息“hello”
ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
client.write(buf);
//将新客户端添加到客户列表里
clients.add(client);
}
else if(key.isReadable())
{
SocketChannel client = (SocketChannel)key.channel();
//接收来自客户端的数据,组装成msg
ByteBuffer buf = ByteBuffer.allocate(1024);
client.read(buf);
buf.flip();
String msg = client.getRemoteAddress()+"==>"+new String(buf.array(),0,buf.limit());
//将msg发送给所有客户端
buf.clear();
buf.put(msg.getBytes());
buf.flip();
for(int i = 0; i < clients.size(); i++)
{
SocketChannel c = clients.get(i);
c.write(buf);
buf.rewind();
}
}
}
}
}
}
4.2 客户端
4.2.1 读线程
class ReadThread implements Runnable
{
private SocketChannel socket = null;
public ReadThread(SocketChannel socket) {
// TODO Auto-generated constructor stub
this.socket = socket;
}
@Override
public void run() {
// TODO Auto-generated method stub
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true)
{
try {
int ret = socket.read(buffer);
if(ret == -1)
break;
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
}
4.2.2 写线程
class WriteThread implements Runnable
{
private SocketChannel socket = null;
public WriteThread(SocketChannel socket) {
// TODO Auto-generated constructor stub
this.socket = socket;
}
@Override
public void run() {
// TODO Auto-generated method stub
ByteBuffer buffer = ByteBuffer.allocate(1024);
String msg = "";
Scanner scanner = new Scanner(System.in);
while(true)
{
msg = scanner.nextLine();
buffer.clear();
buffer.put(msg.getBytes());
buffer.flip();
try {
socket.write(buffer);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
}