一、预备知识
1.BIO
Java BIO(Blocking IO) 是阻塞IO,工作原理图如下:
特点:
- 服务器需要为每一个客户端的连接请求启动一个线程,当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于连接数不是很多,但是需要连续传输大量数据的场景。比如数据库连接:数据库的连接 都是事先创建好的 , 而且 数据库端和java端通常会有大量的数据传输。
2.NIO
NIO(Non-Blocking IO) 同步非阻塞IO,工作原理如下:
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
特点:
- 高性能,吞吐量更高,延迟更低;减少资源消耗;
- 传输快,原有的 IO 是面向流的、阻塞的,NIO 则是面向块的、非阻塞的(buffer的优势)。
三大组件:channel,Buffer,selector
3.Channel(通道)
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
这些是Java NIO中最重要的通道的实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel 从文件中读写数据。
DatagramChannel 能通过UDP读写网络中的数据。
SocketChannel 能通过TCP读写网络中的数据。
ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
4.Buffer(缓冲区)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。
为了理解Buffer的工作原理,需要熟悉它的三个属性:
1) capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
2) position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
3) limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式(调用filp方法)时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
5.Selector
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。
6.简单的nio范例
/**
* <p>Description:客户端</p>
*
*/
public class NIOCilent {
public static void main(String[] args) throws Exception{
//得到网络通道
SocketChannel socketChannel = SocketChannel.open();
//非阻塞
socketChannel.configureBlocking(false);
InetSocketAddress localhost = new InetSocketAddress("127.0.0.1", 6666);
if(!socketChannel.connect(localhost)){
while(!socketChannel.finishConnect()){
System.out.println("因为链接需要时间,客户端不会阻塞");
}
}
String str ="hello";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(buffer);
System.in.read();
}
}
/**
* <p>Description:服务端</p>
*
*/
public class NIOServer {
public static void main(String[] args) throws Exception {
//创建serversocketchannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到selector对象
Selector selector = Selector.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//非阻塞
serverSocketChannel.configureBlocking(false);
//注册到selector
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(1000) == 0) {
System.out.println("服务器等待了1秒,无连接");
continue;
}
//获得相关的集合
//如何返回大于0,表示以获取到关注的时间
//返回关住事件的集合
//通过key反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获得key
SelectionKey key = iterator.next();
//根据key对应的通道发生的事件做处理
//如果是OP_Accept,有新的链接
if (key.isAcceptable()) {
//该客户端生成了一个socketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
//将但钱的channel注册到selector上,关注事件为read,同时关联一个buffer
System.out.println("客户端链接成功,生成一个socketChannel" + socketChannel.hashCode());
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) {
//通过key反向获取对应的channel
SocketChannel channel = (SocketChannel)key.channel();
//获得关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("from 客户端" + new String(buffer.array()));
}
iterator.remove();
}
}
}
}