跳转到内容

进阶实战

前面四页解决了「能通」,这一页解决「能上生产」。

长连接会被中间设备(NAT 网关、防火墙、负载均衡)静默掐断,现象是「不发数据时连接看着还在,一发包就报错」。解法是两端配合:

  • 服务端:超过 N 秒无读写就断开,回收僵尸连接;
  • 客户端:定时发心跳包,证明自己还活着。
import io.github.smartboot.socket.extension.plugins.IdleStatePlugin;
// 90 秒内读写都没有发生 → 关闭连接
processor.addPlugin(new IdleStatePlugin<>(90_000));
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
ScheduledExecutorService heartbeat = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "heartbeat");
t.setDaemon(true);
return t;
});
heartbeat.scheduleAtFixedRate(() -> {
if (session.isInvalid()) {
return; // 连接已断,交给重连逻辑处理
}
try {
FrameWriter.writeString(session.writeBuffer(), "ping");
} catch (IOException e) {
// 写失败通常意味着连接已断,忽略,下一轮重连会兜底
}
}, 30, 30, TimeUnit.SECONDS);

客户端不需要「猜」连接是否断了——AioSession 有明确的状态。

ReconnectClient.java
import io.github.smartboot.socket.AbstractMessageProcessor;
import io.github.smartboot.socket.StateMachineEnum;
import io.github.smartboot.socket.transport.AioQuickClient;
import io.github.smartboot.socket.transport.AioSession;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
public class ReconnectClient {
private final AtomicBoolean reconnecting = new AtomicBoolean(false);
private volatile AioQuickClient client;
private volatile AioSession session;
public void start() throws IOException {
connect();
}
private void connect() {
try {
client = new AioQuickClient("127.0.0.1", 8888, new StringProtocol(),
new AbstractMessageProcessor<String>() {
@Override
public void process0(AioSession session, String msg) {
System.out.println("收到: " + msg);
}
@Override
public void stateEvent0(AioSession session, StateMachineEnum state, Throwable t) {
if (state == StateMachineEnum.SESSION_CLOSED) {
scheduleReconnect();
}
}
});
client.connectTimeout(3000);
session = client.start();
System.out.println("连接成功: " + session.getRemoteAddress());
} catch (IOException e) {
scheduleReconnect();
}
}
private void scheduleReconnect() {
if (!reconnecting.compareAndSet(false, true)) {
return; // 已经在重连,别叠加
}
Thread t = new Thread(() -> {
int attempt = 0;
while (true) {
attempt++;
long backoff = Math.min(30_000L, 1000L * (1L << Math.min(attempt, 5)));
try {
Thread.sleep(backoff);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
try {
if (client != null) {
client.shutdownNow(); // 先彻底清掉旧连接
}
connect();
reconnecting.set(false);
return;
} catch (Exception ignored) {
// 继续下一轮,退避时间递增
}
}
}, "reconnect");
t.setDaemon(true);
t.start();
}
}

要点:

点原因
用 stateEvent(SESSION_CLOSED) 触发比轮询 isInvalid() 及时,且不占线程
重连前调 shutdownNow()旧客户端的线程组和残留缓冲必须释放,否则重连一次泄漏一组线程
AtomicBoolean 去重网络抖动会连续触发多个 SESSION_CLOSED,不去重会并发建连
指数退避 + 上限服务端宕机时避免客户端把重连打成 DDoS
ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleWithFixedDelay(() -> {
if (session == null || session.isInvalid()) {
connect();
}
}, 5, 5, TimeUnit.SECONDS);

够用,但最多会晚 5 秒发现断线。业务对断线敏感就用方案 A。

推送的本质是:把 AioSession 存起来,需要时从任意线程往里写。AioSession 的写操作是线程安全的。

PushProcessor.java
import io.github.smartboot.socket.MessageProcessor;
import io.github.smartboot.socket.StateMachineEnum;
import io.github.smartboot.socket.transport.AioSession;
import io.github.smartboot.socket.transport.WriteBuffer;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class PushProcessor implements MessageProcessor<String> {
/** 在线连接表:ConcurrentHashMap.newKeySet() 是 JDK 8 的最优选择 */
private final Set<AioSession> sessions = ConcurrentHashMap.newKeySet();
@Override
public void process(AioSession session, String msg) {
// 客户端发来的消息,这里可以理解为「上线登记」
session.setAttachment(msg);
System.out.println("收到 " + msg + " 的消息");
}
@Override
public void stateEvent(AioSession session, StateMachineEnum state, Throwable throwable) {
if (state == StateMachineEnum.NEW_SESSION) {
sessions.add(session);
} else if (state == StateMachineEnum.SESSION_CLOSED) {
sessions.remove(session); // 必须移除,否则内存泄漏
}
}
/** 广播:可在任意业务线程调用 */
public void broadcast(String message) {
byte[] data = message.getBytes(StandardCharsets.UTF_8);
for (AioSession session : sessions) {
if (session.isInvalid()) {
sessions.remove(session);
continue;
}
try {
WriteBuffer out = session.writeBuffer();
out.writeInt(data.length);
out.write(data);
out.flush();
} catch (IOException e) {
sessions.remove(session); // 写失败说明连接已不可用
}
}
}
}

用法:

PushProcessor processor = new PushProcessor();
AioQuickServer server = new AioQuickServer(8888, new StringProtocol(), processor);
server.start();
// 任意线程随时推送
new Thread(() -> {
while (true) {
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
return;
}
processor.broadcast("server push: " + System.currentTimeMillis());
}
}).start();
private final Map<String, AioSession> userSessions = new ConcurrentHashMap<>();
// 登录成功后
userSessions.put(userId, session);
session.setAttachment(userId);
// 定向推送
public void sendTo(String userId, String message) throws IOException {
AioSession session = userSessions.get(userId);
if (session == null || session.isInvalid()) {
return;
}
FrameWriter.writeString(session.writeBuffer(), message);
}
// SESSION_CLOSED 里
userSessions.remove(session.getAttachment(), session);
Terminal window
keytool -genkeypair -alias smart-socket \
-keyalg RSA -keysize 2048 -validity 36500 \
-storetype JKS \
-keystore server.keystore \
-storepass 123456 -keypass 123456 \
-dname "CN=localhost, OU=dev, O=smartboot, L=Hangzhou, ST=Zhejiang, C=CN"
import io.github.smartboot.socket.extension.plugins.SslPlugin;
import io.github.smartboot.socket.extension.ssl.ClientAuth;
import io.github.smartboot.socket.extension.ssl.factory.ServerSSLContextFactory;
import java.io.FileInputStream;
import java.io.InputStream;
InputStream keystore = new FileInputStream("server.keystore");
SslPlugin<String> ssl = new SslPlugin<>(
new ServerSSLContextFactory(keystore, "123456", "123456"),
ClientAuth.NONE); // NONE=不校验客户端证书
processor.addPlugin(ssl); // 注意:SSL 要最后加,最贴近字节流
import io.github.smartboot.socket.extension.plugins.SslPlugin;
import io.github.smartboot.socket.extension.ssl.factory.ClientSSLContextFactory;
SslPlugin<String> ssl = new SslPlugin<>(new ClientSSLContextFactory());
processor.addPlugin(ssl);
// 服务端:要求客户端出示证书
new SslPlugin<>(new ServerSSLContextFactory(keystore, "123456", "123456"),
ClientAuth.REQUIRE);
// 客户端:带上自己的证书
new ClientSSLContextFactory(
new FileInputStream("client.truststore"), "123456",
new FileInputStream("client.keystore"), "123456");

