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

- ServerSocket (服务器套接字): 这是服务器端的“监听器”,它在指定的网络端口上监听客户端的连接请求,一旦有客户端尝试连接,
ServerSocket就会创建一个新的Socket对象来与该客户端进行通信。 - Socket (套接字): 这是通信的端点,每个客户端连接到服务器后,服务器都会为该连接创建一个
Socket对象,服务器通过这个Socket的输入流读取客户端发来的数据,通过输出流向客户端发送数据。 - I/O 流 (输入/输出流):
InputStream/OutputStream: 用于原始字节的读写。BufferedReader/PrintWriter(或DataOutputStream): 用于更方便地读写文本行或格式化数据,我们通常会用它们来包装Socket的流。
基础单线程服务器
这是最简单的实现方式,服务器一次只能处理一个客户端连接,处理完一个客户端后才能接受下一个。
特点:
- 代码简单,易于理解。
- 缺点: 性能极差,如果前一个客户端长时间不发送数据,后续所有客户端都无法连接。
代码实现
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 BasicSingleThreadServer {
public static void main(String[] args) {
// 定义服务器监听的端口号
int port = 8080;
// try-with-resources 语句,确保 ServerSocket 被自动关闭
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 阻塞式方法,等待客户端连接,没有客户端连接时,程序会停在这里。
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为客户端的输入流和输出流创建包装器
// BufferedReader 用于读取客户端发送的文本行
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// PrintWriter 用于向客户端发送文本,autoFlush=true 表示在调用 println 后自动刷新缓冲区
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求退出。");
break;
}
// 将接收到的消息转换为大写并发送回客户端
String responseMessage = "服务器回复: " + inputLine.toUpperCase();
out.println(responseMessage);
}
// 关闭客户端连接
System.out.println("与客户端的连接已关闭。");
// clientSocket 会在 try-with-resources 语句块结束时自动关闭
} catch (IOException e) {
System.err.println("服务器发生错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("服务器已关闭。");
}
}
多线程服务器(最常用)
为了解决单线程服务器性能差的问题,我们为每个客户端连接创建一个独立的线程来处理,这样,服务器就可以同时与多个客户端通信。
特点:

