杰瑞科技汇

Java中如何处理socket断开连接?

Socket 连接的断开通常分为两种情况:

Java中如何处理socket断开连接?-图1
(图片来源网络,侵删)
  1. 正常关闭:客户端或服务器主动调用 socket.close() 方法来关闭连接。
  2. 异常断开:网络出现问题,如网线被拔掉、网络设备故障、对方程序崩溃等,导致连接被强制中断。

处理这两种情况的方式有所不同,但核心都是通过 Java I/O 提供的机制来检测连接状态。


核心原理:如何检测连接是否已断开?

无论是客户端还是服务器,当连接断开后,再通过 Socket 的输入流进行读取操作时,Java 会通过不同的方式通知你连接已断开。

  • 对于 TCP Socket (基于字节流)

    • 正常关闭:当对方调用 close() 后,你再从输入流中读取,会返回 -1,这是最可靠的检测方式。
    • 异常断开:当网络连接被中断时,你再从输入流中读取,会抛出 IOExceptionSocketException: Connection resetSocketTimeoutException(如果设置了超时)。
  • 对于 UDP Socket (基于数据报)

    Java中如何处理socket断开连接?-图2
    (图片来源网络,侵删)

    UDP 是无连接的,所以没有“断开”的概念,你发送数据包,它可能到达,也可能丢失,你需要自己处理发送失败或接收超时的情况。

下面我们主要讨论最常见的 TCP Socket。


客户端处理服务器断开

这是最常见的情况,客户端连接到服务器后,会进入一个循环来持续读取服务器的消息,我们需要在这个循环中检测服务器是否已经断开。

示例代码:客户端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
public class ClientExample {
    public static void main(String[] args) {
        String hostname = "localhost";
        int port = 12345;
        try (Socket socket = new Socket(hostname, port);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
            System.out.println("Connected to server.");
            // 启动一个线程来发送消息,避免阻塞主线程(这里简化了,直接在主线程演示)
            // 在实际应用中,发送和接收通常在不同的线程中
            new Thread(() -> {
                try (BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in))) {
                    String userInput;
                    while ((userInput = consoleReader.readLine()) != null) {
                        out.println(userInput);
                        if ("exit".equalsIgnoreCase(userInput)) {
                            break;
                        }
                    }
                } catch (IOException e) {
                    System.err.println("Error reading from console: " + e.getMessage());
                }
            }).start();
            // 主线程用于接收服务器消息并检测断开
            String serverResponse;
            // 循环读取,直到 in.readLine() 返回 null
            while ((serverResponse = in.readLine()) != null) {
                System.out.println("Server: " + serverResponse);
            }
            // 当循环结束时,说明 in.readLine() 返回了 null,表示服务器正常关闭了连接
            System.out.println("Server has closed the connection. Client is shutting down.");
        } catch (UnknownHostException e) {
            System.err.println("Don't know about host " + hostname);
            e.printStackTrace();
        } catch (SocketException e) {
            // 捕获 SocketException,这通常表示网络连接被重置或突然中断
            System.err.println("Connection to server lost: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/O Error: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

代码解析

Java中如何处理socket断开连接?-图3
(图片来源网络,侵删)
  1. try-with-resources:确保 Socket, PrintWriter, BufferedReader 在使用后被正确关闭。
  2. in.readLine():这个方法会阻塞,直到服务器发送一行数据或者连接关闭。
  3. while ((serverResponse = in.readLine()) != null):这是关键!
    • 如果服务器正常关闭,in.readLine() 会返回 null,循环结束。
    • 如果服务器异常断开,in.readLine() 会抛出 IOException,循环也会被中断,程序会跳到 catch 块。
  4. catch (SocketException e):专门捕获网络层面的异常,比如连接被重置(Connection reset by peer)。

服务器处理客户端断开

服务器通常需要为每个客户端连接创建一个独立的线程(或使用线程池),在每个处理线程中,逻辑与客户端类似:循环读取客户端的数据,并检测客户端是否断开。

示例代码:服务器

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
public class ServerExample {
    public static void main(String[] args) {
        int port = 12345;
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Server is listening on port " + port);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("New client connected: " + clientSocket.getInetAddress().getHostAddress());
                // 为每个客户端创建一个新线程来处理
                new Thread(() -> handleClient(clientSocket)).start();
            }
        } catch (IOException e) {
            System.err.println("Server exception: " + e.getMessage());
            e.printStackTrace();
        }
    }
    private static void handleClient(Socket clientSocket) {
        // 使用 try-with-resources 确保流和socket被关闭
        try (
            PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))
        ) {
            String inputLine;
            // 循环读取客户端消息,直到客户端断开连接
            while ((inputLine = in.readLine()) != null) {
                System.out.println("Received from " + clientSocket.getInetAddress() + ": " + inputLine);
                // 简单的回显服务
                out.println("Echo: " + inputLine);
                if ("exit".equalsIgnoreCase(inputLine)) {
                    System.out.println("Client requested to close connection.");
                    break; // 跳出循环,准备关闭资源
                }
            }
            // 当循环结束时,说明客户端已经断开连接
            System.out.println("Client " + clientSocket.getInetAddress() + " has disconnected.");
        } catch (SocketException e) {
            // 捕获客户端异常断开的情况
            System.err.println("Client " + clientSocket.getInetAddress() + " connection lost: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("Error handling client " + clientSocket.getInetAddress() + ": " + e.getMessage());
        } finally {
            // 确保在finally块中关闭socket,防止资源泄露
            try {
                clientSocket.close();
                System.out.println("Socket for client " + clientSocket.getInetAddress() + " closed.");
            } catch (IOException e) {
                System.err.println("Failed to close client socket: " + e.getMessage());
            }
        }
    }
}

