Java IO

Java语言采用流的机制来实现输入/输出,所谓流,就是数据的有序排列,它可以从某个源(流源)出来,到某个目的地(流汇)。Java的IO库提供了一个称为链接(Chaining)的机制,可以将一个流管道与另一个流管道首尾相接,以其中之一的输出为输入,形成一个流管道的链接.

Java语言的I/O库提供了四大等级结构:InputStream,OutputStream,Reader,Writer四个系列的类。InputStream和OutputStream处理8位字节流数据, Reader和Writer处理16位的Unicode 字符流数据。从应用程序的角度确定输入输出方向,InputStream和Reader处理输入, OutputStream和Writer处理输出。

 下面的图表示:以InputStream和OutputStream形成的层次关系

下面的图表示:以Reader和Writer形成的层次关系

 

InputStream

是所有二进制流读取的抽象类,一次读取一个字节,也可以将数据读到指定数组中,读到结尾时返回-1。read(),read(byte b[]),read(byte b[], int off, int len)。

ByteArrayInputStream 

封装(引用)一字节数组,使得对字节数组的读取适配于InputStream定义的读取方式。

FileInputStream

封装了对文件的读取

PipedInputStream

PipedOutputStream 对象只能与 PipedInputStream 对象成对使用,用于线程间的通讯。两个对象最好被不同的线程持有,如果被同一个线程持有,可能引起死锁。PipedOutputStream 对象持有 PipedInputStream 引用,PipedInputStream 内有个byte[] 缓存。PipedOutputStream 往缓存中写数据,PipedInputStream 从缓存中读数据

ObjectInputStream

从底层的字节流中读出对象

SequenceInputStream

合并多个字节流,读完第一个读第二个,按顺序逐个读取封装的字节流。

StringBufferInputStream

本意是把字符串转换为字节流,然后进行读操作,但是在这个类的实现中仅仅使用了字符编码的低8位,不能把字符串中的所有字符(比如中文字符)正确转换为字节,因此这个类已经被废弃,取而代之的是StringReader类 

OutputStream

是所有二进制流输出的抽象类,一次写一个字节到底层,也可以将指定数组写到底层。write(int b),write(byte b[]),write(byte b[], int off, int len)。

ByteArrayOutputStream

内部封装了一个可变长度的字节数组,可以将指定字节或数组写到内部字节数组中。

FileOutputStream

将字节或字节数组写入到

其封装的文件中

PipedOutputStream

与 PipedInputStream 对象成对使用,将字节或字节数组写入到PipedInputStream 内的byte[] 缓存中。

ObjectOutputStream

将Java对象写入到底层的字节输出流中。

Reader

是所有二进制流读取的抽象类,一次读取一个字符或将数据读到指定字符数组中。int read(),read(char cbuf[]),read(char cbuf[], int off, int len),read(java.nio.CharBuffer target).

CharArrayReader 

内部封装了字节数组,可以一次从中读取一个字符,也可以将字符读到指定的字符数组中

FileReader

通过FileInuptStream 从文件中读取字节,再用指定的字符集解码器将一个字节序列按照特定的字符集转换成一个16位的Unicode。

PipedReader

与 PipedWiter 对象成对使用,字符字符数组写入到PipedReader 内的char[] 缓存中。用于不同线程之间字符格式的数据通信,使用机制类似PipedInputStream,PipedOutputStream.

StringReader

以字符串为数据源读出字符或字符数组

Writer

将字符,字符数组或字符串写入到底层

CharArrayWriter

将字符,字符数组或字符串写入到其内部封装的字符数组中。

StringWriter

将字符,字符数组或字符串写入到其内部封装的StringBuffer中

FileWriter

将字符,字符数组或字符串写入到指定的文件中。

PrintWriter

按一定格式将数据以字符流的形式写入到底层流中。

装饰模式的应用 

Java的IO是由一些基本的原始流处理器和围绕它们的装饰流处理器所组成的。所谓链接流处理器,就是可以接收另一个(同种类)的流对象作为流源,并对之进行功能扩展的类。可以知道,所谓有链接流就是装饰角色,而原始流就是具体构件角色.一方面,链接流对象接收一个同类型的原始流对象或者另一个同类型的链接流对象作为流源,另一方面,它们都对流源对象的内部工作方法做了相应的改变,这种改变誻装饰模式所要达到的目的,比如: 
BufferedInputStream装饰了InputStream的内部工作方式,使得流的读入操作使用缓冲机制,在使用缓冲机制后,不会对每一次的流读入操作都产生一个物理读盘动作,从而提高了程序的效率.
 

