- 核心概念:Socket、ServerSocket、IP 地址、端口号。
- 通信流程:客户端和服务器端的基本交互步骤。
- 代码示例:一个完整的、可运行的客户端/服务器端示例,包括字符串和文件传输。
- 关键问题与最佳实践:阻塞与非阻塞、流(Stream)的关闭、异常处理、数据粘包问题及解决方案。
- 进阶:使用 NIO (New I/O) 提高性能。
核心概念
Socket (套接字)
Socket 是网络通信的端点,你可以把它想象成一个电话,应用程序通过这个“电话”来发送和接收数据,在 Java 中,java.net.Socket 类代表客户端,java.net.ServerSocket 类代表服务器端。

ServerSocket (服务器套接字)
服务器使用 ServerSocket 来“监听”某个特定的端口,等待客户端的连接请求,当一个客户端连接时,ServerSocket 会创建一个新的 Socket 对象与该客户端进行一对一的通信。
IP 地址
网络中设备的唯一标识,168.1.100 或 www.google.com,Java 中用 java.net.InetAddress 类来表示。
端口号
一个设备上可以同时运行多个网络服务(如 Web 服务器、邮件服务器等),端口号用于区分这些不同的服务,范围是 0-65535,0-1023 是知名端口,一般应用程序应避免使用,客户端连接时需要指定服务器的 IP 地址和端口号。
通信流程
服务器端
- 创建
ServerSocket:绑定一个端口号,开始监听客户端连接。 - 接受连接:调用
accept()方法,此方法会阻塞(程序暂停执行),直到有客户端连接。 - 获取通信流:一旦有客户端连接,
accept()方法会返回一个新的Socket对象,通过这个Socket对象,我们可以获取输入流(InputStream,用于读取客户端发来的数据)和输出流(OutputStream,用于向客户端发送数据)。 - 读写数据:使用输入流读取数据,使用输出流发送数据。
- 关闭资源:通信结束后,关闭对应的流和
Socket连接。
客户端
- 创建
Socket:指定服务器的 IP 地址和端口号,向服务器发起连接请求。 - 获取通信流:连接成功后,通过
Socket对象获取输入流和输出流。 - 读写数据:使用输出流向服务器发送数据,使用输入流读取服务器返回的数据。
- 关闭资源:通信结束后,关闭流和
Socket连接。
代码示例
下面我们创建一个简单的 "Echo" 服务器,客户端发送什么消息,服务器就原样返回什么。

