• 记一次简单的网络通信遇到的问题点总结


    目录:

            问题1:服务端接收不到客户端发送的消息

            问题2:客户端接收服务端消息不同步问题   

    先上完整且无误的代码:

    服务端EchoServer:

    1. package part1;
    2. import java.io.*;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import static java.lang.System.out;
    6. public class EchoServer {
    7. private int port = 8000;
    8. private ServerSocket serverSocket;
    9. public EchoServer() throws IOException {
    10. serverSocket = new ServerSocket(port);
    11. out.println("服务器启动了");
    12. }
    13. public String echo(String msg) {
    14. return "echo: " + msg;
    15. }
    16. private PrintWriter getWriter(Socket socket) throws IOException {
    17. OutputStream socketOut = socket.getOutputStream();
    18. return new PrintWriter(socketOut, true);
    19. }
    20. private BufferedReader getReader(Socket socket) throws IOException {
    21. InputStream socketIn = socket.getInputStream();
    22. return new BufferedReader(new InputStreamReader(socketIn));
    23. }
    24. public void service() {
    25. while (true) {
    26. Socket socket = null;
    27. try {
    28. socket = serverSocket.accept();
    29. out.println("new connection accepted" + socket.getInetAddress()
    30. + ":" + socket.getPort());
    31. BufferedReader reader = getReader(socket);
    32. PrintWriter writer = getWriter(socket);
    33. String msg = null;
    34. while ((msg = reader.readLine()) != null) {
    35. out.println(msg);
    36. writer.println(echo(msg));
    37. if (msg == "bye") {
    38. break;
    39. }
    40. }
    41. } catch (IOException e) {
    42. e.printStackTrace();
    43. } finally {
    44. if (socket != null) {
    45. try {
    46. socket.close();
    47. } catch (IOException e) {
    48. e.printStackTrace();
    49. }
    50. }
    51. }
    52. }
    53. }
    54. public static void main(String[] args) throws IOException {
    55. new EchoServer().service();
    56. }
    57. }

    客户端EchoClient:

    1. package part1;
    2. import java.io.*;
    3. import java.net.Socket;
    4. import static java.lang.System.out;
    5. public class EchoClient {
    6. private String host = "localhost";
    7. private int port = 8000;
    8. private Socket socket;
    9. public EchoClient() throws IOException {
    10. socket = new Socket(host, port);
    11. }
    12. public static void main(String[] args) throws IOException {
    13. new EchoClient().talk();
    14. }
    15. private PrintWriter getWriter(Socket socket) throws IOException {
    16. OutputStream socketOut = socket.getOutputStream();
    17. return new PrintWriter(socketOut, true);
    18. }
    19. private BufferedReader getReader(Socket socket) throws IOException {
    20. InputStream socketIn = socket.getInputStream();
    21. return new BufferedReader(new InputStreamReader(socketIn));
    22. }
    23. public void talk() throws IOException {
    24. BufferedReader reader = getReader(socket);
    25. PrintWriter writer = getWriter(socket);
    26. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    27. String msg = null;
    28. try {
    29. //循环从标准输入流中读取数据, 发送给服务器,然后接收服务器数据并标准化输出
    30. while ((msg = br.readLine()) != null) { //从标准化输入流中读取信息,当没有内容输入时会阻塞
    31. writer.println(msg); //向服务器发送msg
    32. out.println(reader.readLine()); //接收来自服务器的信息并标准化输出
    33. if (msg == "bye") {
    34. break;
    35. }
    36. }
    37. } finally {
    38. out.println("error");
    39. if (socket != null) {
    40. socket.close();
    41. }
    42. }
    43. }
    44. }

    问题1:

            假如把客户端代码的new PrintWriter(socketOut, true);替换成new PrintWriter(socketOut);

    效果如下:

            可见PrintWriter构造函数中第二个参数autoFlush的重要性了。

    1. /**
    2. * Creates a new PrintWriter from an existing OutputStream. This
    3. * convenience constructor creates the necessary intermediate
    4. * OutputStreamWriter, which will convert characters into bytes using the
    5. * default character encoding.
    6. *
    7. * @param out An output stream
    8. * @param autoFlush A boolean; if true, the println,
    9. * printf, or format methods will
    10. * flush the output buffer
    11. *
    12. * @see java.io.OutputStreamWriter#OutputStreamWriter(java.io.OutputStream)
    13. */
    14. public PrintWriter(OutputStream out, boolean autoFlush) {
    15. this(new BufferedWriter(new OutputStreamWriter(out)), autoFlush);
    16. // save print stream for error propagation
    17. if (out instanceof java.io.PrintStream) {
    18. psOut = (PrintStream) out;
    19. }
    20. }

             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:

    1. private Writer out;
    2. private char cb[];
    3. private int nChars, nextChar;
    4. private static int defaultCharBufferSize = 8192;
    5. /**
    6. * Creates a buffered character-output stream that uses a default-sized
    7. * output buffer.
    8. *
    9. * @param out A Writer
    10. */
    11. public BufferedWriter(Writer out) {
    12. this(out, defaultCharBufferSize);
    13. }
    14. /**
    15. * Creates a new buffered character-output stream that uses an output
    16. * buffer of the given size.
    17. *
    18. * @param out A Writer
    19. * @param sz Output-buffer size, a positive integer
    20. *
    21. * @exception IllegalArgumentException If {@code sz <= 0}
    22. */
    23. public BufferedWriter(Writer out, int sz) {
    24. super(out);
    25. if (sz <= 0)
    26. throw new IllegalArgumentException("Buffer size <= 0");
    27. this.out = out;
    28. cb = new char[sz];
    29. nChars = sz;
    30. nextChar = 0;
    31. lineSeparator = java.security.AccessController.doPrivileged(
    32. new sun.security.action.GetPropertyAction("line.separator"));
    33. }
    34. /**
    35. * Writes a single character.
    36. *
    37. * @exception IOException If an I/O error occurs
    38. */
    39. public void write(int c) throws IOException {
    40. synchronized (lock) {
    41. ensureOpen();
    42. if (nextChar >= nChars)
    43. flushBuffer();
    44. cb[nextChar++] = (char) c;
    45. }
    46. }
    47. /**
    48. * Flushes the output buffer to the underlying character stream, without
    49. * flushing the stream itself. This method is non-private only so that it
    50. * may be invoked by PrintStream.
    51. */
    52. void flushBuffer() throws IOException {
    53. synchronized (lock) {
    54. ensureOpen();
    55. if (nextChar == 0)
    56. return;
    57. out.write(cb, 0, nextChar);
    58. nextChar = 0;
    59. }
    60. }

            当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:

    1. /**
    2. * Prints a String and then terminates the line. This method behaves as
    3. * though it invokes {@link #print(String)} and then
    4. * {@link #println()}.
    5. *
    6. * @param x the String value to be printed
    7. */
    8. public void println(String x) {
    9. synchronized (lock) {
    10. print(x);
    11. println();
    12. }
    13. }
    14. /**
    15. * Terminates the current line by writing the line separator string. The
    16. * line separator string is defined by the system property
    17. * line.separator, and is not necessarily a single newline
    18. * character ('\n').
    19. */
    20. public void println() {
    21. newLine();
    22. }
    23. private void newLine() {
    24. try {
    25. synchronized (lock) {
    26. ensureOpen();
    27. out.write(lineSeparator);
    28. if (autoFlush)
    29. out.flush();
    30. }
    31. }
    32. catch (InterruptedIOException x) {
    33. Thread.currentThread().interrupt();
    34. }
    35. catch (IOException x) {
    36. trouble = true;
    37. }
    38. }

    问题2:客户端接收服务端消息不同步问题   

            假设把客户端代码writer.println(msg);换成writer.println(msg + "\n");会出现客户端接收到来自服务器的消息不同步的问题:

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

  • 相关阅读:
    【SpringCloud学习笔记】服务提供者,服务消费者
    函数指针+回调函数+点云鼠标点选和框选
    【FATE联邦学习】FATE框架的大坑,使用6个月有感
    《数据结构》八大排序(详细图文分析讲解)
    如何选择和使用腾讯云服务器的方法新手教程
    【Django-02】 Model模型和模型描述对象Meta
    老系统如何重构之最全总结
    scipy.optimize.minimize函数介绍
    Linux系统中让$前面显示完整的路径
    ThreadLocal
  • 原文地址:https://blog.csdn.net/liuqinhou/article/details/127828261