面试题答案
一键面试1. 心跳包格式设计
可以设计一个简单的心跳包格式,例如:
public class HeartbeatPacket {
private static final byte[] MAGIC_NUMBER = {0x48, 0x42, 0x54}; // "HBT" 作为标识
private byte[] magicNumber;
private long timestamp;
public HeartbeatPacket() {
this.magicNumber = MAGIC_NUMBER;
this.timestamp = System.currentTimeMillis();
}
// 序列化方法
public byte[] serialize() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
dos.write(magicNumber);
dos.writeLong(timestamp);
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 反序列化方法
public static HeartbeatPacket deserialize(byte[] data) {
if (data.length < MAGIC_NUMBER.length + 8) {
return null;
}
ByteArrayInputStream bis = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bis);
try {
byte[] magic = new byte[MAGIC_NUMBER.length];
dis.readFully(magic);
if (!Arrays.equals(magic, MAGIC_NUMBER)) {
return null;
}
long timestamp = dis.readLong();
HeartbeatPacket packet = new HeartbeatPacket();
packet.timestamp = timestamp;
return packet;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
这样的格式通过魔数来标识心跳包,并且携带时间戳,方便接收方判断心跳的时效性。
2. 重传机制
- 发送端:
- 使用一个定时器,在发送心跳包后启动。如果在设定的时间内(例如心跳间隔的2倍)没有收到心跳响应,就重传心跳包。
- 可以设置最大重传次数,例如3次。超过最大重传次数后,判定连接可能已断开,进行相应处理(如尝试重新连接)。
private static final int MAX_RETRIES = 3;
private static final long HEARTBEAT_INTERVAL = 5000; // 5秒
private int retryCount = 0;
private Timer heartbeatTimer;
public void startHeartbeat(Socket socket) {
heartbeatTimer = new Timer();
heartbeatTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
HeartbeatPacket packet = new HeartbeatPacket();
socket.getOutputStream().write(packet.serialize());
System.out.println("Heartbeat sent");
// 启动重传定时器
startRetryTimer(socket);
} catch (IOException e) {
e.printStackTrace();
// 连接异常,尝试重新连接
reconnect(socket);
}
}
}, 0, HEARTBEAT_INTERVAL);
}
private void startRetryTimer(Socket socket) {
Timer retryTimer = new Timer();
retryTimer.schedule(new TimerTask() {
@Override
public void run() {
if (retryCount < MAX_RETRIES) {
try {
HeartbeatPacket packet = new HeartbeatPacket();
socket.getOutputStream().write(packet.serialize());
System.out.println("Heartbeat re - sent");
retryCount++;
} catch (IOException e) {
e.printStackTrace();
// 连接异常,尝试重新连接
reconnect(socket);
}
} else {
// 超过最大重传次数,尝试重新连接
reconnect(socket);
}
}
}, HEARTBEAT_INTERVAL * 2);
}
- 接收端:
- 接收到心跳包后,立即回复一个响应包,同样可以设计简单的格式,例如只包含魔数和确认标识。
public class HeartbeatResponsePacket {
private static final byte[] MAGIC_NUMBER = {0x52, 0x48, 0x42}; // "RHB" 作为标识
private byte[] magicNumber;
private boolean ack;
public HeartbeatResponsePacket(boolean ack) {
this.magicNumber = MAGIC_NUMBER;
this.ack = ack;
}
// 序列化方法
public byte[] serialize() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
dos.write(magicNumber);
dos.writeBoolean(ack);
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
// 反序列化方法
public static HeartbeatResponsePacket deserialize(byte[] data) {
if (data.length < MAGIC_NUMBER.length + 1) {
return null;
}
ByteArrayInputStream bis = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bis);
try {
byte[] magic = new byte[MAGIC_NUMBER.length];
dis.readFully(magic);
if (!Arrays.equals(magic, MAGIC_NUMBER)) {
return null;
}
boolean ack = dis.readBoolean();
HeartbeatResponsePacket packet = new HeartbeatResponsePacket(ack);
return packet;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
// 接收心跳包并回复响应包
private void handleHeartbeat(Socket socket) {
new Thread(() -> {
try {
byte[] buffer = new byte[1024];
int length;
while ((length = socket.getInputStream().read(buffer)) != -1) {
HeartbeatPacket packet = HeartbeatPacket.deserialize(Arrays.copyOf(buffer, length));
if (packet != null) {
HeartbeatResponsePacket response = new HeartbeatResponsePacket(true);
socket.getOutputStream().write(response.serialize());
System.out.println("Heartbeat received, response sent");
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
3. 连接保持策略
- 定时心跳:通过周期性发送心跳包(如每隔5秒),确保在网络抖动或短暂丢包的情况下,连接不会被认为是空闲而关闭。
- 检测连接状态:发送端通过重传机制和接收响应包来判断连接是否正常。接收端在一定时间内没有收到心跳包,也可以主动发起检测,例如发送一个询问包。
- 异常处理:当检测到连接异常(如重传次数达到上限或长时间未收到心跳),尝试重新连接。可以设置一个最大重试次数,超过后通知上层业务逻辑进行处理,例如提示用户网络异常。
private void reconnect(Socket socket) {
try {
socket.close();
// 重新建立连接逻辑
InetSocketAddress address = new InetSocketAddress("server - address", 12345);
socket = new Socket();
socket.connect(address, 5000);
System.out.println("Reconnected to server");
// 重新启动心跳
startHeartbeat(socket);
} catch (IOException e) {
e.printStackTrace();
// 通知上层业务逻辑连接失败
notifyBusinessConnectionFailed();
}
}
4. 与现有业务逻辑无缝融合
- 封装心跳逻辑:将心跳机制相关的代码封装成独立的类或模块,提供简单的接口,如
startHeartbeat(Socket socket)
和stopHeartbeat()
,方便业务代码调用。 - 事件监听:可以使用观察者模式,当心跳检测到连接异常或重新连接成功时,通知业务逻辑进行相应处理,例如更新UI状态、暂停或恢复业务数据传输。
// 心跳状态监听器接口
public interface HeartbeatListener {
void onConnectionLost();
void onReconnected();
}
private List<HeartbeatListener> listeners = new ArrayList<>();
public void addHeartbeatListener(HeartbeatListener listener) {
listeners.add(listener);
}
private void notifyBusinessConnectionFailed() {
for (HeartbeatListener listener : listeners) {
listener.onConnectionLost();
}
}
private void notifyBusinessReconnected() {
for (HeartbeatListener listener : listeners) {
listener.onReconnected();
}
}
- 与业务线程分离:心跳机制运行在独立的线程或线程池中,避免影响业务逻辑的执行效率。业务逻辑在需要时通过接口调用心跳相关方法,例如在建立连接后启动心跳。
// 业务逻辑示例
public class BusinessLogic {
private Socket socket;
public BusinessLogic() {
try {
InetSocketAddress address = new InetSocketAddress("server - address", 12345);
socket = new Socket();
socket.connect(address, 5000);
HeartbeatManager heartbeatManager = new HeartbeatManager();
heartbeatManager.addHeartbeatListener(new HeartbeatListener() {
@Override
public void onConnectionLost() {
// 业务处理,如暂停数据传输
System.out.println("Connection lost, pause data transfer");
}
@Override
public void onReconnected() {
// 业务处理,如恢复数据传输
System.out.println("Reconnected, resume data transfer");
}
});
heartbeatManager.startHeartbeat(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}