《Java网络编程》过滤器流

137 阅读13分钟

java网络编程

第一章 过滤器流

前言

过滤器流(Filter InputStream)是Java中一种特殊的输入流,它允许你通过链式组合的方式,将多个输入流的功能叠加在一起,从而实现对输入数据的各种处理。


一、过滤器流是什么?有哪些?

过滤器流是什么:

过滤器流是InputStream的一个抽象子类,它本身并不直接提供数据,而是作为其他输入流的包装器(Wrapper),通过在其上添加额外的功能来扩展输入流的行为。

过滤器流有哪些:

缓冲流:BufferedOutputStream 打印流:PrintStream 数据流:DataInputStream 推回流:PushbackInputStream ...等继承了 当然除了上述具体的示例外,他们其他的输入输出类也输入过滤器流

过滤器流的特点

链式组合:过滤器流可以与其他输入流(包括其他过滤器流)组合在一起,形成一个处理链。 功能扩展:通过在过滤器流中添加特定的逻辑,可以实现数据压缩、解密、缓冲等多种功能。 透明性:对于使用过滤器流的代码来说,过滤器流的存在是透明的,它们只需要与InputStream接口进行交互。

二、缓冲流

将过滤器串链在一起

代码如下(示例):

public static void filterFunc() throws IOException{
        InputStream in = new FileInputStream("output.txt");
        in = new BufferedInputStream(in);
		int data;
        while ((data = in.read()) != -1) {
            // 处理读取到的数据
            System.out.println((char) data);
        }
    }

在这个示例中,FileInputStream用于从文件"output.txt"中读取数据,而BufferedInputStream则用于包装FileInputStream,以提高读取效率。通过BufferedInputStream的read方法,我们可以逐字节地读取文件内容,并将其打印到控制台。

...
9
:
;
<
=
>
?
@
A
...

输出结果为output.txt数据逐个读取打印,复习:因为read()方法是逐个获取。

知识点:

每个过滤器输出流都有与java.io.OutputStream相同的write()、close()、和flush()方法。每个输入流都有与java.io.InputStream相同的read()、close()、和available()方法。 过滤存粹是内部操作,不提供任何新的公共接口。在很多情况下,过滤器流会增加一些公共方法提供额外的作用。例如PushbackInputStream的unread()方法。PrintStream的write()方法很少用,而是用print()和println()方法。

为什么要过滤器流,我分开单独写流不行吗?

举个例子,如果你需要从一个文件中读取数据,并对数据进行解压缩和加密处理,那么你可以使用过滤器流来实现这个需求。你可以创建一个GZIPInputStream来解压缩数据,然后将其包装在一个CipherInputStream中进行加密处理。这样,你就可以通过一条链式的输入流来处理所有的数据了。

相比之下,如果你不使用过滤器流,而是分别编写解压缩和加密的代码,那么你需要手动管理数据的流动和状态,这可能会增加代码的复杂性和出错的风险。

BufferedOutputStream类将写入的数据存储在缓冲区(一个名为buf的保护字节数组字段),直到缓冲区满或刷新输出流,然后它将数据一次全部写入底层输出流底层输出流指的是被BufferedOutputStream(OutputStream out,int bufferSize)所包装的流。

代码如下(示例):