服务器端代码 (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 = 12345; // 定义服务器监听的端口号
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已连接。");
// 使用 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("收到客户端消息: " + inputLine);
// 将收到的消息回显给客户端
out.println("服务器回复: " + inputLine);
// 如果客户端发送 "exit",则退出循环
if ("exit".equalsIgnoreCase(inputLine)) {
break;
}
}
} catch (IOException e) {
System.err.println("与客户端通信时发生错误: " + e.getMessage());
} finally {
System.out.println("客户端 " + clientSocket.getInetAddress().getHostAddress() + " 已断开连接。");
clientSocket.close(); // 关闭与客户端的连接
}
} catch (IOException e) {
System.err.println("服务器启动或运行时发生错误: " + e.getMessage());
}
}
}
客户端代码 (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) {
String hostName = "localhost"; // 服务器地址,localhost 表示本机
int port = 12345; // 服务器端口号
try (
// 创建 Socket 并连接到服务器
Socket socket = new Socket(hostName, port);
// 获取输入流,用于读取服务器返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 获取输出流,用于向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 用于从控制台读取用户输入
stdIn = new BufferedReader(new InputStreamReader(System.in));
) {
System.out.println("已连接到服务器 " + hostName + ":" + port);
System.out.println("请输入消息 (输入 'exit' 退出):");
String userInput;
// 循环读取用户输入
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入发送给服务器
out.println(userInput);
// 读取并打印服务器的回复
String response = in.readLine();
System.out.println("服务器回复: " + response);
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + hostName);
System.exit(1);
} catch (IOException e) {
System.err.println("无法连接到主机 " + hostName + " 或在通信时发生错误。");
System.exit(1);
}
}
}
如何运行:
- 先运行
EchoServer类。 - 然后运行
EchoClient类(可以运行多次,模拟多个客户端)。 - 在客户端的控制台输入消息,按回车,就能看到服务器的回复。
关键问题与最佳实践
阻塞与非阻塞
- 阻塞:默认情况下,
socket.accept()、inputStream.read()等方法都是阻塞的,线程会一直等待,直到有数据到达或连接建立,这在高并发场景下效率很低,因为一个线程只能处理一个客户端。 - 非阻塞:可以使用 Java NIO (New I/O) 来实现非阻塞 I/O,一个线程可以管理多个连接,大大提高性能,我们会在后面简单介绍。
流的关闭与资源管理
try-with-resources:这是 Java 7+ 推荐的最佳实践,只要实现了AutoCloseable接口(如Socket,ServerSocket,InputStream,OutputStream等),就可以在try语句中声明,它们会在try块执行完毕后自动关闭,即使发生异常也能正确关闭,避免了资源泄漏。- 关闭顺序:通常先关闭外层流(如
BufferedReader),再关闭底层的Socket,但使用try-with-resources会自动处理好这个问题。
异常处理
网络编程充满了不确定性,因此必须妥善处理 IOException 和其他可能的异常(如 UnknownHostException)。
数据粘包问题
这是基于 TCP 的 Socket 编程中一个经典问题。
- 原因:TCP 是一个“流”协议,它只保证数据按顺序、无差错地到达,但不保证每次
read()读取的数据就是一个完整的“包”,发送方多次write()的数据可能会被 TCP 协议栈合并成一个大的数据包发送,接收方一次read()可能会读取到多个包的数据,或者只读取到一个包的一部分。 - 场景:在上述
EchoServer中,我们使用readLine()读取一行,因为它以换行符\n作为结束标志,所以可以避免粘包问题,但如果传输的是二进制数据(如文件、图片),问题就会暴露。 - 解决方案:
- 固定长度:发送方将每个数据包都填充到固定的长度,接收方每次读取固定长度的数据。
- 特殊分隔符:像
readLine()一样,在数据包之间使用特殊字符(如\n,\r\n或自定义分隔符)进行分割,简单,但分隔符不能出现在数据内容中。 - 长度前缀:最常用和最健壮的方法,在每个数据包的头部加上一个固定长度的字段,用于描述这个数据包的长度,接收方先读取这个长度字段,然后根据长度值去读取指定长度的数据。
- 实现示例:
- 发送方:
out.write(lengthBytes); out.write(dataBytes); out.flush(); - 接收方:
byte[] lenBuf = new byte[4]; in.read(lenBuf); int length = ByteBuffer.wrap(lenBuf).getInt(); byte[] dataBuf = new byte[length]; in.read(dataBuf);
- 发送方:
- 实现示例:
进阶:使用 NIO (New I/O)
传统的 BIO (Blocking I/O) 模型是“一个连接一个线程”,当有成千上万的连接时,会创建大量线程,导致资源耗尽和性能下降。

Java NIO 提供了非阻塞 I/O 和选择器(Selector)机制,可以用一个或少量线程来管理成千上万的连接。
NIO 核心组件
- Channel (通道):类似流,但双向的,可以读也可以写。
SocketChannel和ServerSocketChannel是 NIO 中对应的 Socket 通道。 - Buffer (缓冲区):数据被读入
Buffer或从Buffer写出,所有读写操作都是通过Buffer完成的。 - Selector (选择器):
Selector是一个可以检查一个或多个通道状态的对象的组件,一个线程可以管理多个Channel,通过Selector查询哪些通道已经准备好进行 I/O 操作(如可读、可写)。
NIO 工作流程(服务器端)
- 创建一个
ServerSocketChannel并设置为非阻塞模式。 - 将
ServerSocketChannel注册到Selector上,并监听OP_ACCEPT(接受连接)事件。 - 循环调用
selector.select(),它会阻塞直到至少有一个通道准备好进行 I/O。 - 获取
SelectorKeys(已准备好的通道集合),遍历每个Key。 - 如果是
OP_ACCEPT事件,则接受连接,并将新的SocketChannel注册到Selector上,监听OP_READ事件。 - 如果是
OP_READ事件,则从SocketChannel读取数据到Buffer中,处理数据,然后可以将SocketChannel注册到Selector上监听OP_WRITE事件(如果需要写数据)。 - 处理完
Key后,将其从selectedKeys集合中移除。
NIO 的实现比 BIO 复杂得多,但性能优势巨大,是实现高性能网络服务(如 Netty、Jetty 等框架)的基础。
| 特性 | 传统 BIO (Blocking I/O) | NIO (New I/O) |
|---|---|---|
| 模型 | 一个连接一个线程 | 一个或少量线程管理多个连接 |
| 核心 | Socket, ServerSocket, InputStream/OutputStream |
Channel, Buffer, Selector |
| I/O 模式 | 阻塞 | 非阻塞 |
| 适用场景 | 连接数不多、简单的应用 | 高并发、高性能的服务器应用 |
| 复杂度 | 简单 | 复杂 |
对于初学者,掌握传统的 Socket 编程是基础,当你需要构建高性能、高并发的网络应用时,就应该学习和使用 NIO 或者基于 NIO 的高层框架(如 Netty)。
