0. 前言
I/O是Java技术网络中一个比较重要的点,不仅是平时开发中的家常便饭,也是面试的时候经常被问到的话题。本篇将介绍字节IO、字符IO的基本体系和用法,最后总结一下NIO的一些知识。本文原创,转载请注明出处:
1. 字节I/O
字节流处理单元为1个字节,主要用在处理二进制数据,字节用来与文件打交道,所有文件的储存都是通过字节(byte)的方式,在磁盘上保留的并不是文件的字符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取形成字节序列。
1.1 字节I/O流体系
OutputStream //输出字节流的抽象类|--->FileOutputStream://文件输出流 |--->BufferedOutputStream //字节写入流缓冲区 |--->PrintStream //打印流 InputStream //字节输入流的抽象类|--->FileInputStream //文件字节读取流 |--->BufferedInputStream //字节读取流缓冲区
1.2 字节I/O流应用示例
//文件a中内容拷贝到文件b//创建一个字节输入流 FileInputStream fis=new FileInputStream("D:/a.txt"); //创建一个字节输出流 FileOutputStream fos=new FileOutputStream("D:/b.txt"); int len=0;byte[] b=new byte[1024];//字节缓冲区 while((len=fis.read(b))!=-1){ //判断未读到文件尾 fos.write(b, 0, len); //将读出到的文件内容写到fos中} //关闭流
2. 字符I/O
实际中很多的数据是文本,因此有了字符流的概念,字符流只用于处理文字数据。字符流处理单元为2个字节的Unicode字符。
字符流中的对象融合了编码表,使用的是默认的编码,即当前系统的编码。在实际开发中出现的汉字乱码问题实际上都是在字符流和字节流之间转化不统一而造成的。
2.1 字符I/O流体系
Reader //读取字符流的抽象类,子类必须实现read(char[], int, int)、close()|---BufferedReader //从字符输入流中读取文本,缓冲自定义个字符,从而实现字符、数组和行的高效读取|---LineNumberReader //跟踪行号的缓冲字符输入流,此类的set/getLineNumber()用于设置/获取当前行号|---InputStreamReader //字节流通向字符流的桥梁,使用指定的charset读取字节并将其解码为字符|---FileReader //用来读取字符文件的便捷类Writer //写入字符流的抽象类,子类必须实现write(char[], int, int)、close()和flush()|---BufferedWriter //将文本写入字符输出流,缓冲各个字符,从而提供单个字符、数组和字符串的高效写入|---PrintWriter //向文本输出流打印对象的格式化表示形式|---OutputStreamWriter //是字符流通向字节流的桥梁,使用指定的charset将要写入流中的字符编码成字节|---FileWriter //用来写入字符文件的便捷类
2.2 字符I/O流应用示例
//将中文字符写入文件File file=new File("D:/test.txt"); String charset="UTF-8";//写字符串转换成字节流 OutputStream out=new FileOutputStream(file); OutputStreamWriter writer=new OutputStreamWriter(out,charset);//将字符存储为指定编码集的字节 writer.write("这是写入到文件中的中文字符"); //读取字节转换成字符 FileInputStream inputStream=new FileInputStream(file); InputStreamReader reader=new InputStreamReader(inputStream,charset); StringBuffer buffer=new StringBuffer(); char[] buf=new char[64]; int count=0; while((count=reader.read(buf))!=-1){ buffer.append(buf,0,count); } System.out.println(buffer.toString());
3. NIO
如果你细细分析,一定会发现阻塞I/O存在一些缺点:
(1)当客户端多时,会创建大量的处理线程。且每个线程都会产生性能消耗。
(2)阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。
综上所述,在某种情况下非阻塞式I/O就有了它的应用前景。NIO是Java 4里面提供的新的API,目的就是用来解决传统IO里的这些问题。
3.1 NIO中的重要概念
在NIO中有几个比较关键的概念:Channel(通道),Buffer(缓冲区),Selector(选择器)。
(1)Channel(通道),在传统IO中我们要读取一个文件中的内容时InputStream实际上就是为读取文件提供了一个通道,因此可以将NIO 中的Channel同传统IO中的Stream来类比。但是要注意的是,传统IO中的Stream是单向的,比如InputStream只能进行读取操作,OutputStream只能进行写操作;而Channel是双向的,既可用来进行读操作又可用来进行写操作。
以下是常用的几种通道
FileChannel //可以从对文件进行读写数据
SocketChanel //以TCP向网络连接的两端读写数据
DatagramChannel //以UDP协议向网络连接的两端读写数据
ServerSocketChannel //监听客户端发起的TCP连接,并为每个连接创建新的SocketChannel来进行数据读写
下面给出通过FileChannel来向文件中写入数据的一个例子:
File file = new File("C:/test.txt");FileOutputStream outputStream = new FileOutputStream(file);FileChannel channel = outputStream.getChannel();ByteBuffer buffer = ByteBuffer.allocate(1024);String string = "NIO";buffer.put(string.getBytes());buffer.flip(); channel.write(buffer);
(2)Buffer(缓冲区),实际上是一个容器,是一个连续数组,在NIO中所有数据的读和写都必须经由Buffer。
在NIO中,Buffer是一个顶层父类,它是一个抽象类。
常用的Buffer的子类有:ByteBuffer、IntBuffer、CharBuffer、LongBuffer、DoubleBuffer、FloatBuffer、ShortBuffer。
如果是对于文件读写,上面几种Buffer都可能会用到。但是对于网络读写来说,用的最多的是ByteBuffer。
(3)Selector,可以理解为通道管理器,用单线程处理一个Selector以轮询每个注册的Channel,通过Selector.select()方法来获取到达事件(一个SelectionKey表示一个到达的事件),一旦发现Channel有注册的事件发生,便可以逐个地对这些事件进行处理。
比如,某时刻客户端给服务端发送了一些数据,阻塞I/O会调用read()阻塞地读取数据,而NIO的服务端会在Selector中添加一个读事件,服务端的处理线程会轮询地访问Selector,如果访问Selector时有读事件到达,则处理该事件,如果没有则处理线程会一直阻塞直到读事件到达。
3.2 NIO优点
NIO便解决了上述传统IO的两大缺点:不会创建、维护多个处理线程,只用一个专门的线程就可以管理多个通道,也就是管理多个连接,也避免了多线程之间的上下文切换导致的性能开销。同时也使得只有在真正有读写事件发生时,才会调用函数来进行读写,而不是同步地去监听事件,大大地减少了系统开销。
3.3 NIO使用实例介绍
public class NIOClient { //通道管理器 private Selector selector; // 获得一个Socket通道,并对该通道做一些初始化工作 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(this.selector, SelectionKey.OP_CONNECT); } //采用轮询的方式监听selector上是否有需要处理的事件 public void listen() throws IOException{ while(true){ /* * 选择一组可以进行I/O操作的事件,放在selector中,客户端的该方法不会阻塞, * 这里和服务端的方法不一样,当至少一个通道被选中时, * selector的wakeup方法被调用,方法返回,而对于客户端来说,通道一直是被选中的 */ selector.select(); //获得selector中选中的项的迭代器 Iteratorite = 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("Hello").getBytes())); //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限 channel.register(this.selector, SelectionKey.OP_READ); } else if(key.isReadable()){ //获得了可读的事件 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); } } } } public static void main(String[] args) throws IOException{ NIOClient client = new NIOClient(); client.initClient("localhost", 8080); client.listen(); } } public class NIOServer { private Selector selector; 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); } public void listen() throws IOException { System.out.println("服务端启动成功!"); 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("Hello").getBytes())); //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读权限 channel.register(this.selector, SelectionKey.OP_READ); }else if(key.isReadable()){ //服务器可读取消息:得到事件发生的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);//将消息回送给客户端 } } } } public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8080); server.listen(); } }
最后引用网上的一个对NIO的比喻来帮助理解:
其中Channel对应以前的流,Buffer不是什么新东西,Selector是因为nio可以使用异步的非堵塞模式才加入的东西。以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于水管没有阀门,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上。
NIO的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也只能接一个水管的水,但依赖轮换策略,在水量不大的时候,各个水管里流出来的水,都可以得到妥善接纳,这个关键之处就是增加了一个接水工,也就是Selector,他负责协调,也就是看哪根水管有水了的话,在当前水管的水接到一定程度的时候,就切换一下:临时关上当前水龙头,试着打开另一个水龙头(看看有没有水)。
当其他人需要用水的时候,不是直接去接水,而是事前提了一个水桶给接水工,这个水桶就是Buffer。也就是,其他人虽然也可能要等,但不会在现场等,而是回家等,可以做其它事去,水接满了,接水工会通知他们。
这其实也是非常接近当前社会分工细化的现实,也是统分利用现有资源达到并发效果的一种很经济的手段,而不是动不动就来个并行处理,虽然那样是最简单的,但也是最浪费资源的方式。
关于Java的IO知识就总结到这吧,转载请注明出处:
记得点赞哦~