网络程序所做的很大一部分工作只是输入和输出:从一个系统向另一个系统移动数据。
输出流
Java的基本输出流类是java.io.OutputStream:
public abstract class OutputStream
这个类提供了写入数据所需的基本方法,包括:
public abstract void write(int b) throws IOException
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 IOException
Output子类使用这些方法向某种媒体写入数据,如
FileOutputStream写文件
TelnetOutputStream写网络连接
ByteArrayOutputStream写扩展的字节数组
不管写入哪种媒介,主要也只会使用这5种方法。
write(int b)方法只写入8位,其余舍弃。
输出流要加缓冲,要flush,要close。
流出以太网卡的每个TCP分片至少包含用于路由和纠错等开销的40个字节。如果一个字节一个字节的发送,会导致开销很大。
字符自动生成程序:
每行72个字符,从33到126循环输入。即
33~104
34~105
...
55~126
56~126 33
写字节:
public static void generateCharacters(OutputStream out) throws IOException{int firstPrintableCharactor = 33;int numberOfPrintableCharacters = 94;int numberOfCharactersPerLine = 72;int start = firstPrintableCharacter;while(true){for (int i = start; i < start + numberOfCharacterPerLine; i++){out.write((i-firstPrintableCharacter)%numberOfPrintableCharacters+firstPrintableCharacter);}out.write("\r\n");start = ((start+1)-firstPrintableCharacter)%numberOfPrintableCharacters+firstPrintableCharacter;}
}
写字节数组:
public static void generateCharacters(OutputStream out) throws IOException{int firstPrintableCharactor = 33;int numberOfPrintableCharacters = 94;int numberOfCharactersPerLine = 72;byte[] line = new byte[numberOfCharactersPerLine+2];int start = firstPrintableCharacter;while(true){for (int i = start; i < start + numberOfCharacterPerLine; i++){line[i-start] = (byte)((i-firstPrintableCharacter)%numberOfPrintableCharacters+firstPrintableCharacter);}line[72] = (byte)'\r';line[73] = (byte)'\n';out.write(line);start = ((start+1)-firstPrintableCharacter)%numberOfPrintableCharacters+firstPrintableCharacter;}
}
输入流
Java的基本输入类是java.io.InputStream
public abstract class InputStream
这个类提供了将数据读取为原始字节所需的基本方法:
public abstract int read() throws IOException
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 avaliable() throws IOException
public void close() throws IOException
FileInputStream读文件
TelnetInputStream读网络连接
ByteArrayInputStream读字节数组
read()方法会等待并阻塞其后任何代码的执行,直到有一个字节的数据可用。
输入输出可能很慢,所以如果程序在进行其他重要的操作,请尝试将I/O放在自己的线程中。
byte[] input = new byte[10];
for (int i = 0; i < input.length; i++){int b = in.read();if (b==-1) break;input[i] = (byte)b;
}
常用技术:
int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead){bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);
}
所有3个read()方法都用返回-1表示流的结束。
上面代码存在一个bug,即在读取1024个字节之前,流结束了。修正如下:
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;bytesRead += result;
}
另一种读取方法是,有多少可用就读取多少。
int bytesAvailable = in.available();
byte[] input = new byte[bytesAvailable];
int bytesRead = in.read(intput, 0, bytesAvailable);
//立即继续程序的其他部分,此时bytesRead和bytesAvailable相等。
在流的最后,available()会返回0。
标记和重置
InputStream类还有3个不太常用的方法,允许程序备份和重新读取已经读取的数据。
public boolean markSupported()
public void mark(int readAheadLimit)
public void reset() throws IOException
markSupported()方法返回是否支持标记。
mark()方法标记流的当前位置,之后可用reset()把流重置到标记的位置。
一个流在任何时刻都只能有一个标记,标记第二个位置会清除第一个标记。
不能重置任意远的位置,能重置多远,取决于mark()的readAheadLimit参数。如果试图重置太远,会抛出IOException异常。
标记和重置通过将标记位置之后的所有字节存储于内部缓冲区来实现。
如果流不支持标记,则mark()会什么都不做,而reset()将抛出一个IOException。
java.io中唯一两个时钟支持标记的输入流是BufferedInputStream和ByteArrayInputStream。
而其他输入流,如TelnetInputStream可能在首先连接到缓冲的输入流时才支持标记。
过滤器流
Java提供了很多过滤器类,可以附加到原始流中,在原始字节和各种格式之间来回转换。
过滤器有两个版本:过滤器流及阅读器和书写器。
过滤器流仍然主要讲原始数据作为字节操作;
阅读器和书写器处理各种编码文本。
过滤器流置于原始流(如TelnetInputStream和FileOutputStream)或其他过滤器流之上。
阅读器和书写器置于原始流、过滤器流或其他阅读器和书写器之上。
过滤器流不能放在阅读器和书写器上面。
过滤器以链的形式进行组织。
每个过滤器输出流都有与java.io.OutputStream相同的write()、close()和flush()方法;
每个过滤器输入流都有与java.io.InputStream相同的read()、close()和available()方法。
将过滤器流连接在一起:
保存底层流的引用:
FileInputStream fin = new FileInputStream("data.txt");
BufferedInputStream bin = new BufferedInputStream(fin);
InputStream in = new FileInputStream("data.txt");
in = new BufferedInputStream(in);
DataOutputStream dout = new DataOutputStream(new BufferedOutputStream(new FileOutputStream("data.txt")));
缓冲流
BufferedOutputStream类将待写入的数据存储在缓冲区中(名为buf的保护字节数组字段),直到缓冲区满或刷新输出流。然后将数据一次全部写入底层输出流。
缓冲网络输出通常会带来巨大的性能提升。
BufferedInputStream类也有作为缓冲区的名为buf的保护字节数组。调用某个read()方法时,首先尝试从缓冲区获得请求的数据。当缓冲区没有数据时,流从底层的源中读取。这时,它会从源中读取尽可能多的数据存入缓冲区,而不管它是否马上需要所有这些数据。
对于网络连接,提升性能的效果不明显,这时的瓶颈往往是网络发送数据的速度。缓冲输入不会有坏处,随着网络的速度加快会变得更为重要。
两个构造参数:
public BufferedInputStream(InputStream in) //默认大小2048Byte
public BufferedInputStream(InputStream in, int bufferSize)
public BufferedOutputStream(OutputStream out) //默认大小512Byte
public BufferedOutputStream(OutputStream out, int bufferSize)
最优缓冲区大小取决于所缓冲的流是何种类型。
UDP不超过8K,TCP不超过1K。
BufferedInputStream只是覆盖了InputStream的方法。支持标记和重置。
public int read() 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 boolean markSupported()
public void mark(int readLimit)
public void reset() throws IOException
BufferedOutputStream覆盖了OutputStream的3个方法。
public void write(int b) throws IOException
public void write(byte[] data, int offset, int length) throws IOException
public void flush() throws IOException
PrintStream
public PrintStream(OutputStream out)
public PrintStream(OutputStream out, boolean autoFlush)
默认需要显式刷新。如果autoFlush为true,在每次写入一个字节数组或换行或调用println()时刷新。
除write()、flush()、close()方法外,还重载了9个print()方法和10个println()方法。
public void print(boolean b)
public void print(char c)
public void print(int i)
public void print(long l)
public void print(float f)
public void print(double d)
public void print(char[] text)
public void print(String s)
public void print(Object o)
public void println()
public void println(boolean b)
public void println(char c)
public void println(int i)
public void println(long l)
public void println(float f)
public void println(double d)
public void println(char[] text)
public void println(String s)
public void println(Object o)
每个print()方法都将其参数以可预见的方式转换为字符串,再用默认的编码方式把字符串写入底层输出流。
println()方法进行相同的操作,并在默认添加一个平台有关的分割字符。Linux下是\n,Windows下是\r\n。
PrintStream不提供改变默认编码的机制。
PrintStream吞掉了所有异常。
PrintStream有害,网络程序员不应使用。
PushbackInputStream
数据流
DataInputStream和DataOutputStream类提供了可以用二进制格式读写Java的简单数据类型和字符串的方法。
所用的二进制格式主要用于在两个不同的Java程序之间交换数据,而无论是通过网络连接、数据文件、管道还是其他媒介。
输出流写入什么数据,输入流就读取什么数据。
DataOutputStream类提供了11个写入某种Java数据类型的方法:
public final void writeBoolean(boolean b) throws IOException
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 IOException
DataInputStream和DataOutputStream是相生的。
public final boolean readBoolean() throws IOException
public final byte readByte() throws IOException
public final short readShort() throws IOException
public final char readChar() throws IOException
public final int readInt() throws IOException
public final long readLong() throws IOException
public final float readFloat() throws IOException
public final double readDouble() throws IOException
public final String readUTF() throws IOException
DataInputStream提供了两个读取C程序写入的二进制数据的方法,可以读取无符号字节和无符号短整数。
public final int readUnsignedByte() throws IOException
public final int readUnsignedShort() throws IOException
public final int read(byte[] input) throws IOException
public final int read(byte[] input, int offset, init length) throws IOException
public final void readFully(byte[] input) throws IOException
public final void readFully(byte[] input, int offset, int length) throws IOException
readFully()方法,重复的从底层输入流读取数据放在数组中,直到读取了所请求数量的字节为止;如果没有读取到足够的数据,抛出异常。
应用场景:读取了HTTP首部中的Content-length字段,知道有多少字节的数据。
public final String readLine() throws IOException
读取用行结束符分隔的一行文本,并返回一个字符串。
此方法废弃,存在bug。它在大多数情况不能正确的将非ASCII字符转换为字节。
这项任务由BufferedReader类的readLine()方法处理。
readLine()只识别换行或回车换行对。
如果回车是流的最后一个字符,则readLine()方法会挂起。
压缩流
java.util.zip包中包含压缩和解压zip、gzip和deflate格式流的过滤器流。
除广泛应用于文件外,这个包还允许Java应用程序通过网络轻松地交换压缩数据。
HTTP1.1包括了对压缩文件传输的支持,其中服务器压缩文件,而浏览器解压文件。
输入流解压数据,输出流压缩数据。
public class DeflaterOutputStream extends FilterOutputStream
public class InflaterInputStream extends FilterInputStream
public class GZIPOutputStream extens FilterOutputStream
public class GZIPInputStream extends FilterInputStream
public class ZipOutputStream extends FilterOutputStream
public class ZipInputStream extends FilterInputStream
上述类都使用相同的压缩算法,不同之处在于各种常量和包含于压缩数据中的元信息。
此外zip流可能包含多个压缩文件。
FileInputStream fin = new FileInputStream("allnames.gz");
GZIPInputStream gzin = new GZIPInputStream(fin);
FileOutputStream fout = new FileOutputStream("allnames");
int b = 0;
while((b = gzin.read()) != -1)fout.write(b);
gzin.close();
out.flush();
out.close();
或者直接
InputStream in = new GZIPInputStream(new FileInputStream("allnames.gz"));
ZipInputStream和ZipOutputStream有点复杂,因为zip文件实际上是包含多个项的归档文件,每个项都必须分别读取。
zip归档文件中的每个文件都表示为一个ZipEntry对象,对象的getName()方法会返回最初的文件名。
FileInputStream fin = new FileInputStream("shareware.zip");
ZipInputStream zin = new ZipInputStream(fin);
ZipEntry ze = null;
int b = 0;
while((ze = zin.getNextEntry()) != null){FileOutputStream fout = new FileOutputStream(ze.getName());while((b = zin.read()) != -1)fout.write(b);zin.closeEntry();fout.flush();fout.close();
}
zin.close();
摘要流
java.util.security包中包含了两个过滤器流,可以计算流的消息摘要。
DigestInputStream和DigestOutputStream
消息摘要在Java中用java.util.MessageDigest类表示,它是流的强散列码,是一个很大的整数(一般为二进制格式的20个字节长),从任何长度的流中计算得出。
消息摘要可用于数字签名,检测数据是否在网络传输中遭到破坏。
首先构建一个使用某种算法(如安全散列算法,SHA)的MessageDigest对象;
将这个MessageDigest对象和要加摘要的流一起传递给DigestOutputStream构造函数。
getMessageDigest()方法可获取MessageDigest对象。
MessageDigest对象的digest()方法结束计算实际的摘要。
MessageDigest sha = MessageDigest.getInstance("SHA");
DigestOutputStream dout = new DigestOutputStream(out, sha);
byte[] buffer = new byte[128];
while(true){int byteesRead = in.read(buffer);if (bytesRead < 0)break;dout.write(buffer, 0, bytesRead);
}
dout.flush();
dout.close();
byte[] result = dout.getMessageDigest().digest();
阅读器和书写器
处理字符
java.io.Reader类
java.io.Writer类
最重要的具体子类是InputStreamReader和OutputStreamWriter类。
还有几个不需要直接从底层输入流读取字符的原始阅读器和书写器类:
FileReader
FileWriter
StringReader
StringWriter
CharArrayReader
CharArrayWriter
前两个类可以处理文件,后四个由Java内部使用,在网络编程中不常用。
书写器
Writer类是一个抽象类,有5个write()方法,以及flush()和close()方法。
protected Writer()
protected Writer(Object lock)
public abstract void write(char[] text, int offset, int length) throws IOException
public void write(int c) throws IOException
public void write(char[] text) throws IOException
public void wirte(String s) throws IOException
public void write(String s, int offset, int length) throws IOException
public abstract void flush() throws IOException
public abstract void close() throws IOException
OutputStreamWriter
从Java程序接收字符,根据指定的编码方式将这些字符转换为字节,写入底层输出流中。
public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException
public OutputStreamWriter(OutputStream out)
public String getEncoding()
阅读器
protected Reader()
protected Reader(Object lock)
public abstract int read(char[] text, int offset, int length) throws IOException
public int read() throws IOException
public int read(char[] text) throws IOException
public long skip(long n) throws IOException
public boolean ready()
public boolean markSupported()
public void mark(int readAheadLimit) throws IOException
public void reset() throws IOException
public abstract void close() throws IOException
read(char[] text, int offset, int length)是基本方法。
ready()方法与InputStream的available()的用途相同,判断是否可以读取数据。
InputStreamReader
是Reader的最重要的具体子类。
从底层输入流(如FileInputStream或TelnetInputStream)中读取字节。根据指定的编码方式将这些字节转换为字符。
public InputStreamReader(InputStream in)
public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException
使用MacCyrillic编码方式将其全部转换为Unicode字符串:
public static String getMacCyrillicString(InputStream in) throws IOexception{InputStreamReader r = new InputStreamReader(in, "MacCyrillic");StringBuffer sb = new StringBuffer();int c;while((c = r.read()) != -1)sb.append((char)c);r.close();return sb.toString();
}
过滤器阅读器和书写器
InputStreamReader和OutputStreamWriter类就像装饰器,位于输入流和输出流之上,把面向字节的接口更改为面向字符的接口。
完成之后,其他面向字符的过滤器就可以放在使用java.io.FilterReader和java.io.FilterWriter类的阅读器或书写器上面。
包括:
BufferedReader
BufferedWriter
LineNumberReader
PushbackReader
PrintWriter
BufferedReader和BufferedWriter
默认缓冲区大小为8192字符
public BufferedReader(Reader in, int bufferSize)
public BufferedReader(Reader in)
public BufferedWriter(Writer out, int bufferSize)
public BufferedWriter(Writer out)
public static String getMacCyrillicString(InputStream in) throws IOexception{InputStreamReader r = new InputStreamReader(in, "MacCyrillic");r = new BufferedReader(r, 1024);StringBuffer sb = new StringBuffer();int c;while((c = r.read()) != -1)sb.append((char)c);r.close();return sb.toString();
}
LineNumberReader
是BufferedReader的一个记录当前行号的子类。
getLineNumber()获得。
public int getLineNumber()
默认情况下,第一个行号为0。
当前行号和所有其后的行号可以用setLineNumber()方法改变:
public void setLineNumber(int lineNumber)
两个构造函数
public LineNumberReader(Reader in)
public LineNumberReader(Reader in, int bufferSize)
默认大小为8192字符。
PushbackReader
与PushbackInputStream类相似。
public void unread(int c) throws IOException
public void unread(char[] text) throws IOException
public void unread(char[] text, int offset, int length) throws IOException
默认回压缓冲区的大小只有一个字符。
public PushbackReader(Reader in)
public PushbackReader(Reader in, int bufferSize)
PrintWriter
写入字符而不是字节
如果底层的书写器能够正确处理字符集转换,那么PrintWriter的所有方法也能处理这种转换。
PrintWriter out = new PrintWriter(“myfile.txt”); //写文件