Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
上一篇《NIO简单介绍(一)》中讲解了NIO中本地IO相关的内容,这篇重点介绍的NIO的非阻塞式网络通信
一、阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
二、基础概念
选择器( Selector) 是 SelectableChannel 对象的多路复用器, Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。 Selector 是非阻塞 IO 的核心。
SelectionKey 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。 选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
SocketChannel 是一个连接到 TCP 网络套接字的通道。
DatagramChannel 是一个能收发 UDP 包的通道。
Selector 管理 Channel (多路复用)原理图如下:
管道是2个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink 通道。数据会被写到sink通道,从 source 通道读取。
管道原理图如下:
三、API 介绍
3.1 Selector 的常用方法
方法 |
说明 |
Set keys() |
所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() |
被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() |
监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) |
可以设置超时时长的 select() 操作 |
int selectNow() |
执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() |
使一个还未返回的 select() 方法立即返回 |
void close() |
关闭该选择器 |
3.2 SelectionKey 的常用方法
方法 |
说明 |
int interestOps() |
获取感兴趣事件集合 |
int readyOps() |
获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() |
获取注册通道 |
Selector selector() |
返回选择器 |
boolean isReadable() |
检测 Channal 中读事件是否就绪 |
boolean isWritable() |
检测 Channal 中写事件是否就绪 |
boolean isConnectable() |
检测 Channel 中连接是否就绪 |
boolean isAcceptable() |
检测 Channel 中接收是否就绪 |
四、JDK 7中NIO的新功能
随着 JDK 7 的发布, Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。因为 NIO 提供的一些功能, NIO 已经成为文件处理中越来越重要的部分。
4.1 Path 与 Paths
java.nio.file.Path 接口代表一个平台无关的平台路径,描述了目录结构中文件的位置。
Paths 提供的 get() 方法用来获取 Path 对象:
Path get(String first, String … more) : 用于将多个字符串串连成路径。
Path 常用方法:
方法 |
说明 |
boolean endsWith(String path) |
判断是否以 path 路径结束 |
boolean startsWith(String path) |
判断是否以 path 路径开始 |
boolean isAbsolute() |
判断是否是绝对路径 |
Path getFileName() |
返回与调用 Path 对象关联的文件名 |
Path getName(int idx) |
返回的指定索引位置 idx 的路径名称 |
int getNameCount() |
返回Path 根目录后面元素的数量 |
Path getParent() |
返回Path对象包含整个路径,不包含 Path 对象指定的文件路径 |
Path getRoot() |
返回调用 Path 对象的根路径 |
Path resolve(Path p) |
将相对路径解析为绝对路径 |
Path toAbsolutePath() |
作为绝对路径返回调用 Path 对象 |
String toString() |
返回调用 Path 对象的字符串表示形式 |
4.2 Files
java.nio.file.Files 用于操作文件或目录的工具类。
Files常用方法:
方法 |
说明 |
Path copy(Path src, Path dest, CopyOption … how) |
文件的复制 |
Path createDirectory(Path path, FileAttribute<?> … attr) |
创建一个目录 |
Path createFile(Path path, FileAttribute<?> … arr) |
创建一个文件 |
void delete(Path path) |
删除一个文件 |
Path move(Path src, Path dest, CopyOption…how) |
将 src 移动到 dest 位置 |
long size(Path path) |
返回 path 指定文件的大小 |
boolean exists(Path path, LinkOption … opts) |
判断文件是否存在 |
boolean isDirectory(Path path, LinkOption … opts) |
判断是否是目录 |
boolean isExecutable(Path path) |
判断是否是可执行文件 |
boolean isHidden(Path path) |
判断是否是隐藏文件 |
boolean isReadable(Path path) |
判断文件是否可读 |
boolean isWritable(Path path) |
判断文件是否可写 |
boolean notExists(Path path, LinkOption … opts) |
判断文件是否不存在 |
InputStream newInputStream(Path path, OpenOption…how) |
获取 InputStream 对象,how 指定打开方式。 |
OutputStream newOutputStream(Path path, OpenOption…how) |
获取 OutputStream 对象,how 指定打开方式。 |
DirectoryStream newDirectoryStream(Path path) |
打开 path 指定的目录 |
SeekableByteChannel newByteChannel(Path path, OpenOption…how) |
获取与指定文件的连接,how 指定打开方式 |
4.3 自动资源管理
Java 7 增加了一个新特性,该特性提供了另外一种管理资源的方式,这种方式能自动关闭文件。这个特性有时被称为自动资源管理(Automatic Resource Management, ARM), 该特性以 try 语句的扩展版为基础。自动资源管理主要用于,当不再需要文件(或其他资源)时,可以防止无意中忘记释放它们。
五、测试
5.1 阻塞式
public class TestBlockingNIO {
//客户端
@Test
public void client() throws IOException{
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
FileChannel inChannel = FileChannel.open(Paths.get("d:/test.jpg"), StandardOpenOption.READ);
ByteBuffer buf = ByteBuffer.allocate(1024);
while(inChannel.read(buf) != -1){
buf.flip();
sChannel.write(buf);
buf.clear();
}
sChannel.shutdownOutput();
//接收服务端的反馈
int len = 0;
while((len = sChannel.read(buf)) != -1){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
inChannel.close();
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
ServerSocketChannel ssChannel = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("d:/copy.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
ssChannel.bind(new InetSocketAddress(9898));
SocketChannel sChannel = ssChannel.accept();
ByteBuffer buf = ByteBuffer.allocate(1024);
while(sChannel.read(buf) != -1){
buf.flip();
outChannel.write(buf);
buf.clear();
}
//发送反馈给客户端
buf.put("服务端接收数据成功".getBytes());
buf.flip();
sChannel.write(buf);
sChannel.close();
outChannel.close();
ssChannel.close();
}
}
5.2 非阻塞式( TCP )
public class TestNonBlockingNIO {
//客户端
@Test
public void client() throws IOException{
//1. 获取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 关闭通道
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
}
5.3 非阻塞式( UDP )
public class TestNonBlockingNIO2 {
@Test
public void send() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
@Test
public void receive() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}
}
参考资料:
http://www.importnew.com/17759.html