ClientAuth 三个值:NONE(不校验)、OPTIONAL(有则校验)、REQUIRE(必须校验)。

UDP 的 API 与 TCP 平行,但用的是 UdpBootstrap 而不是 AioQuickServer。它在 aio-pro 模块里。

UdpEchoServer.java
import io.github.smartboot.socket.AbstractMessageProcessor;
import io.github.smartboot.socket.StateMachineEnum;
import io.github.smartboot.socket.buffer.BufferPagePool;
import io.github.smartboot.socket.transport.AioSession;
import io.github.smartboot.socket.transport.UdpBootstrap;
public class UdpEchoServer {
public static void main(String[] args) throws Exception {
AbstractMessageProcessor<String> processor = new AbstractMessageProcessor<String>() {
@Override
public void process0(AioSession session, String msg) {
try {
FrameWriter.writeString(session.writeBuffer(), msg);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void stateEvent0(AioSession session, StateMachineEnum state, Throwable t) {
}
};
new UdpBootstrap(new StringProtocol(), processor)
.setThreadNum(Runtime.getRuntime().availableProcessors())
.setReadBufferSize(2048)
.setBufferPagePool(new BufferPagePool(
Runtime.getRuntime().availableProcessors(), true))
.open(9999);
}
}
UdpBootstrap bootstrap = new UdpBootstrap(new StringProtocol(), processor)
.setReadBufferSize(2048);
UdpChannel channel = bootstrap.open(); // 随机本地端口
AioSession session = channel.connect("127.0.0.1", 9999); // UDP 的「会话」对应一个远端地址
FrameWriter.writeString(session.writeBuffer(), "hello udp");
维度TCP(AioQuickServer)UDP(UdpBootstrap)
连接语义有连接,AioSession 与连接一一对应无连接,AioSession 对应远端地址的四元组
可靠性可靠、有序不保证到达、不保证顺序
单包大小无上限(靠拆包)受 MTU 限制,建议 ≤ 1400 字节
协议要求必须自己拆包天然有报文边界,通常不需要拆包
空闲断开IdleStatePlugin 有效无连接可断,插件意义不大
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("正在停机...");
server.shutdown(); // 关闭监听 + IO 线程组
// 如果你有自己的业务线程池,也在这里关
businessPool.shutdown();
System.out.println("已停机");
}));

server.shutdown() 只关闭监听通道和线程组,不会逐个通知客户端「我要走了」。需要优雅下线协议的话,得自己先广播一条 bye 消息、等客户端主动断开,再 shutdown()。