diff --git a/README.md b/README.md index 4b3544a761bee35113af9e5177c894f96dd8b441..3522421c77d5f56977b3f20914591ebf0b0ab129 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ mvn clean compile ### 2. 运行服务器 ```bash -mvn exec:java -Dexec.mainClass="com.example.echo.EchoServer" +mvn exec:java "-Dexec.mainClass=com.example.echo.EchoServer" ``` 或直接运行: @@ -56,7 +56,7 @@ java -cp target/classes com.example.echo.EchoServer 另开一个终端,运行: ```bash -mvn exec:java -Dexec.mainClass="com.example.echo.EchoClient" +mvn exec:java "-Dexec.mainClass=com.example.echo.EchoClient" ``` 或直接运行: @@ -76,4 +76,54 @@ java -cp target/classes com.example.echo.EchoClient ## 测试 -输入任意消息到客户端,服务器将回传相同的消息加上"Echo: "前缀。输入"bye"可断开连接。 \ No newline at end of file +输入任意消息到客户端,服务器将回传相同的消息加上"Echo: "前缀。输入"bye"可断开连接。 + + + +删除 clientSocket.close() 会出现的问题 +如果从 EchoServer 代码中移除 clientSocket.close() 调用,会出现以下严重问题: +资源管理问题 +套接字资源泄漏:网络套接字是有限的系统资源,即使客户端断开连接后仍会保持分配状态 +内存占用:每个未关闭的 Socket 对象会持续占用内存空间,无法被垃圾回收器回收 +文件描述符耗尽:操作系统对打开的文件描述符数量有限制,套接字也计入此限制 +系统性能影响 +端口耗尽:累积的未使用套接字可能会耗尽可用的网络端口 +内核资源消耗:每个打开的套接字都会消耗内核内存和资源 +连接表膨胀:服务器为不活跃的连接维护连接状态 +功能性问题 +连接状态不一致:服务器认为连接仍然活跃,而客户端已经断开 +重连失败:客户端在尝试重新连接时可能遇到问题 +不可预测的行为:由于陈旧的连接引用导致通信错误 +长期后果 +系统不稳定:在高负载情况下,服务器可能变得无响应 +拒绝服务:当资源耗尽时,无法接受新的连接 +性能下降:随着更多资源被泄漏的连接消耗,系统逐渐变慢 +正确的资源清理通过 clientSocket.close() 确保当客户端连接结束时系统资源能够及时释放,从而保持服务器的稳定性和长期性能。 + + + + +线程模型分析 +当前 EchoServer 实现采用的是每连接一线程模型,即每个客户端连接都由 ExecutorService 中的独立线程处理。这种模型对于中小规模并发场景是可以接受的,但在高并发环境下存在效率问题。 +更高效的替代方案 +1. 事件驱动/异步I/O (NIO) + 使用 Selector 和 Channel API 在单线程中处理多个连接 + 消除创建和管理大量线程的开销 + 更好的资源利用率和可扩展性 +2. Reactor模式 + 单线程或多线程处理多个并发连接 + 事件驱动方式,一个线程可以管理成千上万的连接 + 减少上下文切换和内存开销 +3. 异步服务器套接字 + 使用 AsynchronousServerSocketChannel 和 AsynchronousSocketChannel + 通过回调机制异步处理连接 + 无阻塞操作,更好的吞吐量 +4. 第三方框架 + Netty: 高性能事件驱动网络框架 + Vert.x: 基于事件循环模型的响应式应用框架 + 替代方案优势 + 降低资源消耗: 更少的线程意味着更少的内存占用 + 更高可扩展性: 可以处理数万个并发连接 + 更好性能: 无需线程间上下文切换 + 更低延迟: 负载下的响应时间更可预测 + 每连接一线程模型适合小规模应用,但对于需要高吞吐量的生产系统,通常更倾向于使用事件驱动架构。 \ No newline at end of file diff --git a/src/main/java/com/example/echo/EchoServer.java b/src/main/java/com/example/echo/EchoServer.java index f55f684ccb91a654ac3993a691cb9ef5f97c0396..780f73a24849dfbbd6e25c1a7c3447528046560e 100644 --- a/src/main/java/com/example/echo/EchoServer.java +++ b/src/main/java/com/example/echo/EchoServer.java @@ -2,13 +2,14 @@ package com.example.echo; import java.io.*; import java.net.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; /** - * 单客户端Echo服务器 - * 这个服务器一次只处理一个客户端连接 + * 多客户端Echo服务器 + * 这个服务器可以同时处理多个客户端连接 */ public class EchoServer { private static final int PORT = 8888; @@ -17,7 +18,8 @@ public class EchoServer { private ExecutorService executor; public EchoServer() { - executor = Executors.newSingleThreadExecutor(); + // 使用固定大小线程池来处理多个客户端 + executor = Executors.newFixedThreadPool(10); } /** @@ -28,12 +30,21 @@ public class EchoServer { running = true; System.out.println("Echo Server已启动,监听端口: " + PORT); - // 等待客户端连接 - System.out.println("等待客户端连接..."); - Socket clientSocket = serverSocket.accept(); - System.out.println("客户端已连接: " + clientSocket.getRemoteSocketAddress()); - - handleClient(clientSocket); + // 持续等待客户端连接 + while (running) { + try { + System.out.println("等待客户端连接..."); + Socket clientSocket = serverSocket.accept(); + System.out.println("客户端已连接: " + clientSocket.getRemoteSocketAddress()); + + // 为每个客户端分配一个线程处理 + executor.submit(() -> handleClient(clientSocket)); + } catch (IOException e) { + if (running) { + System.err.println("接受客户端连接时发生错误: " + e.getMessage()); + } + } + } } /** @@ -44,17 +55,24 @@ public class EchoServer { PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { String inputLine; - System.out.println("开始与客户端通信..."); + System.out.println("开始与客户端通信: " + clientSocket.getRemoteSocketAddress()); while ((inputLine = in.readLine()) != null) { if ("bye".equalsIgnoreCase(inputLine.trim())) { - System.out.println("客户端请求断开连接"); + System.out.println("客户端请求断开连接: " + clientSocket.getRemoteSocketAddress()); break; } - System.out.println("收到消息: " + inputLine); - // 将收到的消息回显给客户端 - out.println("Echo: " + inputLine); + System.out.println("收到消息: " + inputLine + " 来自: " + clientSocket.getRemoteSocketAddress()); + + // 处理特殊命令 + if ("time".equalsIgnoreCase(inputLine.trim())) { + String currentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + out.println("当前系统时间: " + currentTime); + } else { + // 将收到的消息回显给客户端 + out.println("Echo: " + inputLine); + } } } catch (IOException e) { @@ -62,7 +80,7 @@ public class EchoServer { } finally { try { clientSocket.close(); - System.out.println("客户端连接已关闭"); + System.out.println("客户端连接已关闭: " + clientSocket.getRemoteSocketAddress()); } catch (IOException e) { System.err.println("关闭客户端连接时发生错误: " + e.getMessage()); } @@ -78,28 +96,23 @@ public class EchoServer { serverSocket.close(); } executor.shutdown(); - try { - if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { - executor.shutdownNow(); - } - } catch (InterruptedException e) { - executor.shutdownNow(); - } System.out.println("服务器已停止"); } public static void main(String[] args) { EchoServer server = new EchoServer(); - try { - server.start(); - } catch (IOException e) { - System.err.println("启动服务器时发生错误: " + e.getMessage()); - } finally { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { server.stop(); } catch (IOException e) { System.err.println("停止服务器时发生错误: " + e.getMessage()); } + })); + + try { + server.start(); + } catch (IOException e) { + System.err.println("启动服务器时发生错误: " + e.getMessage()); } } } \ No newline at end of file