本文目录导读:

断连的常见场景
| 场景 | 表现 |
|---|---|
| 客户端主动关闭 | 正常 FIN 包,read() 返回 -1 |
| 服务端异常崩溃 | 收到 RST 包,write() 抛出 IOException |
| 网络超时/防火墙 | 长时间无响应,SocketTimeoutException |
| 网线断开/路由故障 | 无法感知,直到尝试读写时发现 |
检测连接断开的方法
1 利用 read() 返回值
InputStream in = socket.getInputStream();
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
if (bytesRead == -1) {
// 对方正常关闭连接,此时应关闭自己的 socket
System.out.println("Connection closed by peer");
socket.close();
}
特点: 只能检测到正常关闭(FIN),无法检测到异常断开(如网线物理断开)。
2 捕获 IOException
try {
outputStream.write(data);
outputStream.flush();
} catch (IOException e) {
// 可能是连接已断开,放弃该连接
System.err.println("Connection broken: " + e.getMessage());
closeResources();
}
常见异常:
SocketException: Connection reset— 对端异常关闭(RST)SocketException: Broken pipe— 写入已关闭的 socketSocketTimeoutException— 读写超时(严格说不算断连,但需要处理)
3 设置 SoTimeout 检测死连接
socket.setSoTimeout(5000); // 5秒超时
try {
int data = inputStream.read(); // 阻塞,最多等5秒
// 正常读取到数据
} catch (SocketTimeoutException e) {
// 超时,可能是连接已死,需要进一步判断或关闭
// 注意:超时不等于断连,可能是对方处理慢
handleTimeout();
}
典型策略: 在心跳检测失败后使用,结合发送测试数据。
4 发送心跳包 (Keep-Alive)
检测长时间无数据交换的连接是否活跃。
// 开启 TCP Keep-Alive(系统级,间隔较长,约2小时)
socket.setKeepAlive(true);
// 更可靠的是应用层心跳
// 每30秒发送一个特殊的“PING”消息
public void sendHeartbeat() {
if (!connected) return;
try {
outputStream.writeObject(new HeartbeatMessage());
outputStream.flush();
lastHeartbeatSent = System.currentTimeMillis();
} catch (IOException e) {
handleDisconnect();
}
}
建议: 应用层心跳 + TCP Keep-Alive 双重保障,且应用层心跳间隔建议 30-60 秒。
断连处理最佳实践
1 资源清理模板 (try-with-resources)
最简单且安全,但会立即关闭连接。
try (Socket socket = new Socket("host", port);
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream()) {
// 业务逻辑
} catch (IOException e) {
// 自动关闭资源,无需手动处理
log.error("Connection error", e);
}
2 自定义重连逻辑 (客户端典型模式)
public class ReconnectingClient {
private static final int MAX_RETRIES = 3;
private static final int RETRY_DELAY_MS = 1000;
private String host;
private int port;
private Socket socket;
private volatile boolean running = true;
public void connect() throws IOException {
closeQuietly();
socket = new Socket();
socket.connect(new InetSocketAddress(host, port), 5000);
socket.setSoTimeout(10000);
}
public void sendData(byte[] data) {
for (int i = 0; i <= MAX_RETRIES; i++) {
try {
if (socket == null || !socket.isConnected()) {
connect();
}
OutputStream out = socket.getOutputStream();
out.write(data);
out.flush();
return; // 成功
} catch (IOException e) {
log.warn("Send failed (attempt {})", i, e);
closeResources();
if (i < MAX_RETRIES) {
try {
Thread.sleep(RETRY_DELAY_MS * (i + 1)); // 指数退避
} catch (InterruptedException ignored) {}
}
}
}
throw new RuntimeException("Failed after " + MAX_RETRIES + " attempts");
}
private void closeResources() {
try {
if (socket != null) socket.close();
} catch (IOException ignored) {}
socket = null;
}
public void closeQuietly() {
closeResources();
running = false;
}
}
3 服务端处理断连 (优雅关闭)
public class ServerHandler implements Runnable {
private Socket clientSocket;
private volatile boolean keepRunning = true;
@Override
public void run() {
try (Socket socket = clientSocket;
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while (keepRunning && (bytesRead = in.read(buffer)) != -1) {
processData(buffer, bytesRead);
}
// read() == -1 表示客户端正常关闭
log.info("Client closed connection gracefully");
} catch (SocketTimeoutException e) {
// 读超时但连接还在,可以选择继续等待或关闭
log.warn("Read timeout, closing connection");
} catch (IOException e) {
// 异常断开,如 Connection reset
log.error("Connection broken unexpectedly", e);
} finally {
cleanup();
}
}
private void cleanup() {
// 释放与此客户端相关的所有资源(线程、队列等)
keepRunning = false;
}
public void stop() {
keepRunning = false;
// 中断阻塞的 read()
try {
clientSocket.close();
} catch (IOException ignored) {}
}
}
注意事项与陷阱
1 不要依赖 isConnected() 和 isClosed()
// 这些方法只能检查本地状态,无法检测网络实际状态
if (socket.isConnected()) { // 可能返回 true,实际网络已断
// 危险!可能抛出异常
socket.getOutputStream().write(data);
}
原因: isConnected() 只表示曾经成功连接过,isClosed() 只表示本地是否已关闭。唯一可靠的检测是尝试读写。
2 半关闭状态 (Half-Close)
// 关闭输出流,但保留输入流 socket.shutdownOutput(); // 发送 FIN,告诉对端“我不会再写了” // 仍然可以读取对端的响应 InputStream in = socket.getInputStream(); // ... // 最终完全关闭 socket.close();
场景: HTTP 1.0 或自定义协议中,发送完请求后等待响应。
3 断连后 socket 的复用
绝对不要尝试复用已断开的 socket。 一旦连接断开,必须创建新的 Socket 和 InputStream/OutputStream 实例。
框架级处理(NIO / Netty)
1 NIO 中的断连检测
// 在 Selector 中,SelectionKey 会收到 OP_READ 事件
// 但 read() 返回 -1 表示连接关闭
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
// 连接已关闭,取消 key 并关闭 channel
key.cancel();
channel.close();
}
}
2 Netty 自动处理
Netty 的 ChannelInboundHandler 中:
public class MyHandler extends SimpleChannelInboundHandler<Object> {
@Override
public void channelInactive(ChannelHandlerContext ctx) {
// 连接断开时自动调用
System.out.println("Connection lost: " + ctx.channel().remoteAddress());
// 可在此进行重连等操作
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 捕获所有 IO 异常,包括断连
if (cause instanceof IOException) {
// 连接异常断开
ctx.close();
}
}
}
Netty 框架内部已经正确封装了断连检测,开发者只需实现对应回调方法即可。
总结要点
- 唯一可靠的检测方法: 尝试读写操作,捕获异常或检查返回值。
- 应用层心跳: 对长时间空闲的连接必须发送心跳包(如每 30 秒一次)。
- 设置 SoTimeout: 避免线程永久阻塞。
- 重连策略: 指数退避(如 1s, 2s, 4s...)且设置最大重试次数。
- 资源清理: 断连后务必关闭所有流和 socket,防止内存泄漏。
- 避免重复创建: 多线程环境下,断连处理逻辑要加锁,防止重复连接。
通过以上策略,你可以构建一个健壮的网络通信模块,从容应对各种断连场景。