目录:
问题1:服务端接收不到客户端发送的消息
问题2:客户端接收服务端消息不同步问题
先上完整且无误的代码:
服务端EchoServer:
- package part1;
-
- import java.io.*;
- import java.net.ServerSocket;
- import java.net.Socket;
-
- import static java.lang.System.out;
-
- public class EchoServer {
- private int port = 8000;
- private ServerSocket serverSocket;
-
- public EchoServer() throws IOException {
- serverSocket = new ServerSocket(port);
- out.println("服务器启动了");
- }
-
- public String echo(String msg) {
- return "echo: " + msg;
- }
-
- private PrintWriter getWriter(Socket socket) throws IOException {
- OutputStream socketOut = socket.getOutputStream();
- return new PrintWriter(socketOut, true);
- }
-
- private BufferedReader getReader(Socket socket) throws IOException {
- InputStream socketIn = socket.getInputStream();
- return new BufferedReader(new InputStreamReader(socketIn));
- }
-
- public void service() {
- while (true) {
- Socket socket = null;
- try {
- socket = serverSocket.accept();
- out.println("new connection accepted" + socket.getInetAddress()
- + ":" + socket.getPort());
- BufferedReader reader = getReader(socket);
- PrintWriter writer = getWriter(socket);
- String msg = null;
-
- while ((msg = reader.readLine()) != null) {
- out.println(msg);
- writer.println(echo(msg));
- if (msg == "bye") {
- break;
- }
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- if (socket != null) {
- try {
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
-
- }
- }
-
- public static void main(String[] args) throws IOException {
- new EchoServer().service();
- }
-
- }
客户端EchoClient:
- package part1;
-
- import java.io.*;
- import java.net.Socket;
-
- import static java.lang.System.out;
-
- public class EchoClient {
- private String host = "localhost";
- private int port = 8000;
- private Socket socket;
-
- public EchoClient() throws IOException {
- socket = new Socket(host, port);
- }
-
- public static void main(String[] args) throws IOException {
- new EchoClient().talk();
- }
-
- private PrintWriter getWriter(Socket socket) throws IOException {
- OutputStream socketOut = socket.getOutputStream();
- return new PrintWriter(socketOut, true);
- }
-
- private BufferedReader getReader(Socket socket) throws IOException {
- InputStream socketIn = socket.getInputStream();
- return new BufferedReader(new InputStreamReader(socketIn));
- }
-
- public void talk() throws IOException {
- BufferedReader reader = getReader(socket);
- PrintWriter writer = getWriter(socket);
-
- BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
- String msg = null;
- try {
- //循环从标准输入流中读取数据, 发送给服务器,然后接收服务器数据并标准化输出
- while ((msg = br.readLine()) != null) { //从标准化输入流中读取信息,当没有内容输入时会阻塞
- writer.println(msg); //向服务器发送msg
- out.println(reader.readLine()); //接收来自服务器的信息并标准化输出
- if (msg == "bye") {
- break;
- }
- }
- } finally {
- out.println("error");
- if (socket != null) {
- socket.close();
- }
- }
- }
-
- }
问题1:
假如把客户端代码的new PrintWriter(socketOut, true);替换成new PrintWriter(socketOut);
效果如下:

可见PrintWriter构造函数中第二个参数autoFlush的重要性了。
- /**
- * Creates a new PrintWriter from an existing OutputStream. This
- * convenience constructor creates the necessary intermediate
- * OutputStreamWriter, which will convert characters into bytes using the
- * default character encoding.
- *
- * @param out An output stream
- * @param autoFlush A boolean; if true, the println,
- * printf, or format methods will
- * flush the output buffer
- *
- * @see java.io.OutputStreamWriter#OutputStreamWriter(java.io.OutputStream)
- */
- public PrintWriter(OutputStream out, boolean autoFlush) {
- this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
-
- // save print stream for error propagation
- if (out instanceof java.io.PrintStream) {
- psOut = (PrintStream) out;
- }
- }
autoFlush默认值为false。当autoFlash为true时,代表PrintWriter实例对象的println、printf或format方法被调用时,会清空输入流的缓冲区。从这大概能猜到了服务端接收不到客户端发送过来信息的根因了。autoFlush默认为false,这就导致了客户端PrintWriter实例对象每次调用println方法时都是往buffer缓冲区中写数据,没有执行flush()方法,所以数据都存在缓冲区没有被发送到服务端了。
下面来从源码层面来分析下。首先还是先来看PrintWriter的构造方法。可以看到其函数体内部使用到了装饰器模式。
this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
PrintWriter打印输出流本质上是对BufferedWriter缓冲输入流的功能增强。
我们需要知道,BufferedWriter流是带缓冲区的。当BufferedWriter对象写入数据的时候,会先写入到缓冲区,当调用了flush方法时,才会把缓冲区的数据一次性清空并写入到目标文件中。
BufferedWriter:
- private Writer out;
-
- private char cb[];
- private int nChars, nextChar;
-
- private static int defaultCharBufferSize = 8192;
-
- /**
- * Creates a buffered character-output stream that uses a default-sized
- * output buffer.
- *
- * @param out A Writer
- */
- public BufferedWriter(Writer out) {
- this(out, defaultCharBufferSize);
- }
-
-
- /**
- * Creates a new buffered character-output stream that uses an output
- * buffer of the given size.
- *
- * @param out A Writer
- * @param sz Output-buffer size, a positive integer
- *
- * @exception IllegalArgumentException If {@code sz <= 0}
- */
- public BufferedWriter(Writer out, int sz) {
- super(out);
- if (sz <= 0)
- throw new IllegalArgumentException("Buffer size <= 0");
- this.out = out;
- cb = new char[sz];
- nChars = sz;
- nextChar = 0;
-
- lineSeparator = java.security.AccessController.doPrivileged(
- new sun.security.action.GetPropertyAction("line.separator"));
- }
-
- /**
- * Writes a single character.
- *
- * @exception IOException If an I/O error occurs
- */
- public void write(int c) throws IOException {
- synchronized (lock) {
- ensureOpen();
- if (nextChar >= nChars)
- flushBuffer();
- cb[nextChar++] = (char) c;
- }
- }
-
- /**
- * Flushes the output buffer to the underlying character stream, without
- * flushing the stream itself. This method is non-private only so that it
- * may be invoked by PrintStream.
- */
- void flushBuffer() throws IOException {
- synchronized (lock) {
- ensureOpen();
- if (nextChar == 0)
- return;
- out.write(cb, 0, nextChar);
- nextChar = 0;
- }
- }
当BufferedWriter构造器中没有传入Output-buffer size时,其默认值为defaultCharBufferSize = 8192。并且在构造器中创建了一个大小为sz的字符数组用来作为输入缓冲区。在调用write方法时,会先判断当前缓冲区内字符数量是否大于缓冲区大小,如果大于则调用flushBuffer()清空缓冲区,否则继续往缓冲区中写入数据cb[nextChar++] = (char) c。所以flushBuffer()才是真正地往目标文件中写入数据。
综上,可以看到BufferedWriter只有在缓冲区满了之后才会清空缓冲区。
总结:BufferedWriter是缓冲输出流,意思是调用BufferedWriter的write方法时候。数据先从JVM内存写入到缓冲区里,并没有直接写到目的文件。
接下来再回到PrintWriter, 来看看PrintWriter是如何写入数据的。可以看到
1、println方法中先执行print(x)方法,在这个方法内部会调用BufferedWriter对象out来往缓冲区中写入数据;
2、然后执行println(),在其内部会先写入换行符lineSeparator。然后判断autoFlush是否为true。true代表要清空缓冲区。会调用out。flush()方法来真正地往目标文件中写入数据。
PrintWriter:
- /**
- * Prints a String and then terminates the line. This method behaves as
- * though it invokes
{@link #print(String)} and then - *
{@link #println()}. - *
- * @param x the
String value to be printed - */
- public void println(String x) {
- synchronized (lock) {
- print(x);
- println();
- }
- }
-
-
- /**
- * Terminates the current line by writing the line separator string. The
- * line separator string is defined by the system property
- *
line.separator, and is not necessarily a single newline - * character (
'\n'). - */
- public void println() {
- newLine();
- }
-
- private void newLine() {
- try {
- synchronized (lock) {
- ensureOpen();
- out.write(lineSeparator);
- if (autoFlush)
- out.flush();
- }
- }
- catch (InterruptedIOException x) {
- Thread.currentThread().interrupt();
- }
- catch (IOException x) {
- trouble = true;
- }
- }
假设把客户端代码writer.println(msg);换成writer.println(msg + "\n");会出现客户端接收到来自服务器的消息不同步的问题:

仔细分析一下便知:客户端每次给服务器发送writer.println(msg + "\n"); 服务器接收到消息后又原封不动地返回地客户端。客户端调用reader.readLine()读取一行数据并展示。但其实服务器返回了两行数据,客户端每次循环中只取一行数据展示,这就造成了客户端数据展示不同步的问题。