Java网络编程—(2)I/O输入输出流

时间:2023-02-26 16:38:46

网络程序很大一部分的工作都是做简单或复杂的输入输出工作,将数据(信息)从一个系统转移到另外一个系统,所以在讲到网络编程之前我们首先要先了解到数据的输入输出工作。需要注意的是java 的基本输入输出流是以字节的方式来读/写数据的,输入输出(I/O,Input Output),都基本使用相同的方法,不论是FileInputStrean/FileOutputStream、TelnetInputStream/TelnetOutputStream,所有的流类都使用基本类似的方法来读写数据。当然,创建一个流(Stream)之后,在某些读/写时通常可以忽略读/写的具体细节。

除了基本的流(Stream)之外还有过滤器(Filter),因为流只提供基本的读写方法(或者说没有另外的包装方法),所以说过滤器就是串联到流之上,来进一步包装基本的流,如果说“流”是水,那么“过滤器”就是水瓢、水缸(稍后就会理解这句话的含义)。阅读器(reader)和书写器(writer)可以串联到流之上,来读取/写入字符而不是字节,正确使用阅读器和书写器可以处理很多字符编码,包括多字节字符集(这在java程序和java程序员不可控制的程序之间传递数据很有作用)。

流是同步的,当程序读/写数据是,在做任何操作之前它(当前读写的线程),都会等待所要读/写的数据,当前线程就会出现堵塞问题,当然,也有非阻塞的I/O,非阻塞的I/O是非常复杂的,但是在吞吐量大的应用和某些物联网开发中又是非常实用的,这个以后会说到。所以,一般情况下,普通的I/O以及其包装类就可以满足我们的需求。

输出流/输入流

java的基本输出流类是java.io.OutputStream:

public abstract class outputStream

这个类提供了写入数据的基本方法:

public abstract void write(int b) throws IOExcetion

public void write(byte[] data) throws IOExcetion

public void write(byte[] data,int offset,int length) throws IOExcetion

public void flush() throws IOExcetion

public void close() throws IOExcetion

write(int b)

其中基本方法是write(int b),接受一个0—255之间的整数作为参数,将对应的字节写入到输出流当中,虽然这个方法接受一个int作为参数但是它实际上只会写入一个无符号的字节,java没有无符号字节类型,所以这里用int来代替,当使用write(int b)将int写入一个网络连接的时候,线缆上只会放8个二进制位。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
*
@author Aaron
* @创建日期:2017年8月27日下午5:51:21
* @修改日期
*/

public class WirterForInt {
public static void main(String[] args) throws IOException {
OutputStream out = new FileOutputStream(new File("D:/test.txt"));
for (int i = 0; i < 255; i++) {
//每次都传入一个整数,稍后看结果
out.write(i);
//每次写出去一个“整数”之后回车换行一下,便于观察
out.write('\r');
out.write('\n');
out.flush();
out.close();
}

}
}

Java网络编程—(2)I/O输入输出流(部分输出结果)

因为ASCII是一个7位字符集,所以每个字符都作为单字节写出去,因此我们可以看到,虽然传入的是整数,但是每次都会写出一个字节。

write(byte[] data)/write(byte[] data,int offset,int length)

使用write(byte[] data)/write(byte[] data,int offset,int length)通常比一次性写入1字节要快的多,我们对之前的程序稍作修改。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
*
@author Aaron
* @创建日期:2017年8月27日下午5:51:21
* @修改日期
*/

public class WirterForInt {
public static void main(String[] args) throws IOException {
OutputStream out = new FileOutputStream(new File("D:/test.txt"));
//创建一个字节数组
byte[] line = new byte[255];
//往文件写三次,每次写一个ling
for (int i = 0; i < 3; i++) {
//写满数组,同样使用ASIIC
for (int j = 0; j < line.length; j++) {
line[j] = (byte) j;
}
out.write(line);
//每次写出去一个ling之后回车换行一下,便于观察
out.write('\r');
out.write('\n');
out.flush();
out.close();
}

}
}

Java网络编程—(2)I/O输入输出流(部分输出结果)

可以看到,每次输出就是一行,然后我们换行,如果写出去255个字节的话,在理论上速度基本提高了255倍

经过不完全测试:写出2550000个字节,使用数组一行写出使用了100ms,使用一字节写出使用了17322ms。