- 并发处理: 可以同时服务多个客户端。
- 资源利用: 充分利用多核 CPU 的优势。
- 缺点: 如果有大量并发连接,可能会因为创建过多线程而导致资源耗尽(内存、CPU 切换开销)。
代码实现
我们将服务器逻辑拆分成两部分:
Server类:负责启动ServerSocket并在接收到新连接时,创建一个ClientHandler线程。ClientHandler类:一个实现了Runnable接口的类,专门用于处理单个客户端的通信逻辑。
ClientHandler.java (客户端处理线程)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
/**
* 客户端处理线程
*/
public class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 使用 try-with-resources 确保 Socket 及其流被正确关闭
try (
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("从 " + clientSocket.getInetAddress().getHostAddress() + " 收到消息: " + inputLine);
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 请求退出。");
break;
}
String responseMessage = "服务器回复: " + inputLine.toUpperCase();
out.println(responseMessage);
}
} catch (IOException e) {
System.err.println("处理客户端 " + clientSocket.getInetAddress().getHostAddress() + " 时发生错误: " + e.getMessage());
} finally {
try {
// 确保客户端连接被关闭
if (clientSocket != null && !clientSocket.isClosed()) {
clientSocket.close();
System.out.println("与客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的连接已关闭。");
}
} catch (IOException e) {
System.err.println("关闭客户端连接时出错: " + e.getMessage());
}
}
}
}
MultiThreadServer.java (主服务器类)
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多线程 Socket 服务器
*/
public class MultiThreadServer {
// 使用线程池来管理客户端处理线程,避免无限制创建线程
private final ExecutorService threadPool = Executors.newCachedThreadPool();
public static void main(String[] args) {
int port = 8080;
MultiThreadServer server = new MultiThreadServer();
server.start(port);
}
public void start(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
while (!serverSocket.isClosed()) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("接受到新客户端连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端连接创建一个任务,并提交给线程池执行
ClientHandler clientHandler = new ClientHandler(clientSocket);
threadPool.execute(clientHandler);
}
} catch (IOException e) {
// serverSocket 被外部关闭, accept() 会抛出 SocketException,这是正常的
if (e.getMessage() != null && e.getMessage().contains("Socket closed")) {
System.out.println("服务器正在关闭...");
} else {
System.err.println("服务器发生错误: " + e.getMessage());
e.printStackTrace();
}
} finally {
// 优雅关闭线程池
shutdownThreadPool();
}
}
private void shutdownThreadPool() {
System.out.println("正在关闭线程池...");
threadPool.shutdown(); // 停止接受新任务
// (可选) 可以在这里添加等待现有任务完成的逻辑
// try {
// if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// threadPool.shutdownNow();
// }
// } catch (InterruptedException e) {
// threadPool.shutdownNow();
// Thread.currentThread().interrupt();
// }
System.out.println("线程池已关闭。");
}
}
更优的方案 - NIO (New I/O)
对于需要处理成千上万个并发连接的高性能场景,传统的阻塞式 I/O (BIO) 多线程模型会消耗大量内存和 CPU,Java NIO (Non-blocking I/O) 提供了更高效的解决方案。
NIO 核心组件:
- Channel (通道): 类似于 Stream,但双向的,可以同时进行读写。
- Buffer (缓冲区): 数据的容器,所有读写操作都通过 Buffer 进行。
- Selector (选择器): 这是 NIO 的核心,一个 Selector 可以同时监控多个 Channel 的状态(如连接、读、写),当某个 Channel 准备好时,Selector 会通知你,从而避免了线程的阻塞等待,极大地提高了效率。
NIO 服务器代码示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
// 将 ServerSocketChannel 注册到 Selector,监听 "连接" 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务器已启动,正在监听端口 8080...");
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 阻塞等待,直到至少有一个通道准备好进行 I/O 操作
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 处理新连接
if (key.isAcceptable()) {
handleAccept(serverSocketChannel, selector);
}
// 处理读数据
if (key.isReadable()) {
handleRead(key, buffer);
}
iter.remove(); // 处理完后,从集合中移除
}
}
}
private static void handleAccept(ServerSocketChannel serverSocket, Selector selector) throws IOException {
SocketChannel clientChannel = serverSocket.accept();
if (clientChannel != null) {
clientChannel.configureBlocking(false);
// 将新的客户端通道注册到 Selector,监听 "读" 事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
}
}
private static void handleRead(SelectionKey key, ByteBuffer buffer) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
buffer.clear(); // 清空缓冲区,准备读取新数据
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
// 客户端关闭了连接
System.out.println("客户端断开连接: " + clientChannel.getRemoteAddress());
key.cancel();
clientChannel.close();
return;
}
buffer.flip(); // 切换缓冲区为读模式
byte[] data = new byte[buffer.limit()];
buffer.get(data);
String message = new String(data);
System.out.println("收到来自 " + clientChannel.getRemoteAddress() + " 的消息: " + message);
// 简单回写
String response = "NIO服务器回复: " + message.toUpperCase();
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
总结与对比
| 特性 | 单线程服务器 | 多线程服务器 | NIO 服务器 |
|---|---|---|---|
| 并发能力 | 差(只能处理1个) | 好(可处理多个) | 极高(单线程处理成千上万) |
| 资源消耗 | 低 | 高(线程多) | 低(基于少量线程和事件驱动) |
| 实现复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 学习、简单测试 | 中小型应用、并发量适中 | 高性能、高并发网络服务 |
如何选择?
- 初学者或简单应用: 从 单线程服务器 开始,理解基本流程。
- 大多数实际应用: 多线程服务器 是一个非常好的平衡点,实现相对简单且性能足够,使用
ExecutorService管理线程是关键。 - 高性能/高并发服务: NIO 服务器 是不二之选,但代码更复杂,许多成熟的网络框架(如 Netty)底层都基于 NIO。

