我们将使用 Java 最基础的 java.net.Socket 和 java.net.ServerSocket 来实现一个简单的“回显服务器”(Echo Server),客户端发送任何消息,服务器都会原样返回给客户端。

核心概念
- Socket (套接字):网络通信的端点,它就像是电话机,负责建立连接、发送和接收数据。
- ServerSocket (服务器套接字):服务器端用来“监听”客户端连接请求的“总机”,当有客户端请求连接时,它会“接听”电话,并为每个客户端分配一个独立的
Socket来进行后续通信。 - IP 地址:网络中设备的唯一标识,
0.0.1(本机地址)。 - 端口号:设备上应用程序的唯一标识,范围是 0-65535。
8080,IP 地址 + 端口号可以唯一确定一个网络服务。
项目结构
我们创建两个独立的 Java 类:EchoServer.java 和 EchoClient.java。
SocketDemo/
├── EchoServer.java
└── EchoClient.java
服务器端代码 (EchoServer.java)
服务器的工作流程是:
- 创建一个
ServerSocket并在指定端口上开始监听。 - 调用
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 = 8080;
// 使用 try-with-resources 语句,确保 ServerSocket 被自动关闭
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// accept() 方法会阻塞,直到有客户端连接
try (Socket clientSocket = serverSocket.accept();
// 从客户端获取输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 向客户端获取输出流,用于发送数据给客户端
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 将收到的消息原样回显给客户端
out.println("服务器回显: " + inputLine);
// 如果客户端发送 "bye",则结束通信
if ("bye".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求断开连接。");
break;
}
}
} catch (IOException e) {
System.err.println("与客户端通信时发生错误: " + e.getMessage());
}
System.out.println("服务器已关闭。");
} catch (IOException e) {
System.err.println("服务器无法在端口 " + port + " 上启动或监听: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端代码 (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;
public class EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址 (localhost 代表本机) 和端口号
String host = "127.0.0.1";
int port = 8080;
// 使用 try-with-resources 语句,确保 Socket, PrintWriter, BufferedReader 被自动关闭
try (
Socket socket = new Socket(host, 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("已连接到服务器 " + host + ":" + port);
System.out.println("请输入要发送的消息 (输入 'bye' 退出):");
String userInput;
// 循环读取用户从控制台输入的消息
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入的消息发送给服务器
out.println(userInput);
// 读取并打印服务器返回的回显消息
String response = in.readLine();
System.out.println("服务器响应: " + response);
// 如果用户输入 "bye",则退出循环
if ("bye".equalsIgnoreCase(userInput)) {
break;
}
System.out.println("请输入下一条消息 (输入 'bye' 退出):");
}
} catch (UnknownHostException e) {
System.err.println("无法找到主机: " + host);
e.printStackTrace();
} catch (IOException e) {
System.err.println("无法连接到主机 " + host + " 或发生 I/O 错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
如何运行
-
编译代码:打开终端或命令提示符,进入
SocketDemo目录,运行javac命令。
(图片来源网络,侵删)javac EchoServer.java EchoClient.java
-
启动服务器:在一个终端窗口中,先运行服务器,服务器启动后会进入阻塞状态,等待客户端连接。
java EchoServer
你会看到输出:
服务器已启动,正在监听端口 8080... -
启动客户端:在另一个终端窗口中,运行客户端。
java EchoClient
你会看到客户端的输出:
(图片来源网络,侵删)已连接到服务器 127.0.0.1:8080 请输入要发送消息 (输入 'bye' 退出): -
进行通信:
- 在客户端的终端输入任意消息,
Hello, Server!,然后按回车。 - 客户端会立即收到服务器的回显:
服务器响应: 服务器回显: Hello, Server! - 在服务器的终端,你会看到:
客户端已连接: 127.0.0.1 收到客户端消息: Hello, Server! - 继续输入更多消息进行测试。
- 在客户端的终端输入任意消息,
-
关闭连接:在客户端输入
bye并按回车。- 客户端会退出程序。
- 服务器端也会检测到
bye消息,打印 "客户端请求断开连接。",然后回到等待新连接的状态(accept()再次阻塞)。
代码关键点解释
-
try-with-resources:这是 Java 7 引入的一个非常方便的特性,它确保了在try语句块结束时,所有实现了AutoCloseable接口(如Socket,ServerSocket,PrintWriter,BufferedReader)的资源都会被自动关闭,无需手动调用close()方法,避免了资源泄漏。 -
new PrintWriter(socket.getOutputStream(), true):第二个参数true表示启用自动刷新,这样,每当调用println()或printf()方法后,输出流都会自动刷新,确保数据能立即发送出去,而不需要手动调用out.flush()。 -
阻塞方法:
serverSocket.accept():这是服务器端的核心方法,它会一直“阻塞”(程序暂停执行),直到有一个客户端尝试连接,一旦有连接,它会返回一个新的Socket对象,代表与这个特定客户端的连接。in.readLine():这个方法用于读取一行文本,它也会阻塞,直到输入流中有可读的数据行(以换行符\n。
-
通信协议:我们使用简单的文本行作为协议,客户端发送一行,服务器读取一行,再发送一行,这种基于行的协议非常适合初学者理解,在实际应用中,协议会更复杂,可能会定义消息头、消息体、消息长度等。
扩展与进阶
这个实例是“一问一答”的同步模式,一个更高级的服务器通常需要能够同时处理多个客户端,这可以通过以下两种主要方式实现:
-
为每个客户端创建一个新线程:
- 在
serverSocket.accept()返回一个新的Socket后,立即创建一个新的Thread,并将与该客户端的通信逻辑放在这个新线程中运行。 - 这样,主线程可以继续回到
accept()状态,等待下一个客户端,而其他客户端的通信在各自的线程中并发进行。
- 在
-
使用线程池:
为了避免为每个客户端都创建一个新线程(可能导致资源耗尽),可以使用一个固定大小的线程池来管理客户端连接任务。
-
使用 NIO (New I/O):
- 对于更高性能、更高并发的场景,可以使用 Java NIO (基于
java.nio.channels包),NIO 使用非阻塞 I/O 和选择器,可以用一个线程管理多个连接,极大地提高了系统的扩展性和效率。
- 对于更高性能、更高并发的场景,可以使用 Java NIO (基于
这个基础实例是理解所有网络编程和高级网络框架(如 Netty、Vert.x)的基石,务必掌握其工作原理。
