通过阅读java IO 源码可以发现 IO 的基本架构
注:此篇内容纯粹是为了记录和分享一下学到的内容,图片和代码部分来源于其他博客,请见谅,希望阅读到此篇博客的人能够留下您的意见,给一个学习的机会,在此非常感谢!!!
* 基于字节操作的IO接口: InputStream, OutputStream
*
* 基于字符操作的接口:Reader,Writer
*
* 基于磁盘操作的IO接口:File
*
* 基于网络操作的IO接口 :Socket
*
* 字符解码相关类结构
*
* 字符编码相关类结构
*
*
磁盘IO的工作机制
* (1)标准的访问文件的方式
* 由于访问磁盘文件,需要调用操作系统的接口,程序访问物理设备只能通过操作系统调用的方式工作。
* 利用操作系统调用就会存在用户地址空间和系统内核空间地址切换的问题(操作系统为了保护系统本身运行安全将内核程序的地址空间和应用程地址空间隔离)。
* 标准访问文件的方式就是当用户调用read()接口时,操作系统会检查内核的高速缓存中是否已经缓存,如果有那么直接返回缓存,否则从磁盘中读取,然后缓存到用户的应用缓存中
* 对于写入就是从用户地址空间复制到内核的告诉页面缓存,至于什么时候复制到磁盘由系统决定,应用程序可以调用sync显式的同步
*
* (2)直接文件访问方式
* 直接IO 访问就是应用程序不经过操作系统的内核缓存而直接访问磁盘空间,这样就减少了一次从从内核缓存到应用程序缓存的复制
*
* (3)同步访问文件方式
* 同步访问文件的方式就是当写入到磁盘空间时其才会返回成功写入的标志
*
* (4)异步文件访问方式
* 对于异步文件访问方式来说,当应用程序发出read()的请求时,不需要阻塞的等待内核缓存将聚聚写入应用程序的缓存,
* 当请求的数据返回时才会继续处理请求的数据,异步的方式只是提高了程序的效率,但没有改变访问文件的效率
*
* (5)地址映射方式
* 内存映射的方式就是将磁盘中的文件和操作系统中的一块内存地址映射,当要访问这个文件时直接将内存中的这段数据转换为访问文件的某段数据
* 目的是为了减少数据从内存空间到用户地址空间的复制操作。
*
*
* java 访问磁盘文件的方式
* 虚拟的File 对象, FileDescripter
*
* java序列化技术
* 通用的数据结构 JSON XML
* -
*
NIO的工作方式
*
* 传统的IO方式在访问文件时,可能会出现阻塞,而一旦出现阻塞当前线程就睡失去CPU的使用权,当用采用每个连接建立一个线程的方式,
* 出现阻塞只会阻塞一个线程而不会影响其他工作,为了避免频繁建立线程对系统资源的占用,可以通过建立线程池的方式
* 减少系统资源的开销,
*
* NIO 的相关类图
*
一个关于NIO的例子
/**
* NIO服务端
*
*
* 在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,
* 它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。
缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。
但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
*
*
*/
public class NIOServer {
//通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
* @param port 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException {
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
//当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
@SuppressWarnings("unchecked")
public void listen() throws IOException {
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 客户端请求连接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key
.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给客户端发送信息哦
channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
//在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取客户端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("服务端收到信息:"+msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}
/**
* 启动服务端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
}
}
/**
* NIO客户端
*/
public class NIOClient {
//通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip,int port) throws IOException {
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
//用channel.finishConnect();才能完成连接
channel.connect(new InetSocketAddress(ip,port));
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
@SuppressWarnings("unchecked")
public void listen() throws IOException {
// 轮询访问selector
while (true) {
// 选择一组可以进行I/O操作的事件,放在selector中,客户端的该方法不会阻塞,
//这里和服务端的方法不一样,查看api注释可以知道,当至少一个通道被选中时,
//selector的wakeup方法被调用,方法返回,而对于客户端来说,通道一直是被选中的
selector.select();
// 获得selector中选中的项的迭代器
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 连接事件发生
if (key.isConnectable()) {
SocketChannel channel = (SocketChannel) key
.channel();
// 如果正在连接,则完成连接
if(channel.isConnectionPending()){
channel.finishConnect();
}
// 设置成非阻塞
channel.configureBlocking(false);
//在这里可以给服务端发送信息哦
channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
//在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
} else if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息 的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException{
//和服务端的read方法一样
}
/**
* 启动客户端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
NIOClient client = new NIOClient();
client.initClient("localhost",8000);
client.listen();
}
}
Buffer 的工作方式:
* 通过阅读ByteBuffer源码可以知道其通过底层字节数组实现,里面包括 limit(限制)、position(位置)、mark(标记)、capacity(容量)四个字段
*
* 可以通过 ByteBuffer.allocate(size)或者ByteBuffer.allocateDirect(size)的方式来创建缓冲区,
* 前者的(HeapByteBuffer) 创建位置在java 对内存中,需要用户地址空间和操作系统地址空间之间复制数据,有java GC回收,创建和回收的开销较小
* 而后者(DirectByteBuffer)就是与底层的存储空间关联的缓冲区,不需要复制,但要通过System.gc()释放掉java对象引用的DirectByteBuffer内存
* 可能会导致内存泄露,创建和回收的开销较大。
*
NIO 访问数据的方式
*
* NIO提供了两个更好的访问文件的方式:
* FileChannel.transferTo()、FileChannel.transferFrom()
* 这两种方式数据直接在内存地址空间移动,减少了内存地址空间到用户地址空间的复制
*
* FileChannel.map()
* 将文件映射为一块儿内存区域,应用程序访问这块内存区域可以直接操作文件,省去了从内存地址空间到用户地址空间的复制的损耗。适合于对大文件的只读性操作。
* 具体实现和MappedByteBuffer 有关,可以参考其源码。
*
/**
* 文件复制
* @param srcfilePath 源文件路径
* @param targetPath 目标文件路径
* @throws IOException
*/
private static void copy(String srcfilePath, String targetPath) throws IOException {
File file = new File(targetPath);
if (!file.getParentFile().exists()) {
file.mkdirs();
}
FileChannel inFileChannel = new RandomAccessFile(srcfilePath, "r").getChannel(); //获取源文件的文件通道
MappedByteBuffer inMappedByteBuffer = inFileChannel.map(MapMode.READ_ONLY, 0, inFileChannel.size()); //将其映射到内存空间
inFileChannel.close();
FileChannel outFileChannel = new RandomAccessFile(targetPath, "rw").getChannel(); //获取目标文件的文件通道
MappedByteBuffer outMappedByteBuffer = outFileChannel.map(MapMode.READ_WRITE, 0, inMappedByteBuffer.capacity()); //将其映射的内存空间
outMappedByteBuffer.put(inMappedByteBuffer);
outFileChannel.close();
}
/**
* 向磁盘总追加内容
* @param file 要追加的文件
* @param str 需要追加的字符串内容
* @throws IOException
*/
public static void add(File file,String str) throws IOException
{
FileChannel channel = new RandomAccessFile(file,"rw").getChannel();//获取文件通道
byte[] bytes = str.getBytes(); //要追加的字节内容
int length = bytes.length;//字节的长度
//将文件映射到内存中去
MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, channel.size()+length);
int position = map.limit()-length;//获取map指针对应的位置
map.position(position);
map.put(bytes);
map.force();//强制其将磁盘内容同步,不是必须的;
channel.close();
}