Java网络断连案例如何处理

wen java案例 22

本文目录导读:

Java网络断连案例如何处理

  1. 断连的常见场景
  2. 检测连接断开的方法
  3. 断连处理最佳实践
  4. 注意事项与陷阱
  5. 框架级处理(NIO / Netty)
  6. 总结要点

断连的常见场景

场景 表现
客户端主动关闭 正常 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 — 写入已关闭的 socket
  • SocketTimeoutException — 读写超时(严格说不算断连,但需要处理)

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 框架内部已经正确封装了断连检测,开发者只需实现对应回调方法即可。


总结要点

  1. 唯一可靠的检测方法: 尝试读写操作,捕获异常或检查返回值。
  2. 应用层心跳: 对长时间空闲的连接必须发送心跳包(如每 30 秒一次)。
  3. 设置 SoTimeout: 避免线程永久阻塞。
  4. 重连策略: 指数退避(如 1s, 2s, 4s...)且设置最大重试次数。
  5. 资源清理: 断连后务必关闭所有流和 socket,防止内存泄漏。
  6. 避免重复创建: 多线程环境下,断连处理逻辑要加锁,防止重复连接。

通过以上策略,你可以构建一个健壮的网络通信模块,从容应对各种断连场景。

抱歉,评论功能暂时关闭!