核心概念
在开始编码前,我们先理解几个核心概念:

- IP 地址 (Internet Protocol Address):网络中设备的唯一标识,
0.0.1(本机地址)。 - 端口号 (Port Number):应用程序在网络中的“门牌号”,范围是 0-65535,服务器程序需要监听一个特定的端口,客户端才能连接到它,HTTP 服务通常使用 80 端口。
- Socket (套接字):是网络通信的端点,服务器和客户端各有一个 Socket,它们通过这两个 Socket 来发送和接收数据。
- 服务器端流程:
- 创建一个
ServerSocket并绑定到一个特定的 IP 和端口,开始监听客户端的连接请求。 - 当一个客户端请求连接时,
ServerSocket会接受这个请求,并返回一个新的Socket对象,专门用于与这个客户端进行通信。 - 服务器通过这个新的
Socket获取输入流来读取客户端发来的数据,通过输出流向客户端发送数据。
- 创建一个
- 客户端流程:
- 创建一个
Socket,指定要连接的服务器的 IP 地址和端口号。 - 连接成功后,通过
Socket获取输入流和输出流,与服务器进行双向通信。
- 创建一个
第一步:最简单的服务器端(单客户端)
这个例子创建一个服务器,它只能同时与一个客户端通信,当客户端断开连接后,服务器程序才会结束。
代码实现
// SimpleServer.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 SimpleServer {
public static void main(String[] args) {
// 1. 指定服务器要监听的端口号
int port = 12345;
// 使用 try-with-resources 语句,确保资源(ServerSocket, Socket, I/O Streams)被自动关闭
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 2. accept() 方法会阻塞线程,直到有客户端连接
// 当客户端连接后,accept() 返回一个 Socket 对象,用于与该客户端通信
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 3. 获取客户端的输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 4. 获取客户端的输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // autoFlush=true
String inputLine;
// 5. 循环读取客户端发送的数据
// readLine() 也会阻塞,直到客户端发送一行数据或关闭连接
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 6. 将收到的消息转换为大写后发送回客户端
String responseMessage = "服务器回复: " + inputLine.toUpperCase();
out.println(responseMessage);
}
System.out.println("客户端已断开连接。");
} catch (IOException e) {
System.err.println("服务器端发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
代码详解
-
ServerSocket serverSocket = new ServerSocket(port);- 创建一个
ServerSocket实例,并绑定到指定的端口12345,这就像是在你家门口挂上了写着“12345”的门牌号,告诉邮递员(客户端)可以把信送到这里。
- 创建一个
-
serverSocket.accept();- 这是服务器端最关键的方法,它会阻塞(程序会停在这里,不再往下执行)并等待客户端的连接请求。
- 当一个客户端成功连接后,
accept()方法会返回一个新的Socket对象 (clientSocket),这个Socket是专门为这个客户端服务的通道。
-
new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))
(图片来源网络,侵删)clientSocket.getInputStream()获取一个字节输入流,从客户端读取原始字节数据。InputStreamReader将字节流转换为字符流。BufferedReader为字符流添加缓冲功能,并提供了方便的readLine()方法,可以一次性读取一行文本。
-
new PrintWriter(clientSocket.getOutputStream(), true)clientSocket.getOutputStream()获取一个字节输出流,向客户端写入原始字节数据。PrintWriter将字符流方便地格式化并写入输出流。- 第二个参数
true表示启用自动刷新,这意味着每当调用println(),printf(), 或format()方法后,输出缓冲区会自动刷新,确保数据能立即发送出去,这对于交互式通信非常重要。
-
while ((inputLine = in.readLine()) != null)- 这是一个经典的循环,用于持续读取客户端的数据。
readLine()会读取一行文本,以换行符\n或回车符\r为结束标志。- 当客户端正常关闭连接时,
readLine()会返回null,循环结束。
第二步:如何运行并测试
你需要一个客户端来连接这个服务器,为了方便,我们可以使用 Telnet 客户端(Windows/Mac/Linux 自带)或 nc (netcat) 命令。
运行步骤
-
编译并运行 Java 服务器
javac SimpleServer.java java SimpleServer
你会看到控制台输出:
服务器已启动,正在监听端口 12345... -
打开一个新的终端窗口,连接服务器
- 使用 Telnet (Windows):
telnet 127.0.0.1 12345
- 使用 Telnet (Mac/Linux):
telnet 127.0.0.1 12345
如果系统没有安装
telnet,可以使用nc:nc 127.0.0.1 12345
- 使用 Telnet (Windows):
-
在客户端终端输入消息 输入
hello world然后按回车。 -
观察服务器和客户端的输出
- 服务器终端 会显示:
服务器已启动,正在监听端口 12345... 客户端已连接: 127.0.0.1 收到客户端消息: hello world 客户端已断开连接。 - 客户端终端 会显示服务器的回复:
服务器回复: HELLO WORLD(然后客户端会等待你输入下一行,或者如果你关闭了客户端,连接会断开)
- 服务器终端 会显示:
第三步:处理多客户端(使用多线程)
上面的例子只能同时服务一个客户端,如果另一个客户端在第一个客户端连接时尝试连接,它必须等待,在实际应用中,服务器需要能够同时处理多个客户端的请求。
解决方案:每当 accept() 方法成功接受一个客户端连接后,就立即创建一个新的线程来处理这个客户端的通信,而主线程则继续回到 accept() 方法,等待下一个客户端。
代码实现
我们将服务器端逻辑封装到一个 ClientHandler 类中。
// MultiThreadedServer.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 MultiThreadedServer {
public static void main(String[] args) {
int port = 12345;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
while (true) { // 无限循环,持续接受客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端连接创建一个新的线程
ClientHandler clientHandler = new ClientHandler(clientSocket);
new Thread(clientHandler).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("[" + Thread.currentThread().getName() + "] 收到客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
out.println("服务器回复: " + inputLine.toUpperCase());
}
} catch (IOException e) {
// 当客户端断开连接时,会抛出异常,这里是正常情况
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开连接。");
} finally {
try {
if (clientSocket != null) {
clientSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
代码改进点
-
MultiThreadedServer:- 主线程进入一个
while(true)循环,不断地调用serverSocket.accept()。 - 每当接受一个新连接,就创建一个
ClientHandler实例,并用它来启动一个新线程 (new Thread(clientHandler).start()),这个新线程会立即去处理客户端的通信,而主线程则可以继续去接受下一个连接。
- 主线程进入一个
-
ClientHandler实现Runnable:- 这个类包含了之前
SimpleServer中的通信逻辑。 - 它实现了
Runnable接口,意味着它的run()方法包含了线程要执行的代码。 - 每个线程处理一个独立的客户端,互不干扰。
- 在
finally块中关闭clientSocket,确保即使发生异常,资源也能被释放。
- 这个类包含了之前
总结与最佳实践
- 使用
try-with-resources:这是 Java 7+ 的推荐做法,可以自动关闭实现了AutoCloseable接口的资源(如Socket,ServerSocket,InputStream,OutputStream),避免资源泄露。 - 处理多客户端:单线程服务器几乎没有实际用途,多线程模型是构建可扩展网络服务的基础。
- 异常处理:网络编程充满不确定性(客户端突然断开、网络中断等),必须妥善处理
IOException,特别是readLine()在客户端关闭时返回null的情况。 - 线程管理:为每个客户端创建一个新线程虽然简单,但在面对成千上万的客户端时,会消耗大量系统资源,更高级的方案是使用线程池(
ExecutorService)来管理线程,或者使用更现代的NIO (New I/O) 模型(基于Selector和Channel),它可以用一个或几个线程来处理成千上万的并发连接,但对于初学者和大多数应用场景,多线程模型已经足够强大。
希望这个详细的指南能帮助你理解 Java Socket 服务器端的编程!
