Skip to content

核心概念

This content is not available in your language yet.

smart-socket 的 API 很少,但每个类型都有明确的边界。把这一页的几个概念吃透,后面所有章节都不需要再回来看源码。

┌──────────────────────────────────────────┐
│ AioQuickServer │ 启动 / 配置 / 关闭
└───────────────┬──────────────────────────┘
│ 持有
┌───────────────────────────┼────────────────────────────┐
│ │ │
┌───────▼────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ Protocol<T> │ │ MessageProcessor<T>│ │ BufferPagePool │
│ 字节 → 消息 │ │ 消息 → 业务逻辑 │ │ 内存池(读+写) │
└───────┬────────┘ └─────────┬─────────┘ └─────────┬─────────┘
│ │ │
│ ┌──────▼──────┐ │
│ │ Plugin<T> │ 读写前后 / 接入 / 状态 │
│ └──────┬──────┘ │
│ │ │
└───────────┬───────────────┴────────────────────────────┘
│ 处理
┌───────▼────────┐
│ AioSession │ 一条连接的全部状态:读缓冲、写缓冲、附件、状态机
└────────────────┘
public interface Protocol<T> {
T decode(ByteBuffer readBuffer, AioSession session);
}
  • 输入是框架的读缓冲区,输出是一条业务消息,或者 null(表示还没收完)。
  • 它是有状态的调用——同一个 session 的 decode 会被反复调用,直到你返回非 null。
  • 返回 null 时,框架保留未消费的字节,接着读;返回对象后,剩余字节立刻进入下一轮 decode,所以一次读取可能连续触发多次 process。
public interface MessageProcessor<T> {
void process(AioSession session, T msg);
default void stateEvent(AioSession session, StateMachineEnum state, Throwable throwable) {}
}

process 收到的一定是完整消息,不需要再判半包。

stateEvent 是连接生命周期与异常的统一出口,常见状态:

状态触发时机通常用来做什么
NEW_SESSION新连接建立注册到在线列表、初始化附件
SESSION_CLOSING连接正在关闭停止业务、清理计时器
SESSION_CLOSED连接已关闭从在线列表移除、释放资源
DECODE_EXCEPTION协议解析抛异常打印日志、排查协议 bug
PROCESS_EXCEPTIONprocess 抛异常保护性兜底
INPUT_EXCEPTION / OUTPUT_EXCEPTION读写发生 IO 异常网络故障排查
INPUT_SHUTDOWN对端关闭了写方向(收到 FIN)半关闭场景处理
REJECT_ACCEPT连接被插件拒绝配合黑名单 / 限流统计
ACCEPT_EXCEPTION / INTERNAL_EXCEPTION接入失败 / 框架内部异常告警

Plugin 是 smart-socket 最有特色的设计——它让你在不改动业务代码的前提下,插手读写链路。

方法时机典型用途
shouldAccept(AsynchronousSocketChannel)连接刚接入,握手前黑白名单、PROXY 协议解析、SSL 包装;返回 null 则拒绝
beforeRead(AioSession)每次读之前读限流、读计数
afterRead(AioSession, int readSize)每次读之后流量统计、空闲检测
beforeWrite(AioSession)每次写之前写限流
afterWrite(AioSession, int writeSize)每次写之后出口流量统计
preProcess(AioSession, T)消息交给 process 之前过滤、鉴权;返回 false 则丢弃该消息
stateEvent(StateMachineEnum, AioSession, Throwable)状态变更连接数统计、异常监控

插件怎么挂到框架上?框架只有一条注册路径:MessageProcessor 本身必须实现 Plugin。

// 方式一(推荐):继承 AbstractMessageProcessor —— 它同时实现了 MessageProcessor 和 Plugin
AbstractMessageProcessor<String> processor = new AbstractMessageProcessor<String>() {
@Override public void process0(AioSession session, String msg) { /* ... */ }
@Override public void stateEvent0(AioSession s, StateMachineEnum e, Throwable t) { }
};
processor.addPlugin(new IdleStatePlugin<>(60_000)); // 挂多少个都行,按加入顺序执行
// 方式二:让业务处理器直接实现 Plugin
public class MyProcessor implements MessageProcessor<String>, Plugin<String> {
@Override public void process(AioSession session, String msg) { /* ... */ }
@Override public void afterRead(AioSession session, int readSize) { /* ... */ }
}

