一、Socket 概述
Socket 概念定义
Socket(套接字)是网络通信的抽象接口,本质上是操作系统提供的通信端点。它允许不同主机或同一主机的不同进程之间进行数据交换,是 TCP/IP 协议栈的编程接口。
Socket 核心特性
- 通信基石:
所有网络通信(如 HTTP、FTP)底层均依赖 Socket 实现。 - 双向通道:
支持全双工通信(可同时收发数据)。 - 唯一标识:
通过 IP 地址 + 端口号 组合唯一确定一个通信端点。
Socket 通信流程(TCP为例)
- 服务端:
- 创建 Socket → 绑定端口(
bind())→ 监听连接(listen())→ 接受连接(accept())
- 创建 Socket → 绑定端口(
- 客户端:
- 创建 Socket → 连接服务端(
connect())
- 创建 Socket → 连接服务端(
- 数据传输:
- 双方通过
send()/recv()收发数据。
- 双方通过
- 关闭连接:
- 调用
close()释放资源。
- 调用
示例代码(Java)
服务端
ServerSocket serverSocket = new ServerSocket(8080); // 绑定端口
Socket clientSocket = serverSocket.accept(); // 阻塞等待连接
// 获取输入输出流进行通信
try (BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String request = in.readLine(); // 读取客户端数据
out.println("Response: " + request); // 发送响应
}
客户端
Socket socket = new Socket("localhost", 8080); // 连接服务端
try (PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
out.println("Hello Server!"); // 发送数据
String response = in.readLine(); // 接收响应
System.out.println(response);
}
关键注意事项
- 端口占用:
确保端口未被其他程序占用(如8080冲突)。 - 资源释放:
必须显式关闭 Socket 和流(使用try-with-resources自动管理)。 - 阻塞行为:
accept()和read()会阻塞线程,通常需配合多线程或 NIO 使用。 - 数据边界:
TCP 是字节流协议,需自定义分隔符或长度前缀标识消息边界。
常见使用场景
- 即时通讯(如聊天软件)
- 文件传输
- 游戏联机
- 远程控制工具(如 SSH)
Socket 在网络通信中的作用
概念定义
Socket(套接字)是网络通信的基本抽象,它提供了进程间通信的端点。在 TCP/IP 协议栈中,Socket 是应用层与传输层之间的编程接口,允许不同主机(或同一主机)上的进程交换数据。
核心作用
-
端点标识
- 通过 IP 地址 + 端口号 唯一标识通信双方。
- 例如:
192.168.1.100:8080表示主机的 8080 端口服务。
-
数据传输通道
- 支持双向通信(全双工):可同时发送和接收数据。
- 封装底层协议细节(如 TCP 的可靠性或 UDP 的无连接特性)。
-
协议选择
- 支持多种传输层协议:
- 流式 Socket(SOCK_STREAM):基于 TCP,可靠、有序。
- 数据报 Socket(SOCK_DGRAM):基于 UDP,高效但不可靠。
- 支持多种传输层协议:
使用场景
-
客户端-服务器模型
- 服务器监听端口(如
ServerSocket),客户端主动连接(如Socket)。 - 示例:Web 服务器(HTTP)、数据库连接(MySQL)。
- 服务器监听端口(如
-
实时通信
- 聊天应用、在线游戏等需要低延迟的场景。
-
跨网络设备控制
- IoT 设备通过 Socket 接收远程指令。
关键特点
- 面向连接 vs 无连接
- TCP Socket 需先建立连接(三次握手),UDP Socket 直接发送数据包。
- 阻塞 vs 非阻塞
- 阻塞模式下,读写操作会等待直到完成;非阻塞模式立即返回状态。
示例代码(Java TCP Socket)
// 服务器端
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept(); // 阻塞等待客户端连接
InputStream in = clientSocket.getInputStream();
// 读取客户端数据...
// 客户端
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream out = socket.getOutputStream();
out.write("Hello".getBytes());
注意事项
- 资源释放
- 必须关闭 Socket 和流,避免资源泄漏(用
try-with-resources)。
- 必须关闭 Socket 和流,避免资源泄漏(用
- 端口冲突
- 确保端口未被占用(如
8080常被其他服务使用)。
- 确保端口未被占用(如
- 异常处理
- 网络中断、连接超时等需捕获
IOException。
- 网络中断、连接超时等需捕获
- 性能考量
- 高并发场景需使用线程池或 NIO(如
Selector)。
- 高并发场景需使用线程池或 NIO(如
Socket 通信的基本原理
定义
Socket(套接字)是网络通信的抽象接口,它允许不同主机或同一主机上的进程之间通过网络进行数据交换。Socket 基于 TCP/IP 协议栈,封装了底层网络协议的细节,为开发者提供了简单的编程接口。
核心组成
- IP 地址:标识网络中的主机。
- 端口号:标识主机上的具体应用(0-65535)。
- 协议:通常为 TCP(可靠连接)或 UDP(无连接)。
通信流程(以 TCP 为例)
-
服务端:
- 创建 Socket(
ServerSocket)。 - 绑定 IP 和端口(
bind())。 - 监听连接(
listen())。 - 接受客户端连接(
accept()),返回新的 Socket 用于通信。
- 创建 Socket(
-
客户端:
- 创建 Socket(
Socket)。 - 连接服务端(
connect())。
- 创建 Socket(
-
数据传输:
- 通过输入流(
InputStream)和输出流(OutputStream)读写数据。 - 关闭连接(
close())。
- 通过输入流(
示例代码(Java)
// 服务端
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
// 客户端
Socket socket = new Socket("localhost", 8080);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
关键特性
- 双向通信:全双工模式,双方可同时收发数据。
- 流式传输(TCP):数据无边界,需自行处理分包/粘包。
- 阻塞/非阻塞:默认阻塞式,可通过 NIO 实现非阻塞。
注意事项
- 端口占用:确保端口未被其他程序占用。
- 资源释放:必须关闭 Socket 和流,避免资源泄漏。
- 异常处理:网络中断、超时等需捕获
IOException。 - 并发问题:服务端通常需多线程处理多个客户端连接。
TCP 与 UDP 协议的区别
1. 基本定义
- TCP(传输控制协议):面向连接的、可靠的、基于字节流的传输层协议。确保数据按序、无差错地传输。
- UDP(用户数据报协议):无连接的、不可靠的、基于数据报的传输层协议。不保证数据顺序或可靠性。
2. 连接方式
- TCP:需要三次握手建立连接,传输完成后四次挥手释放连接。
- UDP:无需建立连接,直接发送数据。
3. 可靠性
- TCP:
- 通过确认应答、超时重传、流量控制、拥塞控制等机制保证可靠性。
- 数据丢失会自动重传。
- UDP:
- 不保证数据可靠传输,可能丢失或乱序。
- 无重传机制。
4. 传输效率
- TCP:
- 由于连接管理和可靠性机制,开销较大,传输效率较低。
- UDP:
- 无连接和确认机制,开销小,传输效率高。
5. 数据边界
- TCP:基于字节流,无固定边界,需应用层处理粘包问题。
- UDP:基于数据报,每次发送和接收都是一个完整的数据包。
6. 适用场景
- TCP:
- 需要可靠传输的场景(如网页浏览、文件传输、邮件发送)。
- UDP:
- 实时性要求高、可容忍少量丢失的场景(如视频流、语音通话、在线游戏)。
7. 示例代码对比
TCP 示例(Java)
// 服务端
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = in.read(buffer);
// 客户端
Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
out.write("Hello TCP".getBytes());
UDP 示例(Java)
// 服务端
DatagramSocket socket = new DatagramSocket(8080);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);
// 客户端
DatagramSocket socket = new DatagramSocket();
byte[] data = "Hello UDP".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("localhost"), 8080);
socket.send(packet);
8. 常见误区
- TCP 一定比 UDP 慢:不一定。在丢包率高的网络中,TCP 的重传机制可能导致延迟更高,但在稳定网络中,TCP 的性能可能接近 UDP。
- UDP 完全不可靠:虽然 UDP 本身不提供可靠性,但应用层可以自行实现可靠性机制(如 QUIC 协议)。
Java 中的 Socket 支持
基本概念
Java 中的 Socket 支持是基于 TCP/IP 协议的网络通信机制,允许不同主机上的应用程序进行双向通信。Socket 是网络通信的端点,由 IP 地址和端口号唯一标识。
核心类
-
java.net.Socket
客户端 Socket 类,用于建立与服务器的连接。
示例:Socket socket = new Socket("127.0.0.1", 8080); -
java.net.ServerSocket
服务器端 Socket 类,用于监听客户端连接请求。
示例:ServerSocket serverSocket = new ServerSocket(8080); Socket clientSocket = serverSocket.accept();
通信流程
-
服务器端
- 创建
ServerSocket并绑定端口。 - 调用
accept()阻塞等待客户端连接。 - 通过
InputStream/OutputStream进行数据读写。
- 创建
-
客户端
- 创建
Socket并指定服务器地址和端口。 - 通过
InputStream/OutputStream与服务器交互。
- 创建
示例代码
服务器端:
try (ServerSocket serverSocket = new ServerSocket(8080)) {
Socket clientSocket = serverSocket.accept();
try (BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
out.println("Server: " + inputLine);
}
}
}
客户端:
try (Socket socket = new Socket("localhost", 8080);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
out.println("Hello, Server!");
System.out.println(in.readLine());
}
注意事项
-
资源释放
必须关闭Socket和ServerSocket(推荐使用try-with-resources)。 -
异常处理
需捕获IOException处理网络异常(如连接失败、超时等)。 -
多线程
服务器通常需要为每个客户端连接启动独立线程,避免阻塞主线程。 -
性能优化
- 使用缓冲流(如
BufferedReader/BufferedWriter)提升 I/O 效率。 - 设置合理的超时时间(
setSoTimeout)。
- 使用缓冲流(如
-
NIO 替代方案
高并发场景下,可考虑java.nio包的非阻塞 I/O(如Selector和Channel)。
二、Socket 核心类
Socket 类
基本概念
Socket 是 Java 网络编程的核心类,位于 java.net 包中,用于实现客户端与服务器之间的双向通信。它是对 TCP/IP 协议的封装,提供了端到端的连接服务。
核心功能
- 建立连接:通过 IP 地址和端口号与远程主机建立连接
- 数据传输:通过输入/输出流进行数据读写
- 连接管理:设置超时、关闭连接等
构造函数
// 常用构造方法
Socket(String host, int port) // 连接到指定主机和端口
Socket(InetAddress address, int port) // 使用InetAddress对象
关键方法
InputStream getInputStream() // 获取输入流
OutputStream getOutputStream() // 获取输出流
void close() // 关闭Socket
void setSoTimeout(int timeout) // 设置超时时间(毫秒)
使用示例
// 客户端示例
try (Socket socket = new Socket("localhost", 8080);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
// 发送数据
out.write("Hello Server".getBytes());
// 接收数据
byte[] buffer = new byte[1024];
int len = in.read(buffer);
System.out.println(new String(buffer, 0, len));
} catch (IOException e) {
e.printStackTrace();
}
注意事项
- 使用后必须关闭Socket和流,推荐使用try-with-resources
- 网络操作应放在非UI线程中
- 正确处理各种IOException
- 设置合理的超时时间避免长时间阻塞
常见误区
- 混淆客户端Socket和服务端ServerSocket的角色
- 未处理连接中断等异常情况
- 忘记关闭连接导致资源泄漏
- 未考虑网络字节序问题
性能优化
- 使用缓冲区减少I/O操作次数
- 考虑使用NIO提高并发性能
- 合理设置TCP_NODELAY选项
- 重用Socket连接(HTTP Keep-Alive)
ServerSocket 类概述
ServerSocket 是 Java 中用于实现 TCP 服务端的核心类,监听指定端口并接受客户端连接请求。每个 ServerSocket 实例绑定一个本地端口,通过 accept() 方法阻塞等待客户端连接,返回一个 Socket 对象用于通信。
核心方法
构造方法
-
ServerSocket(int port)
绑定指定端口,默认最大连接队列长度为 50。
示例:ServerSocket server = new ServerSocket(8080); -
ServerSocket(int port, int backlog)
指定端口和连接队列最大长度(超过后拒绝新连接)。
示例:ServerSocket server = new ServerSocket(8080, 100); -
ServerSocket(int port, int backlog, InetAddress bindAddr)
绑定到指定 IP 地址(多网卡场景适用)。
关键方法
-
Socket accept()
阻塞等待客户端连接,返回一个Socket对象。
示例:Socket clientSocket = server.accept(); -
void close()
关闭服务器套接字,释放端口资源。 -
boolean isClosed()
检查ServerSocket是否已关闭。
使用场景
- 基础 TCP 服务端
监听端口并处理客户端请求,如聊天服务器、文件传输服务。 - 多线程服务端
每个客户端连接分配独立线程处理。
示例代码:while (true) { Socket client = server.accept(); new Thread(() -> handleClient(client)).start(); }
注意事项
- 端口占用
若端口被占用,抛出BindException。可通过netstat命令排查。 - 资源释放
务必在finally块中调用close(),避免端口泄漏。 - 队列溢出
高并发时,适当增大backlog参数(需系统支持)。 - 阻塞控制
accept()会阻塞线程,通常需结合多线程或 NIO 使用。
完整示例
public class SimpleServer {
public static void main(String[] args) throws IOException {
try (ServerSocket server = new ServerSocket(8080)) {
System.out.println("Server started on port 8080");
while (true) {
Socket client = server.accept();
System.out.println("Client connected: " + client.getInetAddress());
// 处理客户端逻辑(如读写数据流)
client.close();
}
}
}
}
DatagramSocket 类概述
DatagramSocket 是 Java 中用于实现 UDP(用户数据报协议) 通信的核心类。它不建立持久连接,而是通过数据包(DatagramPacket)进行数据传输,适用于对实时性要求高但允许少量丢包的场景(如视频流、在线游戏等)。
核心特点
- 无连接:无需建立连接即可发送/接收数据。
- 不可靠:不保证数据包的顺序、完整性或可达性。
- 高效:相比 TCP,UDP 头部开销小,传输延迟低。
常用构造方法
// 绑定随机可用端口
DatagramSocket socket = new DatagramSocket();
// 绑定指定端口
DatagramSocket socket = new DatagramSocket(8080);
// 绑定指定 IP 和端口(多网卡场景)
DatagramSocket socket = new DatagramSocket(8080, InetAddress.getByName("192.168.1.100"));
关键方法
| 方法 | 说明 |
|---|---|
void send(DatagramPacket p) | 发送数据包 |
void receive(DatagramPacket p) | 接收数据包(阻塞) |
void close() | 关闭 Socket |
void setSoTimeout(int timeout) | 设置接收超时(毫秒) |
使用示例
发送端代码
DatagramSocket socket = new DatagramSocket();
String message = "Hello UDP!";
byte[] buffer = message.getBytes();
DatagramPacket packet = new DatagramPacket(
buffer,
buffer.length,
InetAddress.getByName("localhost"),
8080
);
socket.send(packet);
socket.close();
接收端代码
DatagramSocket socket = new DatagramSocket(8080);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // 阻塞等待数据
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received: " + received);
socket.close();
注意事项
- 数据包大小限制:UDP 单包最大理论为 65507 字节(实际受 MTU 限制,通常建议 ≤ 1472 字节)。
- 多线程安全:
DatagramSocket非线程安全,多线程访问需同步。 - 资源释放:务必在 finally 块中调用
close()避免资源泄漏。 - 阻塞控制:
receive()会一直阻塞,可通过setSoTimeout()设置超时。
适用场景
- 实时音视频传输(如 Zoom、WebRTC)
- DNS 查询
- 在线多人游戏
- 广播/组播通信
DatagramPacket 类概述
DatagramPacket 是 Java 中用于实现 UDP(用户数据报协议)通信的核心类之一。它表示一个数据包,用于在无连接的 UDP 协议中发送和接收数据。
主要特点
- 无连接性:UDP 不需要建立连接,直接发送数据包。
- 不可靠性:不保证数据包的顺序、完整性或可达性。
- 高效性:相比 TCP,UDP 头部开销小,传输效率高。
构造方法
接收数据包构造方法
DatagramPacket(byte[] buf, int length)
buf:用于存储接收数据的缓冲区。length:要接收的数据长度(必须 ≤buf.length)。
发送数据包构造方法
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
buf:包含要发送数据的字节数组。length:要发送的数据长度。address:目标主机的 IP 地址。port:目标主机的端口号。
常用方法
数据相关
byte[] getData():获取数据包中的数据。int getLength():获取数据包中实际数据的长度。void setData(byte[] buf):设置数据包的数据。void setLength(int length):设置数据包的长度。
地址和端口相关
InetAddress getAddress():获取发送方或接收方的 IP 地址。int getPort():获取发送方或接收方的端口号。void setAddress(InetAddress iaddr):设置目标 IP 地址。void setPort(int iport):设置目标端口号。
使用示例
发送数据包
try {
// 准备要发送的数据
String message = "Hello, UDP!";
byte[] sendData = message.getBytes();
// 创建数据包
InetAddress address = InetAddress.getByName("127.0.0.1");
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length, address, 9876);
// 创建Socket并发送
DatagramSocket socket = new DatagramSocket();
socket.send(sendPacket);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
接收数据包
try {
// 创建接收缓冲区
byte[] receiveData = new byte[1024];
// 创建接收数据包
DatagramPacket receivePacket = new DatagramPacket(
receiveData, receiveData.length);
// 创建Socket并接收
DatagramSocket socket = new DatagramSocket(9876);
socket.receive(receivePacket);
// 处理接收到的数据
String receivedMessage = new String(
receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Received: " + receivedMessage);
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
注意事项
- 数据包大小:UDP 数据包最大为 65507 字节(65,535 - 8字节 UDP 头 - 20字节 IP 头)。
- 缓冲区管理:接收时缓冲区应足够大,否则会截断数据。
- 线程安全:
DatagramPacket不是线程安全的,多线程环境下需要同步。 - 数据边界:UDP 保留发送时的消息边界,接收方会收到完整的发送包。
- 地址重用:
DatagramSocket可以设置SO_REUSEADDR选项来重用地址。
常见应用场景
- 实时音视频传输
- 在线游戏
- DNS 查询
- 广播和多播应用
- 简单的心跳检测机制
通过合理使用 DatagramPacket 类,可以构建高效、低延迟的网络应用,特别适合那些可以容忍少量数据丢失但对实时性要求高的场景。
InetAddress 类
概念定义
InetAddress 是 Java 提供的用于表示 IP 地址(IPv4 或 IPv6)的类。它是 Java 网络编程中处理网络地址的基础类,封装了 IP 地址和主机名的相关信息。
主要功能
- 表示 IP 地址(如
192.168.1.1或2001:db8::1) - 提供主机名解析(DNS 查询)
- 支持本地主机地址获取
- 支持地址格式验证
常见方法
静态方法
// 根据主机名获取 InetAddress 对象(可能触发 DNS 查询)
InetAddress.getByName(String host)
// 获取本地主机的 InetAddress 对象
InetAddress.getLocalHost()
// 根据 IP 地址字符串获取 InetAddress 对象
InetAddress.getByAddress(byte[] addr)
实例方法
// 获取 IP 地址字符串
String getHostAddress()
// 获取主机名
String getHostName()
// 获取原始 IP 地址(字节数组)
byte[] getAddress()
// 检查是否为回环地址
boolean isLoopbackAddress()
// 检查是否为 IPv6 地址
boolean isIPv6Address()
使用示例
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressExample {
public static void main(String[] args) {
try {
// 获取本地主机地址
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("Local Host: " + localHost.getHostName());
System.out.println("IP Address: " + localHost.getHostAddress());
// 通过主机名获取地址
InetAddress googleAddress = InetAddress.getByName("www.google.com");
System.out.println("Google IP: " + googleAddress.getHostAddress());
// 检查地址类型
System.out.println("Is loopback? " + localHost.isLoopbackAddress());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
注意事项
- DNS 查询:
getByName()和getHostName()方法可能触发网络 DNS 查询,会有网络延迟 - 缓存机制:Java 会对 DNS 查询结果进行缓存,默认缓存策略可通过安全策略配置
- IPv6 支持:从 Java 1.4 开始支持 IPv6 地址
- 不可变对象:InetAddress 实例是不可变的,线程安全
- 异常处理:主机名解析失败会抛出 UnknownHostException
性能考虑
- 频繁的 DNS 查询会影响性能,应考虑缓存结果
- 对于已知 IP 地址,使用
getByAddress()比getByName()更高效 - 批量查询时,考虑使用
getAllByName()方法获取所有地址
实际应用场景
- 客户端连接服务器前的地址解析
- 网络服务日志记录客户端 IP
- 访问控制列表(ACL)实现
- 网络诊断工具开发
三、TCP Socket 编程
TCP 客户端基础
概念定义
TCP客户端是使用传输控制协议(TCP)与服务器建立连接并进行通信的网络程序端点。它通过IP地址和端口号定位目标服务器,建立可靠的、面向连接的通信通道。
核心特点
- 面向连接:需先建立连接才能通信
- 可靠传输:保证数据顺序和完整性
- 流式传输:数据作为字节流处理
创建步骤
- 创建Socket对象
- 连接服务器
- 获取输入/输出流
- 进行数据交换
- 关闭连接
Java实现示例
import java.io.*;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
try {
// 1. 创建客户端Socket
Socket clientSocket = new Socket("127.0.0.1", 8888);
// 2. 获取输出流,向服务器发送数据
OutputStream os = clientSocket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
pw.write("Hello Server");
pw.flush();
clientSocket.shutdownOutput(); // 关闭输出流
// 3. 获取输入流,读取服务器响应
InputStream is = clientSocket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String response = br.readLine();
System.out.println("服务器响应: " + response);
// 4. 关闭资源
br.close();
is.close();
pw.close();
os.close();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
关键注意事项
- 异常处理:必须处理IOException
- 资源释放:所有流和Socket必须关闭
- 端口选择:避免使用0-1023的知名端口
- 连接超时:建议设置连接超时时间
clientSocket.connect(new InetSocketAddress(host, port), 5000);
常见问题排查
- 连接拒绝:检查服务器是否启动/端口是否正确
- 读写阻塞:确保双方协议一致(如结束标记)
- 地址错误:验证IP地址/主机名有效性
性能优化建议
- 使用缓冲流提升I/O效率
- 考虑连接池管理高频连接
- 合理设置Socket超时参数
- 大数据传输时考虑分块处理
TCP 服务端创建
基本概念
TCP服务端是通过ServerSocket类实现的,它负责监听指定端口的连接请求,并与客户端建立可靠的、面向连接的通信通道。
核心步骤
- 创建ServerSocket对象
- 绑定监听端口
- 等待客户端连接(accept())
- 获取输入/输出流
- 进行数据通信
- 关闭连接
示例代码
import java.io.*;
import java.net.*;
public class TCPServer {
public static void main(String[] args) throws IOException {
// 1. 创建ServerSocket并绑定端口
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器已启动,等待客户端连接...");
// 2. 等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接:" + clientSocket.getInetAddress());
// 3. 获取输入输出流
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(
clientSocket.getOutputStream(), true);
// 4. 通信处理
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息:" + inputLine);
out.println("服务器回复:" + inputLine);
}
// 5. 关闭连接
in.close();
out.close();
clientSocket.close();
serverSocket.close();
}
}
关键方法说明
ServerSocket(int port):创建绑定到指定端口的服务器套接字accept():阻塞等待客户端连接,返回Socket对象getInputStream()/getOutputStream():获取通信流
注意事项
- accept()是阻塞方法,会一直等待直到有客户端连接
- 需要正确处理IO异常(示例中为简化直接throws)
- 实际应用中应考虑多线程处理多个客户端连接
- 通信完成后必须关闭所有资源(Socket和流)
- 端口号应大于1024(0-1023为系统保留端口)
典型应用场景
- 需要可靠传输的网络应用(如文件传输、远程控制)
- 客户端/服务器架构的系统
- 需要保持长连接的实时通信系统
TCP 连接建立流程(三次握手)
基本概念
TCP 连接通过**三次握手(Three-way Handshake)**建立,确保双方通信能力正常。核心是同步双方的初始序列号(ISN)。
详细步骤
-
第一次握手(SYN)
- 客户端发送
SYN=1包(携带初始序列号seq=x) - 进入
SYN_SENT状态
- 客户端发送
-
第二次握手(SYN+ACK)
- 服务端返回
SYN=1和ACK=1(确认号ack=x+1,携带自己的序列号seq=y) - 进入
SYN_RCVD状态
- 服务端返回
-
第三次握手(ACK)
- 客户端发送
ACK=1(确认号ack=y+1) - 双方进入
ESTABLISHED状态
- 客户端发送
关键字段说明
| 字段 | 作用 |
|---|---|
SYN | 同步序列号,发起连接 |
ACK | 确认标志,值为1时有效 |
seq | 当前数据包的序列号 |
ack | 期望收到的下一个序列号(值为对方seq+1) |
为什么需要三次握手?
- 防止历史连接:避免因网络延迟导致的无效连接请求
- 同步初始序列号:确保双方序列号能被正确确认
- 资源浪费防护:避免服务端因无效请求占用资源
Java 示例代码
// 服务端
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept(); // 阻塞等待连接
// 客户端
Socket clientSocket = new Socket("127.0.0.1", 8080);
常见问题
- 握手失败处理:客户端会重发SYN包(默认重试5次)
- SYN Flood攻击:恶意发送大量SYN包耗尽服务端资源
- 序列号随机化:ISN采用动态生成避免预测攻击
TCP 数据传输过程
基本概念
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它在数据传输过程中通过三次握手建立连接,通过四次挥手终止连接,确保数据的可靠传输。
数据传输步骤
1. 建立连接(三次握手)
- 第一次握手:客户端发送SYN=1(同步序列号)和初始序列号seq=x到服务器,进入SYN_SENT状态。
- 第二次握手:服务器收到SYN后,发送SYN=1、ACK=1(确认)、ack=x+1(确认号)和初始序列号seq=y,进入SYN_RCVD状态。
- 第三次握手:客户端收到后,发送ACK=1、ack=y+1和seq=x+1,进入ESTABLISHED状态。服务器收到后也进入ESTABLISHED状态。
2. 数据传输
- 可靠传输:TCP通过确认机制(ACK)、超时重传、滑动窗口和流量控制确保数据可靠传输。
- 滑动窗口:允许发送方在未收到确认前发送多个数据包,提高传输效率。
- 流量控制:通过窗口大小动态调整发送速率,避免接收方缓冲区溢出。
- 拥塞控制:通过慢启动、拥塞避免、快速重传和快速恢复算法避免网络拥塞。
3. 终止连接(四次挥手)
- 第一次挥手:客户端发送FIN=1和seq=u,进入FIN_WAIT_1状态。
- 第二次挥手:服务器收到FIN后,发送ACK=1和ack=u+1,进入CLOSE_WAIT状态。客户端收到后进入FIN_WAIT_2状态。
- 第三次挥手:服务器发送FIN=1和seq=v,进入LAST_ACK状态。
- 第四次挥手:客户端收到FIN后,发送ACK=1和ack=v+1,进入TIME_WAIT状态。服务器收到后关闭连接。客户端等待2MSL后关闭连接。
示例代码(Java)
// 客户端
Socket socket = new Socket("127.0.0.1", 8080);
OutputStream out = socket.getOutputStream();
out.write("Hello, Server!".getBytes());
socket.close();
// 服务器
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept();
InputStream in = clientSocket.getInputStream();
byte[] buffer = new byte[1024];
int length = in.read(buffer);
System.out.println(new String(buffer, 0, length));
clientSocket.close();
serverSocket.close();
注意事项
- 资源释放:确保关闭Socket和ServerSocket,避免资源泄漏。
- 异常处理:捕获IOException,处理网络异常。
- 数据边界:TCP是字节流协议,需自行处理数据边界(如固定长度或分隔符)。
- 性能优化:合理设置缓冲区大小,使用NIO或Netty提升高并发性能。
常见误区
- 粘包问题:误以为TCP会保持发送时的数据包边界,实际需应用层处理。
- 连接状态:忽略TIME_WAIT状态,导致端口未释放无法立即重用。
- 单向关闭:仅关闭输出流或输入流,未彻底释放连接资源。
关闭 TCP 连接
概念定义
TCP 连接的关闭是指通信双方通过四次挥手(Four-Way Handshake)过程终止连接,释放占用的系统资源。关闭过程由一方主动发起(FIN),另一方响应(ACK),最终双方确认连接终止。
使用场景
- 客户端/服务器完成数据传输后主动终止连接。
- 异常情况下强制断开连接(如超时、程序崩溃)。
- 长连接空闲超时后自动关闭。
关闭流程(四次挥手)
- 主动方发送 FIN 报文
- 被动方返回 ACK 确认
- 被动方发送自己的 FIN 报文
- 主动方返回最终 ACK 确认
Java 实现示例
// 客户端关闭连接
Socket socket = new Socket("127.0.0.1", 8080);
// ...数据传输...
socket.close(); // 发送FIN
// 服务端关闭连接
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept();
// ...数据处理...
client.close(); // 发送FIN
server.close(); // 关闭监听端口
注意事项
- 半关闭状态:调用
shutdownOutput()/shutdownInput()可单向关闭通道 - TIME_WAIT 状态:主动关闭方会保持 2MSL(通常1-4分钟)状态
- 资源泄漏:必须显式调用
close(),推荐使用 try-with-resources - 异常处理:网络中断可能导致 FIN 未正常发送
常见误区
- 认为调用
close()会立即释放所有资源(实际有 TIME_WAIT 等待) - 忽略关闭顺序导致端口长时间不可用
- 未处理
SocketException导致程序异常退出 - 在多线程环境中未同步关闭操作
优化建议
- 使用连接池管理长连接
- 设置
SO_LINGER参数控制关闭行为 - 对重要连接实现心跳机制检测存活状态
- 在 finally 块中执行关闭操作确保资源释放
四、UDP Socket 编程
UDP 客户端基础
概念定义
UDP(User Datagram Protocol)是一种无连接的传输层协议,提供不可靠但高效的数据传输服务。UDP 客户端是使用 UDP 协议向服务器发送和接收数据的程序端点。
核心特点
- 无连接:无需建立持久连接
- 不可靠:不保证数据送达和顺序
- 高效:头部开销小(仅8字节)
- 支持广播/多播
使用场景
- 实时应用(视频会议、在线游戏)
- DNS 查询
- 传感器数据上报
- 广播通知
Java 实现步骤
- 创建 DatagramSocket
- 准备发送数据包
- 发送数据包
- 接收响应(可选)
- 关闭 socket
示例代码
import java.net.*;
public class UDPClient {
public static void main(String[] args) throws Exception {
// 1. 创建socket(系统自动分配端口)
DatagramSocket clientSocket = new DatagramSocket();
// 2. 准备服务器地址
InetAddress serverIP = InetAddress.getByName("localhost");
int serverPort = 9876;
// 3. 准备发送数据
String sentence = "Hello UDP Server";
byte[] sendData = sentence.getBytes();
// 4. 创建发送包
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length, serverIP, serverPort);
// 5. 发送数据
clientSocket.send(sendPacket);
System.out.println("Sent: " + sentence);
// 6. 准备接收响应
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket =
new DatagramPacket(receiveData, receiveData.length);
// 7. 接收响应(设置超时时间)
clientSocket.setSoTimeout(3000);
try {
clientSocket.receive(receivePacket);
String modifiedSentence =
new String(receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Received: " + modifiedSentence);
} catch (SocketTimeoutException e) {
System.out.println("Timeout: No response received");
}
// 8. 关闭socket
clientSocket.close();
}
}
关键注意事项
- 数据包大小:UDP 包最大 65507 字节(IPv4)
- 可靠性处理:需要自己实现重传机制
- 多线程安全:DatagramSocket 不是线程安全的
- 端口重用:通过 setReuseAddress(true) 启用
- 超时设置:避免 receive() 永久阻塞
常见问题排查
- 数据丢失:检查网络状况和防火墙设置
- 无法接收响应:确认服务器端口和客户端端口匹配
- 性能问题:适当调整缓冲区大小
- 地址错误:检查IP地址和端口号是否正确
UDP 服务端创建
基本概念
UDP(用户数据报协议)是一种无连接的传输层协议,提供简单的不可靠信息传送服务。与TCP不同,UDP不需要建立连接,直接发送数据包。
核心步骤
- 创建DatagramSocket对象
- 准备接收缓冲区
- 创建DatagramPacket接收包
- 接收数据(receive方法)
- 处理接收到的数据
- 可选:发送响应
示例代码
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
public class UDPServer {
public static void main(String[] args) {
try {
// 1. 创建DatagramSocket,指定端口
DatagramSocket serverSocket = new DatagramSocket(9876);
byte[] receiveData = new byte[1024];
while(true) {
// 2. 创建接收包
DatagramPacket receivePacket =
new DatagramPacket(receiveData, receiveData.length);
// 3. 接收数据(阻塞方法)
serverSocket.receive(receivePacket);
// 4. 处理数据
String message = new String(
receivePacket.getData(), 0, receivePacket.getLength());
System.out.println("Received: " + message);
// 5. 获取客户端地址和端口
InetAddress clientAddress = receivePacket.getAddress();
int clientPort = receivePacket.getPort();
// 6. 发送响应(可选)
String response = "ACK: " + message;
byte[] sendData = response.getBytes();
DatagramPacket sendPacket = new DatagramPacket(
sendData, sendData.length, clientAddress, clientPort);
serverSocket.send(sendPacket);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
关键注意事项
- 无连接特性:不需要建立连接,直接收发数据
- 数据包大小:UDP包最大长度受限于MTU(通常1500字节)
- 阻塞接收:receive()方法是阻塞式的
- 线程安全:DatagramSocket是线程安全的
- 资源释放:应在finally块或try-with-resources中关闭socket
典型应用场景
- 实时音视频传输
- DNS查询
- 在线游戏
- 广播/多播应用
- 简单状态监测
与TCP服务端的主要区别
- 不需要accept()方法
- 直接处理数据包而非流
- 每个数据包包含完整的发送方信息
- 不保证数据顺序和可靠性
UDP 数据包发送接收
概念定义
UDP(User Datagram Protocol)是一种无连接的传输层协议,提供简单的不可靠数据包传输服务。与TCP不同,UDP不保证数据包的顺序、可靠性或重复性,但具有低延迟和高吞吐量的特点。
使用场景
- 实时应用:如视频会议、在线游戏(延迟敏感)
- 广播/多播:如DHCP、DNS查询
- 简单查询响应:如NTP时间同步
- 容忍丢包的场景:如流媒体传输
核心类(Java实现)
// 发送端
DatagramSocket sender = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(data, length, address, port);
// 接收端
DatagramSocket receiver = new DatagramSocket(port);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
发送流程
- 创建
DatagramSocket(无需指定端口) - 准备数据字节数组
- 构建
DatagramPacket(指定目标地址和端口) - 调用
socket.send(packet)
接收流程
- 创建绑定端口的
DatagramSocket - 准备接收缓冲区
- 构建空
DatagramPacket - 调用
socket.receive(packet)(阻塞方法) - 从packet中提取数据
示例代码
// 发送端示例
byte[] data = "Hello UDP".getBytes();
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(data, data.length, address, 9876);
new DatagramSocket().send(packet);
// 接收端示例
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
DatagramSocket socket = new DatagramSocket(9876);
socket.receive(packet); // 阻塞等待
String received = new String(packet.getData(), 0, packet.getLength());
注意事项
- 数据包大小:UDP单包最大约64KB(实际建议≤1472字节避免分片)
- 无连接特性:每次发送都需指定目标地址
- 非阻塞替代:可用
socket.setSoTimeout()设置接收超时 - 多线程安全:
DatagramSocket非线程安全 - 资源释放:务必在finally块中关闭socket
常见误区
- 认为UDP绝对不可靠(可通过应用层实现可靠性)
- 忽略数据包边界(recv()每次读取一个完整包)
- 未处理SocketTimeoutException(阻塞接收时)
- 混淆TCP的流式传输与UDP的数据包特性
UDP 无连接特性
概念定义
UDP(User Datagram Protocol)是一种无连接的传输层协议。其核心特点是通信前无需建立连接,直接发送数据包。每个数据包(Datagram)都是独立的,没有顺序保证和可靠性保证。
关键特点
- 无握手过程:无需像TCP那样通过三次握手建立连接。
- 无状态性:服务端不维护客户端连接状态。
- 数据包独立:每个UDP包自带目标地址和端口,可单独路由。
使用场景
- 实时性要求高的应用:如视频会议、在线游戏(延迟敏感场景)。
- 广播/多播通信:如DHCP服务发现、流媒体分发。
- 简单查询响应:如DNS查询(通常单个包即可完成交互)。
代码示例(Java)
// 发送端
DatagramSocket sender = new DatagramSocket();
byte[] data = "Hello UDP".getBytes();
DatagramPacket packet = new DatagramPacket(
data, data.length, InetAddress.getByName("127.0.0.1"), 6000);
sender.send(packet);
// 接收端
DatagramSocket receiver = new DatagramSocket(6000);
byte[] buffer = new byte[1024];
DatagramPacket receivedPacket = new DatagramPacket(buffer, buffer.length);
receiver.receive(receivedPacket);
System.out.println(new String(receivedPacket.getData(), 0, receivedPacket.getLength()));
注意事项
- 丢包风险:网络拥堵时可能直接丢弃数据包。
- 乱序问题:后发的包可能先到达。
- 大小限制:单个UDP包建议不超过1472字节(以太网MTU 1500减去IP/UDP头)。
- 无拥塞控制:持续高速发送可能导致网络瘫痪。
与TCP对比
| UDP | TCP | |
|---|---|---|
| 连接性 | 无连接 | 面向连接 |
| 可靠性 | 不保证 | 保证 |
| 顺序性 | 不保证 | 保证 |
| 传输效率 | 高(无控制开销) | 较低(有握手/确认) |
| 头部大小 | 8字节 | 20字节 |
UDP 广播
概念定义
UDP 广播是一种将数据包发送到同一局域网内所有主机的通信方式。广播地址是特定网络的保留地址(如IPv4的255.255.255.255或子网广播地址192.168.1.255)。
使用场景
- 局域网服务发现:如DHCP客户端寻找服务器。
- 实时消息通知:如局域网内的聊天广播。
- 设备状态同步:如智能家居设备状态更新。
注意事项
- 网络负载:广播会占用整个局域网的带宽。
- 安全性:所有主机都能接收数据,需自行过滤。
- 范围限制:路由器默认不转发广播包,仅限本地网络。
示例代码(Java)
DatagramSocket socket = new DatagramSocket();
socket.setBroadcast(true); // 启用广播
byte[] data = "Hello LAN!".getBytes();
DatagramPacket packet = new DatagramPacket(
data,
data.length,
InetAddress.getByName("255.255.255.255"),
8888
);
socket.send(packet);
UDP 多播(组播)
概念定义
UDP 多播通过组播地址(IPv4的224.0.0.0到239.255.255.255)将数据发送给订阅该组的主机,实现一对多的高效通信。
使用场景
- 视频会议:多人实时视频流分发。
- 股票行情推送:向多个客户端同步数据。
- 在线游戏:同步多个玩家的状态。
注意事项
- TTL设置:通过
MulticastSocket.setTimeToLive()控制传播范围(默认1,仅限本地网络)。 - 组管理协议:需依赖IGMP协议管理组成员。
- 地址冲突:避免使用预留组播地址(如
224.0.0.1为所有主机组)。
示例代码(Java)
// 发送端
MulticastSocket socket = new MulticastSocket();
InetAddress group = InetAddress.getByName("230.0.0.1");
byte[] data = "Multicast Test".getBytes();
DatagramPacket packet = new DatagramPacket(
data,
data.length,
group,
8888
);
socket.send(packet);
// 接收端
MulticastSocket receiver = new MulticastSocket(8888);
receiver.joinGroup(group); // 加入组播组
byte[] buffer = new byte[1024];
DatagramPacket recvPacket = new DatagramPacket(buffer, buffer.length);
receiver.receive(recvPacket);
System.out.println(new String(recvPacket.getData()));
广播 vs 多播对比
| 特性 | 广播 | 多播 |
|---|---|---|
| 地址范围 | 全网或子网所有主机 | 仅订阅组的主机 |
| 网络负载 | 高(所有主机处理) | 低(仅组内主机处理) |
| 适用规模 | 小型局域网 | 跨路由器的大规模网络 |
五、Socket 高级特性
Socket 超时设置
概念定义
Socket 超时是指网络通信过程中等待响应的最长时间限制。当超过这个时间仍未收到响应时,Socket 会抛出超时异常(SocketTimeoutException),避免程序无限期阻塞。
常见超时类型
-
连接超时(Connect Timeout)
建立连接时的等待时间,通过Socket.connect(SocketAddress endpoint, int timeout)设置。 -
读取超时(Read Timeout)
等待读取数据的最大时间,通过Socket.setSoTimeout(int timeout)设置。
使用场景
- 防止网络故障导致程序永久阻塞
- 在弱网络环境下控制响应等待时间
- 实现超时重试机制
示例代码
// 设置连接超时(3秒)
Socket socket = new Socket();
socket.connect(new InetSocketAddress("example.com", 80), 3000);
// 设置读取超时(5秒)
socket.setSoTimeout(5000);
try {
InputStream in = socket.getInputStream();
in.read(); // 若5秒内未收到数据会抛出SocketTimeoutException
} catch (SocketTimeoutException e) {
System.out.println("读取超时");
}
注意事项
- 超时单位是毫秒
setSoTimeout(0)表示无限等待(默认值)- 超时设置必须在IO操作之前
- 对已关闭的Socket设置超时会抛出异常
- 连接超时和读取超时需分别设置
最佳实践
- 生产环境必须设置合理的超时时间
- 结合重试机制处理超时异常
- 根据业务需求调整超时阈值(如支付接口需要比普通接口更长的超时)
Socket 缓冲区设置
概念定义
Socket 缓冲区是操作系统为每个 Socket 分配的内存区域,用于临时存储发送和接收的数据。它分为:
- 发送缓冲区(SO_SNDBUF):存储待发送的数据
- 接收缓冲区(SO_RCVBUF):存储接收到的数据
作用原理
- 发送数据时,应用将数据写入发送缓冲区,由操作系统异步发送
- 接收数据时,操作系统将数据存入接收缓冲区,应用从中读取
- 缓冲区满时会导致阻塞或数据丢失
设置方法(Java示例)
// 创建Socket
Socket socket = new Socket();
// 设置发送缓冲区大小(单位:字节)
socket.setSendBufferSize(64 * 1024); // 64KB
// 设置接收缓冲区大小
socket.setReceiveBufferSize(128 * 1024); // 128KB
// 获取当前缓冲区大小
int sendBufSize = socket.getSendBufferSize();
int recvBufSize = socket.getReceiveBufferSize();
最佳实践
-
典型设置值:
- 局域网应用:8KB-64KB
- 广域网应用:32KB-256KB
- 视频流等高带宽应用:1MB+
-
调整原则:
- 带宽延迟积(BDP)决定理想缓冲区大小
- 公式:缓冲区大小 ≥ 带宽(Mbps) × 往返时延(ms) / 8
-
注意事项:
- 设置值可能被操作系统限制(实际值可能小于设定值)
- 调用必须在connect()/bind()之前
- 过大的缓冲区会导致内存浪费
- 过小的缓冲区会增加系统调用次数
常见问题
- 性能瓶颈:缓冲区过小导致频繁阻塞
- 数据粘包:需配合应用层协议处理
- 设置无效:可能被操作系统参数覆盖(如Linux的net.core.wmem_max)
操作系统级设置(Linux示例)
# 查看当前系统限制
sysctl net.core.rmem_max
sysctl net.core.wmem_max
# 临时修改限制
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
保持连接机制
概念定义
保持连接机制(Keep-Alive)是一种网络通信技术,允许客户端和服务器在完成一次请求/响应后,保持TCP连接不立即关闭,以便后续的请求可以复用该连接。这减少了频繁建立和断开连接的开销,提高了通信效率。
使用场景
- HTTP/1.1:默认启用Keep-Alive,减少网页加载时的连接建立时间。
- 数据库连接池:复用连接以提高数据库操作性能。
- 实时通信:如聊天应用或在线游戏,需要长时间保持连接。
常见误区或注意事项
- 资源占用:长时间保持连接会占用服务器资源,需合理设置超时时间。
- 并发限制:某些服务器对Keep-Alive连接数有限制,可能导致性能瓶颈。
- 协议兼容性:HTTP/1.0需要显式设置
Connection: Keep-Alive头部,而HTTP/1.1默认支持。
示例代码(Java Socket实现)
// 服务器端示例
ServerSocket serverSocket = new ServerSocket(8080);
Socket clientSocket = serverSocket.accept();
clientSocket.setKeepAlive(true); // 启用Keep-Alive
// 客户端示例
Socket socket = new Socket("localhost", 8080);
socket.setKeepAlive(true); // 启用Keep-Alive
参数配置
- 超时时间:通过
setSoTimeout(int timeout)设置空闲连接的超时时间(毫秒)。 - 探测包:TCP层会自动发送探测包检测连接活性,默认时间由系统决定(如Linux的
tcp_keepalive_time参数)。
性能优化
- 合理设置超时:避免连接长时间空闲占用资源。
- 连接池管理:如结合Apache HttpClient等库,可优化连接复用策略。
Socket 异常处理
常见 Socket 异常类型
- IOException:最常见的 Socket 异常,涵盖网络 I/O 错误
- SocketTimeoutException:连接或读取超时
- ConnectException:连接被拒绝(目标未监听/防火墙拦截)
- UnknownHostException:主机名无法解析
- BindException:端口已被占用
关键处理场景
-
连接建立阶段:
- 处理网络不可达/服务未启动情况
- 示例:
try { socket = new Socket(host, port); } catch (ConnectException e) { System.err.println("服务未启动或拒绝连接"); }
-
数据传输阶段:
- 处理网络中断/对方异常关闭
- 示例:
try { in.read(buffer); } catch (SocketTimeoutException e) { System.err.println("读取超时"); } catch (IOException e) { System.err.println("连接异常中断"); }
-
资源释放阶段:
- 确保 finally 块关闭资源
- 示例:
finally { if (socket != null) try { socket.close(); } catch (IOException e) { /* 记录日志 */ } }
最佳实践
- 精细化捕获:不要笼统捕获 Exception,应区分异常类型
- 重试机制:对临时性错误(如网络抖动)实现有限次重试
- 资源泄漏防护:使用 try-with-resources 语法(Java 7+)
try (Socket socket = new Socket(host, port); InputStream in = socket.getInputStream()) { // 使用资源 } - 超时设置:
socket.setSoTimeout(3000); // 设置读写超时3秒
注意事项
- 连接关闭后:继续读写会触发 IOException
- 多线程环境:共享 Socket 需同步处理
- 异常日志:应记录完整异常链(e.printStackTrace() 仅限调试)
多线程 Socket 服务器
概念定义
多线程 Socket 服务器是一种能够同时处理多个客户端连接的服务器程序。它通过为每个客户端连接创建一个独立的线程,实现并发处理,提高服务器的吞吐量和响应速度。
核心特点
- 并发处理:每个客户端连接由独立线程处理
- 资源共享:所有线程共享服务器资源(如端口、内存等)
- 线程管理:需要合理控制线程数量,避免资源耗尽
基本实现步骤
- 创建ServerSocket监听端口
- 循环接受客户端连接
- 为每个连接创建新线程处理
- 线程完成处理后关闭连接
示例代码
public class MultiThreadedServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("Server started on port 8080");
while (true) {
Socket clientSocket = serverSocket.accept();
// 为每个客户端创建新线程
new ClientHandler(clientSocket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ClientHandler extends Thread {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
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("Received: " + inputLine);
out.println("Server response: " + inputLine);
}
} catch (IOException e) {
System.out.println("Exception in client handler");
} finally {
try {
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
使用场景
- 聊天服务器
- 多人在线游戏服务器
- 实时数据推送服务
- 需要高并发的网络应用
注意事项
- 线程安全问题:共享资源需要同步控制
- 线程数量控制:避免无限制创建线程(可使用线程池)
- 资源释放:确保每个线程结束时释放所有资源
- 异常处理:妥善处理客户端断开等异常情况
优化建议
- 使用线程池(如
ExecutorService)替代直接创建线程 - 考虑NIO(非阻塞IO)实现更高并发
- 合理设置连接超时和读写超时
- 实现心跳机制检测无效连接
常见问题
- 线程泄漏:忘记关闭线程导致资源耗尽
- 性能瓶颈:过多线程导致上下文切换开销
- 竞态条件:多个线程同时访问共享数据
- 死锁:线程间相互等待资源释放
六、Socket 编程实践
实现简单聊天程序
概念定义
简单聊天程序是基于 Socket 编程实现的网络通信应用,允许两个或多个客户端通过服务器进行实时文本消息交换。通常采用客户端-服务器(C/S)架构,服务器负责消息中转,客户端负责消息收发。
核心组件
- 服务器端:
- 监听指定端口,等待客户端连接。
- 维护客户端连接列表,广播消息。
- 客户端:
- 连接服务器,发送和接收消息。
实现步骤
服务器端实现
import java.io.*;
import java.net.*;
import java.util.*;
public class ChatServer {
private static final int PORT = 12345;
private static Set<PrintWriter> clientWriters = new HashSet<>();
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
System.out.println("服务器启动,监听端口:" + PORT);
while (true) {
new ClientHandler(serverSocket.accept()).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static class ClientHandler extends Thread {
private Socket socket;
private PrintWriter out;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
out = new PrintWriter(socket.getOutputStream(), true);
synchronized (clientWriters) {
clientWriters.add(out);
}
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String message;
while ((message = in.readLine()) != null) {
System.out.println("收到消息: " + message);
broadcast(message);
}
} catch (IOException e) {
System.out.println("客户端断开连接");
} finally {
if (out != null) {
synchronized (clientWriters) {
clientWriters.remove(out);
}
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static void broadcast(String message) {
synchronized (clientWriters) {
for (PrintWriter writer : clientWriters) {
writer.println(message);
}
}
}
}
客户端实现
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class ChatClient {
private static final String SERVER_IP = "localhost";
private static final int SERVER_PORT = 12345;
public static void main(String[] args) {
try (Socket socket = new Socket(SERVER_IP, SERVER_PORT)) {
System.out.println("已连接到服务器");
// 启动消息接收线程
new Thread(() -> {
try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
String message;
while ((message = in.readLine()) != null) {
System.out.println("收到: " + message);
}
} catch (IOException e) {
System.out.println("与服务器断开连接");
}
}).start();
// 发送消息
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
out.println(message);
}
} catch (IOException e) {
System.out.println("无法连接到服务器");
}
}
}
注意事项
- 多线程处理:服务器需要为每个客户端连接创建独立线程。
- 资源释放:确保关闭 Socket 和流,避免资源泄漏。
- 线程安全:使用
synchronized保护共享资源(如clientWriters)。 - 异常处理:捕获
IOException处理网络中断情况。
运行流程
- 启动服务器:
java ChatServer - 启动多个客户端:
java ChatClient - 在任何客户端输入消息,其他客户端将收到广播消息。
扩展方向
- 添加用户昵称功能
- 实现私聊功能
- 增加图形界面(GUI)
- 支持文件传输
文件传输实现
概念定义
文件传输实现是指通过Socket编程在网络中传输文件内容的过程。核心原理是将文件数据拆分为字节流,通过Socket的输入输出流进行传输,并在接收端重新组装为完整文件。
关键步骤
- 建立连接:创建ServerSocket(服务端)和Socket(客户端)
- 读取文件:使用FileInputStream读取本地文件
- 传输数据:通过Socket的OutputStream发送数据
- 接收保存:通过InputStream接收数据并用FileOutputStream写入目标文件
- 关闭资源:按顺序关闭所有流和Socket连接
示例代码(TCP实现)
// 服务端(接收文件)
ServerSocket serverSocket = new ServerSocket(8888);
Socket socket = serverSocket.accept();
try (InputStream is = socket.getInputStream();
FileOutputStream fos = new FileOutputStream("received_file.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
// 客户端(发送文件)
Socket socket = new Socket("localhost", 8888);
try (OutputStream os = socket.getOutputStream();
FileInputStream fis = new FileInputStream("source_file.txt")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
}
注意事项
-
大文件处理:
- 必须分块传输(如示例中的buffer机制)
- 考虑显示传输进度条
-
传输验证:
- 建议添加文件校验(如MD5校验)
- 实现ACK确认机制
-
性能优化:
- 使用BufferedInputStream/BufferedOutputStream
- 考虑NIO的非阻塞传输
-
异常处理:
- 必须处理SocketTimeoutException
- 网络中断时的重连机制
-
协议设计:
- 建议自定义文件头(包含文件名、大小等信息)
- 区分二进制模式和文本模式传输
扩展实现
-
断点续传:
- 记录已传输的字节位置
- 使用RandomAccessFile进行文件定位
-
多文件传输:
- 设计文件列表协议
- 为每个文件添加分隔标识
-
UDP实现:
// 需要自行实现分片重组和丢包重传 DatagramSocket socket = new DatagramSocket(); byte[] buffer = Files.readAllBytes(Paths.get("file.txt")); DatagramPacket packet = new DatagramPacket(buffer, buffer.length, InetAddress.getByName("localhost"), 8888); socket.send(packet);
常见问题
-
文件损坏:
- 未正确处理流关闭顺序
- 网络丢包(UDP场景)
-
内存溢出:
- 错误地一次性读取大文件(如用Files.readAllBytes()处理GB级文件)
-
编码问题:
- 文本文件跨平台传输时的换行符差异
- 字符编码未统一(建议显式指定UTF-8)
网络数据序列化
概念定义
网络数据序列化是将数据结构或对象状态转换为可存储或传输的格式(如字节流)的过程,以便在网络中传输或存储。反序列化则是将序列化的数据重新转换为原始数据结构或对象的过程。
使用场景
- 网络通信:在客户端-服务器模型中,序列化用于传输对象数据。
- 持久化存储:将对象序列化后保存到文件或数据库中。
- 远程过程调用(RPC):如 gRPC、Dubbo 等框架中,参数和返回值需要序列化传输。
常见序列化方式
-
Java 原生序列化:
- 实现
Serializable接口。 - 示例代码:
public class User implements Serializable { private String name; private int age; // getters and setters }
- 实现
-
JSON 序列化:
- 使用库如 Jackson、Gson。
- 示例代码(Jackson):
ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); User deserializedUser = mapper.readValue(json, User.class);
-
Protocol Buffers(protobuf):
- 高效二进制格式,需定义
.proto文件。 - 示例:
message User { string name = 1; int32 age = 2; }
- 高效二进制格式,需定义
注意事项
- 版本兼容性:
- 修改序列化类的字段可能导致反序列化失败(如 Java 原生序列化的
serialVersionUID)。
- 修改序列化类的字段可能导致反序列化失败(如 Java 原生序列化的
- 性能:
- 二进制序列化(如 protobuf)通常比文本格式(如 JSON)更高效。
- 安全性:
- 反序列化不受信任的数据可能导致漏洞(如 Java 反序列化攻击)。
- 跨语言支持:
- JSON 和 protobuf 支持多语言,而 Java 原生序列化仅限 Java。
示例对比(JSON vs. protobuf)
- JSON:人类可读,但体积较大。
{"name":"Alice","age":30} - protobuf:二进制格式,紧凑高效(不可读)。
0A 05 41 6C 69 63 65 10 1E
性能优化技巧
概念定义
性能优化是指通过调整代码、算法或系统配置,以提高程序的执行效率、减少资源消耗(如CPU、内存、网络带宽等),从而提升整体性能。
使用场景
- 高并发系统:如电商秒杀、即时通讯等需要快速响应的场景。
- 大数据处理:如批量数据导入、复杂计算任务。
- 资源受限环境:如移动设备、嵌入式系统等。
常见误区
- 过早优化:在未明确性能瓶颈时盲目优化,可能导致代码可读性下降。
- 过度优化:牺牲代码可维护性换取微小的性能提升。
- 忽略测试:未通过性能测试验证优化效果。
优化方向
代码层面
- 减少对象创建:重用对象(如使用对象池)。
- 选择高效数据结构:如用
HashMap替代ArrayList查找。 - 避免重复计算:缓存中间结果。
// 优化前:每次循环都计算长度 for (int i = 0; i < list.size(); i++) {...} // 优化后:预先计算长度 int size = list.size(); for (int i = 0; i < size; i++) {...}
算法层面
- 时间复杂度优化:如用快速排序(O(nlogn))替代冒泡排序(O(n²))。
- 空间换时间:如使用缓存(Redis)减少数据库查询。
JVM层面
- 合理设置堆内存:通过
-Xms和-Xmx避免频繁GC。 - 选择垃圾回收器:如G1适用于大堆内存场景。
数据库层面
- 添加索引:加速查询,但避免过度索引。
- 批量操作:用
batchUpdate替代多次单条SQL。
网络层面
- 连接复用:如数据库连接池、HTTP连接池。
- 数据压缩:传输前压缩(如GZIP)。
工具支持
- Profiler工具:如JProfiler、Arthas定位性能瓶颈。
- 监控系统:如Prometheus、Grafana跟踪长期性能趋势。
注意事项
- 量化指标:优化前记录基准性能(如QPS、延迟)。
- 渐进式优化:每次只修改一个点并验证效果。
- 权衡取舍:某些优化可能增加代码复杂度,需评估性价比。
常见问题排查
连接失败
-
端口占用
- 使用
netstat -ano(Windows)或lsof -i :端口号(Linux/Mac)检查端口是否被占用。 - 解决方案:更换端口或终止占用进程。
- 使用
-
防火墙/安全组拦截
- 确保服务器防火墙或云服务商安全组规则允许目标端口通信。
-
IP/域名错误
- 验证客户端连接的IP或域名是否正确,可通过
ping或nslookup测试。
- 验证客户端连接的IP或域名是否正确,可通过
数据读写异常
-
流未正确关闭
- 未关闭
InputStream/OutputStream可能导致资源泄漏或数据未刷新。 - 示例代码:
try (Socket socket = new Socket("host", port); OutputStream out = socket.getOutputStream()) { out.write("data".getBytes()); } // 自动关闭资源
- 未关闭
-
粘包/拆包问题
- TCP是流式协议,需自行处理消息边界(如添加长度前缀或分隔符)。
-
编码不一致
- 客户端与服务端需统一字符编码(如UTF-8),避免乱码。
性能问题
-
阻塞调用
- 默认的
accept()、read()会阻塞线程,高并发场景建议使用NIO或Netty。
- 默认的
-
缓冲区大小不当
- 合理设置
SO_RCVBUF和SO_SNDBUF参数(默认值可能不满足需求)。
- 合理设置
其他注意事项
-
多线程同步
- 多线程共享Socket时需同步读写操作,避免并发冲突。
-
超时设置
- 通过
setSoTimeout()设置读写超时,防止无限等待。
- 通过
-
连接状态检查
isConnected()仅表示是否曾连接成功,需结合异常捕获判断实际连通性。
七、NIO Socket 简介
NIO 与传统 IO 的区别
1. 基本概念
- 传统 IO(BIO):基于流(Stream)模型,面向字节流或字符流,采用阻塞式读写。
- NIO(New IO):基于通道(Channel)和缓冲区(Buffer)模型,支持非阻塞和多路复用。
2. 核心差异
| 特性 | 传统 IO(BIO) | NIO |
|---|---|---|
| 数据流方向 | 单向(输入/输出流) | 双向(通道可读可写) |
| 阻塞模式 | 阻塞(线程等待数据) | 非阻塞(立即返回结果) |
| 多路复用 | 不支持 | 通过 Selector 实现 |
| 数据操作单位 | 流(Stream) | 缓冲区(Buffer) |
| 性能 | 低并发时简单高效 | 高并发时更优 |
3. 关键组件
- NIO 核心三件套:
- Channel:双向数据传输管道(如
SocketChannel、FileChannel)。 - Buffer:数据容器(如
ByteBuffer),需手动flip()切换读写模式。 - Selector:监听多个通道的事件(如连接、读写),实现单线程多连接管理。
- Channel:双向数据传输管道(如
4. 使用场景
- 传统 IO:适合低并发、简单数据传输(如文件读写)。
- NIO:适合高并发网络应用(如聊天服务器、RPC框架)。
5. 代码示例(NIO 非阻塞读取)
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false); // 非阻塞模式
channel.connect(new InetSocketAddress("example.com", 80));
while (!channel.finishConnect()) {
// 等待连接完成(非阻塞)
}
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer); // 非阻塞读取
if (bytesRead > 0) {
buffer.flip();
// 处理数据...
}
}
6. 注意事项
- NIO 复杂性:需手动管理缓冲区、事件循环,代码更复杂。
- 传统 IO 资源消耗:每个连接需独立线程,高并发时线程开销大。
- 零拷贝优化:NIO 的
FileChannel.transferTo()可减少数据拷贝次数。
Selector 机制
概念定义
Selector(选择器)是 Java NIO(非阻塞 I/O)中的核心组件,用于多路复用 I/O 操作。它允许单个线程监控多个通道(Channel)的 I/O 事件(如连接、读、写),从而高效管理高并发网络连接。
核心特点
- 非阻塞:通过
SelectableChannel.configureBlocking(false)设置为非阻塞模式。 - 事件驱动:监听
OP_ACCEPT(接受连接)、OP_READ(读就绪)、OP_WRITE(写就绪)、OP_CONNECT(连接完成)等事件。 - 单线程多通道:一个 Selector 可管理多个 Channel,减少线程资源消耗。
使用场景
- 高并发服务器(如聊天服务器、游戏服务器)
- 需要同时处理大量网络连接的场景
- 资源受限但需高效 I/O 的系统
核心类与方法
- Selector:通过
Selector.open()创建。 - SelectionKey:表示 Channel 注册到 Selector 的令牌,包含:
interestOps():关注的事件集合readyOps():已就绪的事件集合
- 注册方法:
channel.register(selector, SelectionKey.OP_READ);
工作流程
- 创建 Selector 和 ServerSocketChannel。
- 将 Channel 注册到 Selector,指定监听事件。
- 调用
selector.select()阻塞等待事件。 - 通过
selectedKeys()获取就绪的 SelectionKey 集合。 - 处理事件后,必须手动移除已处理的 Key(
iterator.remove())。
示例代码
try (Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 阻塞直到有事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须移除
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 处理读数据
}
}
}
}
注意事项
- Key 未移除:会导致重复处理同一事件。
- 线程安全:Selector 本身线程安全,但关联的 Channel 和 Key 需自行保证。
- 空轮询 Bug:某些 JDK 版本中
select()可能立即返回,需通过计数器或超时规避。
性能优化
- 对高频事件(如
OP_READ)使用缓冲区池 - 避免在事件处理中执行耗时操作
- 结合
selectNow()实现部分非阻塞逻辑
Channel 基本概念
定义
Channel(通道)是 Java NIO(New I/O)中的核心组件之一,用于在缓冲区(Buffer)和文件/网络套接字之间进行数据传输。它类似于传统 I/O 中的流(Stream),但提供了更高效、更灵活的 I/O 操作方式。
特点
- 双向性:与传统的输入/输出流不同,Channel 可以同时支持读写操作。
- 非阻塞模式:Channel 可以设置为非阻塞模式,适用于高并发场景。
- 直接操作缓冲区:数据通过 Buffer 与 Channel 交互,减少数据拷贝次数,提升性能。
常见类型
- FileChannel:用于文件 I/O 操作。
- SocketChannel:用于 TCP 网络通信。
- ServerSocketChannel:用于监听 TCP 连接。
- DatagramChannel:用于 UDP 通信。
基本用法示例
// 文件复制示例(FileChannel)
try (FileChannel srcChannel = FileChannel.open(Paths.get("source.txt"));
FileChannel destChannel = FileChannel.open(Paths.get("dest.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
srcChannel.transferTo(0, srcChannel.size(), destChannel);
}
// SocketChannel 示例
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("example.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer);
注意事项
- 资源释放:Channel 使用后必须关闭(通过 try-with-resources 或手动调用 close())。
- 缓冲区管理:读写操作前需要正确设置 Buffer 的 position 和 limit。
- 线程安全:大多数 Channel 实现不是线程安全的,需要外部同步。
性能优势
- 零拷贝:FileChannel 的 transferTo()/transferFrom() 方法可以利用操作系统零拷贝特性。
- 分散/聚集:支持 Scatter/Gather 操作,实现单个读写操作处理多个缓冲区。
Buffer 工作原理
概念定义
Buffer(缓冲区)是 Java NIO 中的一个核心组件,用于高效地存储和传输数据。它本质上是一个固定大小的内存块,可以临时存储数据,减少 I/O 操作的次数,提升性能。
核心属性
- Capacity(容量):Buffer 的最大存储容量,创建后不可更改。
- Position(位置):当前读写的位置,初始为 0。
- Limit(限制):可读写数据的最大位置,初始等于 Capacity。
- Mark(标记):临时标记的位置,可通过
reset()恢复。
工作流程
- 写入数据:数据从 Channel 写入 Buffer,Position 增加。
- 切换为读模式:调用
flip(),Limit 设为当前 Position,Position 重置为 0。 - 读取数据:从 Buffer 读取数据,Position 增加。
- 清空或复用:调用
clear()(清空)或compact()(保留未读数据)。
常见类型
ByteBuffer:最常用,支持直接内存(allocateDirect())。CharBuffer、IntBuffer等:基本类型的 Buffer。
示例代码
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配 1KB
// 写入数据
buffer.put("Hello".getBytes());
// 切换为读模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 清空缓冲区
buffer.clear();
注意事项
- 直接缓冲区(Direct Buffer):分配在 JVM 堆外,适合大文件或高频 I/O,但创建和销毁成本高。
- 非直接缓冲区(Heap Buffer):分配在 JVM 堆内,性能稍低但管理简单。
- 线程安全:Buffer 不是线程安全的,需自行同步。
性能优化
- 复用 Buffer 避免频繁分配/回收。
- 对于大文件或高频 I/O,优先使用 Direct Buffer。
NIO 编程模型
概念定义
NIO(New I/O)是 Java 提供的一种非阻塞 I/O 编程模型,核心组件包括:
- Channel:双向数据传输通道(如
SocketChannel、ServerSocketChannel) - Buffer:数据容器(如
ByteBuffer) - Selector:多路复用器,监控多个 Channel 的事件
与 BIO 的区别
| 特性 | BIO(阻塞 I/O) | NIO(非阻塞 I/O) |
|---|---|---|
| 阻塞方式 | 线程阻塞等待数据 | 线程轮询检查数据就绪 |
| 连接处理 | 1 线程 = 1 连接 | 1 线程可处理多连接 |
| 适用场景 | 低并发短连接 | 高并发长连接 |
核心组件详解
Channel
- 特点:支持异步读写,可注册到 Selector
- 常见实现:
// 客户端通道 SocketChannel clientChannel = SocketChannel.open(); // 服务端通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); // 非阻塞模式
Buffer
- 工作流程:
write -> flip -> read -> clear - 示例:
ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("Hello".getBytes()); // 写入数据 buffer.flip(); // 切换读模式 while(buffer.hasRemaining()) { System.out.print((char)buffer.get()); } buffer.clear(); // 清空缓冲区
Selector
-
事件类型:
SelectionKey.OP_READ(可读)SelectionKey.OP_WRITE(可写)SelectionKey.OP_CONNECT(连接就绪)SelectionKey.OP_ACCEPT(接收连接)
-
使用示例:
Selector selector = Selector.open(); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while(true) { int readyChannels = selector.select(); // 阻塞直到有事件 Set<SelectionKey> keys = selector.selectedKeys(); // 处理事件... }
使用场景
- 高并发服务器(如聊天服务器)
- 需要大量长连接的场景(如即时通讯)
- 文件大文件传输(零拷贝特性)
注意事项
- 正确处理事件:每次 select() 后必须手动清除已处理的事件集合
iterator.remove(); // 清除已处理的 SelectionKey - 缓冲区分配:根据业务场景选择合适大小的 Buffer,避免频繁扩容
- 线程安全:NIO 本身非线程安全,多线程环境需要额外同步
完整示例(简易服务器)
public class NioServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
// 处理新连接
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// 处理业务逻辑...
}
iter.remove();
}
}
}
}
Socket编程基础全面解析
1万+

被折叠的 条评论
为什么被折叠?



