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

- Socket (套接字):是网络通信的端点,你可以把它想象成一个电话,通过它来发送和接收数据。
- IP 地址:网络中设备的唯一标识,就像房子的地址。
- 端口号:设备上应用程序的标识,就像房子里的不同房间号,IP 地址 + 端口号 唯一确定了一个网络进程。
- 服务器:等待客户端连接,并提供服务的程序,它会创建一个
ServerSocket来监听特定端口。 - 客户端:主动连接到服务器,并请求服务的程序,它会创建一个
Socket来发起连接。 - 流:
InputStream:用于从网络中读取数据(输入流)。OutputStream:用于向网络中写入数据(输出流)。
简单的单次通信(同步阻塞)
这是最基础的例子,服务器和客户端只进行一次“你问我答”的通信,然后程序结束。
服务器端代码
服务器会监听一个端口,等待客户端连接,一旦客户端连接,它会读取客户端发送的消息,然后回复一条消息。
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 = 12345; // 服务器监听的端口号
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,等待客户端连接...");
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流,用于读取客户端发送的数据
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("收到客户端消息: " + inputLine);
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求退出。");
break;
}
// 向客户端回复消息
String response = "服务器已收到你的消息: " + inputLine;
out.println(response);
System.out.println("已回复客户端: " + response);
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("服务器已关闭。");
}
}
客户端代码
客户端会连接到服务器的指定 IP 和端口,发送一条消息,然后读取服务器的回复。

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 = "127.0.0.1"; // 服务器的IP地址,本地回环地址
int port = 12345; // 服务器的端口号
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);
// 向服务器发送消息
String messageToSend = "你好,服务器!";
out.println(messageToSend);
System.out.println("已发送消息: " + messageToSend);
// 读取服务器的回复
String responseFromServer = in.readLine();
System.out.println("收到服务器回复: " + responseFromServer);
// 再次发送消息,触发服务器退出
out.println("exit");
System.out.println("已发送退出指令。");
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + hostName);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
如何运行
- 首先编译两个 Java 文件:
javac SimpleServer.java SimpleClient.java
- 先启动服务器:
java SimpleServer
你会看到控制台输出:
服务器已启动,等待客户端连接... - 再启动客户端:
java SimpleClient
你会看到客户端控制台输出连接和收到的回复。 服务器控制台会显示客户端连接信息和收到的消息。
这个例子的问题是,服务器一次只能处理一个客户端,当第一个客户端连接后,serverSocket.accept() 会阻塞,无法处理其他客户端的连接请求。
多线程服务器
为了解决单线程服务器无法处理多个客户端的问题,我们使用多线程,每当有新的客户端连接时,服务器就创建一个新的线程来专门处理这个客户端的通信。
服务器端代码
MultiThreadServer.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 MultiThreadServer {
public static void main(String[] args) {
int port = 12345;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,等待客户端连接...");
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 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("[" + clientSocket.getInetAddress() + "] 收到消息: " + inputLine);
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("[" + clientSocket.getInetAddress() + "] 客户端请求退出。");
break;
}
String response = "服务器已收到来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine;
out.println(response);
}
} catch (IOException e) {
System.err.println("处理客户端 " + clientSocket.getInetAddress() + " 时发生错误: " + e.getMessage());
} finally {
try {
clientSocket.close();
System.out.println("[" + clientSocket.getInetAddress() + "] 连接已关闭。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码
客户端代码与实例一完全相同,无需修改。
如何运行
- 编译:
javac MultiThreadServer.java SimpleClient.java
- 启动服务器:
java MultiThreadServer
- 启动多个客户端(可以打开多个终端窗口):
java SimpleClient
你会发现,服务器可以同时为多个客户端服务,每个客户端的请求和响应都能被正确处理。
基于 NIO 的高性能服务器(进阶)
传统的 Socket I/O 是阻塞式的,一个线程只能处理一个连接,当有成千上万的连接时,创建大量线程会消耗大量内存和 CPU 上下文切换开销。
Java NIO (New I/O) 提供了非阻塞 I/O 模型,核心是 通道、缓冲区 和 选择器。
- 通道:类似流,但可以双向读写。
- 缓冲区:数据读写都必须通过缓冲区。
- 选择器:允许单个线程监视多个通道的事件(如连接、读、写),当一个或多个通道准备好时,选择器才会通知线程。
这是一个非常经典的 NIO 服务器实现,使用了 Reactor 模式。
NIOServer.java
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 {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private final int port = 12346;
public void start() throws IOException {
// 1. 创建选择器
selector = Selector.open();
// 2. 创建 ServerSocketChannel 并配置为非阻塞模式
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(port));
// 3. 将 ServerSocketChannel 注册到选择器,监听连接事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO服务器已启动,监听端口: " + port);
// 4. 循环等待新的事件
while (true) {
// select() 会阻塞,直到至少有一个通道准备好
selector.select();
// 获取所有“已准备好”的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 处理后从集合中移除
try {
handleKey(key);
} catch (IOException e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
System.err.println("处理 SelectionKey 时出错: " + e.getMessage());
}
}
}
}
private void handleKey(SelectionKey key) throws IOException {
// 5. 处理新连接
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
// 将新的客户端通道注册到选择器,监听读事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端连接: " + clientChannel.getRemoteAddress());
}
// 6. 处理读事件
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
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.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到来自 [" + clientChannel.getRemoteAddress() + "] 的消息: " + message);
// 回复客户端
String response = "NIO服务器已收到你的消息: " + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
public static void main(String[] args) {
try {
new NIOServer().start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
配套的 NIO 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NIOClient {
public static void main(String[] args) throws IOException {
String host = "127.0.0.1";
int port = 12346;
try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port))) {
socketChannel.configureBlocking(false);
ByteBuffer sendBuffer = ByteBuffer.wrap("你好,NIO服务器!".getBytes());
socketChannel.write(sendBuffer);
System.out.println("消息已发送。");
ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(receiveBuffer); // 非阻塞,可能立即返回0
if (bytesRead > 0) {
receiveBuffer.flip();
byte[] data = new byte[receiveBuffer.remaining()];
receiveBuffer.get(data);
String response = new String(data);
System.out.println("收到服务器回复: " + response);
}
}
}
}
| 特性 | 实例一 (简单) | 实例二 (多线程) | 实例三 (NIO) |
|---|---|---|---|
| 模型 | 阻塞I/O | 阻塞I/O + 多线程 | 非阻塞I/O + 单线程(或少量线程) |
| 并发能力 | 差,只能处理1个客户端 | 好,可处理多个客户端 | 极高,可处理成千上万个连接 |
| 资源消耗 | 低(但利用率低) | 高(线程开销大) | 低(线程数少) |
| 编程复杂度 | 简单 | 中等 | 复杂 |
| 适用场景 | 简单测试、学习 | 中小型应用,并发量不高 | 大型应用、高并发、高性能网络服务 |
对于初学者和大多数常规应用,多线程服务器(实例二) 是一个非常好的选择,它在性能和开发复杂度之间取得了很好的平衡,NIO(实例三)则是构建高性能网络框架(如 Netty)的基础,适合需要处理海量连接的场景。
