Java Nio简介
java Nio指的是java在1.4版本后推出的new io,nio的主要特性提升是io操作不再是阻塞的了,通常io我们会分为文件io和网络io,对于文件io操作nio还是阻塞的,但是对于socket的操作则不是阻塞的了。
由于oio(旧io)是阻塞的所以是无法在单线程内实现并发的,只能依赖一个io操作一个线程的方式来实现并发,但是线程的开销对于系统来说是太大了,而nio的实现就可以解决这一问题,nio可以通过单线程管理多个网络连接,并且每一个连接都是非阻塞的,单线程通过判断每个连接的就绪状态来管理并发,这也是IO多路复用的模式。
Java Nio的三大组件
- Buffer 缓存区
- Channel 通道
- Selector 选择器
Buffer
buffer是缓存区的意思,就好像是一个容器,用来存储需要io操作的数据,Java Nio一共有8个buffer类型:
分别对应java的四类八种基本数据类型。
Buffer源码分析
四大关键属性
private int mark = -1; // 记录的是position
private int position = 0; // 当前写或者读的位置
private int limit; // buffer的最大读写限制
private int capacity; // 容量
三大关键方法
/**
* 清空buffer,buffer转为可写
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
/**
* 将当前位置转为buffer最大限制,并将位置重为0,buffer转为可读
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
/**
* 将当前位置重置为0,buffer可重复读
*/
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
有人会问buffer作为一个数据容器没有存取的操作吗?当然有,只是buffer的存取操作非常简单,就是简单的put和get方法,但是put和get方法的实现依赖于上述的四大属性,无论是从buffer中读数据还是像buffer中写数据,都要移动postion,即position+1,当position=limit时数据存取到头,最后要注意buffer默认是可写的
另外,需要理解的是buffer只是一个容器,对于IO操作来说还要把数据写出去,将数据写出去的操作是由Channel来完成的,而Channel在写数据时会先读取buffer里的数据,Channel在读取数据时会写进buffer,也即是buffer的读写和Channel的读写正好反过来形成一个闭合回路。
Channel
channel总共分为大类型:
- 文件通道 FileChannel
- socket通道 ServerSocketChannel SocketChannel
- 数据通道 DatagramChannel
channel其实可以类比oio的stream,下面写一个Nio的文件拷贝demo
/**
* 文件拷贝
* @param src
* @param desc
*/
private static void fileCopy(String src,String desc){
File srcFile = new File(src);
File descFile = new File(desc);
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(descFile);
inChannel = fis.getChannel();
outChannel = fos.getChannel();
// 定义一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int length = -1;
while ((length = inChannel.read(buffer)) != -1){
// 此时将buffer转为可读 outChannel 读取buffer并写出去
System.out.println("读入字节:"+length);
buffer.flip();
int outLength = 0;
while ((outLength = outChannel.write(buffer)) != 0){
System.out.println("写入字节:"+outLength);
}
buffer.clear();
}
outChannel.force(true);
}catch (Exception e){
System.out.println(e.toString());
}finally {
close(fis);
close(fos);
close(inChannel);
close(outChannel);
}
}
以上代码的流程可以拆分为一下四步:
- inChannel将数据读进buffer
- buffer.flip() 将buffer变成可读
- outChannel将buffer写出去
- buffer.clear() 将buffer变成可写
由于socket通道和数据通道都用到了选择器,所以放在下面讲,其实用法文件通道一样,所有IO操作无非是读和写。
Selector
selector故名选择器,是用来监控和管理channel的,之所以单线程实现并发就是基于selector的,原理是将channel注册进selector里,写个socket编程的demo
服务端:
public class NioReceiveServer {
static class Client{
// 文件名称
String fileName;
// 文件长度
long length;
// 文件开始传输时间
long startTime;
// 客户端地址
InetSocketAddress remoteAddress;
// 文件输出通道
FileChannel fileChannel;
}
private ByteBuffer buffer = ByteBuffer.allocate(1024);
private Charset charset = Charset.forName("UTF-8");
Map<SelectableChannel,Client> clientMap = new HashMap<SelectableChannel,Client>();
private static final String DESC_PATH = "/Users/wengyuzhu/Desktop/demo";
public void startServer() throws IOException {
// 1.获取选择器
Selector selector = Selector.open();
// 2.获取socket通道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
// 3.获取socket
ServerSocket socket = socketChannel.socket();
// 4.通道设置非阻塞
socketChannel.configureBlocking(false);
// 5.端口绑定
socket.bind(new InetSocketAddress(18888));
// 6.将通道注册到选择器上
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("=========Server start=========");
while (selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()){
SelectionKey key = it.next();
if (key.isAcceptable()){
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
if (channel == null) {
continue;
}
channel.configureBlocking(false);
channel.register(selector,SelectionKey.OP_READ);
// 初始化客户端
Client client = new Client();
client.remoteAddress =(InetSocketAddress) channel.getRemoteAddress();
clientMap.put(channel,client);
System.out.println("=========Client connect=========");
}else if (key.isReadable()){
System.out.println("=========Client write=========");
processData(key);
}
it.remove();
}
}
}
private void processData(SelectionKey key){
Client client = clientMap.get(key.channel());
SocketChannel channel =(SocketChannel) key.channel();
int length = -1;
try {
//
buffer.clear();
while ((length = channel.read(buffer)) > 0){
buffer.flip();
if (client.fileName == null) {
String fileName = "1.txt";
System.out.println("文件名称:"+fileName);
File descPath = new File(DESC_PATH);
if (!descPath.exists()) {
descPath.mkdir();
}
String fullName = descPath.getAbsolutePath() + File.separator + fileName;
File file = new File(fullName);
FileChannel fileChannel = new FileOutputStream(file).getChannel();
client.fileChannel = fileChannel;
client.fileName = fileName;
System.out.println("文件名称:"+fileName);
}else if (client.length == 0){
long longa = buffer.getLong();
client.length = longa;
client.startTime = System.currentTimeMillis();
System.out.println("文件大小:"+longa);
}else {
// 文件内容
client.fileChannel.write(buffer);
System.out.println("接收文件内容:"+length);
}
buffer.clear();
}
client.fileChannel.force(true);
if (length == -1){
// 传输完毕
channel.close();
client.fileChannel.close();
key.cancel();
System.out.println("========上传完毕========");
System.out.println("======文件名称:"+client.fileName);
System.out.println("======文件大小:"+client.length);
}
}catch (Exception e){
key.cancel();
System.out.println("服务器异常:error = "+e.toString());
}
}
public static void main(String[] args) throws IOException {
NioReceiveServer server = new NioReceiveServer();
server.startServer();
}
}
客户端:
public class NioClient {
private static final String SRC = "/Users/wengyuzhu/Desktop/JAVA并发编程实战.pdf";
private Charset charset = Charset.forName("UTF-8");
private void sendFile() {
try{
File file = new File(SRC);
if (!file.exists()){
System.out.println("文件不存在");
return;
}
// 文件通道
FileChannel fileChannel = new FileInputStream(file).getChannel();
// socket连接
SocketChannel socketChannel = SocketChannel.open();
socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",18888));
socketChannel.configureBlocking(false);
System.out.println("开始连接服务器");
// 自旋连接
while (!socketChannel.finishConnect()){
}
// 写出文件名
ByteBuffer bf = charset.encode(file.getName());
System.out.println(file.getName());
bf.flip();
socketChannel.write(bf);
// 写出文件大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putLong(file.length());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
// 写出文件
System.out.println("开始传输文件");
int length = -1;
long pos = 0;
while ((length = fileChannel.read(buffer)) != -1){
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
pos += length;
System.out.println("传输文件大小:"+pos);
}
fileChannel.close();
socketChannel.shutdownInput();
socketChannel.close();
System.out.println("文件传输完成");
}catch (Exception e){
System.out.println("文件传输失败");![在这里插入图片描述](https://img-blog.csdnimg.cn/20200114213255601.jpg)
}
}
public static void main(String[] args) {
NioClient client = new NioClient();
client.sendFile();
}
}
总结
nio的重点我觉得在于理解buffer的数据结构和学习selector的编程思想,至于channel的用法和之前流的用法并无明显区别。
扫码关注个人公众号
来源:CSDN
作者:yutian_1999
链接:https://blog.csdn.net/yutian_1999/article/details/103979202