public static void ouputBufFunc() throws IOException {
        FileOutputStream fos = null;
        BufferedOutputStream bos = null;
        try {
            // 创建 FileOutputStream,指向一个文件(假设文件名为 output.txt)
            fos = new FileOutputStream("output.txt");
            // 创建 BufferedOutputStream,包装 FileOutputStream
            bos = new BufferedOutputStream(fos);
            // 写入一些数据到 BufferedOutputStream(实际上数据先写入缓冲区)
            String data = "Hello, world!";
            bos.write(data.getBytes());
            // 此时数据还在缓冲区中,没有写入到文件中
            // 如果我们想确保数据现在就被写入到文件,我们需要刷新输出流
            bos.flush();
            // 注意:在关闭 BufferedOutputStream 时,它会自动刷新缓冲区
            // 因此,下面的 close() 调用也会确保所有数据都被写入
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 在 finally 块中关闭流,以确保资源被释放
            try {
                if (bos != null) {
                    bos.close();
                }
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

BufferedInputStream类也有一个缓冲区的保护字节数组,名为buf。当调用某个流的read()方法时,首先尝试从缓冲区获得请求的数据。只有当缓冲区没有数据时,流才从底层的源中读取数据。他会尽可能多的从源中读取数据放入缓冲区。 关于BufferedStream 类中默认缓冲区字节,书本上为2048,但实际编码时,我所使用的jdk1.8(semeru-1.8.0_432)java.io包中默认字节数已经提升至8192。

代码如下(示例):

public
class BufferedOutputStream extends FilterOutputStream {
    /**
     * The internal buffer where data is stored.
     */
    protected byte buf[];

    /**
     * The number of valid bytes in the buffer. This value is always
     * in the range <tt>0</tt> through <tt>buf.length</tt>; elements
     * <tt>buf[0]</tt> through <tt>buf[count-1]</tt> contain valid
     * byte data.
     */
    protected int count;

    /**
     * Creates a new buffered output stream to write data to the
     * specified underlying output stream.
     *
     * @param   out   the underlying output stream.
     */
    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }
    ...
    ...
    public
class BufferedInputStream extends FilterInputStream {

    private static int DEFAULT_BUFFER_SIZE = 8192;

    /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;

    /**
     * The internal buffer array where the data is stored. When necessary,
     * it may be replaced by another array of
     * a different size.
     */
    protected volatile byte buf[];
    }
    }
    ...

当从本地磁盘读取文件时,从底层流中读取几百字节的数据和读取1字节速度几乎一样快。 缓冲区可以显著提升性能。对于网络连接,效果则不明显,瓶颈往往是网络传播数据速度,而不是网络接口向程序传送数据的速度或程序运行速度。

三、PrintStream

PrintStream类应该是大家用到的第一个过滤器输出流。第一眼感到陌生的同学,看看这串代码熟悉吗?

 System.out.println(data);

System.out就是一个PrintStream,可以使用两个构造函数将其他输出流串链到打印流:

public PrintStream(OutputStream out)
public PrintStream(OutputStream out, boolean autoFlush)

System.out.println()看不出来哪里用到了PrintStream呀?
//走进System类,就一幕了然了,out是一个PrintStream类型的静态成员变量
public final class System {

	public static final InputStream in = null;

	public static final PrintStream out = null;
	...

默认情况下,打印流应当显示刷新输出。如果autoFlush为true,则每次换行、写入一个字节,或调用println()时都会刷新输出流 除了认识到printStream 是一个过滤器流可以串链其他流外,还能复习一个java多态的特性之一重载。为什么print方法可以打印我们放进去的任意类型数据呢?原因就是有很多重载的print、println方法。

代码如下(示例):

public void print(boolean b) {
        write(b ? "true" : "false");
    }
    public void print(char c) {
        write(String.valueOf(c));
    }

    public void print(int i) {
        write(String.valueOf(i));
    }

    public void print(long l) {
        write(String.valueOf(l));
    }

    public void print(float f) {
        write(String.valueOf(f));
    }

    public void print(double d) {
        write(String.valueOf(d));
    }

    public void print(char s[]) {
        write(s);
    }
    public void print(String s) {
        if (s == null) {
            s = "null";
        }
        write(s);
    }

    public void print(Object obj) {
        write(String.valueOf(obj));
    }
    public void println() {
        newLine();
    }

    public void println(boolean x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(char x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(int x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(long x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(float x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(double x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
    public void println(char x[]) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

    public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }

每个打印方法都将参数以正确的方式转换为一个字符串,再用默认编码的方式将字符串写入底层输出流。而Println()方法还会在末尾根据运行代码的操作系统平台添加对应的换行符号。UNIX为\n,Mac为\r,windows为\r\n

PrintStream存在的问题,在网络编程中应当避免使用

问题一:

println()的输出与平台相关。在控制台输出时不会有什么问题,但对于编写必须遵循明确协议的网络客户端和服务器而言,不相同的行结束符会带来错误。 例如,假设你正在编写一个遵循HTTP协议的网络客户端。HTTP协议要求请求和响应的头字段以回车(\r)后跟换行(\n)结束,即 \r\n。如果你的客户端或服务器在打印这些头字段时使用 println() 方法,并且它运行在一个只使用换行(\n)作为行结束符的平台上,那么生成的HTTP请求或响应将不符合协议规范,这可能导致通信失败或服务器返回错误。

问题二:

PrintStream使用所在平台的默认编码,而这种编码不一定是服务器或客户端所适配的。

问题三:

PrintStream吞掉了所有异常。在网络连接中,可能会由于网络阻塞、对端系统有错等原因而断开。当吞掉异常后就无法即使处理对应异常。虽然PrintStream捕获其底层流的抛出的异常,但要依靠一个checkError()方法来检查一个错误标志,如果使用PrintStream完成所有错误检查,一旦出现错误,就没有办法重置这个标志进行进一步错误检测,也没有关于这个错误的更多的信息。

总之PrintStream提供的错误通知对于不可靠的网络连接来说不够用。


四、数据流

DataInputStream 和 DataOutputStream 是 Java 中的两个类,它们分别用于从输入流中以二进制格式读取数据,以及向输出流中以二进制格式写入数据。 所用二进制格式主要是为了在两个不同JAVA程序之间传输数据。而大多数交换二进制数的网络协议所用的格式相同,例如,时间协议使用32位big-endian整数。但也不是所有网络协议都适合,例如,网络时间协议NTP,他会把时间表示为64位无符号定点数,前32位是整数部分,后32位为小数部分。 无符号定点数: 指的是在机器字长中,所有的二进制位都用来表示数值,而没有专门的符号位来表示数的正负。定点数则意味着小数点在数中的位置是固定的,不会改变。

DataOutputStream和DataInputStream 会提供多种方法进行写入java数据类型与读取

例如:

public final void writeBoolean(boolean v) throws IOException {
        out.write(v ? 1 : 0);
        incCount(1);
    }
public final void writeByte(int v) throws IOException {
        out.write(v);
        incCount(1);
    }
public final void writeShort(int v) throws IOException {
        out.write((v >>> 8) & 0xFF);
        out.write((v >>> 0) & 0xFF);
        incCount(2);
    }
public final void writeChar(int v) throws IOException
public final void writeInt(int v) throws IOException {
        out.write((v >>> 24) & 0xFF);
        out.write((v >>> 16) & 0xFF);
        out.write((v >>>  8) & 0xFF);
        out.write((v >>>  0) & 0xFF);
        incCount(4);
    }
public final void writeLong(long v) throws IOException
public final void writeFloat(float v) throws IOException
public final void writeDouble(double v) throws IOException
public final void writeBytes(String s) throws IOException
public final void writeChars(String s) throws IOException
public final void writeUTF(String str) throws IOException
...

所有数据都以big-endian格式写入。例如整数用尽可能少的字节写为2的补码

2的补码表示法

2的补码是一种二进制数的表示方法,用于表示有符号整数。在这种表示法中,正数的二进制表示与其原码(即直接按位表示的二进制数)相同,而负数的二进制表示则是通过取其正数的二进制表示,然后所有位取反(即0变为1,1变为0),最后加1来得到的。

例如,对于8位二进制数:

正数5的二进制表示为 00000101(原码也是00000101)。 负数-5的二进制表示则是先取5的二进制表示00000101,然后所有位取反得到11111010,最后加1得到11111011(这就是-5的2的补码表示)。

big-endian格式

big-endian,指数据在内存或存储中的表示方式。在big-endian格式中,最重要的字节(即最高有效位字节)存储在最低的内存地址或数据的起始位置。

以writeInt()为例

out.write((v >>> 24) & 0xFF);:这行代码首先将整数v无符号右移24位(>>>是无符号右移运算符),这样原来的最高8位(即第24到31位)就被移动到了最低8位的位置。然后,使用位与运算符&和0xFF(即二进制的11111111)进行运算,以保留这8位的值,并去掉其他位。最后,调用out.write方法将这个8位的值(现在是一个字节)写入输出流。 接下来的三行代码类似地处理了整数v的接下来的三个8位段(从高到低分别是16到23位、8到15位、0到7位),并将它们分别写入输出流。

其他函数

writeBytes()方法迭代处理String参数,但只写入每个字符的低字节。在字符串中如果包含Latin-1字符集以外的字符(比如“中文”),其中信息会丢失。所以尽量避免使用。 writeUTF()方法包括了字符串长度,它将字符串本身用Unicode UTF-8编码的变体进行编码,由于这个变体与非JAVA程序不兼容,所以应只与其他使用DataInputStream读取字符串的java程序进行交互。为了与其他软件进行UTF-8文本传输,应使用适当编码的InputStreamReader。 当然DataOutputStream和DataInputStream除了上述自有函数,也有所有流类的write()、flush()、close()等基础方法。 用一个程序来试试DataOutputStream和DataInputStream

代码示例

public static void dataStreamFunc() {
        String filename = "data.txt";

        // 使用 DataOutputStream 写入数据
        try (FileOutputStream fos = new FileOutputStream(filename);
             DataOutputStream dos = new DataOutputStream(fos)) {

            dos.writeInt(12345);        // 写入一个 int
            dos.writeFloat(3.14f);      // 写入一个 float
            dos.writeUTF("Hello, World!"); // 写入一个字符串

        } catch (IOException e) {
            e.printStackTrace();
        }
            // 使用 DataInputStream 读取数据
        try (FileInputStream fis = new FileInputStream(filename);
             DataInputStream dis = new DataInputStream(fis)) {
            int intValue = dis.readInt();       // 读取一个 int
            float floatValue = dis.readFloat(); // 读取一个 float
            String strValue = dis.readUTF();    // 读取一个字符串
            System.out.println("读取到的 int 值: " + intValue);
            System.out.println("读取到的 float 值: " + floatValue);
            System.out.println("读取到的字符串: " + strValue);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

程序结果

读取到的 int 值: 12345
读取到的 float 值: 3.14
读取到的字符串: Hello, World!

最后,DataInputStream提供了流行的readLine() 它读取用行结束符分割的一行文本数据,并返回一个字符串。任何情况下都不要使用!

public final String readLine() throws IOException

因为它只能识别换行或回车/换行对。如果回车是最后一个字符,那么readLine()会挂起,一直等待最后字符出现。在读取文件时这个问题不明显,因为文件没有下个字符会以-1表示结束。而在网络连接中,服务器或客户端可能发送一些数据后停止发送,并等待响应,不会关闭连接。这样就会导致连接超时或挂起。

总结

以上就是今天要分享的关于过滤器的内容,本文仅仅简单介绍一些过滤器流类的使用与注意事项。最后总结一下过滤器的作用:过滤器流可以通过拦截、处理和转换数据流中的信息,实现数据的清洗、格式化、加密、解密、压缩、解压等功能,以及过滤器流使用的包装的设计模式。