如之前所说,如果把write(int b)比作是往外一滴水一滴水的倒的话,那么使用数组方式就是一瓢水一瓢水的往外倒了。

在写完数据后,刷新(flush)输出流非常重要,加入你向某个服务器写出500个字节,通常你会等待相应,然后再发送更多的数据,不过如果输出流有一个1024字节的缓冲区,那么这个流为慢慢1024,服务器认为你没有发送完毕他一直在等待,而与此同时你也一直在等待,双向等待就会形成死锁状态,所以刷新输出流很重要。与此同时,在刷新流之后,关闭流(close)释放资源也是很重要的。如果只关闭而未刷新,还有可能造成数据的丢失问题。

在一个长时间运行的程序中,如果没有关闭一个流,则可能会泄露文件句柄、网络端口和其他资源,所以,在Java6之前一般的做法都是在finally中刷新和关闭流。在Java7中引入了带资源的try,可以更简洁的完成这个清理-“释放模式”

try(OutputStream outPut = new FileOutputStream(new File("D:/test.txt"))) {
//处理数据流
} catch (Exception e) {
System.err.println(e);
}

java的基本输出流类是java.io.InputStream:

public abstract class InputStream

这个类提供了写入数据的基本方法:

public abstract int read() throws IOException

public int read(byte[] input) throws IOexception

public int read(bytr[] input,int offset,int length) throws IOException

public long skip(long n) throws IOException

public int available() throws IOException

public void close() throws IOException

其中read()和write方法类似,都是一字节处理,其余的不在赘述,值得一提的是:当读取数据流结束后该方法会返回-1,所以有时候我们可以用此方法的返回值来判断当前流是否读取完毕。

重要:在使用read(byte[] input)方法的时候,与使用write(byte[] data)在院里上是互通的,只不过在rede中,如果按照普通的写法加入一次性读取1024字节,有可能会发生这种情况:连接未关闭,但是没有了数据,这时候就会线程堵塞一直在redeing,永远不会返回-1,也就是说永远不会结束读取,所以,要修复这个bug,需要首先测试read()的返回值,然后再增加到bytesread中。

/**
*
@author Aaron
* @创建日期:2017年8月27日下午5:51:21
* @修改日期
*/

public class WirterForInt {

public static void main(String args[]) throws FileNotFoundException,
IOException
{
InputStream in = new FileInputStream(new File("D:/test.txt"));
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;
}
System.out.println(bytesRead);
}
}

这样写就是有多少内容就会读取多少内容,如果就算连接未关闭但是流字节已经没有了,同样也会返回-1结束读取的操作。

当然,我们还可以使用available()方法来确定不阻塞的情况下有多少字节可以读取,它会返回可以读取的最少字节数,

/**
*
@author Aaron
* @创建日期:2017年8月27日下午5:51:21
* @修改日期
*/

public class WirterForInt {

public static void main(String args[]) throws FileNotFoundException,
IOException
{
InputStream in = new FileInputStream(new File("D:/test.txt"));
int bytessAvailable = in.available();
byte[] input = new byte[bytessAvailable];
int bytesRead = in.read(input,0,bytessAvailable);
System.out.println(bytesRead);
//继续执行其余代码
}
}

使用这种方法,我们同样可以避免上述bug的发生。

skip方法很简单,因为在java中没有指针等,所以利用skip方法可以简单的时限为重新指定文件的位置,而不需要处理需要跳过的个字节。

/**
*
@author Aaron
* @创建日期:2017年8月27日下午5:51:21
* @修改日期
*/

public class WirterForInt {
public static void main(String[] args) throws IOException {

FileInputStream fis = null;
int i = 0;
char c;

try {
fis = new FileInputStream("D:/test.txt");
int temp = fis.available();
fis.skip(4);
for (int j = 0; j < temp-4; j++) {
i = fis.read();
c = (char) i;
System.out.print(c);
}

} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (fis != null)
fis.close();
}
}

如果“test.txt”的内容为HelloWorld,那么输出的内容为:oWorld

同样,在read的时候也需要close释放资源。

标记和重置

inputStream类还有3个不太常用的方法,允许程序备份和重新读取已经读取的数据

public void mark(int readAheadLimit)

public void reset() throws IOException

public boolean markSupported()

过滤器流

过滤流有两个版本:过滤器流以及阅读器和书写器。

Java网络编程—(2)I/O输入输出流

