问题描述
在编写Socket代码时,客户端使用BufferedWriter来进行数据输出,服务器端使用BufferedReader来进行数据读取,当客户端发出数据后,服务端一直没有取到数据。
解决
需要使用BufferedWriter的flush()方法,以及注意BufferedReader的readLine()方法。
flush()方法会将缓存中的数据输出到底层输入流中。
readLine()方法会读取一行数据,因此如果没有遇到\n(回车),则读取不到数据。
详解
Java IO的整体体系如下图
如果不使用缓冲流,进行写入操作时,会将数据以字节为单位,一个一个字节的向内存中写入,由于每次写入操作都涉及到用户控件和内核空间的转换,因此以字节为单位进行写入效率会特别低下。对读取操作同样如此,如果不使用缓冲流,会每次都从内核空间读取一个字节到用户空间中,出现大量的时间损耗。
以BufferedOutputStream为例做个简单的介绍
看看官方对BufferedOutputStream的介绍public class BufferedOutputStream
extends FilterOutputStream
The class implements a buffered output stream. By setting up such an output stream, an application can write bytes to the underlying output stream without necessarily causing a call to the underlying system for each byte written.
在向底层输出流写入字节时,不必为写入的每个字节进行底层系统的调用。BufferedOutputStream中有如下几个参数
protected byte buf[]
The internal buffer where data is stored. 即保存数据的一个字节数组。
protected int count
The number of valid bytes in the buffer. This value is always in the range 0 through buf.length; elements buf[0] through buf[count-1] contain valid byte data. 即有效字节的数量。
BufferedOutputStream中的构造函数如下
public BufferedOutputStream(OutputStream out) { this(out, 8192); } public BufferedOutputStream(OutputStream out, int size) { super(out); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; }
可见,在默认情况下,buf数组的长度为8192,也就是说默认可以存储8192字节的缓存。
接下来看看BufferedOutputStream的write方法,就可以看出缓存流和普通的输入输出流的区别。
public synchronized void write(int b) throws IOException { if (count >= buf.length) { flushBuffer(); } buf[count++] = (byte)b; }
可见如果count>=buf.length,才会调用flushBuffer()方法,该方法的作用是将buf数组中的存储数据写入内核空间中,到后面讨论。而如果count小于buf.length,则仅仅将该字节存储进buf数组中。
接下来看看flush方法,调用flush方法可以将这一时刻buf数组中的数据写入底层输出流中,不需等待buf数组溢出。
public synchronized void flush() throws IOException { flushBuffer(); out.flush(); } private void flushBuffer() throws IOException { if (count > 0) { out.write(buf, 0, count); count = 0; } }
可见,调用flush方法,会调用write方法,把buf中的数据写入底层的输出流中,out就是底层输出流,out存在于BufferedOutputStream的父类FilterOutputStream中,out是一个OutputStream对象,当然可以是OutputStream的子类。
现在假设我们使用的out是一个SocketOutputStream对象,看看其重写了OutputStream中的write方法,如下。
public void write(byte b[], int off, int len) throws IOException { socketWrite(b, off, len); }
再看看socketWrite方法。
private void socketWrite(byte b[], int off, int len) throws IOException { if (len <= 0 || off < 0 || len > b.length - off) { if (len == 0) { return; } throw new ArrayIndexOutOfBoundsException("len == " + len + " off == " + off + " buffer length == " + b.length); } FileDescriptor fd = impl.acquireFD(); try { socketWrite0(fd, b, off, len); } catch (SocketException se) { if (se instanceof sun.net.ConnectionResetException) { impl.setConnectionResetPending(); se = new SocketException("Connection reset"); } if (impl.isClosedOrPending()) { throw new SocketException("Socket closed"); } else { throw se; } } finally { impl.releaseFD(); } }
其中fd是一个FileDescriptor对象,被称为文件描述符,IO的通信实际上都是基于文件描述符的,对fd的理解可查看该博客
在socketWrite方法中,调用了socketWrite0方法,该方法是一个native方法,就不深究了。private native void socketWrite0(FileDescriptor fd, byte[] b, int off, int len) throws IOException;
close方法继承于FilterOutputStream,如下
public void close() throws IOException { try (OutputStream ostream = out) { flush(); } }
该方法调用了flush()方法,因此,在将输出流关闭时,会将buf数组中存储的数据写入底层输出流中。
由此可见,使用BufferedOutputStream可以实现一次系统调用就将多字节写入底层输出流中,而不是每一个字节都进行一次系统调用。