InputStream类型中的装饰模式 

ConcreteComponentByteArrayInputStream,FileInputStream,PipedInputStream,StringBufferInputStream 
ConcreteDecorator
 
所谓链接流处理器,就是可以接收另一个(同种类)的流对象作为流源,并对之进行功能扩展的类。

FilterInputStream:称为过滤输入流,封装/引用 了一个InputStream,有如下四个子类

BufferInputStream 内部持有byte[] 缓存为被装饰的输入流提供缓存功能,可以有效减少对输入流的访问次数。

DataInputStream   提供基于多字节的读取方法,可以读取原始数据类型的数据。

LineNumberInputStream  为被装饰的输入流提供行计数功能

PushpackInputStream  可以将已经读取的字节"推回"到输入流中。其持有一个缓存,将要退回的数据先存放在缓存中,下一次读取是先从缓存中读,当缓存中的数据读完,再从封装的底层流中读取数据。

ObjectInputStream 可以将ObjectOutputStream串行化的原始数据类型和对象重新并行化 

SeqcueneInputStream可以将两个已有的输入流连接起来,形成一个输入流,从而将多个输入流排列构成一个输入流序列。读完第一个读第二个,按顺序逐个读取封装的字节流。


OutputStream类型中的装饰模式 

ConcreteComponent:ByteArrayOutputStream,FileOutputStream,PipedOutputStream.
ConcreteDecorator
FilterOutputStream:封装/引用 一输出流,有三个子类

BufferOutputStream:持有一个缓,当写数据时先尝试往存中写,如果存中空间不够,则将存中的数据和本次要写的数据都写到被装饰的流中。 
DataOutputStream:提供基于多字节的写出方法,可以写出原始数据类型的数据 
PrintStream:用于产生格式化输出,System.out就是一个PrintStream 
ObjectOutputStream


Reader类型中的装饰模式 
ConcreteComponent :CharArrayReader,InputStreamReader:这个类有一个子类FileReader ,PipedReader,StringReader
ConcreteDecorator:  
BufferedReader:内部持有char[] 缓存为被装饰的输入流提供缓存功能,读取时先尝试从此缓存中读取数据,当缓存中数据不够时,从封装流中读取数据填充缓存,有效减少对被装饰流的访问次数。它的子类有LineNumberReader. 
FilterReader:成为过滤输入流,它将另一个输入流作为流的来源,这个类的子类有PushbackReader,其持有一个缓存,将要退回的数据先存放在缓存中,下一次读取是先从缓存中读,当缓存中的数据读完,再从封装的底层流中读取数据。

Writer中的装饰模式 
Writer类型是一个与Reader类型平行的等级结构,而且Writer类型的等级结构几乎是与Reader类型的等级结构关于输入/输出是对称的. 
ConcreteComponent :FileWriter,CharArrayWriter, OutputStreamWriter 含有一个具体子类的FileWriter,PipedWriter,StringWriter 
ConcreteDecorator:   
BufferedWriter:为Writer类型的流处理提供缓冲区功能 
FilterWriter:称为过滤输入流,它将别一个输入流作为流的来源,这是一个没有子类的抽象类,没有实际用处。 
PrintWriter:支持格式化的文字输出. 

适配器模式的应用 

InputStreamReader 

是典型的适配器模式的应用。当把InputStreamReader与任何InputStream的具体子类链接时,可以从InputStream的输出读入byte类型的数据,并根据指定的编码方式或平台可接受的缺省编码方式将之转换成char类型的数据

OutputStreamWriter 

同样地,OutputStreamWriter是从byte输出流到char输出流的一个适配器,或者说OutputStreamWriter是从OutputStream到Writer的适配器,当与任何一个OutputStream的具体子类相链接时,OutputStreamWriter可以将char类型的数据转换成byte类型的数据,再交给输出流。


Java IO 的一般使用原则

一、按数据来源(去向)分类:

1 、是文件: FileInputStream, FileOutputStream, ( 字节流 )FileReader, FileWriter( 字符 )

2 、是 byte[] : ByteArrayInputStream, ByteArrayOutputStream( 字节流 )

3 、是 Char[]: CharArrayReader, CharArrayWriter( 字符流 )

4 、是 String: StringBufferInputStream, StringBufferOuputStream ( 字节流 )StringReader, StringWriter( 字符流 )

5 、网络数据流: InputStream, OutputStream,( 字节流 ) Reader, Writer( 字符流 )

二、按是否格式化输出分:

1 、要格式化输出: PrintStream, PrintWriter

三、按是否要缓冲分:

1 、要缓冲: BufferedInputStream, BufferedOutputStream,( 字节流 ) BufferedReader, BufferedWriter( 字符流 )

四、按数据格式分:

1 、二进制格式(只要不能确定是纯文本的) : InputStream, OutputStream 及其所有带 Stream 结束的子类

2 、纯文本格式(含纯英文与汉字或其他编码方式); Reader, Writer 及其所有带 Reader, Writer 的子类

五、按输入输出分:

1 、输入: Reader, InputStream 类型的子类

2 、输出: Writer, OutputStream 类型的子类

六、特殊需要:

1 、从 Stream 到 Reader,Writer 的转换类: InputStreamReader, OutputStreamWriter

2 、对象输入输出: ObjectInputStream, ObjectOutputStream

3 、进程间通信: PipeInputStream, PipeOutputStream, PipeReader, PipeWriter

4 、合并输入: SequenceInputStream

5 、更特殊的需要: PushbackInputStream, PushbackReader, LineNumberInputStream, LineNumberReader

决定使用哪个类以及它的构造进程的一般准则如下(不考虑特殊需要):

首先,考虑最原始的数据格式是什么: 原则四

第二,是输入还是输出:原则五

第三,是否需要转换流:原则六第 1 点

第四,数据来源(去向)是什么:原则一

第五,是否要缓冲:原则三 (特别注明:一定要注意的是 readLine() 是否有定义,有什么比 read, write 更特殊的输入或输出方法)

第六,是否要格式化输出:原则二

磁盘IO工作机制

  io中数据写到何处也是重要的一点,其中最主要的就是将数据持久化到磁盘。数据在磁盘上最小的描述就是文件,上层应用对磁盘的读和写都是针对文件而言的。在java中,以File类来表示文件,如:

File file = new File("D:/test.txt");

  但是严格来说,File并不表示一个真实的存在于磁盘上的文件。就像上面代码的文件其实并不存在,File做的只是根据你所提供的文件描述符,返回某一路径的虚拟对象,它并不关心文件或路径是否存在,可能存在,也可能是捏造的。就好象一张名片,名片的背后代表的是人。为什么要这么设计?在我看来还是要提高访问磁盘的效率,有点延迟加载的意思。大部分情况下,我们最关心的并不是文件存不存在,而是文件要如何操作。比如你手里有很多名片,你可能更关心的是有没有某某局长的名片,而只有在需要联系时,才发现名片是假的。也就是关心名片本身要强过名片的真伪。

  以FileInputStream读取文件为例,过程是这样的:当传入一个文件路径时,会根据这个路径创建File对象,作为这个文件的一个“名片”。当我们试图通过FileInputStream对象去操作文件的时候,将会真正创建一个关联真实存在的磁盘文件的文件描述符FileDescriptor,通过FileInputStream构造方法可以看出:

fd = new FileDescriptor();

  如果说File是文件的名片,那么FileDescriptor就是真正指向了一个打开的文件,可以操作磁盘文件。例如FileDescriptor.sync()方法可以将缓存中的数据强制刷新到磁盘文件中。如果我们需要读取的是字符,还需要通过StreamDecoder类将字节解码成字符。至于如何从物理磁盘上读取数据,那就是操作系统做的事情了。过程如图(图摘自网上):

  图 7. 从磁盘读取文件

Socket工作机制

  Socket要说起来并不那么形象,它的中文翻译是“插座”,至于“套接字”这个翻译我实在不知道从何而来。可以这样理解插座的概念,由于本身有电网的存在,如果我们买了一台新电器,我们只要插上插座连接到电网上就能够使用。Socket就像一个插座,计算机通过Socket就能和网络或者其他计算机上进行通讯;当有数据通讯的需求时,只需要建立一个Socket“插座”,通过网卡与其他计算机相连获取数据。

  Socket位于传输层和应用层之间,向应用层统一提供编程接口,应用层不必知道传输层的协议细节。Java中对Socket的支持主要是以下两种:

  (1)基于TCP的Socket:提供给应用层可靠的流式数据服务,使用TCP的Socket应用程序协议:BGP,HTTP,FTP,TELNET等。优点:基于数据传输的可靠性。

  (2)基于UDP的Socket:适用于数据传输可靠性要求不高的场合。基于UDP的Socket应用程序协议:RIP,SNMP,L2TP等。

  大部分情况下我们使用的都是基于TCP/IP协议的流Socket,因为它是一种稳定的通信协议。以此为例:

  一台计算机要和另一台计算机进行通讯,获取其上应用程序的数据,必须通过Socket建立连接,要知道对方的IP和端口号。建立一个Socket连接需要通过底层TCP/IP协议来建立TCP连接,而建立TCP连接必须通过底层IP协议根据给定的IP在网络中找到目标主机。目标计算机上可能跑着多个应用,所以我们必须要根据端口号来制定目标应用程序,这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。

  那么Socket是如何建立通讯链路的呢?

  假设有一台计算机作为客户端,另一台作为服务端。当客户端需要向服务端通信,客户端首先要创建一个Socket实例:

Socket socket = new Socket("127.0.0.1",1234);

  若没有指定端口号,操作系统将为这个Socket实例分配一个没有被使用的本地端口号。此外创建了一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭,代码如下:

复制代码
public Socket(String host, int port)
    throws UnknownHostException, IOException
{
    this(host != null ? new InetSocketAddress(host, port) :
         new InetSocketAddress(InetAddress.getByName(null), port),
         (SocketAddress) null, true);
}
复制代码

  客户端试图和服务端建立TCP连接,此时会进行三次握手。

  第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

  第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

  第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

  

  完成三次握手后Socket的构造函数成功返回,Socket实例创建完毕。

  互联网是一种尽力而为(best-effort)的网络,客户端的起始消息或服务器端的回复消息都可能在传输过程中丢失。出于这个原因,TCP 协议实现将以递增的时间间隔重复发送几次握手消息。如果TCP客户端在一段时间后还没有收到服务器的返回消息,则发生超时并放弃连接。这种情况下,构造函数将抛出IOException 异常。

  而服务端也需要创建与之对应的ServerSocket,ServerSocket的创建比较简单,只需要指定端口号:

ServerSocket serverSocket = new ServerSocket(10001);

  同时操作系统也会为ServerSocket实例创建一个底层数据结构:

bind(new InetSocketAddress(bindAddr, port), backlog);  //见构造方法

  这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下是监听所有地址,下面是比较典型的ServerSocket代码:

复制代码
public void testSocket() throws Exception
{
    ServerSocket serverSocket = new ServerSocket(10002);
    Socket socket = null;
    try
    {
        while (true)
        {
            socket = serverSocket.accept();
            System.out.println("socket连接:" + socket.getRemoteSocketAddress().toString());
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while(true)
            {
                String readLine = in.readLine();
                System.out.println("收到消息" + readLine);
                if("end".equals(readLine))
                {
                    break;
                }
                //客户端断开连接
                socket.sendUrgentData(0xFF);
            }
        }
    }
    catch (SocketException se)
    {
        System.out.println("客户端断开连接");
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
    finally
    {
        System.out.println("socket关闭:" + socket.getRemoteSocketAddress().toString());
        socket.close();
    }
}   
复制代码

   当调用accept()方法时,服务端将进入阻塞状态,等待客户端的请求。当有客户端请求到来时,将为这个链接创建一个套接字数据结构,包括请求客户端的地址和端口号。该数据结构将被关联到ServerSocket实例的一个未连接列表里。此时连接并没有成功建立,处于三次握手阶段,Socket构造函数并未成功返回。当三次握手成功后,会将Socket实例对应的数据结构从未完成列表移到完成列表中。所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接。

  当连接成功创建后,我们要做的就是传输数据,这才是主要目的。如上例代码,在客户端和服务端都有一个Socket实例,而每个Socket实例都会拥有一个InputStream和OutputStream,我们正是通过它们传输数据。当Socket对象创建时,操作系统将会为InputStream和OutputStream分别分配一定大小的缓冲区,数据的写入和读取都是通过缓存区完成的。发送端的缓冲区称之为SendQ,是一个FIFO的队列,接收端的缓冲区称之为RecvQ,同样也是FIFO队列。

  数据传输时,发送端将数据写入到OutputStream对应的SendQ队列中,以字节为单位发送到接收端InputStream的RecvQ队列中。当SendQ队列填满时,发送端的write方法将会阻塞住;而当RecvQ队列中没有数据时,接收端的read方法也将被阻塞。

  一些情况下,客户端和服务端之间可能会产生死锁问题,例如:

  • 如果在连接建立后,客户端和服务器端都立即尝试接收数据,显然将导致死锁。
  • 客户端和服务端都尝试向对方write数据,并且数据长度大于两端缓冲区的和。此时会导致不管客户端还是服务端RecvQSendQ都满了,剩下的数据无法发送,两个write操作都不能完成,两个程序都将永远保持阻塞状态,产生死锁。

  死锁的问题是要注意的,需要对数据的写入和读取做一个协调,解决死锁的方式可以使用多线程,也可以使用非阻塞的io,这里就不再深究了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值