本文目录导读:

断连的常见场景
| 场景 | 表现 |
|---|---|
| 客户端主动关闭 | 正常 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,防止内存泄漏。
- 避免重复创建: 多线程环境下,断连处理逻辑要加锁,防止重复连接。
通过以上策略,你可以构建一个健壮的网络通信模块,从容应对各种断连场景。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播