如图所示,每个环节都接受前一个过滤器或流的数据,然后传递给下一个环节,处理成不同的数据流,这就是过滤器。

我们着重讲一下BufferedOutputStreamBufferedInputStream(缓冲流)

之前我们就说过如果把流比作是水,原始的读写流的操作是一滴一滴的做的话,那么byte【】数组的方法就是一瓢一瓢的做,BufferedOutputStreamBufferedInputStream就是一缸一缸的做。

public BufferedInputStream(InputStream in)

public BufferedInputStream(InputStream in,int bufferSize)

public BufferedOutputStream(OutputStream out)

public BufferedOutputStream(OutputStream out,int bufferSize)

这分别是缓冲流的两个构造函数。第一个参数是底层流,可以从中读取未缓冲的数据,或者向其写入数据。第二个参数是缓冲区大小,默认输入流缓冲区是2018字节,默认输出流缓冲区大小是512字节。这两个方法没有再生命自己的任何新方法,只是覆盖了原始流的方法。所以在使用read和write的时候基本和原来一样,区别在于,他们会放入缓冲区然后进入底层的流,因此需要发送数据流一定要刷新输出流,这一点很重要。

数据流

DataInputStream和DataOutputStream提供了一些方法,用二进制格式读写java 的基本数据类型和字符串。这在两个不同的java程序和java与其他程序之间交换数据时显得尤为重要。因为输出流写入什么数据输入流就能读取什么数据,不用担心编码问题。

void write(byte[] b,int off,int len);//将byte数组off角标开始的len个字节写到OutputStream 输出流对象中。

void write(int b);//将指定字节的最低8位写入基础输出流。

 void write(byte b[]) throws IOException;//;//将byte数组写到OutputStream 输出流对象中。

void writeBoolean(boolean b);//将一个boolean值以1-byte形式写入基本输出流。

void writeByte(int v);//将一个byte值以1-byte值形式写入到基本输出流中。

void writeBytes(String s);//将字符串按字节顺序写入到基本输出流中。

void writeShort(int s);//将字符串按字节顺序写入到基本输出流中。

void writeChar(int v);//将一个char值以2-byte形式写入到基本输出流中。先写入高字节。

void writeInt(int v);//将一个int值以4-byte值形式写入到输出流中先写高字节。

void writeLong(long v) throws IOException;

void writeFloat(float v) throws IOException;

void writeDouble(double v) throws IOException;

 void writeChars(String s) throws IOException;

void writeUTF(String str);//以机器无关的的方式用UTF-8修改版将一个字符串写到基本输出流。该方法先用writeShort写入两个字节表示后面的字节数。

int size();//返回written的当前值。

与此同时都会有相对于的reader方法来读取数据。需要注意的是wriUtf方法将字符串用Unicode UTF-8编码的一个变体进行编码,由于这个变体的编码方式与大多数java程序有点不兼容,所以应当只用于其他使用readUTF方法来配合使用。

阅读器和书写器

许多程序员在编码时都有一个坏习惯,好像所有的文本都是Asiic,这样就会很容易出现编码问题,因此,对应输入输出流层次体系,java提供了一个基本上完整的镜像,用来处理字符而不是字节——java.io.Writer/java.io.reader

最重要的子类是InputStreamReader和OutputStreamWriter,分别可以读取底层输入流然后转换成Unicode字符或者写出为Unicode字符。

public OutputStreamWriter(OutputStream out,String encoding) throws UnsupportedEncodingException它根据制定的编码方式将这些字符转换为字节,并写入底层输出流。另外还有个返回编码的方法:public String getEncoding();

public InputStreamReader(InputStream out,String encoding) throws UnsupportedEncodingException它根据制定的编码方式将这些字节转换为字符,并返回这些字符。,如果没指定编码就会按照平台的默认编码方式读取。

过滤阅读器和书写器

BufferedReader

BufferedWriter

LineNumberReader

PrintWriter

BufferedReader(reader in,int bufferSize)

BufferedReader(reader in)

BufferedReader(Writer out)

BufferedReader(Writer out,int bufferSize)

ReaderLine方法可以代替DatainptStream中赢飞起的readLine方法,它与该方法的行为基本相同。

最后对于过滤器没有做过多的描述,因为java的I/O完全可以单独提取出来作为一项技术来讲解,所以在网络编程中做简单的介绍和方法的使用,如果有不明白的地方请大家随时联系我。