一、bug描述
今天在写上传交互程序中,当我把服务器和客户端成功启动后,在客户端输入请求指令的时候,出现了上述情况。
下面是服务器的代码
package network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class TcpEchoServer { ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务 } // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 public void processConnection(Socket clientSocket) throws IOException { System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream() ) { Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套 PrintWriter printWriter = new PrintWriter(outputStream); while (true) { if (!scannerNet.hasNext()) { // 说明此时连接中断,服务器无法继续从客户端读取到请求 // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 1、读取客户端的请求 String request = scannerNet.nextLine(); // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器, // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束) // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符 // 2、处理客户端发来的请求 String response = process(request); // 3、把响应发送给客户端 printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符, printWriter.flush(); // 刷新缓冲区 // 打印日志 System.out.printf("request: %s, response: %s\n", request, response); } } finally { clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏 } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(8000); server.start(); } }
客户端的代码:
package network; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class TcpEchoClient { Socket socket = null; public TcpEchoClient() throws IOException { // 指定客户端要连接的服务器 socket = new Socket("127.0.0.1", 8000); // 程序走到了这里,说明客户端和服务器就已经成功建立了连接 } public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("当前是客户端,请输入请求"); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送 // 对我们的读取和发送进行写包装,使得读取和包装更方便 Scanner scannerNet = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 while (true) { System.out.print("> "); // 1、客户端从键盘输入请求 String request = scanner.nextLine(); // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了 // 2、把请求发送给服务器 printWriter.write(request); // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符 // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞 printWriter.flush(); // 刷新缓冲区 // 3、从服务器读取响应内容 String response = scannerNet.nextLine(); // 4、打印日志 System.out.printf("request: %s, response: %s\n", request, response); } } finally { socket.close(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient(); client.start(); } }
二、问题分析
于是我打开java的工具包jconsole.exe查看相关的线程执行情况
客户端相关代码:
那么客户端在这里出现异常正常吗?很正常因为我们的服务器在正常情况下,当把响应发送给客户端后,是要打印日志的。但从我们上述的运行结果来看,我们的服务器并没有打印响应(这说明服务器没有成功的把响应发送给客户端,而是陷入了阻塞)
所以客户端在这里的阻塞是正常的
对应的服务器代码:
那么我们在来看看服务器的代码究竟在哪里出现了问题:’
对应的服务器代码:
为什么会在scannerNet.nextLine()这里陷入阻塞呢?nextLine不是遇到回车就结束吗?我们在客户端输入请求后明明敲了一个回车呀!
通过下面这个栗子我们可以发现一些问题:
那么如果这样的话, 我们服务器从客户端读取request的时候,接收到的数据也是没有换行符的——也就是说:
String request = scannerNet.nextLine();
这一行的读取一直无法正常的结束,线程自然就陷入了阻塞当中。
三、问题解决
清楚了这一点就好办了,我们在下面的客户端代码中:
对应的服务器代码:
🍑改过后的服务器代码
package network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class TcpEchoServer { ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务 } // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 public void processConnection(Socket clientSocket) throws IOException { System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream() ) { Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套 PrintWriter printWriter = new PrintWriter(outputStream); while (true) { if (!scannerNet.hasNext()) { // 说明此时连接中断,服务器无法继续从客户端读取到请求 // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 1、读取客户端的请求 String request = scannerNet.nextLine(); // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器, // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束) // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符 // 2、处理客户端发来的请求 String response = process(request); // 3、把响应发送给客户端 printWriter.println(response); // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符, printWriter.flush(); // 刷新缓冲区 // 打印日志 System.out.printf("request: %s, response: %s\n", request, response); } } finally { clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏 } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(8000); server.start(); } }
🍑改过后的客户端代码:
package network; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class TcpEchoClient { Socket socket = null; public TcpEchoClient() throws IOException { // 指定客户端要连接的服务器 socket = new Socket("127.0.0.1", 8000); // 程序走到了这里,说明客户端和服务器就已经成功建立了连接 } public void start() throws IOException { Scanner scanner = new Scanner(System.in); System.out.println("当前是客户端,请输入请求"); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { // InputStream用于读取, OutputStream用于发送 // 对我们的读取和发送进行写包装,使得读取和包装更方便 Scanner scannerNet = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 while (true) { System.out.print("> "); // 1、客户端从键盘输入请求 String request = scanner.nextLine(); // 在这里我们虽然在输入内容后用换行符来进行了结束,但我们用于接收的request并没有读取的换行符——它被nextLine()给吞吃了 // 2、把请求发送给服务器 printWriter.println(request); // printWriter.write(request); // 这么写会产生一个bug, 要printWriter.println()会自动给我们要写入的添加一个换行符 // 可以让服务器在读取请求时遇到换行就结束,不至于陷入阻塞 printWriter.flush(); // 刷新缓冲区 // 3、从服务器读取响应内容 String response = scannerNet.nextLine(); // 4、打印日志 System.out.printf("request: %s, response: %s\n", request, response); } } finally { socket.close(); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient(); client.start(); } }
测试案例
四、进一步的改进
实现多线程服务器
package network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; public class TcpEchoServer { ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { while (true) { // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket // 如果当前没有客户端来建立连接, 就会阻塞等待!! Socket clientSocket = serverSocket.accept(); // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务 // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信 // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的. Thread thread = new Thread(() -> { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } }); thread.start(); } } // 通过这个方法, 给当前连上的这个客户端, 提供服务!! // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 public void processConnection(Socket clientSocket) throws IOException { System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream() ) { Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套 PrintWriter printWriter = new PrintWriter(outputStream); while (true) { if (!scannerNet.hasNext()) { // 说明此时连接中断,服务器无法继续从客户端读取到请求 // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 falseSystem.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 1、读取客户端的请求 String request = scannerNet.nextLine(); // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器, // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束) // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符 // 2、处理客户端发来的请求 String response = process(request); // 3、把响应发送给客户端 printWriter.println(response); // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符, printWriter.flush(); // 刷新缓冲区 // 打印日志 System.out.printf("[%s:%d] request: %s, response: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response); } } finally { clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏 } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(8000); server.start(); } }
通过线程池实现多线程服务器
package network; import java.io.*; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TcpEchoServer { ServerSocket serverSocket = null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { ExecutorService service = Executors.newCachedThreadPool();// 此处不太适合使用 "固定个数的" while (true) { // 服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket // 如果当前没有客户端来建立连接, 就会阻塞等待!! Socket clientSocket = serverSocket.accept(); // processConnection(clientSocket); // 单线程模式——即该服务器无法同时为多个客户端同时提供服务 // [版本2] 多线程版本. 主线程负责拉客, 新线程负责通信 // 虽然比版本1 有提升了, 但是涉及到频繁创建销毁线程, 在高并发的情况下, 负担比较重的. // Thread thread = new Thread(() -> { // try { // processConnection(clientSocket); // } catch (IOException e) { // e.printStackTrace(); // } // }); // thread.start(); // [版本3] 使用线程池, 来解决频繁创建销毁线程的问题. // 此处不太适合使用 "固定个数的" service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } } // 通过这个方法, 给当前连上的这个客户端, 提供服务!! // 这里我们建立长连接——一次连接中,会有 N次请求,N次响应 public void processConnection(Socket clientSocket) throws IOException { System.out.printf("连接成功![%s:%d]\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream() ) { Scanner scannerNet = new Scanner(inputStream); // 对我们的读取进行嵌套 PrintWriter printWriter = new PrintWriter(outputStream); while (true) { if (!scannerNet.hasNext()) { // 说明此时连接中断,服务器无法继续从客户端读取到请求 // 连接断开. 当客户端断开连接的时候, 此时 hasNext 就会返回 false System.out.printf("[%s:%d] 断开连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort()); break; } // 1、读取客户端的请求 String request = scannerNet.nextLine(); // 从客户端读取到的请求,如果客户端在输入请求的时候,只是通过nextLine敲了回车,然后就通过printWrite.write发送给我们的服务器, // 这时服务器接收到的数据是不带换行符的,换行符在我们的客户端nextLine输入的过程中就已经被吞吃了,所有为了避免String request = scanner.nextLine();无法正常读取(nextLine只有遇到换行才结束) // 所以在我们的客户端在发送时要通过PrintWrite.println()来发送,会给我们自动添加一个换行符 // 2、处理客户端发来的请求 String response = process(request); // 3、把响应发送给客户端 printWriter.println(response); // printWriter.write(response); // 这样写有bug,因为我们当前的response中没有带换行符, printWriter.flush(); // 刷新缓冲区 // 打印日志 System.out.printf("[%s:%d] request: %s, response: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(), request, response); } } finally { clientSocket.close(); // 每一次建立连接都会创建一个clientSocket,该连接结束后要及时关闭,避免内存泄漏 } } public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(8000); server.start(); } }