框架在启动时会判断 processor instanceof Plugin,是则把它当作插件链的入口;否则用一个空实现兜底。所以纯 lambda 写法的 MessageProcessor 无法挂插件——想要插件,就必须换成上面两种写法之一。

public abstract class AioSession {
public abstract WriteBuffer writeBuffer(); // 写
public abstract ByteBuffer readBuffer(); // 读缓冲(一般够不到)
public abstract void close(boolean immediate); // 关闭
public boolean isInvalid(); // 是否已不可用
public <A> A getAttachment(); // 业务附件
public <A> void setAttachment(A attachment);
public abstract InetSocketAddress getRemoteAddress() throws IOException;
public abstract InetSocketAddress getLocalAddress() throws IOException;
public final int read(); // 同步式等待读(阻塞调用线程)
}

三个高频用法:

// 1. 判断连接是否还活着
if (!session.isInvalid()) {
session.writeBuffer().write(data);
session.writeBuffer().flush();
}
// 2. 给连接绑业务数据(如 userId、登录态),避免自己维护大 Map
session.setAttachment(userId);
String userId = session.getAttachment();
// 3. 拿到对端地址做日志 / 风控
InetSocketAddress remote = session.getRemoteAddress();

这是最容易踩坑的地方。

业务线程 IO 线程组(threadNum 个)
│ │
│ session.writeBuffer().write(...) │
├───────────────┐ │
│ ▼ │
│ 写队列(chunk 数组) │
│ │ 异步投递 │
│ └──────────────────▶│ select / read
│ │ ↓
│ │ decode → process ← 你的业务代码在这里
│ │ ↓
│ │ write → 网卡

三条铁律:

  1. threadNum 默认是 1。意味着默认只有一条 IO 线程在跑所有连接的 decode 和 process。多核机器上不显式调大,吞吐会被单线程卡死。
  2. process 在 IO 线程执行,不要阻塞。一次 sleep(100ms),这条线程上所有连接都跟着停 100ms。耗时操作请转交自己的业务线程池。
  3. 写操作是线程安全的,读不是。多个线程同时 writeBuffer().write() 是允许的(内部有同步),但你不需要、也不应该在多个线程里读同一个 session。

推荐配置:

server.setThreadNum(Runtime.getRuntime().availableProcessors() + 1);

smart-socket 支持百万连接的关键在于不做无节制的对象分配:读写都用池化的直接内存。

BufferPagePool(整池)
└── BufferPage × pageNum 每页一块大内存(默认 1MB 对齐)
└── VirtualBuffer × N 切分出的 chunk,按 chunkSize 分配
├── 读:每个 session 独占一个 readBuffer
└── 写:每个 session 一个 writeInBuf + 一个待发队列
概念说明
BufferPagePool(pageNum, isDirect)内存池。pageNum 是页数,isDirect=true 用堆外内存
chunkSize / chunkCount由 setWriteBuffer(chunkSize, chunkCount) 控制:单块大小、队列长度
VirtualBuffer池化缓冲的包装,带引用计数与释放回调
retainReadBuffer()让读缓冲跨读周期保留(省去反复分配,但每连接常驻内存)

write(ByteBuffer, Runnable releaseCallback) 这个重载值得单独记住:它把缓冲区所有权交给框架,等框架不再引用时回调你的 Runnable,你可以放心复用这块内存——这是零拷贝发送大文件(如文件下载、消息转发)的正确姿势。

ByteBuffer fileChunk = mapFileChunk();
session.writeBuffer().write(fileChunk, () -> {
// 框架已用完,可以回收或复用了
recycle(fileChunk);
});
配置默认值说明
port8888监听端口
threadNum1IO 线程数,生产环境务必调大
readBufferSize512单连接读缓冲字节数
writeChunkSize128写缓冲单块字节数
writeChunkCount16写队列可缓存的块数
backlog1000TCP 半连接队列长度
bannerEnabledtrue启动时是否打印 banner
retainReadBufferfalse是否跨读周期保留读缓冲

这些值只是「能跑」的下限,不是「能扛」的配置。怎么调,见配置详解。