diff --git a/README.md b/README.md index 4b3544a761bee35113af9e5177c894f96dd8b441..caa9fa6fd8cd6bee028b27458437bee7ec6c6108 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ## 项目简介 -本项目包含一个基础的TCP回声服务器和相应的客户端,服务器将客户端发送的每条消息原样返回给客户端,直到客户端发送"bye"断开连接。 +本项目包含一个多客户端TCP回声服务器和相应的客户端,支持多个客户端同时连接,提供消息回传、时间查询等功能,直到客户端发送"bye"断开连接。 ## 技术栈 @@ -15,10 +15,12 @@ ## 功能特性 -- 单客户端连接处理 -- 消息回声功能 -- 支持断开连接命令("bye") -- 基础错误处理 +- **多客户端并发连接** - 支持多个客户端同时连接 +- **消息回声功能** - 服务器将客户端消息原样返回 +- **时间查询功能** - 发送"TIME"命令获取服务器当前时间 +- **连接日志记录** - 详细记录客户端连接、断开和消息日志 +- **优雅断开连接** - 支持"bye"命令断开连接 +- **完善的错误处理** - 包含详细的错误信息和异常处理 ## 项目结构 @@ -71,9 +73,32 @@ java -cp target/classes com.example.echo.EchoClient ## 端口配置 -- 默认服务器端口: 8888 -- 客户端默认连接地址: localhost:8888 +- 默认服务器端口: 8889 +- 客户端默认连接地址: localhost:8889 ## 测试 -输入任意消息到客户端,服务器将回传相同的消息加上"Echo: "前缀。输入"bye"可断开连接。 \ No newline at end of file +### 基本功能测试 +输入任意消息到客户端,服务器将回传相同的消息加上"Echo: "前缀。 + +### 时间查询测试 +输入"TIME"命令,服务器将返回当前系统时间。 + +### 多客户端测试 +可以同时启动多个客户端连接到服务器,每个客户端独立通信。 + +### 断开连接测试 +输入"bye"可断开当前客户端连接。 + +### 日志输出示例 +``` +[2023-12-05 14:30:15] Echo Server已启动,监听端口: 8889 +[2023-12-05 14:30:15] 等待客户端连接... +[2023-12-05 14:30:20] 新客户端连接: /127.0.0.1:52341 +[2023-12-05 14:30:20] 开始与客户端通信: /127.0.0.1:52341 +[2023-12-05 14:30:25] 收到来自 /127.0.0.1:52341 的消息: Hello +[2023-12-05 14:30:28] 收到来自 /127.0.0.1:52341 的消息: TIME +[2023-12-05 14:30:28] 向客户端 /127.0.0.1:52341 返回时间: 2023-12-05 14:30:28 +[2023-12-05 14:30:30] 客户端请求断开连接: /127.0.0.1:52341 +[2023-12-05 14:30:30] 客户端连接已关闭: /127.0.0.1:52341 +``` \ No newline at end of file diff --git a/src/main/java/com/example/echo/EchoClient.java b/src/main/java/com/example/echo/EchoClient.java index 12bf370680d6c970c5ebc423893e79ebdf7fd2d5..dafd8f86c99da348c76d714e265e425c7524f6b5 100644 --- a/src/main/java/com/example/echo/EchoClient.java +++ b/src/main/java/com/example/echo/EchoClient.java @@ -2,43 +2,104 @@ package com.example.echo; import java.io.*; import java.net.*; +import java.util.concurrent.TimeUnit; /** * Echo服务器的客户端测试类 * 用于连接到Echo服务器并发送消息进行测试 */ public class EchoClient { - private static final String SERVER_HOST = "localhost"; - private static final int SERVER_PORT = 8888; + private static final String DEFAULT_SERVER_HOST = "localhost"; + private static final int DEFAULT_SERVER_PORT = 8888; + private static final int CONNECTION_TIMEOUT = 5000; // 5秒连接超时 public static void main(String[] args) { - try (Socket socket = new Socket(SERVER_HOST, SERVER_PORT); - PrintWriter out = new PrintWriter(socket.getOutputStream(), true); - BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) { - - System.out.println("已连接到Echo服务器: " + SERVER_HOST + ":" + SERVER_PORT); - System.out.println("输入消息发送到服务器 (输入 'bye' 断开连接):"); - - String userInput; - while ((userInput = stdIn.readLine()) != null) { - // 发送消息到服务器 - out.println(userInput); - - // 如果用户输入bye,则断开连接 - if ("bye".equalsIgnoreCase(userInput.trim())) { - System.out.println("断开连接..."); - break; - } + // 支持命令行参数配置服务器地址和端口 + String serverHost = args.length > 0 ? args[0] : DEFAULT_SERVER_HOST; + int serverPort = args.length > 1 ? Integer.parseInt(args[1]) : DEFAULT_SERVER_PORT; + + Socket socket = null; + try { + System.out.println("正在连接到Echo服务器: " + serverHost + ":" + serverPort); + + // 创建socket并设置连接超时 + socket = new Socket(); + socket.connect(new InetSocketAddress(serverHost, serverPort), CONNECTION_TIMEOUT); + socket.setSoTimeout(10000); // 10秒读取超时 + + try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) { + + System.out.println("已连接到Echo服务器: " + serverHost + ":" + serverPort); + System.out.println("输入消息发送到服务器 (输入 'bye', 'exit', 'quit' 断开连接):"); + System.out.println("提示: 可以通过命令行参数指定服务器地址和端口"); + System.out.println("用法: java EchoClient [host] [port]"); + System.out.println(); + + String userInput; + while ((userInput = stdIn.readLine()) != null) { + // 检查空输入 + if (userInput.trim().isEmpty()) { + System.out.println("请输入有效消息,或输入 'bye' 断开连接"); + continue; + } + + // 发送消息到服务器 + out.println(userInput); - // 读取服务器的响应 - String response = in.readLine(); - System.out.println("服务器响应: " + response); + // 检查是否为退出命令 + if (isExitCommand(userInput.trim())) { + System.out.println("正在断开连接..."); + break; + } + + // 读取服务器的响应 + try { + String response = in.readLine(); + if (response != null) { + System.out.println("服务器响应: " + response); + } else { + System.out.println("服务器已断开连接"); + break; + } + } catch (SocketTimeoutException e) { + System.err.println("等待服务器响应超时"); + break; + } + } } + } catch (NumberFormatException e) { + System.err.println("端口号格式错误: " + e.getMessage()); + System.err.println("用法: java EchoClient [host] [port]"); } catch (UnknownHostException e) { - System.err.println("无法找到主机: " + e.getMessage()); + System.err.println("无法找到主机: " + serverHost + " - " + e.getMessage()); + } catch (SocketTimeoutException e) { + System.err.println("连接服务器超时,请检查服务器是否运行"); + } catch (ConnectException e) { + System.err.println("无法连接到服务器 " + serverHost + ":" + serverPort + " - " + e.getMessage()); + System.err.println("请确保服务器正在运行并且端口正确"); } catch (IOException e) { System.err.println("与服务器通信时发生错误: " + e.getMessage()); + } finally { + // 确保socket被正确关闭 + if (socket != null && !socket.isClosed()) { + try { + socket.close(); + System.out.println("连接已关闭"); + } catch (IOException e) { + System.err.println("关闭连接时发生错误: " + e.getMessage()); + } + } } } + + /** + * 检查是否为退出命令 + */ + private static boolean isExitCommand(String command) { + return "bye".equalsIgnoreCase(command) || + "exit".equalsIgnoreCase(command) || + "quit".equalsIgnoreCase(command); + } } \ 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..22bcbfa6d9b231b662ef0aac5fa7172345a2ef12 100644 --- a/src/main/java/com/example/echo/EchoServer.java +++ b/src/main/java/com/example/echo/EchoServer.java @@ -2,22 +2,26 @@ 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; private ServerSocket serverSocket; private boolean running = false; private ExecutorService executor; + private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public EchoServer() { - executor = Executors.newSingleThreadExecutor(); + // 使用线程池支持多客户端连接 + executor = Executors.newCachedThreadPool(); } /** @@ -27,56 +31,86 @@ public class EchoServer { serverSocket = new ServerSocket(PORT); 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 { + Socket clientSocket = serverSocket.accept(); + String clientInfo = clientSocket.getRemoteSocketAddress().toString(); + System.out.println("[" + getCurrentTime() + "] 新客户端连接: " + clientInfo); + + // 为每个客户端创建独立的处理线程 + executor.submit(() -> handleClient(clientSocket)); + } catch (IOException e) { + if (running) { + System.err.println("接受客户端连接时发生错误: " + e.getMessage()); + } + } + } } /** * 处理客户端连接 */ private void handleClient(Socket clientSocket) { + String clientInfo = clientSocket.getRemoteSocketAddress().toString(); + try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { String inputLine; - System.out.println("开始与客户端通信..."); + System.out.println("[" + getCurrentTime() + "] 开始与客户端通信: " + clientInfo); while ((inputLine = in.readLine()) != null) { if ("bye".equalsIgnoreCase(inputLine.trim())) { - System.out.println("客户端请求断开连接"); + System.out.println("[" + getCurrentTime() + "] 客户端请求断开连接: " + clientInfo); break; } - System.out.println("收到消息: " + inputLine); - // 将收到的消息回显给客户端 - out.println("Echo: " + inputLine); + System.out.println("[" + getCurrentTime() + "] 收到来自 " + clientInfo + " 的消息: " + inputLine); + + // 检查是否是TIME命令 + if ("TIME".equalsIgnoreCase(inputLine.trim())) { + String currentTime = getCurrentTime(); + out.println("Server Time: " + currentTime); + System.out.println("[" + getCurrentTime() + "] 向客户端 " + clientInfo + " 返回时间: " + currentTime); + } else { + // 将收到的消息回显给客户端 + out.println("Echo: " + inputLine); + } } } catch (IOException e) { - System.err.println("处理客户端连接时发生错误: " + e.getMessage()); + System.err.println("[" + getCurrentTime() + "] 处理客户端连接时发生错误 " + clientInfo + ": " + e.getMessage()); } finally { try { clientSocket.close(); - System.out.println("客户端连接已关闭"); + System.out.println("[" + getCurrentTime() + "] 客户端连接已关闭: " + clientInfo); } catch (IOException e) { - System.err.println("关闭客户端连接时发生错误: " + e.getMessage()); + System.err.println("[" + getCurrentTime() + "] 关闭客户端连接时发生错误 " + clientInfo + ": " + e.getMessage()); } } } + /** + * 获取当前时间字符串 + */ + private String getCurrentTime() { + return LocalDateTime.now().format(timeFormatter); + } + /** * 停止服务器 */ public void stop() throws IOException { running = false; + System.out.println("[" + getCurrentTime() + "] 正在停止服务器..."); + if (serverSocket != null && !serverSocket.isClosed()) { serverSocket.close(); } + executor.shutdown(); try { if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { @@ -84,8 +118,9 @@ public class EchoServer { } } catch (InterruptedException e) { executor.shutdownNow(); + Thread.currentThread().interrupt(); } - System.out.println("服务器已停止"); + System.out.println("[" + getCurrentTime() + "] 服务器已停止"); } public static void main(String[] args) {