核心概念
This content is not available in your language yet.
smart-socket 的 API 很少,但每个类型都有明确的边界。把这一页的几个概念吃透,后面所有章节都不需要再回来看源码。
一张图看清全貌
Section titled “一张图看清全貌” ┌──────────────────────────────────────────┐ │ AioQuickServer │ 启动 / 配置 / 关闭 └───────────────┬──────────────────────────┘ │ 持有 ┌───────────────────────────┼────────────────────────────┐ │ │ │┌───────▼────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐│ Protocol<T> │ │ MessageProcessor<T>│ │ BufferPagePool ││ 字节 → 消息 │ │ 消息 → 业务逻辑 │ │ 内存池(读+写) │└───────┬────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │ │ │ │ ┌──────▼──────┐ │ │ │ Plugin<T> │ 读写前后 / 接入 / 状态 │ │ └──────┬──────┘ │ │ │ │ └───────────┬───────────────┴────────────────────────────┘ │ 处理 ┌───────▼────────┐ │ AioSession │ 一条连接的全部状态:读缓冲、写缓冲、附件、状态机 └────────────────┘四个核心类型
Section titled “四个核心类型”Protocol<T>:把字节流切成消息
Section titled “Protocol<T>:把字节流切成消息”public interface Protocol<T> { T decode(ByteBuffer readBuffer, AioSession session);}- 输入是框架的读缓冲区,输出是一条业务消息,或者
null(表示还没收完)。 - 它是有状态的调用——同一个
session的decode会被反复调用,直到你返回非null。 - 返回
null时,框架保留未消费的字节,接着读;返回对象后,剩余字节立刻进入下一轮decode,所以一次读取可能连续触发多次process。
MessageProcessor<T>:处理业务消息
Section titled “MessageProcessor<T>:处理业务消息”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_EXCEPTION | process 抛异常 | 保护性兜底 |
INPUT_EXCEPTION / OUTPUT_EXCEPTION | 读写发生 IO 异常 | 网络故障排查 |
INPUT_SHUTDOWN | 对端关闭了写方向(收到 FIN) | 半关闭场景处理 |
REJECT_ACCEPT | 连接被插件拒绝 | 配合黑名单 / 限流统计 |
ACCEPT_EXCEPTION / INTERNAL_EXCEPTION | 接入失败 / 框架内部异常 | 告警 |
Plugin<T>:切入框架的七个时机
Section titled “Plugin<T>:切入框架的七个时机”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 和 PluginAbstractMessageProcessor<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)); // 挂多少个都行,按加入顺序执行
// 方式二:让业务处理器直接实现 Pluginpublic 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 无法挂插件——想要插件,就必须换成上面两种写法之一。
AioSession:一条连接的句柄
Section titled “AioSession:一条连接的句柄”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、登录态),避免自己维护大 Mapsession.setAttachment(userId);String userId = session.getAttachment();
// 3. 拿到对端地址做日志 / 风控InetSocketAddress remote = session.getRemoteAddress();这是最容易踩坑的地方。
业务线程 IO 线程组(threadNum 个) │ │ │ session.writeBuffer().write(...) │ ├───────────────┐ │ │ ▼ │ │ 写队列(chunk 数组) │ │ │ 异步投递 │ │ └──────────────────▶│ select / read │ │ ↓ │ │ decode → process ← 你的业务代码在这里 │ │ ↓ │ │ write → 网卡三条铁律:
threadNum默认是 1。意味着默认只有一条 IO 线程在跑所有连接的decode和process。多核机器上不显式调大,吞吐会被单线程卡死。process在 IO 线程执行,不要阻塞。一次sleep(100ms),这条线程上所有连接都跟着停 100ms。耗时操作请转交自己的业务线程池。- 写操作是线程安全的,读不是。多个线程同时
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);});| 配置 | 默认值 | 说明 |
|---|---|---|
port | 8888 | 监听端口 |
threadNum | 1 | IO 线程数,生产环境务必调大 |
readBufferSize | 512 | 单连接读缓冲字节数 |
writeChunkSize | 128 | 写缓冲单块字节数 |
writeChunkCount | 16 | 写队列可缓存的块数 |
backlog | 1000 | TCP 半连接队列长度 |
bannerEnabled | true | 启动时是否打印 banner |
retainReadBuffer | false | 是否跨读周期保留读缓冲 |
这些值只是「能跑」的下限,不是「能扛」的配置。怎么调,见配置详解。