代码解析

  1. serverSocket.accept():阻塞等待客户端连接。
  2. handleClient 方法:每个客户端连接的处理逻辑都在这个方法里,它在一个独立的线程中运行。
  3. while ((inputLine = in.readLine()) != null):和客户端一样,这是检测客户端是否断开的核心,如果客户端关闭,这里会返回 null 或抛出异常。
  4. finally 块:非常重要!无论连接是正常关闭还是异常中断,finally 块中的代码都会被执行,确保 clientSocket 被关闭,防止服务器资源耗尽。

关键点与最佳实践

  1. 永远不要假设连接是稳定的:任何网络调用都可能失败,必须用 try-catch 包裹。
  2. 使用 try-with-resources:这是管理 I/O 资源(如 Socket, InputStream, OutputStream)的最佳方式,可以确保它们在使用后被自动关闭,避免资源泄露。
  3. 区分正常关闭和异常断开
    • in.read() / in.readLine() 返回 -1null正常关闭
    • in.read() / in.readLine() 抛出 IOException异常断开
  4. 设置读取超时:为了避免 read() 方法无限期阻塞,可以设置读取超时。
    socket.setSoTimeout(5000); // 设置5秒读取超时

    如果超过5秒没有数据到达,read() 方法会抛出 java.net.SocketTimeoutException,你可以据此决定是继续等待还是认为连接已断开。

  5. 优雅关闭:在关闭连接前,最好先通知对方,客户端发送一个 "logout" 或 "exit" 消息,服务器收到后主动关闭 Socket,这样对方就能通过 read() 返回 null 来得知连接是正常结束的,而不是异常中断。
情况 检测方式 表现
正常关闭 对方调用 close() InputStream.read() 返回 -1
BufferedReader.readLine() 返回 null
异常断开 网络问题(拔网线、崩溃等) InputStream.read() / BufferedReader.readLine() 抛出 IOException (如 SocketException)
读取超时 调用 socket.setSoTimeout() InputStream.read() 抛出 SocketTimeoutException

在 Java 中处理 Socket 断开连接,核心就是在读取数据的循环中,正确处理返回 null 的情况和捕获 IOException,并始终确保 Socket 和相关流被正确关闭。

分享:
扫描分享到社交APP
上一篇
下一篇