一、引入
-
准备两台设备,设备 A(Android 设备)用作 Server 端,设备 B(Windows 设备)用作 Client 端
-
Server 端与 Client 端建立 TCP 通信后,Server 端断网(异常断开),观察 Client 端的感知情况
二、测试案例
1、Server
public static final String TAG = TcpCommunicateTestClientActivity.class.getSimpleName();
private ExecutorService pool;
private static int PORT = 9999;
private ServerSocket serverSocket;
private Socket socket;
private InputStream inputStream;
private OutputStream outputStream;
private EditText etContent;
private Button btnSend;
pool = new ThreadPoolExecutor(2, 3, 10, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
pool.execute(() -> {
try {
serverSocket = new ServerSocket(PORT);
Log.i(TAG, "Server 在 " + PORT + " 端口等待连接");
socket = serverSocket.accept();
Log.i(TAG, "Server 得到连接");
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
byte[] dataArr = new byte[100];
int length;
while ((length = inputStream.read(dataArr)) != -1) {
String res = new String(dataArr, 0, length);
Log.i(TAG, "Server 接收:" + res);
}
} catch (IOException e) {
e.printStackTrace();
}
});
etContent = findViewById(R.id.et_content);
btnSend = findViewById(R.id.btn_send);
btnSend.setOnClickListener(v -> {
String content = etContent.getText().toString();
pool.execute(() -> {
try {
outputStream.write(content.getBytes());
Log.i(TAG, "Server 发送:" + content);
} catch (IOException e) {
e.printStackTrace();
}
});
});
2、Client
public class TcpCommunicateTestClient {
private static String IP = "127.0.0.1";
private static int PORT = 9999;
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress(IP, PORT));
System.out.println("Client 连接成功");
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
new Thread(() -> {
try {
byte[] dataArr = new byte[100];
int length;
while ((length = inputStream.read(dataArr)) != -1) {
String res = new String(dataArr, 0, length);
System.out.println("Client 接收:" + res);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
Scanner scanner = new Scanner(System.in);
while (true) {
String str = scanner.next();
System.out.println("Client 发送:" + str);
outputStream.write(str.getBytes());
}
}
}
三、断网(异常断开)测试流程
- Server 端启动
# Server 端输出内容
Server 在 9999 端口等待连接
- Client 端启动
# Server 端输出内容
Server 得到连接
# Client 端输出内容
Client 连接成功
- Server 端断网,此时,Client 端还无法感知连接断开
# Server 端输出内容
java.net.SocketException: Software caused connection abort
发生在【while ((length = inputStream.read(dataArr)) != -1) {】
- Client 端发送一些内容,稍后,读取位置抛出异常,Client 端感知连接断开
# Client 端输出内容
Client 发送:123
java.net.SocketException: 由于连接方在一段时间后没有正确答复或连接的主机没有反应,连接尝试失败。
发生在【while ((length = inputStream.read(dataArr)) != -1) {】
- Client 端再发送一些内容,发送位置抛出异常
# Client 端输出内容
Client 发送:123
Exception in thread "main" java.net.SocketException: Connection reset by peer
发生在【outputStream.write(str.getBytes());】
四、流程解析
-
Client 端与 Server 端成功建立 TCP 连接
-
Server 端断网,但 Client 端的 TCP 连接仍然处于半开状态
-
Client 端的
inputStream.read()
一直阻塞,等待 Server 端发送数据 -
当 Client 端尝试发送数据时,操作系统发现无法将数据送达,触发 TCP 重传机制
-
经过多次重传后,操作系统认为连接已经断开,Socket 的状态被标记为异常
-
此时,Client 端的
inputStream.read()
抛出 SocketException 异常 -
此时,当 Client 端再次尝试发送数据时,
outputStream.write()
也会抛出 SocketException 异常
五、原理解析
1、TCP 通信半开状态
-
在 TCP 通信中,Server 断网,Client 端无法立刻感知
-
TCP 通信本身没有提供实时检测连接状态的机制
-
此时,Client 端进入半开状态
-
在半开状态下,Client 端的操作系统仍然认为连接是有效的,因为它没有收到 Server 端的任何关闭信号
2、Client 端 inputStream.read()
阻塞
-
Client 端
inputStream.read()
是一个阻塞操作,它会一直等待,直到有数据从 Server 端发送过来 -
如果 Server 端断网,Client 端
inputStream.read()
会一直阻塞 -
因为此时 TCP 连接仍然被认为是有效的,操作系统不会主动通知 Client 端连接已经断开
3、重传机制
-
Client 端尝试发送数据时,如果 Server 端断网,操作系统会发现无法将数据送达,触发 TCP 的重传机制
-
在经过多次重传仍然无法成功发送数据后,操作系统会认为连接已经断开,并触发
java.net.SocketException
异常 -
此时,Socket 的状态会被标记为异常
4、Client 端 inputStream.read()
与 outputStream.write()
异常抛出
-
outputStream 与 inputStream 是同一个 Socket 的输入输出流
-
它们都依赖于底层的 Socket 连接状态,任何一方的异常都会影响另一方
-
当 Socket 的状态被标记为异常时,
inputStream.read()
会立即抛出异常,而不是继续阻塞 -
如果此时 Client 端再次尝试发送数据时,
outputStream.write()
也会抛出 SocketException 异常