前言
大部分网络程序做的事情就是接受输入并产生输出。读服务器发送过来的数据与读取本地文件的数据并没有多大的区别,同时服务器将数据发送给客户端与写数据到本地文件也很像。
Java的IO操作基于streams实现的。输入流读数据,输出流写数据。
该系列文章是《Java网络编程》书籍的读书笔记
输出流(Output Streams)
所有输出流的基类是OutputStream
定义的方法如下:
public void write(byte[] data) throws IOException
public void write(byte[] data, int offset, int length) throws IOException
public void flush() throws IOException
public void close() throws
基础的方法是write(int b),将参数b的低8位写到输出流中,高24位会被忽略。也就是说b的范围是0~255(二进制11111111是255)。其实就是传了一个无符号字节,因为Java没有无符号字节类型,所以通过int来替代。
但是每次如果只写一个字节很不方便,下面两个write(byte[] data)方法可以一次传输一个字节数组。
其实write(byte [] data)的核心代码还是回调用write(int)方法:
for (int i = 0 ; i < len ; i++) {write(b[off + i]);
}
可以把需要发送的数据缓存起来,等到满足一定条件或调用flush方法再发送。Java通过BufferedOutputStream或BufferedWriter来实现。BufferedOutputStream继承自FilterOutputStream,而这个FilterOutputStream是装饰器模式的一个例子,它底层维护了一个OutputStream。
不管你觉得有没有必要,调用flush方法就行了。在关闭stream之前,调用flush方法!
常见的关闭流的姿势:
OutputStream out = null;try {
out = new FileOutputStream("/tmp/data.txt");
// work with the output stream...
} catch (IOException ex) {
System.err.println(ex.getMessage());
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ex) {
// ignore
Java7引入的更加清爽的姿势:
try (OutputStream out = new FileOutputStream("/tmp/data.txt")) {// work with the output stream...
} catch
叫做try-with-resources 语句,如果有多个资源呢?
try (InputStream fis = new FileInputStream(source);OutputStream fos = new FileOutputStream(target)){
byte[] buf = new byte[8192];
int i;
while ((i = fis.read(buf)) != -1) {
fos.write(buf, 0, i);
}
}
catch
数据流会在 try 执行完毕后自动被关闭,前提是,这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。
输入流(Input Stream)
所有输入流的基类是InputStream
定义的方法如下:
public int read(byte[] input) throws IOException
public int read(byte[] input, int offset, int length) throws IOException
public long skip(long n) throws IOException
public int available() throws IOException
public void close() throws
基础的是这个无参数的read()方法,从输入流中读取一个字节,并以int类型(0~255)返回,如果读到stream的某位,返回-1。
在输入数据可用、检测到文件末尾或者抛出异常前,此方法一直阻塞。
每次只读一个字节和每次只写以字节一样不方便,read(byte[] input)方法就出来了。
read(byte[] input)方法尝试填充input数组,注意这里的措辞是尝试,当read的时候遇到了IOException或假设你传入了1024大小的数组,但是只读取了512个字节,还有512个字节正在传输中(你的网络比较慢啊老哥),这个方法就会返回实际读取的字节数。
int bytesRead = in.read(input);
考虑到上面所描述的,为了保证你能读到1024字节,将read放到循环里面:
int bytesRead = 0;int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);
}
上面的代码有一个bug,它没有考虑到实际上可能没有1024字节这么多的数据,可能读取了200字节就返回-1了。
int bytesRead = 0;int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
int result = in.read(input, bytesRead, bytesToRead - bytesRead);
if (result == -1) break; // end of stream
调用available()方法可以得知有多少个字节的数据能马上读取而不需要阻塞
int bytesAvailable = in.available();//注意,可能返回0byte[] input = new byte[bytesAvailable];
int bytesRead = in.read(input, 0, bytesAvailable);
有时,你可能想跳过一些数据,这时可调用skip()方法来达到这个要求。
过滤流(Filter Streams)
Filter有两种方式:filter stream,readers和writers。filter stream主要处理字节数据:比如以二进制的形式来处理字节数据;readers和writers负责处理各种编码的文本。
Filter通过链条的形式组织起来。链路中每个Filter从上个Filter(或stream)上接收数据,自己进行某些处理,然后传递给下一位。
上图展示了一个加密、压缩的文本文件的传输过程。首先传递给TelnetInputStream,然后通过BufferedInputStream来缓存数据以提交传输速度,接下来CipherInputStream解密传递过来的数据并交给GZIPInputStream来解压,最后到了InputStreamReader将解压且解密了的数据转换为Unicode文本的形式。
链接Filter
Filter通过构造函数来接收stream。如下所示:
FileInputStream fin = new FileInputStream("data.txt");BufferedInputStream bin = new
此时,既可以使用fin的read()方法又可以使用bin的read()方法来从文件data.txt中读取数据。然而,混合调用连接到同一个源文件的不同stream违反了filter stream的约束。一般,应该使用链路上最后一个filter来进行读写操作。
一旦这种链接关系建立起来就无法脱离。
Buffered Streams
BufferedOutputStream存储了需要写的数据,直到buffer满了或stream被flush了,然后它将数据一次性写到底层的output stream。一次写很多数据的速度远超过多次写很少的数据。尤其是在网络环境下。
BufferedInputStream类也有一个名为buf的protected字节数组作为缓冲区。 当调用其中一个流的read()方法时,它首先尝试获取从缓冲区请求的数据。 只有在缓冲区的数据用完时才会从底层源stream中读取数据。因此,它读取尽可能多的数据到缓冲区,而不管是否为马上需要的数据。 在阅读文件时,从本地磁盘读取几百字节的数据几乎和读取一个字节的数据的速度一样快。 因此,缓冲可以大幅度提高性能。
它们的构造函数定义如下:
public BufferedInputStream(InputStream in)public BufferedInputStream(InputStream in, int bufferSize)
public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int
PrintStream
PrintStream是大多数人遇到的第一个filter output stream。因为System.out是一个PrintStream。
PrintStream同样也是通过构造函数来接收stream:
public PrintStream(OutputStream out)public PrintStream(OutputStream out, boolean autoFlush)
autoFlush默认为false,如果true:byte数组被写入(调用了write(byte buf[])方法);println方法被调用;\n字符被写入 都会触发flush方法。
除了常用的write(),flush()和close()方法。PrintStream还重载了9个print()方法和10个println()方法。
public void print(long l) {write(String.valueOf(l));
}
public void print(float
和上面的代码类似,每个print()方法将它的参数转变为string然后以默认的编码写到底层的output stream。
温馨提示:网络编程时最好不要使用PrintStream!
Data Streams
DataInputStream和DataOutputStream提供了以二进制形式读取和写入原始数据类型和string的方法。
DataOutputStream为特定的类型提供了11种写入方法:
public final void writeByte(int b) throws IOException
public final void writeShort(int s) throws IOException
public final void writeChar(int c) throws IOException
public final void writeInt(int i) throws IOException
public final void writeLong(long l) throws IOException
public final void writeFloat(float f) throws IOException
public final void writeDouble(double d) throws IOException
public final void writeChars(String s) throws IOException
public final void writeBytes(String s) throws IOException
public final void writeUTF(String s) throws
writeUTF()方法以UTF-8来编码string。
DataInputStream也提供了类似的读取方法,就不在这里列出了。
Readers和Writers
Writer类与Reader类是以字符流传输数据,一个字符两个字节。
字符流除了是以字符方式(两个字节)传输数据外,另外一点与字节流不同的是字符流使用缓冲区,通过缓冲区再对文件进行操作。字节流字节对文件进行操作。使用字符流类时关闭字符流会强制将字符流缓冲区的类容输出,如果不想关闭也将字符流进行输出,使用Writer类的flush()方法。
Writers
下面列出几个方法:
public abstract void write(char[] text, int offset, int length) throws IOExceptionpublic void write(String s) throws IOException
public void write(String s, int offset, int length) throws
给你一个writer对象,和一个String “Network”,可以通过下面的方法执行写出操作:
char[] network = {'N', 'e', 't', 'w', 'o', 'r', 'k'};w.write(network, 0, network.length);
类似的也可以这样:
String network = "Network";w.write(network);
for (int i = 0; i < network.length; i++) w.write(network[i]);
w.write("Network");
w.write("Network", 0, 7);
Writers底层有一个Buffered Stream,因此任何write操作都会被缓存,可以调用flush()方法强制flush stream。
OutputStreamWriter
OutputStreamWriter是最重要的Writer子类。
OutputStreamWriter从Java程序中接收字符,然后通过特定的编码转换为字节并将这些字节写到底层的输出流。
通过构造函数来指定底层的输出流和字符编码:
public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException除了构造函数和常用的Writer类方法,OutputStreamWriter提供了一个返回编码的方法getEncoding()
Readers
public abstract int read(char[] text, int offset, int length) throws IOExceptionpublic int read() throws IOException
public int read(char[] text) throws
它的大多数方法可以与InputStream 中的方法对应。 read()方法以int型(从0到65,535)返回一个单一的Unicode字符或读到流结束时返回-1。
同样InputStreamReader也是Reader子类中最重要的一个。InputStreamReader从底层的输入流中读取字节,然后通过特定的编码转换为字符。
构造函数:
public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException
举个例子,下面的方法从in中读取字节并以MacCyrillic编码转换为Unicode字符串。
public static String getMacCyrillicString(InputStream in) throws IOException {InputStreamReader r = new InputStreamReader(in, "MacCyrillic");
StringBuilder sb = new StringBuilder();
int c;
while ((c = r.read()) != -1) sb.append((char) c);
return
Filter Readers and Writers
BufferedReader有一个readLine()方法,读取一行文本并以String的形式返回:
public String readLine() throws IOException
BufferedWriter类加了一个新的方法newLine()
public void newLine() throws IOException
该方法根据不同的平台插入不同的换行符到输出流。
InputStreamReader和OutputStreamWriter,IO中的类要么以Stream结尾,要么以Reader或者Writer结尾,那这两个同时以字节流和字符流的类名后缀结尾的类是什么用途呢?简单来说,这两个类把字节流转换成字符流,中间做了数据的转换,类似适配器模式的思想。
PrintWriter
PrintWriter用以取代PrintStream来处理多种格式的字符集和国际化文本。