a2c-smcp 0.2.3

A2C-SMCP Rust SDK - Agent, Computer, and Server implementation
Documentation

# SMCP Socket.IO Server:`smcp-server-core``smcp-server-hyper` 如何配合

本文档介绍当前仓库里,`crates/smcp-server-core` 如何与 `crates/smcp-server-hyper` 组合,实现 **A2C-SMCP 的 Socket.IO Server**。

面向读者:Rust 新手;目标是让你理解“为什么这样分层、数据怎么流动、hyper 为什么能接上 core”。

## 1. 总体分层:core 负责“协议与业务”,hyper 负责“传输与运行”

你可以把整个 Server 拆成两层:

- **`smcp-server-core`(协议/业务层)**
  - 定义并实现 SMCP 的 Socket.IO namespace、事件名、事件处理函数
  - 做认证(`AuthenticationProvider`)、会话管理(`SessionManager`  - 维护共享状态(`ServerState`  - 最关键:构建并导出一个可以被任何 HTTP 框架挂载的 **`tower::Layer`**(见 `SmcpServerLayer`
- **`smcp-server-hyper`(传输适配层 / runtime 层)**
  - 选择 Hyper 作为 HTTP server 实现
  - 接受 TCP 连接、跑 HTTP/1.1、开启 upgrade(WebSocket)
  - 把 core 的 `SocketIoLayer` 挂到 tower service 链上
  - 提供可运行的 `run_server` / `HyperServer::run` 之类入口

这种拆法的直接收益:

- **core 不绑定具体 Web 框架**(hyper/axum/warp/actix 的差异都被隔离)
- **hyper crate 只做 I/O 与 glue code**,不关心 SMCP 事件细节
- 你未来想换运行时/框架时,通常只需要改 adapter(hyper crate),core 基本不动

## 2. 核心连接点:`SmcpServerLayer` = `SocketIo` + `SocketIoLayer` + `ServerState`

关键类型在:`crates/smcp-server-core/src/server.rs`

- `SmcpServerBuilder`
  - 收集配置(认证 provider、会话管理器等)
  - 调用 `build_layer()` 产出 `SmcpServerLayer`

- `SmcpServerLayer`
  - `io: SocketIo`:Socket.IO “服务器对象”(用于注册 namespace/事件,也可用于跨 socket 广播)
  - `layer: SocketIoLayer`:一个 tower layer,会“拦截并处理”与 Socket.IO 相关的 HTTP 请求(例如 `/socket.io/` 握手、upgrade 等)
  - `state: ServerState`:你业务侧需要的共享状态(会话管理、认证、以及一个 `Arc<SocketIo>`
`build_layer()` 里面最重要的 3 步(概念化解释):

1. `let (layer, io) = SocketIo::builder().build_layer();`
   - 这一步来自 `socketioxide`:它同时给你
     - 一个“协议入口”`layer`(放进 HTTP service 链里)
     - 一个“编程 API”`io`(用来注册 namespace、事件回调)

2. 组装 `ServerState``Arc` 共享)
   - `ServerState``crates/smcp-server-core/src/handler.rs`
   - 里面带:`session_manager``auth_provider``io: Arc<SocketIo>`

3. `SmcpHandler::register_handlers(&io, state.clone());`
   - 这里把 SMCP 的 namespace 与事件回调注册到 `SocketIo`   - 这一步完成后,**core 侧就“声明式地”完成了协议行为**

## 3. `SmcpHandler`:把 SMCP 映射成 Socket.IO 的 namespace + event handlers

代码在:`crates/smcp-server-core/src/handler.rs`

### 3.1 namespace 级别的“连接钩子”

`register_handlers()` 会对固定 namespace(例如 `SMCP_NAMESPACE`)做:

- `io.ns(SMCP_NAMESPACE, |socket| async move { ... })`
- 连接建立时会先跑 `on_connect()`  -`socket.req_parts().headers` 拿到请求头
  - 调用 `state.auth_provider.authenticate(&headers, auth_data).await?`

你可以把它理解为:

- **HTTP 握手阶段**(Engine.IO / Socket.IO 内部流程)
- 成功后进入 **Socket.IO 连接态**,才能收发事件

### 3.2 per-socket 的事件注册

连接通过认证后,会对这个 socket 注册一堆事件:

- `socket.on_disconnect(...)`
- `socket.on(smcp::events::SERVER_JOIN_OFFICE, ...)`(带 ack)
- `socket.on(smcp::events::CLIENT_TOOL_CALL, ...)`(带 ack)
- 等等

这里的模式是:

- `socket.on(event, handler)` 把“事件名”绑定到 async handler
- handler 入参通常是 `SocketRef` + `Data<T>` + `AckSender`
- handler 内部调用 `SmcpHandler::on_xxx(...)`,返回结果用 `ack.send(&result)` 发回去

这就是 SMCP 在 Socket.IO 之上的 RPC-ish 通信方式。

## 4. `SessionManager``ServerState`:让 handler “无状态”但系统“有状态”

`SessionManager` 在:`crates/smcp-server-core/src/session.rs`

它使用 `DashMap` 做并发 map,核心目的:

- `sid -> SessionData` 保存连接的会话信息
-`name -> sid` 做反查(并处理“同名冲突”)

`ServerState`(在 `handler.rs`)把以下对象用 `Arc` 包起来:

- `session_manager: Arc<SessionManager>`
- `auth_provider: Arc<dyn AuthenticationProvider>`
- `io: Arc<SocketIo>`

这样每个事件 handler 都可以 `clone()` 一份 `ServerState`,实现:

- handler 函数本身更像“纯函数”(入参 + 共享状态 + 输出)
- 共享状态在内部是线程安全/并发安全的

## 5. `AuthenticationProvider`:依赖倒置点(可插拔认证)

认证抽象在:`crates/smcp-server-core/src/auth.rs`

- `trait AuthenticationProvider`
  - `async fn authenticate(&self, headers: &HeaderMap, auth: Option<&Value>) -> Result<(), AuthError>`

- `DefaultAuthenticationProvider`
  - 默认从 header 里读 `x-api-key`(可配置字段名)

这里体现了一个非常经典的设计:

- **依赖倒置(DIP)**:handler 依赖抽象 `AuthenticationProvider`,而不是依赖某个具体实现
- 你可以在 builder 里注入自定义认证(查 DB、JWT、签名校验等),core 的事件逻辑不用改

## 6. hyper 侧怎么“接入”core:tower service 栈 + `SocketIoLayer`

关键代码在:`crates/smcp-server-hyper/src/lib.rs`

你可以把 hyper crate 理解为:

- 启动 TCP listener
- 对每条 TCP 连接跑 `hyper::server::conn::http1::Builder::new().serve_connection(...).with_upgrades()`
-`tower::ServiceBuilder` 把 core 的 `layer.layer`(即 `SocketIoLayer`)挂上去

核心几行是:

- `let layer = self.layer.ok_or("SMCP layer not configured")?;`
- `let service = ServiceBuilder::new().layer(layer.layer).service(service_fn(...));`

解释一下这个 service 链:

- `service_fn` 你可以理解成“兜底的 HTTP handler”(比如 `/health`- `SocketIoLayer` 是一个 **中间件层**  - 当请求是 Socket.IO/Engine.IO 相关路径(通常是 `/socket.io/` 及其升级流程)时,中间件会接管
  - 否则就把请求交给下游的 `service_fn`

也就是说:

- **Socket.IO 相关请求走 core 提供的 layer**
- **普通 HTTP 路由走 hyper crate 自己的 handler**

这就是“为什么 hyper 能与 core 配合”的本质:**tower Layer 把协议处理做成可组合的中间件**。

## 7. 典型调用链(从 TCP 到你的事件函数)

下面用“文字时序图”描述一次典型流程:

1. 客户端发起 HTTP 请求:`GET /socket.io/?EIO=4&transport=polling...`
2. Hyper 接收到请求,进入 tower service 栈
3. `SocketIoLayer` 匹配到 `/socket.io/`,接管请求并完成 Engine.IO/Socket.IO 的握手逻辑
4. 协议内部在合适时机建立 Socket.IO 连接对象(socket)
5. 因为 core 在启动时已执行过 `SmcpHandler::register_handlers(&io, state)`   - socket 进入 `io.ns(SMCP_NAMESPACE, ...)` 的连接回调
6. `on_connect()`
   - 从请求头提取 API key
   - `AuthenticationProvider::authenticate()`
7. 认证通过后,`handle_connection()` 对该 socket 注册所有事件
8. 客户端 `emit` 一个 SMCP 事件(例如 `CLIENT_TOOL_CALL`9. socketioxide 将 payload 反序列化为 `Data<ToolCallReq>`,调用对应 handler
10. handler 执行业务逻辑、查/改 `SessionManager`,并用 `AckSender` 返回结果

## 8. 这里用到的设计模式(用 Rust 术语重新理解)

- **分层架构(Layered Architecture)**
  - core = 业务/协议层
  - hyper = 传输/运行层

- **适配器模式(Adapter)**
  - `smcp-server-hyper` 把 Hyper 的“连接/请求模型”适配成 tower service,并通过 `SocketIoLayer` 接入协议处理
  - 如果将来你写 `smcp-server-axum`,本质也是相同的 adapter

- **依赖注入 + 依赖倒置(DI + DIP)**
  - `AuthenticationProvider` 用 trait 抽象认证
  - `SmcpServerBuilder::with_auth_provider(...)` 注入实现

- **共享状态 + 并发安全容器**
  - `Arc<T>` 在 async/多任务间共享
  - `DashMap` 处理并发读写

- **事件驱动(Event-driven)**
  - SMCP 的交互以 Socket.IO event 为边界
  - handler = event 的消费者

## 9. 最小启动示例(伪代码,帮助你建立心智模型)

下面是“你想自己起一个 server”时的最小思路(非完整可编译示例,偏概念):

```rust
// 1) core:构建 layer(里面包含 SocketIoLayer + SocketIo + ServerState)
let smcp_layer = smcp_server_core::SmcpServerBuilder::new()
    .with_default_auth(Some("admin-secret".into()), None)
    .build_layer()?;

// 2) hyper adapter:把 layer 挂到 service 栈并运行
smcp_server_hyper::HyperServerBuilder::new()
    .with_layer(smcp_layer)
    .with_addr("127.0.0.1:3000".parse()?)
    .build()
    .run("127.0.0.1:3000".parse()?)
    .await?;
```

如果你只想快速跑起来:`smcp-server-hyper` 里也提供了 `run_server(addr)`,内部会默认构建 `SmcpServerBuilder::new().build_layer()`。

## 10. 你作为新手可以从哪里开始读代码

建议阅读顺序(每个文件都不长):

1. `crates/smcp-server-core/src/server.rs`
   - 先理解 `SmcpServerBuilder::build_layer()` 为什么同时产出 `io``layer`
2. `crates/smcp-server-core/src/handler.rs`
   -`register_handlers()``on_connect()`、以及事件注册模式
3. `crates/smcp-server-core/src/session.rs`
   - 看会话如何注册/注销/查找
4. `crates/smcp-server-hyper/src/lib.rs`
   -`ServiceBuilder::layer(layer.layer)` + `.with_upgrades()`

## 11. 常见疑问

### 11.1 为什么 `handle_request()` 里对 `/socket.io/` 返回 404 也没关系?

因为真正处理 `/socket.io/*` 的不是 `handle_request()`,而是挂在它“上面”的 `SocketIoLayer`。

当 `SocketIoLayer` 匹配到 Socket.IO 请求时,它会提前处理并生成响应,下游根本不会走到你写的 match 分支。

### 11.2 `ServerState` 里为什么要放一个 `Arc<SocketIo>`

某些业务场景需要在一个事件 handler 内,对“其他 socket / 房间”进行广播或定向发送。

通过 `Arc<SocketIo>`,你可以在任意 handler 中拿到同一个 `io` 引用,实现跨连接通信(具体 API 取决于 socketioxide 的接口)。

---

## 状态

- 已在本文档说明 core/hyper 的协作机制与设计模式。
- 如你希望我补一段“从测试用例看真实交互”的讲解,我可以继续基于 `crates/smcp-server-core/tests` 或集成测试补充一节。