我们将创建两个独立的 Java 程序:

- EchoServer.java: 一个服务器端程序,它在指定端口上监听客户端连接,当收到客户端的消息后,它会将消息原封不动地回显给客户端。
- EchoClient.java: 一个客户端程序,它连接到服务器,向服务器发送一条消息,并接收服务器回显的消息。
服务器端程序 (EchoServer.java)
服务器端的核心逻辑是:
- 创建一个
ServerSocket并绑定到一个特定的端口(8888),开始监听客户端连接。 - 使用
accept()方法阻塞,等待并接受一个客户端的连接请求,当有客户端连接时,accept()返回一个代表该客户端连接的Socket对象。 - 为每个客户端连接创建一个新的线程来处理,这样服务器就可以同时为多个客户端服务(多线程模型)。
- 在线程中,通过
Socket获取输入流和输出流。 - 从输入流中读取客户端发送的数据。
- 将读取到的数据通过输出流写回给客户端。
- 关闭连接和资源。
EchoServer.java 代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 一个简单的回显服务器
*/
public class EchoServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 循环等待客户端连接
while (true) {
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端创建一个新的线程来处理,以实现多客户端并发
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
/**
* 客户端处理线程
*/
class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 使用 try-with-resources 语句自动关闭资源
try (
// 获取输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)
) {
String inputLine;
// 循环读取客户端发送的每一行数据
while ((inputLine = in.readLine()) != null) {
System.out.println("来自客户端 [" + clientSocket.getInetAddress() + "] 的消息: " + inputLine);
// 将接收到的消息回显给客户端
out.println("服务器回显: " + inputLine);
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求断开连接。");
break;
}
}
} catch (IOException e) {
// 如果客户端异常断开,会抛出 SocketException,这里可以忽略或打印日志
if (e.getMessage() != null && !e.getMessage().equals("Connection reset")) {
System.err.println("处理客户端时发生IO异常: " + e.getMessage());
}
} finally {
// 确保在连接结束时关闭Socket
try {
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
System.out.println("与客户端 " + clientSocket.getInetAddress() + " 的连接已关闭。");
}
} catch (IOException e) {
System.err.println("关闭客户端Socket时出错: " + e.getMessage());
}
}
}
}
客户端程序 (EchoClient.java)
客户端的核心逻辑是:
- 创建一个
Socket对象,指定服务器的 IP 地址(或主机名)和端口号,发起连接请求。 - 连接成功后,通过
Socket获取输入流和输出流。 - 使用输出流向服务器发送消息。
- 使用输入流读取服务器回显的消息。
- 关闭连接和资源。
EchoClient.java 代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
/**
* 一个简单的回显客户端
*/
public class EchoClient {
public static void main(String[] args) {
String hostName = "127.0.0.1"; // 服务器地址,本地回环地址
int port = 8888; // 服务器端口
try (
// 创建Socket并连接到服务器
Socket socket = new Socket(hostName, port);
// 获取输出流,用于向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 获取输入流,用于读取服务器返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
) {
System.out.println("已成功连接到服务器 " + hostName + ":" + port);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
Scanner scanner = new Scanner(System.in);
String userInput;
// 循环读取用户从控制台输入的消息
while (true) {
System.out.print("客户端> ");
userInput = scanner.nextLine();
// 将用户输入发送给服务器
out.println(userInput);
// 如果用户输入 "exit",则退出循环
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
// 读取服务器回显的消息
String response = in.readLine();
if (response != null) {
System.out.println("服务器> " + response);
}
}
} catch (UnknownHostException e) {
System.err.println("不知道主机: " + hostName);
e.printStackTrace();
} catch (IOException e) {
System.err.println("IO异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
如何运行和测试
-
保存代码: 将上面的两段代码分别保存为
EchoServer.java和EchoClient.java文件,放在同一个目录下。 -
编译: 打开终端或命令提示符,进入该目录,使用
javac命令编译两个文件。javac EchoServer.java EchoClient.java
这会生成
EchoServer.class和EchoClient.class文件。 -
启动服务器: 在终端中,先运行服务器程序。
java EchoServer
你会看到输出:
服务器已启动,正在监听端口 8888...服务器会一直等待客户端连接。
-
启动客户端: 打开另一个新的终端窗口,运行客户端程序。
java EchoClient
你会在客户端终端看到:
已成功连接到服务器 127.0.0.1:8888 请输入要发送的消息 (输入 'exit' 退出): 客户端> -
进行通信:
- 在客户端终端输入任意文本,
你好,服务器!,然后按回车。 - 你会在客户端终端立即看到服务器的回显:
客户端> 你好,服务器! 服务器> 服务器回显: 你好,服务器! - 在服务器终端,你会看到相应的日志:
服务器已启动,正在监听端口 8888... 客户端已连接: 127.0.0.1 来自客户端 [/127.0.0.1] 的消息: 你好,服务器!
- 在客户端终端输入任意文本,
-
关闭连接:
- 在客户端输入
exit,然后按回车。 - 客户端和服务器都会打印关闭信息,然后客户端程序退出,服务器端会继续监听下一个客户端的连接。
- 在客户端输入
代码关键点解析
| 类/方法/概念 | 作用 | 备注 |
|---|---|---|
ServerSocket(int port) |
创建一个服务器套接字,并绑定到指定的端口。 | 服务器端专用。 |
serverSocket.accept() |
监听并接受到此套接字的连接,这是一个阻塞方法,没有连接时会一直等待。 | 返回一个 Socket 对象,代表与客户端的连接通道。 |
Socket(String host, int port) |
创建一个套接字,并尝试连接到指定主机和端口。 | 客户端专用,这是一个阻塞方法,连接成功或失败才会返回。 |
socket.getInputStream() |
返回此套接字的输入流,用于读取从对方(客户端/服务器)发送过来的数据。 | 通常用 BufferedReader 包装,以便按行读取。 |
socket.getOutputStream() |
返回此套接字的输出流,用于向对方发送数据。 | 通常用 PrintWriter 包装,并启用 autoFlush (true),这样调用 println() 后会自动刷新缓冲区。 |
| 多线程 | 在服务器端,为每个 accept() 返回的 Socket 创建一个新线程来处理,这是实现并发处理多个客户端的标准模式。 |
如果不使用多线程,服务器一次只能处理一个客户端的请求。 |
| 阻塞与非阻塞 | accept(), Socket 的构造函数,以及 InputStream 的 read() 方法都是阻塞的,程序会暂停执行,直到有预期的结果(如连接、数据到达)。 |
这是同步 I/O 的特点,Java NIO 提供了非阻塞 I/O 的解决方案,性能更高,但更复杂。 |
try-with-resources |
try (Resource res = ...) 语法,确保在 try 块执行完毕后,资源(如 Socket, InputStream, OutputStream)会被自动关闭,即使发生异常。 |
强烈推荐使用,可以避免资源泄漏。 |
可能遇到的问题与解决方法
-
java.net.BindException: Address already in use- 原因: 端口 8888 已经被另一个程序占用(可能是你之前运行的服务器没有正常关闭)。
- 解决: 在终端运行
netstat -ano | findstr "8888"(Windows) 或lsof -i :8888(macOS/Linux) 查找占用端口的进程,然后终止它,或者换一个端口。
-
java.net.ConnectException: Connection refused- 原因: 客户端尝试连接时,服务器没有运行,或者 IP 地址/端口号错误。
- 解决: 确保服务器已经成功启动,并且客户端的
hostName和port与服务器配置一致。0.0.1是本机地址,如果服务器和客户端在同一台电脑上,就用这个。
-
如何在不同电脑上测试?
- 将
EchoClient.java中的hostName从"127.0.0.1"改为服务器的实际 IP 地址("192.168.1.100")。 - 确保服务器的防火墙允许在指定端口(8888)上的入站连接。
- 将
这个例子是 Java Socket 编程的基础,掌握了它,你就可以在此基础上构建更复杂的网络应用,如聊天室、文件传输、远程调用等。
