核心概念:Socket 和 ServerSocket
在开始编程之前,必须理解两个核心类:

java.net.ServerSocket
- 角色:服务器端的“接待员”或“监听器”。
- 功能:
- 在一个指定的端口上绑定,并开始监听客户端的连接请求。
- 当一个客户端尝试连接时,
accept()方法会阻塞(程序会暂停执行,直到有连接进来),并返回一个新的Socket对象。 - 这个新的
Socket对象代表了与那个特定客户端的一对一连接通道。
- 简单比喻:
ServerSocket就像一家公司的前台,负责接听总机电话(监听端口),一旦有电话进来(客户端连接),前台就会把电话转接给一个空闲的员工(Socket),由这个员工专门与这位客户进行后续沟通。
java.net.Socket
- 角色:客户端和服务器端用于通信的“电话听筒”或“数据通道”。
- 功能:
- 对于客户端:
Socket对象是客户端主动发起连接后,用来与服务器进行数据交换的通道。 - 对于服务器端:
accept()返回的Socket对象是服务器用来与已连接的特定客户端进行数据交换的通道。
- 对于客户端:
- 简单比喻:
Socket就像你和客户之间已经建立好的电话线路,你可以通过它来发送(OutputStream)和接收(InputStream)信息。
通信流程:
- 服务器 创建一个
ServerSocket并绑定到某个端口(8080)。 - 客户端 创建一个
Socket,并指定服务器的 IP 地址和端口号,尝试连接。 - 服务器 的
ServerSocket的accept()方法检测到连接,返回一个代表该连接的Socket。 - 双方通过各自
Socket获取的InputStream和OutputStream进行双向数据传输。 - 通信结束后,双方关闭
Socket和相关的流。
最简单的 Echo 服务器(单线程)
这是一个最基础的例子,服务器一次只能处理一个客户端的请求,处理完当前客户端后才能接受下一个。
服务器端代码: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) {
int port = 8080;
try ( // 使用 try-with-resources 自动关闭资源
ServerSocket serverSocket = new ServerSocket(port);
) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 1. 阻塞,等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 2. 获取输入流,用于读取客户端发送的数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 3. 获取输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
// 4. 读取客户端发送的每一行数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 5. 将收到的消息回显给客户端
out.println("服务器回显: " + inputLine);
// 如果客户端发送 "bye",则退出循环
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
System.out.println("客户端断开连接。");
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
客户端代码(用于测试):SimpleClient.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 SimpleClient {
public static void main(String[] args) {
String hostname = "localhost"; // 如果服务器在同一台机器上
int port = 8080;
try (
Socket socket = new Socket(hostname, 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("已连接到服务器,输入消息并发送,输入 'bye' 退出。");
String userInput;
// 从控制台读取用户输入
while ((userInput = stdIn.readLine()) != null) {
// 发送消息到服务器
out.println(userInput);
// 从服务器读取回显
String response = in.readLine();
System.out.println("服务器响应: " + response);
if ("bye".equalsIgnoreCase(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
System.exit(1);
} catch (IOException e) {
System.err.println("I/O 对于主机 " + hostname + " 端口 " + port + " 失败。");
System.exit(1);
}
}
}
如何运行:
- 先运行
SimpleServer。 - 再运行
SimpleClient。 - 在客户端的控制台输入任何文本,按回车,服务器会回显同样的内容,输入
bye结束。
进阶:多线程服务器
上面的简单服务器有一个致命的缺点:serverSocket.accept() 和 while 循环都是阻塞的,当一个客户端连接后,服务器会一直与它通信,期间无法接受其他客户端的连接,这在实际应用中是不可接受的。
解决方案:为每个客户端连接创建一个新的线程来处理。
服务器端代码: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 = 8080;
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());
}
}
}
// 客户端处理任务
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
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() + "] 消息: " + inputLine);
out.println("服务器回显: " + inputLine);
if ("bye".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
// 可以更精确地处理异常,比如客户端主动断开连接
System.out.println("客户端 [" + clientSocket.getInetAddress() + "] 断开连接或发生错误。");
} finally {
try {
clientSocket.close();
System.out.println("与客户端 [" + clientSocket.getInetAddress() + "] 的连接已关闭。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
改进点:
- 主线程 (
main) 的唯一职责就是accept()新的连接。 - 每个客户端的通信逻辑(
readLine,println)都被放在一个独立的ClientHandler线程中执行。 - 这样,服务器就可以同时为多个客户端服务,大大提高了并发能力。
最佳实践与健壮性优化
多线程模型已经很好了,但还可以进一步优化,使其更健壮、更高效。
1. 使用线程池
为每个连接都创建一个新线程,如果连接数非常多(比如数万个),会消耗大量系统资源,可能导致性能下降甚至系统崩溃。
解决方案:使用线程池 (ExecutorService) 来管理线程。
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolServer {
// 线程池大小,可以根据需要调整
private static final int THREAD_POOL_SIZE = 10;
public static void main(String[] args) {
int port = 8080;
// 创建一个固定大小的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("线程池服务器已启动,监听端口 " + port + "...");
while (true) {
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
// 将客户端处理任务提交给线程池执行,而不是创建新线程
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
} finally {
// 优雅关闭线程池
threadPool.shutdown();
}
}
}
// ClientHandler 类与上一个例子中的完全相同
2. 使用 try-with-resources
在之前的例子中我们已经使用了,这里再次强调,它能确保像 Socket, InputStream, OutputStream 这样的实现了 AutoCloseable 接口的资源,在代码块执行完毕后自动关闭,即使发生异常也能保证资源被释放,避免资源泄漏。
3. 正确处理字符编码
默认情况下,InputStreamReader 和 OutputStreamWriter 使用平台的默认字符编码(如 Windows 可能是 GBK,Linux 可能是 UTF-8),这在不同环境下可能导致乱码。
最佳实践:始终显式指定字符编码,通常是 StandardCharsets.UTF_8。
import java.nio.charset.StandardCharsets;
// ...
try (
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8), true);
) {
// ...
}
4. 处理半关闭与异常
客户端可以调用 socket.shutdownOutput() 来告知服务器“我已经发送完数据了,但我还想接收你的回复”,服务器需要检测到这个“流结束”的信号,而不是一直等待 readLine() 返回 null(null 通常表示客户端完全关闭了连接)。
- 对于
readLine(),如果客户端调用了shutdownOutput(),readLine()会返回null。 - 对于
read()(字节流),如果客户端调用了shutdownOutput(),read()会返回-1。
PrintWriter 的 autoFlush 参数设为 true 是一个好习惯,它会在每次调用 println() 后自动刷新缓冲区,确保数据能立即发送出去。
更高级的 NIO 模型(非阻塞 I/O)
当并发连接数达到数十万甚至上百万时,传统的基于线程和线程池的模型(称为 BIO - Blocking I/O)会变得力不从心,因为每个连接都需要一个线程,线程切换的开销巨大。
解决方案:使用 Java NIO (New I/O)。
NIO 的核心思想是非阻塞和多路复用。
- 非阻塞:线程在等待 I/O 操作(如读取数据)时不会被阻塞,可以继续执行其他任务,当数据准备好时,操作系统会通知线程。
- 多路复用:一个线程可以同时监视多个
Channel(通道,NIO 中对 Socket 的抽象),通过一个Selector对象,可以知道哪些Channel已经准备好可以进行 I/O 操作。
NIO 核心组件:
- Channel:类似流,但可以双向读写,并且可以非阻塞。
- Buffer:数据被读取到一个
Buffer中,而不是直接读取到流。Buffer是一个数据容器。 - Selector:一个线程通过
Selector可以管理多个Channel,实现“一个线程服务多个连接”。
NIO 编程模型:
- 创建一个
ServerSocketChannel并设置为非阻塞模式。 - 将
ServerSocketChannel注册到Selector上,并指定对OP_ACCEPT(新的连接事件)感兴趣。 - 主线程进入一个循环,调用
Selector.select()方法,这个方法是阻塞的,但它不会阻塞线程,而是等待某个注册的Channel上发生了感兴趣的事件。 - 当
select()返回时,获取SelectedKeys(发生了事件的Channel集合)。 - 遍历这些
Key:- 如果是
OP_ACCEPT事件,说明有新连接,接受连接,获取新的SocketChannel,也将其设置为非阻塞模式,并注册到Selector上,这次对OP_READ(可读事件)感兴趣。 - 如果是
OP_READ事件,说明有数据可读,从SocketChannel读取数据到Buffer中,进行处理。
- 如果是
NIO 的实现比 BIO 复杂得多,但性能极高,是构建高并发网络服务器的标准选择,在 Netty、Mina 等成熟的网络框架中,底层都大量使用了 NIO。
| 特性 | 简单服务器 (BIO) | 多线程服务器 (BIO) | 线程池服务器 (BIO) | NIO 服务器 |
|---|---|---|---|---|
| 并发模型 | 单线程,阻塞 | 一个连接一个线程 | 线程池管理线程 | 单/多线程,非阻塞,多路复用 |
| 优点 | 简单,易于理解 | 能处理多个客户端 | 控制资源消耗,提高稳定性 | 高并发,高吞吐量,资源占用低 |
| 缺点 | 无法处理并发 | 资源消耗大,性能差 | 线程上下文切换仍有开销 | 编程模型复杂 |
| 适用场景 | 学习、简单测试 | 中小型应用,并发连接数不多 | 中大型应用,需要控制资源 | 超高并发场景(如 IM、游戏、直播) |
对于初学者和大多数应用场景,多线程服务器和线程池服务器已经足够强大和高效,只有在面对海量连接时,才需要深入学习并使用 NIO 或基于 NIO 的框架(如 Netty)。
