• 《bug记录》在利用TCP协议创建【服务器-客户端交互程序】中出现的一些问题


    目录

    一、bug描述

     二、问题分析

    三、问题解决

    🍑改过后的服务器代码

     🍑改过后的客户端代码:

    四、进一步的改进


    一、bug描述

     

    今天在写上传交互程序中,当我把服务器和客户端成功启动后,在客户端输入请求指令的时候,出现了上述情况。

    下面是服务器的代码

    1. package network;
    2. import java.io.*;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import java.util.Scanner;
    6. public class TcpEchoServer {
    7. ServerSocket serverSocket = null;
    8. public TcpEchoServer(int port) throws IOException {
    9. serverSocket = new ServerSocket(port);
    10. }
    11. public void start() throws IOException {
    12. // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
    13. Socket clientSocket = serverSocket.accept();
    14. processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    15. }
    16. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    17. public void processConnection(Socket clientSocket) throws IOException {
    18. System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    19. try (InputStream inputStream = clientSocket.getInputStream();
    20. OutputStream outputStream = clientSocket.getOutputStream() ) {
    21. Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
    22. PrintWriter printWriter = new PrintWriter(outputStream);
    23. while (true) {
    24. if (!scannerNet.hasNext()) {
    25. // 说明此时连接中断,服务器无法继续从客户端读取到请求
    26. // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
    27. System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    28. break;
    29. }
    30. // 1、读取客户端的请求
    31. String request = scannerNet.nextLine();
    32. // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
    33. // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
    34. // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
    35. // 2、处理客户端发来的请求
    36. String response = process(request);
    37. // 3、把响应发送给客户端
    38. printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
    39. printWriter.flush(); // 刷新缓冲区
    40. // 打印日志
    41. System.out.printf("request: %s, response: %s\n", request, response);
    42. }
    43. }
    44. finally {
    45. clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
    46. }
    47. }
    48. public String process(String request) {
    49. return request;
    50. }
    51. public static void main(String[] args) throws IOException {
    52. TcpEchoServer server = new TcpEchoServer(8000);
    53. server.start();
    54. }
    55. }

    客户端的代码:

    1. package network;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.io.OutputStream;
    5. import java.io.PrintWriter;
    6. import java.net.Socket;
    7. import java.util.Scanner;
    8. public class TcpEchoClient {
    9. Socket socket = null;
    10. public TcpEchoClient() throws IOException {
    11. // 指定客户端要连接的服务器
    12. socket = new Socket("127.0.0.1", 8000);
    13. // 程序走到了这里,说明客户端和服务器就已经成功建立了连接
    14. }
    15. public void start() throws IOException {
    16. Scanner scanner = new Scanner(System.in);
    17. System.out.println("当前是客户端,请输入请求");
    18. try (InputStream inputStream = socket.getInputStream();
    19. OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送
    20. // 对我们的读取和发送进行写包装,使得读取和包装更方便
    21. Scanner scannerNet = new Scanner(inputStream);
    22. PrintWriter printWriter = new PrintWriter(outputStream);
    23. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    24. while (true) {
    25. System.out.print("> ");
    26. // 1、客户端从键盘输入请求
    27. String request = scanner.nextLine();
    28. // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了
    29. // 2、把请求发送给服务器
    30. printWriter.write(request);
    31. // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符
    32. // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞
    33. printWriter.flush(); // 刷新缓冲区
    34. // 3、从服务器读取响应内容
    35. String response = scannerNet.nextLine();
    36. // 4、打印日志
    37. System.out.printf("request: %s, response: %s\n", request, response);
    38. }
    39. }
    40. finally {
    41. socket.close();
    42. }
    43. }
    44. public static void main(String[] args) throws IOException {
    45. TcpEchoClient client = new TcpEchoClient();
    46. client.start();
    47. }
    48. }

     

     二、问题分析

    于是我打开java的工具包jconsole.exe查看相关的线程执行情况 

     ​​​​​​​

     

    客户端相关代码: 

     

     

    那么客户端在这里出现异常正常吗?很正常因为我们的服务器在正常情况下,当把响应发送给客户端后,是要打印日志的。但从我们上述的运行结果来看,我们的服务器并没有打印响应(这说明服务器没有成功的把响应发送给客户端,而是陷入了阻塞)

    所以客户端在这里的阻塞是正常的

     对应的服务器代码:

     

     

    那么我们在来看看服务器的代码究竟在哪里出现了问题:’

     

     对应的服务器代码: 

    为什么会在scannerNet.nextLine()这里陷入阻塞呢?nextLine不是遇到回车就结束吗?我们在客户端输入请求后明明敲了一个回车呀!

     

     通过下面这个栗子我们可以发现一些问题:

     

    那么如果这样的话, 我们服务器从客户端读取request的时候,接收到的数据也是没有换行符的——也就是说:

    String request = scannerNet.nextLine();

     这一行的读取一直无法正常的结束,线程自然就陷入了阻塞当中。


     

    三、问题解决

    清楚了这一点就好办了,我们在下面的客户端代码中: 

     

     对应的服务器代码:

    🍑改过后的服务器代码

    1. package network;
    2. import java.io.*;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import java.util.Scanner;
    6. public class TcpEchoServer {
    7. ServerSocket serverSocket = null;
    8. public TcpEchoServer(int port) throws IOException {
    9. serverSocket = new ServerSocket(port);
    10. }
    11. public void start() throws IOException {
    12. // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
    13. Socket clientSocket = serverSocket.accept();
    14. processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    15. }
    16. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    17. public void processConnection(Socket clientSocket) throws IOException {
    18. System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    19. try (InputStream inputStream = clientSocket.getInputStream();
    20. OutputStream outputStream = clientSocket.getOutputStream() ) {
    21. Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
    22. PrintWriter printWriter = new PrintWriter(outputStream);
    23. while (true) {
    24. if (!scannerNet.hasNext()) {
    25. // 说明此时连接中断,服务器无法继续从客户端读取到请求
    26. // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
    27. System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    28. break;
    29. }
    30. // 1、读取客户端的请求
    31. String request = scannerNet.nextLine();
    32. // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
    33. // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
    34. // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
    35. // 2、处理客户端发来的请求
    36. String response = process(request);
    37. // 3、把响应发送给客户端
    38. printWriter.println(response);
    39. // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
    40. printWriter.flush(); // 刷新缓冲区
    41. // 打印日志
    42. System.out.printf("request: %s, response: %s\n", request, response);
    43. }
    44. }
    45. finally {
    46. clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
    47. }
    48. }
    49. public String process(String request) {
    50. return request;
    51. }
    52. public static void main(String[] args) throws IOException {
    53. TcpEchoServer server = new TcpEchoServer(8000);
    54. server.start();
    55. }
    56. }

     

     🍑改过后的客户端代码:

    1. package network;
    2. import java.io.IOException;
    3. import java.io.InputStream;
    4. import java.io.OutputStream;
    5. import java.io.PrintWriter;
    6. import java.net.Socket;
    7. import java.util.Scanner;
    8. public class TcpEchoClient {
    9. Socket socket = null;
    10. public TcpEchoClient() throws IOException {
    11. // 指定客户端要连接的服务器
    12. socket = new Socket("127.0.0.1", 8000);
    13. // 程序走到了这里,说明客户端和服务器就已经成功建立了连接
    14. }
    15. public void start() throws IOException {
    16. Scanner scanner = new Scanner(System.in);
    17. System.out.println("当前是客户端,请输入请求");
    18. try (InputStream inputStream = socket.getInputStream();
    19. OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送
    20. // 对我们的读取和发送进行写包装,使得读取和包装更方便
    21. Scanner scannerNet = new Scanner(inputStream);
    22. PrintWriter printWriter = new PrintWriter(outputStream);
    23. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    24. while (true) {
    25. System.out.print("> ");
    26. // 1、客户端从键盘输入请求
    27. String request = scanner.nextLine();
    28. // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了
    29. // 2、把请求发送给服务器
    30. printWriter.println(request);
    31. // printWriter.write(request);
    32. // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符
    33. // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞
    34. printWriter.flush(); // 刷新缓冲区
    35. // 3、从服务器读取响应内容
    36. String response = scannerNet.nextLine();
    37. // 4、打印日志
    38. System.out.printf("request: %s, response: %s\n", request, response);
    39. }
    40. }
    41. finally {
    42. socket.close();
    43. }
    44. }
    45. public static void main(String[] args) throws IOException {
    46. TcpEchoClient client = new TcpEchoClient();
    47. client.start();
    48. }
    49. }

     

    测试案例

    四、进一步的改进

     实现多线程服务器

    1. package network;
    2. import java.io.*;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import java.util.Scanner;
    6. public class TcpEchoServer {
    7. ServerSocket serverSocket = null;
    8. public TcpEchoServer(int port) throws IOException {
    9. serverSocket = new ServerSocket(port);
    10. }
    11. public void start() throws IOException {
    12. while (true) {
    13. // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
    14. // 如果当前没有客户端来建立连接, 就会阻塞等待!!
    15. Socket clientSocket = serverSocket.accept();
    16. // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    17. // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
    18. // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的.
    19. Thread thread = new Thread(() -> {
    20. try {
    21. processConnection(clientSocket);
    22. } catch (IOException e) {
    23. e.printStackTrace();
    24. }
    25. });
    26. thread.start();
    27. }
    28. }
    29. // 通过这个方法, 给当前连上的这个客户端, 提供服务!!
    30. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    31. public void processConnection(Socket clientSocket) throws IOException {
    32. System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    33. try (InputStream inputStream = clientSocket.getInputStream();
    34. OutputStream outputStream = clientSocket.getOutputStream() ) {
    35. Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
    36. PrintWriter printWriter = new PrintWriter(outputStream);
    37. while (true) {
    38. if (!scannerNet.hasNext()) {
    39. // 说明此时连接中断,服务器无法继续从客户端读取到请求
    40. // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 falseSystem.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    41. break;
    42. }
    43. // 1、读取客户端的请求
    44. String request = scannerNet.nextLine();
    45. // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
    46. // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
    47. // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
    48. // 2、处理客户端发来的请求
    49. String response = process(request);
    50. // 3、把响应发送给客户端
    51. printWriter.println(response);
    52. // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
    53. printWriter.flush(); // 刷新缓冲区
    54. // 打印日志
    55. System.out.printf("[%s:%d] request: %s, response: %s\n",
    56. clientSocket.getInetAddress(), clientSocket.getPort(),
    57. request, response);
    58. }
    59. }
    60. finally {
    61. clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
    62. }
    63. }
    64. public String process(String request) {
    65. return request;
    66. }
    67. public static void main(String[] args) throws IOException {
    68. TcpEchoServer server = new TcpEchoServer(8000);
    69. server.start();
    70. }
    71. }

     

     

    通过线程池实现多线程服务器

    1. package network;
    2. import java.io.*;
    3. import java.net.ServerSocket;
    4. import java.net.Socket;
    5. import java.util.Scanner;
    6. import java.util.concurrent.Executor;
    7. import java.util.concurrent.ExecutorService;
    8. import java.util.concurrent.Executors;
    9. public class TcpEchoServer {
    10. ServerSocket serverSocket = null;
    11. public TcpEchoServer(int port) throws IOException {
    12. serverSocket = new ServerSocket(port);
    13. }
    14. public void start() throws IOException {
    15. ExecutorService service = Executors.newCachedThreadPool();// 此处不太适合使用 "固定个数的"
    16. while (true) {
    17. // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket
    18. // 如果当前没有客户端来建立连接, 就会阻塞等待!!
    19. Socket clientSocket = serverSocket.accept();
    20. // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务
    21. // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信
    22. // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的.
    23. // Thread thread = new Thread(() -> {
    24. // try {
    25. // processConnection(clientSocket);
    26. // } catch (IOException e) {
    27. // e.printStackTrace();
    28. // }
    29. // });
    30. // thread.start();
    31. // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题.
    32. // 此处不太适合使用 "固定个数的"
    33. service.submit(new Runnable() {
    34. @Override
    35. public void run() {
    36. try {
    37. processConnection(clientSocket);
    38. } catch (IOException e) {
    39. e.printStackTrace();
    40. }
    41. }
    42. });
    43. }
    44. }
    45. // 通过这个方法, 给当前连上的这个客户端, 提供服务!!
    46. // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应
    47. public void processConnection(Socket clientSocket) throws IOException {
    48. System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    49. try (InputStream inputStream = clientSocket.getInputStream();
    50. OutputStream outputStream = clientSocket.getOutputStream() ) {
    51. Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套
    52. PrintWriter printWriter = new PrintWriter(outputStream);
    53. while (true) {
    54. if (!scannerNet.hasNext()) {
    55. // 说明此时连接中断,服务器无法继续从客户端读取到请求
    56. // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false
    57. System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
    58. break;
    59. }
    60. // 1、读取客户端的请求
    61. String request = scannerNet.nextLine();
    62. // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器,
    63. // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束)
    64. // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符
    65. // 2、处理客户端发来的请求
    66. String response = process(request);
    67. // 3、把响应发送给客户端
    68. printWriter.println(response);
    69. // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符,
    70. printWriter.flush(); // 刷新缓冲区
    71. // 打印日志
    72. System.out.printf("[%s:%d] request: %s, response: %s\n",
    73. clientSocket.getInetAddress(), clientSocket.getPort(),
    74. request, response);
    75. }
    76. }
    77. finally {
    78. clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏
    79. }
    80. }
    81. public String process(String request) {
    82. return request;
    83. }
    84. public static void main(String[] args) throws IOException {
    85. TcpEchoServer server = new TcpEchoServer(8000);
    86. server.start();
    87. }
    88. }

     

  • 相关阅读:
    基础算法篇——前缀和与差分
    金仓数据库KingbaseES数据库开发指南(6. 业务系统开发建议)
    Postgresql-xl全局快照与GTM代码走读(支线)
    Bert基础(十七)--Bert实战:中文情感识别
    每天五分钟机器学习:支持向量机和逻辑回归损失函数的区别和联系
    Maven 项目配置使用备忘录
    nodejs midway+typeorm搭建后台方案
    18 软专
    Spring之AOP源码解析(中)
    【附源码】计算机毕业设计JAVA在线图书超市
  • 原文地址:https://blog.csdn.net/weixin_61061381/article/details/126193709