docs: 新增 libhttp/libtry/pymsg/README.robot.md 技术手册,更新 BotOps/README.md
This commit is contained in:
1 parent
8a14247aed
commit
e4831522a4
3 files changed
+2226
No files matched your search
@@ -0,0 +1,606 @@
|
|||||||
|
# libhttp — C++20 HTTP / WebSocket 网络库
|
||||||
|
|
||||||
|
## 1. 模块概览
|
||||||
|
|
||||||
|
`libhttp` 是 `micro_servers` 项目的基础网络通信层,封装了 HTTP/HTTPS 服务器、HTTP 客户端(基于 libcurl)、WebSocket 服务器和 WebSocket 客户端四大功能。
|
||||||
|
以 C++20 标准编写,编译为动态库 `libus_http.so` 和静态库 `libus_http_static.a`,命名空间 `pmc::net`。
|
||||||
|
|
||||||
|
| 属性 | 值 |
|
||||||
|
|----------|-----------------------------|
|
||||||
|
| 版本 | 0.0.1 |
|
||||||
|
| 语言标准 | C++20 |
|
||||||
|
| 许可证 | MIT |
|
||||||
|
| 框架名 | `us` |
|
||||||
|
| 库名 | `libus_http` |
|
||||||
|
| 命名空间 | `pmc::net` |
|
||||||
|
|
||||||
|
**项目角色**:`libhttp` 被 `gateway` 模块和 `core` 模块直接依赖,承担所有入站请求的接收、路由、SSL 卸载和 WebSocket 长连接管理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 文件清单
|
||||||
|
|
||||||
|
```
|
||||||
|
libhttp/
|
||||||
|
├── .gitignore # 构建产物忽略规则
|
||||||
|
├── CMakeLists.txt # CMake 构建 (v3.12+)
|
||||||
|
├── LICENSE # MIT License
|
||||||
|
├── Makefile # GNU Make 构建
|
||||||
|
├── _set.sh # 一键编译安装脚本
|
||||||
|
├── inc/us/
|
||||||
|
│ ├── HttpClient.hpp # HTTP 客户端 (header-only)
|
||||||
|
│ ├── HttpServer.hpp # HTTP/HTTPS 服务器声明
|
||||||
|
│ ├── WebSocketClient.hpp # WebSocket 客户端声明
|
||||||
|
│ ├── WebSocketClientBase.hpp # WebSocket 客户端抽象基类
|
||||||
|
│ └── WebSocketServer.hpp # WebSocket 服务器声明
|
||||||
|
└── src/
|
||||||
|
├── HttpServer.cpp # HTTP/HTTPS 服务器实现
|
||||||
|
├── WebSocketClient.cpp # WebSocket 客户端实现
|
||||||
|
└── WebSocketServer.cpp # WebSocket 服务器实现
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 外部依赖
|
||||||
|
|
||||||
|
| 依赖 | 用途 | 备注 |
|
||||||
|
|------------------|-----------------------------------|---------------------------|
|
||||||
|
| Boost 1.91+ | Asio (I/O)、Beast (HTTP/WS)、Algorithms | 默认 `BOOST_ROOT=C:/local/boost_1_91_0`,可通过环境变量覆盖 |
|
||||||
|
| OpenSSL | TLS/SSL 加密传输 | 通过 Boost.Asio.SSL 调用 |
|
||||||
|
| libcurl | HTTP 客户端请求 | 仅 `HttpClient.hpp` 依赖 |
|
||||||
|
| pthread (Unix) | 多线程 | CMake 自动链接 |
|
||||||
|
| ws2_32 (Windows) | Winsock | CMake 自动链接 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 构建
|
||||||
|
|
||||||
|
### 4.1 GNU Make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译 (仅 HttpServer.o → libus_http.so)
|
||||||
|
make
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
make clear
|
||||||
|
|
||||||
|
# 安装到 /lib 和 /usr/include/us/
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 卸载
|
||||||
|
make uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:Makefile 第 17 行默认只链接 `HttpServer.o`。如需 WebSocket 支持,需取消注释 `WebSocketServer.o` 和 `WebSocketClient.o`。
|
||||||
|
|
||||||
|
### 4.2 CMake
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir build && cd build
|
||||||
|
cmake .. -DBOOST_ROOT=/path/to/boost
|
||||||
|
cmake --build .
|
||||||
|
cmake --install . --prefix /usr/local
|
||||||
|
```
|
||||||
|
|
||||||
|
CMake 会同时产出 `libus_http.so` (动态) 和 `libus_http_static.a` (静态)。
|
||||||
|
|
||||||
|
### 4.3 一键脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash _set.sh # make clear → make → make install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API 参考
|
||||||
|
|
||||||
|
### 5.1 HttpServer — HTTP/HTTPS 服务器
|
||||||
|
|
||||||
|
**头文件**: `<us/HttpServer.hpp>`
|
||||||
|
**实现**: `src/HttpServer.cpp`
|
||||||
|
|
||||||
|
#### 5.1.1 类型别名
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using HttpRequestHandler = std::function<http::response<http::string_body>(
|
||||||
|
const http::request<http::string_body>& req,
|
||||||
|
const std::unordered_map<std::string, std::string>& params)>;
|
||||||
|
|
||||||
|
using MiddlewareHandler = std::function<bool(
|
||||||
|
const http::request<http::string_body>& req,
|
||||||
|
http::response<http::string_body>& res,
|
||||||
|
const std::unordered_map<std::string, std::string>& params)>;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`HttpRequestHandler`**: 路由处理函数,接收 Boost.Beast 请求对象和已解析的查询参数,返回 HTTP 响应。
|
||||||
|
- **`MiddlewareHandler`**: 中间件函数,返回 `false` 表示拦截(短路后续处理),`true` 表示放行。
|
||||||
|
|
||||||
|
#### 5.1.2 构造函数
|
||||||
|
|
||||||
|
| 签名 | 说明 |
|
||||||
|
|----------------------------------------------------------------------------------------|------------------------|
|
||||||
|
| `HttpServer(address, port, threads = 1)` | 纯 HTTP 模式 |
|
||||||
|
| `HttpServer(address, port, cert_file, key_file, threads = 1)` | HTTPS 模式(自动启用 SSL) |
|
||||||
|
|
||||||
|
- `address`: 监听地址,如 `"0.0.0.0"`
|
||||||
|
- `port`: 监听端口
|
||||||
|
- `threads`: 工作线程数(即 `io_context` 线程池大小)
|
||||||
|
- `cert_file` / `key_file`: PEM 格式证书和私钥路径
|
||||||
|
|
||||||
|
#### 5.1.3 路由注册
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void get(const std::string& path, HttpRequestHandler handler);
|
||||||
|
void post(const std::string& path, HttpRequestHandler handler);
|
||||||
|
void put(const std::string& path, HttpRequestHandler handler);
|
||||||
|
void del(const std::string& path, HttpRequestHandler handler);
|
||||||
|
void options(const std::string& path, HttpRequestHandler handler);
|
||||||
|
void use(MiddlewareHandler middleware);
|
||||||
|
```
|
||||||
|
|
||||||
|
- 路径支持通配符:`*` (任意字符) 和 `?` (单个字符),自动编译为 `std::regex`(大小写不敏感)。
|
||||||
|
- 匹配优先级:精确匹配 > 通配符匹配。
|
||||||
|
- 请求方法字符串对应:`"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, `"OPTIONS"`。
|
||||||
|
|
||||||
|
#### 5.1.4 中间件
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void use(MiddlewareHandler middleware);
|
||||||
|
```
|
||||||
|
|
||||||
|
中间件在路由匹配之前执行,按注册顺序依次调用。任意中间件返回 `false` 时,请求处理结束,响应直接返回客户端。
|
||||||
|
|
||||||
|
#### 5.1.5 静态文件服务
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void setStaticDirectory(const std::string& path);
|
||||||
|
```
|
||||||
|
|
||||||
|
设置静态资源根目录。当请求路径未匹配任何路由时,自动尝试从该目录读取文件。支持 MIME 类型自动识别:`.html`, `.css`, `.js`, `.json`, `.png`, `.jpg/.jpeg`。
|
||||||
|
|
||||||
|
#### 5.1.6 OPTIONS 预检请求
|
||||||
|
|
||||||
|
当收到 `OPTIONS` 请求时:
|
||||||
|
1. 优先查找自定义 `OPTIONS` 路由处理器
|
||||||
|
2. 若未注册自定义处理器且请求包含 `Origin` 头,自动生成 CORS 响应
|
||||||
|
- 自动检测该路径下已注册的方法(GET/POST/PUT/DELETE/OPTIONS)
|
||||||
|
- 返回 `Access-Control-Allow-Origin`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`, `Access-Control-Max-Age`
|
||||||
|
|
||||||
|
#### 5.1.7 SSL 配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void setPrivateKeyPassword(const std::string& password);
|
||||||
|
```
|
||||||
|
|
||||||
|
- SSL 上下文使用 TLS 协议,禁用 SSLv2/SSLv3
|
||||||
|
- 仅允许 ECDHE/DHE 密钥交换的强密码套件:`ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS`
|
||||||
|
- 支持 PEM 加密私钥密码
|
||||||
|
|
||||||
|
#### 5.1.8 生命周期
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void start(); // 启动服务器(非阻塞,线程池跑 io_context)
|
||||||
|
void stop(); // 停止服务器(关闭所有会话,join 线程)
|
||||||
|
void run(); // 启动并在当前线程运行 io_context(阻塞)
|
||||||
|
bool isRunning() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.1.9 调试
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void showStatus() const; // 打印服务器状态和已注册路由
|
||||||
|
```
|
||||||
|
|
||||||
|
内嵌类 `HttpServer::debug` 提供 `print_all_header_fields()` 静态方法,打印请求的全部头字段。
|
||||||
|
|
||||||
|
#### 5.1.10 内部架构
|
||||||
|
|
||||||
|
```
|
||||||
|
HttpServer
|
||||||
|
├── Listener (内部类)
|
||||||
|
│ └── acceptor_ (tcp::acceptor)
|
||||||
|
│ └── async_accept → Session 实例
|
||||||
|
└── Session (内部类,enable_shared_from_this)
|
||||||
|
├── HTTP: socket_ (tcp::socket)
|
||||||
|
├── HTTPS: ssl_stream_ (asio::ssl::stream<tcp::socket>)
|
||||||
|
├── strand_ 保证线程安全
|
||||||
|
└── 读取 → handleRequest() → 写入 → [keep-alive 则继续读取]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.2 HttpClient — HTTP 客户端
|
||||||
|
|
||||||
|
**头文件**: `<us/HttpClient.hpp>` (header-only,无独立 .cpp)
|
||||||
|
**依赖**: libcurl
|
||||||
|
|
||||||
|
#### 5.2.1 枚举 `HttpMethod`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class HttpMethod { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS };
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.2.2 公共方法
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
HttpClient(); // 初始化 CURL 句柄,失败抛 runtime_error
|
||||||
|
~HttpClient(); // 清理 CURL 和 header 链表
|
||||||
|
|
||||||
|
void set_header(const std::string& header);
|
||||||
|
void clear_headers();
|
||||||
|
|
||||||
|
const std::map<std::string, std::string>& get_response_headers() const;
|
||||||
|
long get_response_code() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
流式请求(通过回调函数接收响应体,保留原始字节):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void request_stream_sync(HttpMethod method, const std::string& url,
|
||||||
|
const std::string& data,
|
||||||
|
std::function<void(const std::string&)> callback);
|
||||||
|
|
||||||
|
// 便捷方法
|
||||||
|
void get_stream_sync(const std::string& url, std::function<void(const std::string&)> callback);
|
||||||
|
void post_stream_sync(const std::string& url, const std::string& data,
|
||||||
|
std::function<void(const std::string&)> callback);
|
||||||
|
void put_stream_sync(const std::string& url, const std::string& data,
|
||||||
|
std::function<void(const std::string&)> callback);
|
||||||
|
void delete_stream_sync(const std::string& url, std::function<void(const std::string&)> callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 同步阻塞调用
|
||||||
|
- 响应体通过 `CURLOPT_WRITEFUNCTION` 直接追加到 `std::string`,**保留所有换行符和特殊字符**
|
||||||
|
- 超时设置:连接超时 10 秒,总超时 30 秒
|
||||||
|
- 每次请求调用 `curl_easy_reset` 重置状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.3 WebSocketServer — WebSocket 服务器
|
||||||
|
|
||||||
|
**头文件**: `<us/WebSocketServer.hpp>`
|
||||||
|
**实现**: `src/WebSocketServer.cpp`
|
||||||
|
|
||||||
|
#### 5.3.1 回调类型
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using WebSocketMessageHandler = std::function<void(
|
||||||
|
const std::string& clientId,
|
||||||
|
const std::string& message,
|
||||||
|
std::function<void(const std::string&)> sendCallback)>;
|
||||||
|
|
||||||
|
using WebSocketConnectHandler = std::function<void(
|
||||||
|
const std::string& clientId, const std::string& remoteAddress)>;
|
||||||
|
|
||||||
|
using WebSocketDisconnectHandler = std::function<void(
|
||||||
|
const std::string& clientId)>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.3.2 公共接口
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
WebSocketServer(unsigned short port, unsigned int threads = 2);
|
||||||
|
~WebSocketServer();
|
||||||
|
|
||||||
|
void start();
|
||||||
|
void stop();
|
||||||
|
bool isRunning() const;
|
||||||
|
unsigned short getPort() const;
|
||||||
|
|
||||||
|
// 事件注册
|
||||||
|
void onMessage(const std::string& path, WebSocketMessageHandler handler);
|
||||||
|
void onConnect(WebSocketConnectHandler handler);
|
||||||
|
void onDisconnect(WebSocketDisconnectHandler handler);
|
||||||
|
|
||||||
|
// 消息/客户端管理
|
||||||
|
bool sendToClient(const std::string& clientId, const std::string& message);
|
||||||
|
size_t broadcast(const std::string& message);
|
||||||
|
void disconnectClient(const std::string& clientId);
|
||||||
|
size_t getClientCount() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- PIMPL 模式:内部实现封装于 `Impl` 结构体
|
||||||
|
- 每个 `Session` 包含一个发送队列 (`std::deque<std::string>`),保证消息顺序
|
||||||
|
- 客户端 ID 由全局原子计数器自动生成,格式 `"client_N"`
|
||||||
|
- 线程安全:客户端列表受 `clientsMutex_` 保护
|
||||||
|
- 消息处理器支持路径路由(按 `path` 键索引)
|
||||||
|
|
||||||
|
**内部架构**:
|
||||||
|
|
||||||
|
```
|
||||||
|
WebSocketServer
|
||||||
|
├── Impl
|
||||||
|
│ ├── io_context
|
||||||
|
│ ├── Listener (内部类) → tcp::acceptor
|
||||||
|
│ └── threads_
|
||||||
|
└── Session (内部类,enable_shared_from_this)
|
||||||
|
├── websocket::stream<beast::tcp_stream>
|
||||||
|
├── sendQueue_ (std::deque)
|
||||||
|
└── 读取循环 → onMessage 回调 → sendCallback 写回
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5.4 WebSocketClient — WebSocket 客户端
|
||||||
|
|
||||||
|
**头文件**: `<us/WebSocketClient.hpp>`, `<us/WebSocketClientBase.hpp>`
|
||||||
|
**实现**: `src/WebSocketClient.cpp`
|
||||||
|
|
||||||
|
#### 5.4.1 抽象基类 `WebSocketClientBase`
|
||||||
|
|
||||||
|
定义统一接口:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using MessageCallback = std::function<void(const std::string&)>;
|
||||||
|
using ErrorCallback = std::function<void(const std::string&)>;
|
||||||
|
using ConnectCallback = std::function<void(bool)>;
|
||||||
|
using CloseCallback = std::function<void()>;
|
||||||
|
|
||||||
|
// 回调设置
|
||||||
|
virtual void setMessageCallback(MessageCallback) = 0;
|
||||||
|
virtual void setErrorCallback(ErrorCallback) = 0;
|
||||||
|
virtual void setConnectCallback(ConnectCallback) = 0;
|
||||||
|
virtual void setCloseCallback(CloseCallback) = 0;
|
||||||
|
|
||||||
|
// 连接管理
|
||||||
|
virtual bool connect(int timeout_ms = 5000) = 0;
|
||||||
|
virtual void disconnect() = 0;
|
||||||
|
virtual bool isConnected() const = 0;
|
||||||
|
|
||||||
|
// 消息发送
|
||||||
|
virtual bool send(const std::string& message) = 0;
|
||||||
|
virtual bool sendBinary(const void* data, size_t size) = 0;
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
virtual void setAutoReconnect(bool enable, int interval_ms = 3000) = 0;
|
||||||
|
virtual void setHeartbeatInterval(int interval_ms = 30000) = 0;
|
||||||
|
|
||||||
|
// SSL 相关(纯虚函数,非 SSL 版本忽略参数)
|
||||||
|
virtual bool isSecure() const = 0;
|
||||||
|
virtual void setVerifyCertificate(bool verify) = 0;
|
||||||
|
virtual void setCertificateFile(const std::string& cert_file) = 0;
|
||||||
|
virtual void setPrivateKeyFile(const std::string& key_file) = 0;
|
||||||
|
virtual void setCertificateAuthorityFile(const std::string& ca_file) = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5.4.2 公共接口
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
WebSocketClient(const std::string& host, const std::string& port,
|
||||||
|
const std::string& path = "/");
|
||||||
|
~WebSocketClient();
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- PIMPL 模式:`Impl` 结构体封装所有 Boost 对象和状态
|
||||||
|
- 构造时自动启动 IO 线程
|
||||||
|
- 异步连接流程:`resolve → connect → WebSocket handshake`
|
||||||
|
- 发送队列受 `std::mutex` 和 `std::condition_variable` 保护
|
||||||
|
- 自动重连:`onError` 或 `onClose` 中若 `auto_reconnect=true` 则自动发起重连
|
||||||
|
- 非 SSL 版本:`isSecure()` 返回 `false`,SSL 配置方法为空实现
|
||||||
|
- `sendBinary` 将二进制数据包装为 `std::string` 后调用 `send`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 使用示例
|
||||||
|
|
||||||
|
### 6.1 基本 HTTP 服务器
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <us/HttpServer.hpp>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pmc::net::HttpServer server("0.0.0.0", 8080, 4);
|
||||||
|
|
||||||
|
server.get("/hello", [](const auto& req, const auto& params) {
|
||||||
|
http::response<http::string_body> res{http::status::ok, req.version()};
|
||||||
|
res.set(http::field::content_type, "text/plain");
|
||||||
|
res.body() = "Hello, world!";
|
||||||
|
res.prepare_payload();
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.post("/echo", [](const auto& req, const auto& params) {
|
||||||
|
http::response<http::string_body> res{http::status::ok, req.version()};
|
||||||
|
res.set(http::field::content_type, "application/json");
|
||||||
|
res.body() = req.body();
|
||||||
|
res.prepare_payload();
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.start(); // 非阻塞启动
|
||||||
|
std::cout << "Server running on port " << server.getPort() << std::endl;
|
||||||
|
server.isRunning(); // 阻塞直到手动 stop()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 HTTPS 服务器
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
pmc::net::HttpServer server("0.0.0.0", 443,
|
||||||
|
"/etc/ssl/certs/server.crt",
|
||||||
|
"/etc/ssl/private/server.key",
|
||||||
|
4);
|
||||||
|
server.setPrivateKeyPassword("mysecret");
|
||||||
|
server.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 中间件 + 静态文件
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
pmc::net::HttpServer server("0.0.0.0", 8080);
|
||||||
|
|
||||||
|
// 日志中间件
|
||||||
|
server.use([](const auto& req, auto&, const auto&) {
|
||||||
|
std::cout << "[" << req.method_string() << "] " << req.target() << std::endl;
|
||||||
|
return true; // 继续处理
|
||||||
|
});
|
||||||
|
|
||||||
|
// 认证中间件
|
||||||
|
server.use([](const auto& req, auto& res, const auto&) {
|
||||||
|
auto auth = req.find(http::field::authorization);
|
||||||
|
if (auth == req.end()) {
|
||||||
|
res.result(http::status::unauthorized);
|
||||||
|
res.body() = "Unauthorized";
|
||||||
|
res.prepare_payload();
|
||||||
|
return false; // 拦截
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setStaticDirectory("/var/www/html");
|
||||||
|
server.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 HTTP 客户端
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
pmc::net::HttpClient client;
|
||||||
|
|
||||||
|
client.set_header("Authorization: Bearer token123");
|
||||||
|
client.set_header("Content-Type: application/json");
|
||||||
|
|
||||||
|
client.post_stream_sync("https://api.example.com/data",
|
||||||
|
R"({"key":"value"})",
|
||||||
|
[](const std::string& body) {
|
||||||
|
std::cout << "Response: " << body << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
long status = client.get_response_code();
|
||||||
|
auto& headers = client.get_response_headers();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 WebSocket 服务器
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
pmc::net::WebSocketServer ws_server(9000, 2);
|
||||||
|
|
||||||
|
ws_server.onConnect([](const std::string& clientId, const std::string& addr) {
|
||||||
|
std::cout << "Client connected: " << clientId << " from " << addr << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws_server.onMessage("/chat", [&ws_server](const std::string& clientId,
|
||||||
|
const std::string& message,
|
||||||
|
auto sendCallback) {
|
||||||
|
std::cout << "[" << clientId << "]: " << message << std::endl;
|
||||||
|
ws_server.broadcast(clientId + ": " + message);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws_server.onDisconnect([](const std::string& clientId) {
|
||||||
|
std::cout << "Client disconnected: " << clientId << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws_server.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.6 WebSocket 客户端
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
pmc::net::WebSocketClient ws_client("localhost", "9000", "/chat");
|
||||||
|
|
||||||
|
ws_client.setConnectCallback([](bool success) {
|
||||||
|
std::cout << (success ? "Connected!" : "Failed!") << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws_client.setMessageCallback([](const std::string& msg) {
|
||||||
|
std::cout << "Received: " << msg << std::endl;
|
||||||
|
});
|
||||||
|
|
||||||
|
ws_client.setAutoReconnect(true, 3000);
|
||||||
|
ws_client.connect();
|
||||||
|
|
||||||
|
// 阻塞主线程保持连接
|
||||||
|
while (ws_client.isConnected()) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 请求处理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端 → TCP 连接
|
||||||
|
├─ HTTP: Session::doRead()
|
||||||
|
└─ HTTPS: Session::doSSLHandshake() → Session::doRead()
|
||||||
|
↓
|
||||||
|
HttpServer::handleRequest(req)
|
||||||
|
├─ 解析 target_path + query params
|
||||||
|
├─ 执行中间件链 (MiddlewareHandler[])
|
||||||
|
│ └─ 任意中间件返回 false → 短路返回
|
||||||
|
├─ OPTIONS 请求?
|
||||||
|
│ ├─ 有自定义处理器 → 执行
|
||||||
|
│ └─ 有 Origin → 自动生成 CORS 响应
|
||||||
|
├─ 路由匹配 (精确匹配 → 通配符匹配)
|
||||||
|
│ └─ 命中 → 执行 HttpRequestHandler
|
||||||
|
├─ 路由未命中 + static_directory_ 非空
|
||||||
|
│ └─ 尝试读取静态文件 → 自动设 Content-Type
|
||||||
|
└─ 全部未命中 → 404
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 设计要点
|
||||||
|
|
||||||
|
### 8.1 线程模型
|
||||||
|
|
||||||
|
- **HttpServer**: 每个工作线程独立运行 `io_context::run()`,所有异步操作通过 `strand` 序列化,同一 Session 的读写不会并发冲突
|
||||||
|
- **WebSocketServer**: 同 HttpServer,工作线程池跑 `io_context`
|
||||||
|
- **WebSocketClient**: 单 IO 线程,所有异步回调在该线程中执行
|
||||||
|
- **HttpClient**: 同步阻塞,无额外线程
|
||||||
|
|
||||||
|
### 8.2 PIMPL 模式
|
||||||
|
|
||||||
|
`WebSocketClient` 和 `WebSocketServer` 使用 PIMPL 隐藏 Boost 依赖,保持头文件轻量。
|
||||||
|
|
||||||
|
### 8.3 会话管理
|
||||||
|
|
||||||
|
- `HttpServer` 通过 `weak_ptr<Session>` 跟踪所有活跃会话,`stop()` 时遍历清理
|
||||||
|
- `WebSocketServer` 通过 `unordered_map<string, shared_ptr<Session>>` + mutex 管理
|
||||||
|
|
||||||
|
### 8.4 发送队列
|
||||||
|
|
||||||
|
`WebSocketServer::Session` 和 `WebSocketClient::Impl` 均采用 `std::deque` 消息队列,确保消息按序发送且不会因并发写入冲突。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 安全特性
|
||||||
|
|
||||||
|
| 特性 | 实现位置 |
|
||||||
|
|---------------------------|------------------------------|
|
||||||
|
| SSL/TLS 加密 | HttpServer SSL 构造函数 |
|
||||||
|
| 禁止 SSLv2/SSLv3 | `initSSLContext()` |
|
||||||
|
| 仅强密码套件 (ECDHE/DHE) | `initSSLContext()` 密码列表 |
|
||||||
|
| 禁用 NULL/MD5/DSS | `initSSLContext()` 密码列表 |
|
||||||
|
| 加密私钥密码支持 | `setPrivateKeyPassword()` |
|
||||||
|
| CORS 自动处理 | `handleRequest()` OPTIONS 分支 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 与其他模块的关系
|
||||||
|
|
||||||
|
```
|
||||||
|
micro_servers/
|
||||||
|
├── gateway/ ──依赖──→ libhttp (HttpServer 接收外部请求)
|
||||||
|
├── core/ ──依赖──→ libhttp (HTTP 服务基础能力)
|
||||||
|
└── libhttp/ ──依赖──→ Boost, OpenSSL, libcurl
|
||||||
|
```
|
||||||
|
|
||||||
|
- **gateway** 使用 `pmc::net::HttpServer` 作为 API 网关入口,处理 JWT 认证、令牌刷新、请求转发
|
||||||
|
- **core** 模块使用 libhttp 提供的 HTTP 服务能力
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 已知限制
|
||||||
|
|
||||||
|
1. **Makefile 默认不编译 WebSocket**:第 17 行仅链接 `HttpServer.o`,需手动启用
|
||||||
|
2. **WebSocketClient 无 SSL 实现**:当前仅有非加密版本;SSL 接口已声明但为空实现
|
||||||
|
3. **无 HTTP/2 支持**:仅 HTTP/1.1
|
||||||
|
4. **静态文件 MIME 有限**:仅支持 6 种常见类型
|
||||||
|
5. **无请求体大小限制**:依赖 Boost.Beast 默认配置
|
||||||
|
6. **HttpClient 仅同步**:无异步 HTTP 客户端实现
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,856 @@
|
|||||||
|
# libtry 模块技术手册
|
||||||
|
|
||||||
|
> 文档版本:2026-07-30
|
||||||
|
> 代码版本:try.c(~741 行)
|
||||||
|
> 语言:C(嵌入式解释器)
|
||||||
|
> 定位:教学级脚本解释器,用于操作界面 shell
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、模块概述
|
||||||
|
|
||||||
|
### 1.1 定位
|
||||||
|
|
||||||
|
**libtry** 是一个基于 C 语言手写的**教学级解释器**,魔改自 GitHub 上的 tryC 项目。用于解析 `.try` 脚本文件,设计目标是作为**操作界面的 shell**。
|
||||||
|
|
||||||
|
### 1.2 特点
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 手写解释器 | 仅 741 行实现完整的词法/语法/执行引擎 |
|
||||||
|
| 零依赖 | 仅依赖标准 C 库 + `console.h` 输出接口 |
|
||||||
|
| 宽字符支持 | 使用 `wchar_t`,支持中文/多语言脚本 |
|
||||||
|
| 递归下降 | 递归下降语法分析器,边解析边执行 |
|
||||||
|
| 嵌入式设计 | 提供 `init_tryc`/`enqueue_tryc`/`eval_tryc`/`free_tryc` 四个核心 API |
|
||||||
|
|
||||||
|
### 1.3 与微服务体系的关系
|
||||||
|
|
||||||
|
| 属性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 模块关系 | **独立底层库**,不直接与任何微服务交互 |
|
||||||
|
| 可能用途 | 作为前端 UI 的脚本解释器,或嵌入式脚本引擎 |
|
||||||
|
| 启动方式 | 被其他模块(可能是 gateway 或 static)动态加载调用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、技术架构
|
||||||
|
|
||||||
|
### 2.1 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 上层应用 │
|
||||||
|
│ (gateway / static / 其他模块) │
|
||||||
|
└─────────────────────────┬───────────────────────────────────┘
|
||||||
|
│ 调用 API
|
||||||
|
┌─────────────────────────▼───────────────────────────────────┐
|
||||||
|
│ libtry 解释器 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 词法分析器 (next) │ │
|
||||||
|
│ │ 逐字符扫描 → 识别 token → 更新符号表 │ │
|
||||||
|
│ └──────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ token 流 │
|
||||||
|
│ ┌──────────────────────────▼──────────────────────────────┐ │
|
||||||
|
│ │ 递归下降语法分析器 │ │
|
||||||
|
│ │ statement() / expression() / factor() / term() │ │
|
||||||
|
│ │ 边解析边执行(解释器模式) │ │
|
||||||
|
│ └──────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────▼──────────────────────────────┐ │
|
||||||
|
│ │ 符号表 │ │
|
||||||
|
│ │ symtab[128] — 符号栈(变量/函数/数组) │ │
|
||||||
|
│ │ levelNum — 嵌套层级,支持作用域 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────▼───────────────────────────────────┐
|
||||||
|
│ 执行结果输出 │
|
||||||
|
│ printtovga() / 控制台输出 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 执行流程
|
||||||
|
|
||||||
|
```
|
||||||
|
init_tryc() 初始化
|
||||||
|
│
|
||||||
|
├── memset(symtab, 0, ...) 清零符号表
|
||||||
|
├── 预注册关键字 (array, func, else, if, return, while, print, puts, read)
|
||||||
|
└── malloc(128) 分配源代码缓冲区
|
||||||
|
│
|
||||||
|
enqueue_tryc(wc) 追加字符(交互式输入)
|
||||||
|
│
|
||||||
|
└── src[top_src] = wc; src[++top_src] = L'\0';
|
||||||
|
│
|
||||||
|
eval_tryc() 执行
|
||||||
|
│
|
||||||
|
├── next() 获取第一个 token
|
||||||
|
├── while (token != L'\0') { statement(); } 逐条执行
|
||||||
|
└── 重置缓冲区
|
||||||
|
│
|
||||||
|
free_tryc() 释放资源
|
||||||
|
│
|
||||||
|
└── free(p_src);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 词法分析流程
|
||||||
|
|
||||||
|
```
|
||||||
|
next() 函数:
|
||||||
|
│
|
||||||
|
├── 跳过空白符 (L' ', L'\t')
|
||||||
|
├── 跳过注释 (# 开头到换行)
|
||||||
|
├── 识别标识符 (字母/数字/下划线/中文)
|
||||||
|
│ ├── 在符号表中搜索
|
||||||
|
│ │ ├── 找到:根据 type 返回对应 token (Sym, FuncSym, ArraySym, Str)
|
||||||
|
│ │ └── 未找到:创建新符号,类型为 Void
|
||||||
|
├── 识别数字 (整数 + 小数)
|
||||||
|
├── 识别字符字面量 ('a')
|
||||||
|
├── 识别字符串字面量 ("hello")
|
||||||
|
├── 识别运算符 (=, ==, !=, <, >, <=, >=, ||, &&)
|
||||||
|
└── 识别标点符号 (;, ,, +, -, *, /, (, ), {, }, [, ])
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、代码结构
|
||||||
|
|
||||||
|
### 3.1 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
libtry/
|
||||||
|
├── try.c # 核心解释器实现(~741 行)
|
||||||
|
├── t.try # 示例脚本(斐波那契数列)
|
||||||
|
├── test/ # 测试目录
|
||||||
|
│ ├── t_assign_basic.try # 赋值测试
|
||||||
|
│ ├── t_function_basic.try # 函数测试
|
||||||
|
│ └── t_output_basic.try # 输出测试
|
||||||
|
├── .gitignore
|
||||||
|
└── README.robot.md # 本文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 try.c 函数索引
|
||||||
|
|
||||||
|
| 函数 | 行号 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `init_tryc()` | L650 | 初始化解释器,预注册关键字 |
|
||||||
|
| `free_tryc()` | L672 | 释放源代码缓冲区 |
|
||||||
|
| `enqueue_tryc(wc)` | L677 | 追加一个字符到缓冲区 |
|
||||||
|
| `eval_tryc()` | L687 | 解析并执行当前缓冲区脚本 |
|
||||||
|
| `next()` | L85 | 词法分析,获取下一个 token |
|
||||||
|
| `match(tk)` | L289 | 匹配并消费期望 token |
|
||||||
|
| `statement()` | L466 | 解析并执行语句 |
|
||||||
|
| `expression()` | L362 | 解析加减法表达式 |
|
||||||
|
| `term()` | L311 | 解析乘除法表达式 |
|
||||||
|
| `factor()` | L326 | 解析因子(变量/常量/函数调用/数组) |
|
||||||
|
| `function()` | L616 | 执行函数调用 |
|
||||||
|
| `boolexp()` | L378 | 解析布尔表达式 |
|
||||||
|
| `boolAND()` | L423 | 解析 AND 表达式(短路) |
|
||||||
|
| `boolOR()` | L438 | 解析 OR 表达式(短路) |
|
||||||
|
| `clear(level)` | L269 | 清理指定层级的符号 |
|
||||||
|
| `clearThis()` | L280 | 清理单个符号的动态内存 |
|
||||||
|
| `skipStatments()` | L453 | 跳过 if/else 未执行分支 |
|
||||||
|
| `skipBoolExpr()` | L413 | 跳过短路布尔表达式 |
|
||||||
|
| `__exit()` | L702 | 错误处理(死循环挂起) |
|
||||||
|
| `main()` | L710 | 文件执行模式(仅 `_DEBUG` 编译) |
|
||||||
|
|
||||||
|
### 3.3 核心数据结构
|
||||||
|
|
||||||
|
#### 符号表(symStruct)
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct symStruct {
|
||||||
|
int type; // 类型:Num, Char, Str, Array, Func
|
||||||
|
wchar_t name[16]; // 符号名(MAXNAMESIZE)
|
||||||
|
double value; // 值或数组/字符串长度
|
||||||
|
union {
|
||||||
|
wchar_t *funcp; // 函数指针或字符串内容
|
||||||
|
struct symStruct* list; // 数组元素指针
|
||||||
|
} pointer;
|
||||||
|
int levelNum; // 嵌套层级(作用域)
|
||||||
|
} symbol;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Token 类型枚举
|
||||||
|
|
||||||
|
```c
|
||||||
|
enum {
|
||||||
|
Num = 128, // 数字
|
||||||
|
Char, // 字符字面量
|
||||||
|
Str, // 字符串
|
||||||
|
Array, // array 关键字
|
||||||
|
Func, // func 关键字
|
||||||
|
Else, // else 关键字
|
||||||
|
If, // if 关键字
|
||||||
|
Return, // return 关键字
|
||||||
|
While, // while 关键字
|
||||||
|
Print, // print 关键字
|
||||||
|
Puts, // puts 关键字
|
||||||
|
Read, // read 关键字
|
||||||
|
Assign, // = 赋值
|
||||||
|
OR, // ||
|
||||||
|
AND, // &&
|
||||||
|
Equal, // ==
|
||||||
|
Sym, // 变量符号
|
||||||
|
FuncSym, // 函数符号
|
||||||
|
ArraySym, // 数组符号
|
||||||
|
Void, // 未初始化
|
||||||
|
Nequal, // !=
|
||||||
|
LessEqual, // <=
|
||||||
|
GreatEqual // >=
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 全局变量
|
||||||
|
|
||||||
|
| 变量 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `symtab[128]` | `symbol[]` | 符号表栈(SYMTABSIZE) |
|
||||||
|
| `symPointer` | `int` | 符号表栈指针 |
|
||||||
|
| `currentlevel` | `int` | 当前嵌套层级 |
|
||||||
|
| `src` | `wchar_t*` | 当前扫描位置 |
|
||||||
|
| `old_src` | `wchar_t*` | 行首位置(调试用) |
|
||||||
|
| `p_src` | `wchar_t*` | 源代码缓冲区起始 |
|
||||||
|
| `top_src` | `int` | 缓冲区写入位置 |
|
||||||
|
| `token` | `int` | 当前 token 类型 |
|
||||||
|
| `token_val` | `union tokenValue` | 当前 token 值 |
|
||||||
|
| `return_val` | `double` | 函数返回值 |
|
||||||
|
| `compileState` | `int` | 编译状态(debug/run) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、API 接口
|
||||||
|
|
||||||
|
### 4.1 初始化与销毁
|
||||||
|
|
||||||
|
#### init_tryc()
|
||||||
|
|
||||||
|
```c
|
||||||
|
int init_tryc(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:初始化解释器
|
||||||
|
- 清零符号表
|
||||||
|
- 预注册 8 个关键字(array, func, else, if, return, while, print, puts, read)
|
||||||
|
- 分配 128 字节宽字符缓冲区
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
- `0`:成功
|
||||||
|
- `-1`:内存分配失败
|
||||||
|
|
||||||
|
#### free_tryc()
|
||||||
|
|
||||||
|
```c
|
||||||
|
void free_tryc(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:释放源代码缓冲区
|
||||||
|
|
||||||
|
### 4.2 输入与执行
|
||||||
|
|
||||||
|
#### enqueue_tryc(wc)
|
||||||
|
|
||||||
|
```c
|
||||||
|
int enqueue_tryc(wchar_t wc);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:追加一个字符到源代码缓冲区(用于交互式输入)
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `wc`:要追加的宽字符
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
- `0`:成功
|
||||||
|
- `-1`:缓冲区已满
|
||||||
|
|
||||||
|
#### eval_tryc()
|
||||||
|
|
||||||
|
```c
|
||||||
|
void eval_tryc(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:解析并执行当前缓冲区中的脚本
|
||||||
|
|
||||||
|
**说明**:执行完成后会重置缓冲区。
|
||||||
|
|
||||||
|
### 4.3 典型使用示例
|
||||||
|
|
||||||
|
#### 交互式模式
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "try.c"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
init_tryc();
|
||||||
|
|
||||||
|
// 交互式输入
|
||||||
|
wchar_t input;
|
||||||
|
while (read_input(&input)) {
|
||||||
|
enqueue_tryc(input);
|
||||||
|
if (input == L'\n') {
|
||||||
|
eval_tryc(); // 遇到换行执行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
free_tryc();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 文件执行模式
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "try.c"
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
init_tryc();
|
||||||
|
|
||||||
|
// 读取文件
|
||||||
|
FILE *file = fopen(argv[1], "r");
|
||||||
|
wchar_t wc;
|
||||||
|
int i = 0;
|
||||||
|
while ((wc = fgetwc(file)) != WEOF) {
|
||||||
|
src[i++] = wc;
|
||||||
|
}
|
||||||
|
src[i] = L'\0';
|
||||||
|
fclose(file);
|
||||||
|
|
||||||
|
// 执行
|
||||||
|
eval_tryc();
|
||||||
|
|
||||||
|
clear(0); // 清理全局符号
|
||||||
|
free_tryc();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、语言特性
|
||||||
|
|
||||||
|
### 5.1 数据类型
|
||||||
|
|
||||||
|
| 类型 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `Num` | 数字(整数或浮点) | `a = 5;`, `b = 3.14;` |
|
||||||
|
| `Char` | 单个字符 | `c = 'A';` |
|
||||||
|
| `Str` | 字符串(宽字符) | `s = "hello";` |
|
||||||
|
| `Array` | 数组(动态分配) | `array arr(15);` |
|
||||||
|
| `Func` | 函数 | `func name{...};` |
|
||||||
|
|
||||||
|
### 5.2 变量与作用域
|
||||||
|
|
||||||
|
#### 变量声明
|
||||||
|
|
||||||
|
变量通过首次赋值自动声明:
|
||||||
|
|
||||||
|
```
|
||||||
|
x = 5; // 声明并初始化数字变量
|
||||||
|
name = "hello"; // 声明并初始化字符串变量
|
||||||
|
flag = 'A'; // 声明并初始化字符变量
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 作用域
|
||||||
|
|
||||||
|
- 全局变量:在顶层声明
|
||||||
|
- 局部变量:在函数内声明,函数结束后自动销毁
|
||||||
|
- 符号表使用栈结构,`levelNum` 标识嵌套层级
|
||||||
|
|
||||||
|
### 5.3 运算符
|
||||||
|
|
||||||
|
#### 算术运算符
|
||||||
|
|
||||||
|
| 运算符 | 说明 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `+` | 加法 | `a + b` |
|
||||||
|
| `-` | 减法 | `a - b` |
|
||||||
|
| `*` | 乘法 | `a * b` |
|
||||||
|
| `/` | 除法 | `a / b` |
|
||||||
|
|
||||||
|
#### 比较运算符
|
||||||
|
|
||||||
|
| 运算符 | 说明 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `>` | 大于 | `a > b` |
|
||||||
|
| `<` | 小于 | `a < b` |
|
||||||
|
| `>=` | 大于等于 | `a >= b` |
|
||||||
|
| `<=` | 小于等于 | `a <= b` |
|
||||||
|
| `==` | 等于 | `a == b` |
|
||||||
|
| `!=` | 不等于 | `a != b` |
|
||||||
|
|
||||||
|
#### 逻辑运算符
|
||||||
|
|
||||||
|
| 运算符 | 说明 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `\|\|` | 逻辑或(短路) | `a \|\| b` |
|
||||||
|
| `&&` | 逻辑与(短路) | `a && b` |
|
||||||
|
| `!` | 逻辑非 | `!a` |
|
||||||
|
|
||||||
|
#### 赋值运算符
|
||||||
|
|
||||||
|
| 运算符 | 说明 | 示例 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `=` | 赋值 | `a = 5;` |
|
||||||
|
|
||||||
|
### 5.4 控制流
|
||||||
|
|
||||||
|
#### if/else 语句
|
||||||
|
|
||||||
|
```
|
||||||
|
if (condition) {
|
||||||
|
// true 分支
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// false 分支
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### while 循环
|
||||||
|
|
||||||
|
```
|
||||||
|
while (condition) {
|
||||||
|
// 循环体
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### return 语句
|
||||||
|
|
||||||
|
```
|
||||||
|
func name{
|
||||||
|
return(value);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 函数
|
||||||
|
|
||||||
|
#### 函数定义
|
||||||
|
|
||||||
|
```
|
||||||
|
func function_name{
|
||||||
|
// 函数体
|
||||||
|
return(return_value);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**特性**:
|
||||||
|
- 支持递归调用
|
||||||
|
- 支持参数传递(通过符号表)
|
||||||
|
- `return` 语句会终止函数执行并返回值
|
||||||
|
|
||||||
|
#### 函数调用
|
||||||
|
|
||||||
|
```
|
||||||
|
result = function_name(arg1, arg2);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 数组
|
||||||
|
|
||||||
|
#### 数组声明
|
||||||
|
|
||||||
|
```
|
||||||
|
array arr(size);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `size` 必须是正整数
|
||||||
|
- 数组元素初始化为 0
|
||||||
|
- 数组元素类型为 `Num`(double)
|
||||||
|
|
||||||
|
#### 数组访问与赋值
|
||||||
|
|
||||||
|
```
|
||||||
|
arr[0] = 10;
|
||||||
|
value = arr[i];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.7 输入输出
|
||||||
|
|
||||||
|
#### print()
|
||||||
|
|
||||||
|
```
|
||||||
|
print(expression);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:输出数字
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
print(42); // 输出: 42.000000
|
||||||
|
print(3.14); // 输出: 3.140000
|
||||||
|
print(arr[0]); // 输出数组元素
|
||||||
|
```
|
||||||
|
|
||||||
|
#### puts()
|
||||||
|
|
||||||
|
```
|
||||||
|
puts("string");
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:输出字符串
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```
|
||||||
|
puts("Hello, World!");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### read()
|
||||||
|
|
||||||
|
```
|
||||||
|
read(variable);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:读取输入(当前实现返回 -1)
|
||||||
|
|
||||||
|
### 5.8 注释
|
||||||
|
|
||||||
|
```
|
||||||
|
# This is a comment
|
||||||
|
```
|
||||||
|
|
||||||
|
注释从 `#` 开始到行尾结束。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、示例脚本
|
||||||
|
|
||||||
|
### 6.1 斐波那契数列
|
||||||
|
|
||||||
|
```
|
||||||
|
# Fibonacci sequence
|
||||||
|
func fun{
|
||||||
|
if(x <= 2){
|
||||||
|
return(1);
|
||||||
|
}
|
||||||
|
y = 0;
|
||||||
|
x = x - 1;
|
||||||
|
y = fun(x);
|
||||||
|
x = x - 1;
|
||||||
|
return(y + fun(x));
|
||||||
|
};
|
||||||
|
|
||||||
|
# 计算 1-15 的斐波那契数列并存入数组
|
||||||
|
array arr(15);
|
||||||
|
x = 0;
|
||||||
|
while(x<15){
|
||||||
|
arr[x] = fun(x) + 1000;
|
||||||
|
x = x + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
puts("Fibonacci sequence:");
|
||||||
|
i = 0;
|
||||||
|
while(i < 15){
|
||||||
|
print(arr[i]);
|
||||||
|
i=i+1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 基础赋值
|
||||||
|
|
||||||
|
```
|
||||||
|
a = 5;
|
||||||
|
b = "ok";
|
||||||
|
|
||||||
|
print(5);
|
||||||
|
puts(b);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 基础函数
|
||||||
|
|
||||||
|
```
|
||||||
|
func f_test{
|
||||||
|
return(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
r = f_test();
|
||||||
|
print(r);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 基础输出
|
||||||
|
|
||||||
|
```
|
||||||
|
print(1);
|
||||||
|
puts("1");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、已知限制
|
||||||
|
|
||||||
|
### 🔴 严重问题
|
||||||
|
|
||||||
|
| ID | 位置 | 描述 | 影响 |
|
||||||
|
|----|------|------|------|
|
||||||
|
| L1 | [L20](file:///Users/qingsong/micro_servers/libtry/try.c#L20) | **缓冲区大小固定**:`POOLSIZE = 128` 字节,无法修改 | 长脚本将溢出 |
|
||||||
|
| L2 | [L702](file:///Users/qingsong/micro_servers/libtry/try.c#L702) | **`__exit()` 死循环**:`while(1);` 错误时直接挂死程序 | 无法恢复 |
|
||||||
|
| L3 | [L186](file:///Users/qingsong/micro_servers/libtry/try.c#L186) | **字符串内存泄漏**:`malloc` 分配的字符串在重新赋值时未释放 | 长期运行泄漏 |
|
||||||
|
| L4 | [L528](file:///Users/qingsong/micro_servers/libtry/try.c#L528) | **数组越界**:无越界检查 | 未定义行为 |
|
||||||
|
|
||||||
|
### 🟡 中等问题
|
||||||
|
|
||||||
|
| ID | 位置 | 描述 |
|
||||||
|
|----|------|------|
|
||||||
|
| L5 | [L598](file:///Users/qingsong/micro_servers/libtry/try.c#L598) | **`puts` 实现存疑**:原 `wprintf` 被注释,改用 `printtovga` |
|
||||||
|
| L6 | [L518](file:///Users/qingsong/micro_servers/libtry/try.c#L518) | **`clearThis()` 被注释**:作者注释说"不知为何需要注释了才能跑" |
|
||||||
|
| L7 | [L706](file:///Users/qingsong/micro_servers/libtry/try.c#L706) | **文件模式仅限 `_DEBUG`**:正常构建时 `main()` 不编译 |
|
||||||
|
| L8 | [L103](file:///Users/qingsong/micro_servers/libtry/try.c#L103) | **栈缓冲区溢出风险**:`nameBuffer[16]` 但标识符长度限制由 `MAXNAMESIZE=16` 控制 |
|
||||||
|
| L9 | [L21](file:///Users/qingsong/micro_servers/libtry/try.c#L21) | **符号表大小固定**:`SYMTABSIZE = 128` 个符号 |
|
||||||
|
|
||||||
|
### 🟢 轻微问题
|
||||||
|
|
||||||
|
| ID | 描述 |
|
||||||
|
|----|------|
|
||||||
|
| L10 | 错误处理过于简单,`match()` 失败直接 `__exit(-1)` |
|
||||||
|
| L11 | 大量被注释的调试代码(`//wprintf(...)`) |
|
||||||
|
| L12 | 全局变量过多(`symtab`, `token`, `token_val`, `src`, `old_src`, `currentlevel` 等) |
|
||||||
|
| L13 | 无并发支持,非线程安全 |
|
||||||
|
| L14 | 无单元测试 |
|
||||||
|
|
||||||
|
### 修复建议
|
||||||
|
|
||||||
|
#### 优先级 1(安全相关)
|
||||||
|
|
||||||
|
1. **修复数组越界**:在 [L528](file:///Users/qingsong/micro_servers/libtry/try.c#L528) 添加检查
|
||||||
|
```c
|
||||||
|
if (index < 0 || index >= s->value) {
|
||||||
|
printtovga(L"Array index out of bounds\n");
|
||||||
|
__exit(-1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **修复 `__exit()`**:改为返回错误码
|
||||||
|
```c
|
||||||
|
static int error_code = 0;
|
||||||
|
static void __exit(int code) {
|
||||||
|
error_code = code;
|
||||||
|
// 不挂起,让调用者处理错误
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **修复字符串内存泄漏**:在赋值前释放旧字符串
|
||||||
|
```c
|
||||||
|
else if (token == Str) {
|
||||||
|
if (s->pointer.funcp != NULL) {
|
||||||
|
free(s->pointer.funcp); // 释放旧字符串
|
||||||
|
}
|
||||||
|
s->pointer.funcp = (wchar_t*)token_val.ptr;
|
||||||
|
s->type = Str;
|
||||||
|
match(Str);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优先级 2(功能增强)
|
||||||
|
|
||||||
|
4. **动态缓冲区**:将 `POOLSIZE` 改为可配置或动态扩容
|
||||||
|
5. **错误回调**:添加错误回调函数,让调用者处理错误
|
||||||
|
6. **符号表扩容**:允许 `SYMTABSIZE` 动态增长
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、开发指南
|
||||||
|
|
||||||
|
### 8.1 添加新关键字
|
||||||
|
|
||||||
|
在 [init_tryc()](file:///Users/qingsong/micro_servers/libtry/try.c#L650) 中添加:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 1. 在 enum 中添加新 token 类型
|
||||||
|
enum {
|
||||||
|
Num = 128, Char, Str, Array, Func,
|
||||||
|
Else, If, Return, While, Print, Puts, Read,
|
||||||
|
Assign, OR, AND, Equal, Sym, FuncSym, ArraySym, Void,
|
||||||
|
Nequal, LessEqual, GreatEqual,
|
||||||
|
NewKeyword // 新增
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 在 init_tryc() 中注册
|
||||||
|
src = L"array func else if return while print puts read new_keyword";
|
||||||
|
for (i = Array; i <= NewKeyword; ++i) {
|
||||||
|
next();
|
||||||
|
symtab[symPointer - 1].type = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 在 statement() 中添加处理逻辑
|
||||||
|
else if (token == NewKeyword) {
|
||||||
|
match(NewKeyword);
|
||||||
|
// 处理逻辑
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 添加新运算符
|
||||||
|
|
||||||
|
在 [next()](file:///Users/qingsong/micro_servers/libtry/try.c#L85) 中添加识别逻辑:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 例如:+= 运算符
|
||||||
|
else if (token == L'+') {
|
||||||
|
if (*src == L'=') {
|
||||||
|
src++;
|
||||||
|
token = AddAssign; // 新增 token 类型
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在 [statement()](file:///Users/qingsong/micro_servers/libtry/try.c#L466) 中添加执行逻辑。
|
||||||
|
|
||||||
|
### 8.3 自定义输出
|
||||||
|
|
||||||
|
修改 `printtovga()` 的实现,或添加可配置的输出回调:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 函数指针类型
|
||||||
|
typedef void (*output_func)(const wchar_t*);
|
||||||
|
|
||||||
|
// 全局输出函数
|
||||||
|
static output_func current_output = default_output;
|
||||||
|
|
||||||
|
// 设置输出函数
|
||||||
|
void set_output_func(output_func func) {
|
||||||
|
current_output = func;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 构建说明
|
||||||
|
|
||||||
|
#### 编译器要求
|
||||||
|
|
||||||
|
- C 编译器(GCC/Clang)
|
||||||
|
- 支持 C99 标准
|
||||||
|
- 支持宽字符(`wchar.h`)
|
||||||
|
|
||||||
|
#### 编译选项
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本编译
|
||||||
|
gcc -c try.c -o try.o -std=c99
|
||||||
|
|
||||||
|
# 调试模式(启用 main 函数)
|
||||||
|
gcc -c try.c -o try.o -std=c99 -D_DEBUG
|
||||||
|
|
||||||
|
# 优化编译
|
||||||
|
gcc -c try.c -o try.o -std=c99 -O2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 链接使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建测试程序
|
||||||
|
cat > test_main.c << 'EOF'
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "try.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
init_tryc();
|
||||||
|
|
||||||
|
// 注入脚本
|
||||||
|
wchar_t script[] = L"print(42);";
|
||||||
|
for (int i = 0; script[i] != L'\0'; i++) {
|
||||||
|
enqueue_tryc(script[i]);
|
||||||
|
}
|
||||||
|
eval_tryc();
|
||||||
|
|
||||||
|
free_tryc();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 编译链接
|
||||||
|
gcc test_main.c try.o -o test_try -std=c99
|
||||||
|
./test_try
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、测试
|
||||||
|
|
||||||
|
### 9.1 现有测试
|
||||||
|
|
||||||
|
| 测试文件 | 内容 |
|
||||||
|
|----------|------|
|
||||||
|
| `test/t_assign_basic.try` | 变量赋值与输出 |
|
||||||
|
| `test/t_function_basic.try` | 函数定义与调用 |
|
||||||
|
| `test/t_output_basic.try` | print/puts 输出 |
|
||||||
|
| `t.try` | 斐波那契数列(复杂示例) |
|
||||||
|
|
||||||
|
### 9.2 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译(调试模式)
|
||||||
|
gcc try.c -o try_interp -std=c99 -D_DEBUG
|
||||||
|
|
||||||
|
# 执行测试脚本
|
||||||
|
./try_interp test/t_assign_basic.try
|
||||||
|
./try_interp test/t_function_basic.try
|
||||||
|
./try_interp test/t_output_basic.try
|
||||||
|
./try_interp t.try
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 添加新测试
|
||||||
|
|
||||||
|
在 `test/` 目录下创建 `.try` 文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
# test/test_new_feature.try
|
||||||
|
# 描述新功能
|
||||||
|
|
||||||
|
a = 10;
|
||||||
|
b = 20;
|
||||||
|
c = a + b;
|
||||||
|
print(c);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 主要变更 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 当前 | 2025-08-27 | 增加宽字符支持,修复 Str 匹配 BUG,使用 longjmp 替代 exit() |
|
||||||
|
| 初始 | — | 魔改自 tryC 项目,手写简易解释器 |
|
||||||
|
|
||||||
|
### 修改记录(代码注释)
|
||||||
|
|
||||||
|
| 日期 | 作者 | 变更 |
|
||||||
|
|------|------|------|
|
||||||
|
| 2025-08-27 | qing | 增加宽字符支持 |
|
||||||
|
| 2025-08-27 | qing | 修复 Str 漏匹配的 BUG |
|
||||||
|
| 2025-08-27 | qing | 注明内存泄漏风险 |
|
||||||
|
| 2025-08-27 | qing | 使用 `longjmp()` 替代 `exit()` |
|
||||||
|
| 2026-07-07 | qing | 增加 `clearThis()` 函数,用于释放变量 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. 完整关键字列表
|
||||||
|
|
||||||
|
```
|
||||||
|
array, func, else, if, return, while, print, puts, read
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. Token 类型完整列表
|
||||||
|
|
||||||
|
```
|
||||||
|
Num(128), Char(129), Str(130), Array(131), Func(132),
|
||||||
|
Else(133), If(134), Return(135), While(136), Print(137), Puts(138), Read(139),
|
||||||
|
Assign(140), OR(141), AND(142), Equal(143), Sym(144), FuncSym(145), ArraySym(146), Void(147),
|
||||||
|
Nequal(148), LessEqual(149), GreatEqual(150)
|
||||||
|
```
|
||||||
|
|
||||||
|
### C. 限制常量
|
||||||
|
|
||||||
|
| 常量 | 值 | 说明 |
|
||||||
|
|------|-----|------|
|
||||||
|
| `POOLSIZE` | 128 | 源代码缓冲区大小(字节) |
|
||||||
|
| `SYMTABSIZE` | 128 | 符号表大小 |
|
||||||
|
| `MAXNAMESIZE` | 16 | 标识符最大长度 |
|
||||||
|
| `RETURNFLAG` | DBL_MAX | return 语句标记 |
|
||||||
|
|
||||||
|
### D. 参考资源
|
||||||
|
|
||||||
|
- [Crafting Interpreters](https://craftinginterpreters.com/) — 解释器编写经典教程
|
||||||
|
- [Writing an Interpreter in Go](https://interpreterbook.com/) — Go 实现的解释器教程
|
||||||
|
- [tryC (GitHub)](https://github.com/) — 原始项目(已魔改)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档基于 try.c 源码分析生成,供开发团队参考。使用前请评估已知限制。*
|
||||||
@@ -0,0 +1,764 @@
|
|||||||
|
# pymsg 模块技术手册
|
||||||
|
|
||||||
|
> 文档版本:2026-07-30
|
||||||
|
> 代码版本:0.1.0
|
||||||
|
> 语言:Python 3.9+(核心)、C++(gateway 集成)、Go(waf 集成)
|
||||||
|
> 定位:跨语言 TCP 消息帧协议库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、模块概述
|
||||||
|
|
||||||
|
### 1.1 定位
|
||||||
|
|
||||||
|
**pymsg** 是一个极简跨语言 TCP 消息帧协议库,基于长度前缀(4 字节大端 uint32)+ UTF-8 载荷。为微服务体系提供轻量级 RPC 通信层。
|
||||||
|
|
||||||
|
### 1.2 设计哲学
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 零依赖 | 仅使用 Python stdlib,无需任何第三方包 |
|
||||||
|
| 极简协议 | 4 字节头 + 载荷,实现只需 20 行代码 |
|
||||||
|
| 跨语言 | Python/C++/Go 三端实现完全兼容 |
|
||||||
|
| 二进制安全 | 长度前缀帧,JSON 中的换行符无需转义 |
|
||||||
|
| fail-safe | 日志推送在 db 不可用时静默丢弃,不影响业务 |
|
||||||
|
|
||||||
|
### 1.3 与微服务体系的关系
|
||||||
|
|
||||||
|
pymsg 是微服务的**核心通信层**:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ pymsg ┌──────────────┐
|
||||||
|
│ dnfs:8006 │ ───────→│ db:8005 │
|
||||||
|
│ (Python) │ 短连接 │ (Python) │
|
||||||
|
└──────────────┘ RPC └──────────────┘
|
||||||
|
┌──────────────┐ pymsg ┌──────────────┐
|
||||||
|
│ gateway:8443 │ ───────→│ db:8005 │
|
||||||
|
│ (C++20) │ 短连接 │ │
|
||||||
|
└──────────────┘ RPC └──────────────┘
|
||||||
|
┌──────────────┐ pymsg ┌──────────────┐
|
||||||
|
│ waf:443 │ ───────→│ db:8005 │
|
||||||
|
│ (Go) │ 短连接 │ │
|
||||||
|
└──────────────┘ RPC └──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、技术架构
|
||||||
|
|
||||||
|
### 2.1 协议格式
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────┬──────────────────────────┐
|
||||||
|
│ 4 字节(大端序) │ N 字节(UTF-8) │
|
||||||
|
│ uint32 载荷字节长度 │ JSON / 文本载荷 │
|
||||||
|
└────────────────────────┴──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 架构图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 微服务调用方 │
|
||||||
|
│ (dnfs / gateway / waf / static / smtp / captcha) │
|
||||||
|
└─────────────────────────┬───────────────────────────────────┘
|
||||||
|
│ MsgClient.call() / send()
|
||||||
|
┌─────────────────────────▼───────────────────────────────────┐
|
||||||
|
│ pymsg 协议层 │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 底层协议函数 │ │
|
||||||
|
│ │ send(sock, data) → [4字节长度头][UTF-8载荷] │ │
|
||||||
|
│ │ recv(sock) → 读4字节→解析长度→读对应字节→返回 │ │
|
||||||
|
│ └──────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────▼──────────────────────────────┐ │
|
||||||
|
│ │ MsgClient 客户端 │ │
|
||||||
|
│ │ send(msg) — 短连接,发送即忘(带 name 前缀) │ │
|
||||||
|
│ │ call(msg) — 短连接,RPC 请求-应答 │ │
|
||||||
|
│ │ listen(cb) — 长连接,回调模式 │ │
|
||||||
|
│ └──────────────────────────┬──────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────────────────▼──────────────────────────────┐ │
|
||||||
|
│ │ MsgServer 服务端 │ │
|
||||||
|
│ │ start(on_message) — 启动监听 │ │
|
||||||
|
│ │ reply(sock, msg) — 回复客户端 │ │
|
||||||
|
│ │ broadcast(msg) — 广播到所有客户端 │ │
|
||||||
|
│ │ stop() — 停止服务 │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ TCP
|
||||||
|
┌─────────────────────────▼───────────────────────────────────┐
|
||||||
|
│ db:8005 服务端 │
|
||||||
|
│ MsgServer + 路由分发 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 消息流
|
||||||
|
|
||||||
|
#### RPC 请求-应答
|
||||||
|
|
||||||
|
```
|
||||||
|
调用方 服务端 (db)
|
||||||
|
────── ──────────
|
||||||
|
MsgClient.call(json_str)
|
||||||
|
│
|
||||||
|
├── socket.connect(127.0.0.1:8005)
|
||||||
|
├── send(sock, json_str)
|
||||||
|
│ ├── payload = json_str.encode("utf-8")
|
||||||
|
│ ├── header = struct.pack("!I", len(payload))
|
||||||
|
│ └── sock.sendall(header + payload)
|
||||||
|
│
|
||||||
|
├── recv(sock) ←── on_message(sock, addr, text)
|
||||||
|
│ ├── sock.recv(4) ├── json.loads(text)
|
||||||
|
│ ├── length = struct.unpack("!I", header)[0]
|
||||||
|
│ ├── sock.recv(length) ├── handler(req)
|
||||||
|
│ └── payload.decode("utf-8") └── server.reply(sock, response)
|
||||||
|
│ ├── send(sock, response)
|
||||||
|
├── close(sock) │ └── sock.sendall(header + payload)
|
||||||
|
└── return response ←── recv(sock) 返回 response
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 日志推送
|
||||||
|
|
||||||
|
```
|
||||||
|
各服务 (gateway/static/smtp) db:8005
|
||||||
|
───────────────────────────── ──────────
|
||||||
|
log_client.push(entry)
|
||||||
|
│
|
||||||
|
├── 入内存队列 (ch, 容量 256)
|
||||||
|
│
|
||||||
|
├── 后台 goroutine 批量 flush
|
||||||
|
│ ├── 满 50 条 或 2 秒超时
|
||||||
|
│ └── 逐条 MsgClient.call(json)
|
||||||
|
│
|
||||||
|
└── (同 RPC 流程,fail-safe:失败静默丢弃)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、代码结构
|
||||||
|
|
||||||
|
### 3.1 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
pymsg/
|
||||||
|
├── pymsg/
|
||||||
|
│ └── __init__.py # 核心实现(~370 行)
|
||||||
|
├── README.md # 用户文档
|
||||||
|
├── pyproject.toml # 项目配置
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 跨语言实现位置
|
||||||
|
|
||||||
|
| 语言 | 文件 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Python | [pymsg/__init__.py](file:///Users/qingsong/micro_servers/pymsg/pymsg/__init__.py) | 主实现 |
|
||||||
|
| C++ | [gateway/inc/Msg.hpp](file:///Users/qingsong/micro_servers/gateway/inc/Msg.hpp) | gateway 使用 |
|
||||||
|
| Go | [waf/internal/logclient/logclient.go](file:///Users/qingsong/micro_servers/waf/internal/logclient/logclient.go) | waf 使用 |
|
||||||
|
| Python (黑盒) | [tests/blackbox/helpers.py](file:///Users/qingsong/micro_servers/tests/blackbox/helpers.py) | 纯 stdlib 测试实现 |
|
||||||
|
|
||||||
|
### 3.3 代码索引
|
||||||
|
|
||||||
|
#### Python 实现(__init__.py)
|
||||||
|
|
||||||
|
| 函数/类 | 行号 | 功能 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `send(sock, data, *, timeout=None)` | L48 | 底层发送 |
|
||||||
|
| `recv(sock, *, timeout=None)` | L72 | 底层接收 |
|
||||||
|
| `_recv_exact(sock, n)` | L103 | 精确读 n 字节 |
|
||||||
|
| `_timeout(sock, seconds)` | L34 | 超时 context manager |
|
||||||
|
| `TimeoutError` | L30 | 超时异常 |
|
||||||
|
| `MsgClient` | L120 | TCP 客户端 |
|
||||||
|
| `MsgClient.__init__` | L136 | 构造器 |
|
||||||
|
| `MsgClient.connect()` | L144 | 建立长连接 |
|
||||||
|
| `MsgClient.send(msg)` | L150 | 短连接发送(带 name 前缀) |
|
||||||
|
| `MsgClient.call(msg, *, timeout=None)` | L163 | 短连接 RPC |
|
||||||
|
| `MsgClient.listen(on_message)` | L193 | 长连接监听 |
|
||||||
|
| `MsgClient.stop()` | L220 | 停止监听 |
|
||||||
|
| `MsgServer` | L234 | TCP 服务端 |
|
||||||
|
| `MsgServer.start(on_message)` | L259 | 启动服务 |
|
||||||
|
| `MsgServer.broadcast(msg, except_sock)` | L296 | 广播 |
|
||||||
|
| `MsgServer.reply(sock, msg)` | L321 | 回复 |
|
||||||
|
| `MsgServer.stop()` | L329 | 停止服务 |
|
||||||
|
|
||||||
|
#### C++ 实现(Msg.hpp)
|
||||||
|
|
||||||
|
| 函数/类 | 行号 | 功能 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `MsgException` | L47 | 基础异常 |
|
||||||
|
| `MsgTimeoutException` | L52 | 超时异常 |
|
||||||
|
| `MsgConnectionException` | L57 | 连接异常 |
|
||||||
|
| `writeUint32BE(buf, val)` | L72 | 4 字节大端编码 |
|
||||||
|
| `readUint32BE(buf)` | L79 | 4 字节大端解码 |
|
||||||
|
| `setSockTimeout(sockfd, seconds)` | L88 | 设置 socket 超时 |
|
||||||
|
| `recvExact(sockfd, buf, n)` | L99 | 精确读 n 字节 |
|
||||||
|
| `sendAll(sockfd, data, len)` | L121 | 完整发送 |
|
||||||
|
| `openConnection(addr, port, timeout)` | L139 | 打开短连接 |
|
||||||
|
| `pmsg_send(sockfd, data)` | L174 | 协议发送 |
|
||||||
|
| `pmsg_recv(sockfd)` | L188 | 协议接收 |
|
||||||
|
| `MsgClient` | L210 | C++ 客户端 |
|
||||||
|
| `MsgClient.connect()` | L220 | 长连接 |
|
||||||
|
| `MsgClient.send(msg)` | L228 | 短连接发送 |
|
||||||
|
| `MsgClient.call(msg, timeout)` | L248 | 短连接 RPC |
|
||||||
|
| `MsgClient.listen(on_message)` | L269 | 长连接监听 |
|
||||||
|
| `MsgClient.stop()` | L311 | 停止 |
|
||||||
|
|
||||||
|
#### Go 实现(logclient.go)
|
||||||
|
|
||||||
|
| 函数 | 行号 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| `pymsgCall(payload)` | L226 | 发送并接收一条 pymsg 消息 |
|
||||||
|
| `Client.sendOne(entry, ts)` | L179 | 发送单条日志 |
|
||||||
|
| `Client.send(batch)` | L171 | 批量发送 |
|
||||||
|
| `Client.flushLoop()` | L125 | 后台 flush goroutine |
|
||||||
|
| `Push(service, level, message, extra)` | L82 | 全局推送 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、API 参考
|
||||||
|
|
||||||
|
### 4.1 底层函数
|
||||||
|
|
||||||
|
#### send(sock, data, *, timeout=None)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def send(sock: socket.socket, data: str, *, timeout: float | None = None) -> None
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:在已连接的 socket 上发送一条长度前缀帧消息
|
||||||
|
- 将字符串编码为 UTF-8
|
||||||
|
- 前面加上 4 字节大端长度头
|
||||||
|
- 调用 `sock.sendall()` 发送
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `sock`:已连接的 socket 对象
|
||||||
|
- `data`:要发送的字符串
|
||||||
|
- `timeout`:可选超时(秒),超时抛出 `TimeoutError`
|
||||||
|
|
||||||
|
#### recv(sock, *, timeout=None)
|
||||||
|
|
||||||
|
```python
|
||||||
|
def recv(sock: socket.socket, *, timeout: float | None = None) -> str
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**:从已连接的 socket 接收一条长度前缀帧消息
|
||||||
|
- 读取 4 字节头解出长度
|
||||||
|
- 读取指定字节数
|
||||||
|
- 返回解码后的 UTF-8 字符串
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `sock`:已连接的 socket 对象
|
||||||
|
- `timeout`:可选超时(秒)
|
||||||
|
|
||||||
|
**返回值**:解码后的字符串
|
||||||
|
|
||||||
|
**异常**:
|
||||||
|
- `ConnectionError`:连接在完整消息接收前断开
|
||||||
|
- `TimeoutError`:操作超时
|
||||||
|
|
||||||
|
#### TimeoutError
|
||||||
|
|
||||||
|
```python
|
||||||
|
class TimeoutError(Exception)
|
||||||
|
```
|
||||||
|
|
||||||
|
`send` 或 `recv` 超过指定超时时抛出的异常。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 MsgClient(客户端)
|
||||||
|
|
||||||
|
#### 构造
|
||||||
|
|
||||||
|
```python
|
||||||
|
client = MsgClient(addr: str, port: int, name: str)
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `addr`:服务端地址或 IP
|
||||||
|
- `port`:服务端端口
|
||||||
|
- `name`:客户端标识(`send()` 模式下以 `"name: message"` 格式发送)
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
| 方法 | 模式 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `connect()` | 长连接 | 创建持久 TCP 连接(供 `listen()` 使用) |
|
||||||
|
| `send(msg)` | 短连接 | 发送即忘,消息带 `name: msg` 前缀 |
|
||||||
|
| `call(msg, timeout=10.0)` | 短连接 | RPC 模式,阻塞等待回复 |
|
||||||
|
| `listen(on_message)` | 长连接 | 阻塞当前线程,持续接收消息 |
|
||||||
|
| `stop()` | — | 中断 `listen()`,可从另一线程调用 |
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pymsg import MsgClient
|
||||||
|
|
||||||
|
# RPC 调用(短连接)
|
||||||
|
client = MsgClient("127.0.0.1", 8005, "my-service")
|
||||||
|
resp = client.call('{"_route":"execute","action":"select","table":"Users"}', timeout=10.0)
|
||||||
|
print(resp) # {"success": true, "data": [...]}
|
||||||
|
|
||||||
|
# 发送即忘
|
||||||
|
client.send("hello")
|
||||||
|
|
||||||
|
# 长连接监听
|
||||||
|
def on_message(text):
|
||||||
|
print(f"收到: {text}")
|
||||||
|
|
||||||
|
client.connect()
|
||||||
|
client.listen(on_message) # 阻塞,直到 stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 MsgServer(服务端)
|
||||||
|
|
||||||
|
#### 构造
|
||||||
|
|
||||||
|
```python
|
||||||
|
server = MsgServer(addr: str, port: int)
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `addr`:监听地址(`0.0.0.0` 表示所有网卡)
|
||||||
|
- `port`:监听端口
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
| 方法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `start(on_message)` | 启动服务器(阻塞),回调签名:`on_message(sock, addr, text)` |
|
||||||
|
| `reply(sock, msg)` | 向指定客户端回复 |
|
||||||
|
| `broadcast(msg, except_sock=None)` | 广播到所有客户端(可排除某个 socket) |
|
||||||
|
| `stop()` | 停止服务(可从另一线程调用) |
|
||||||
|
|
||||||
|
#### 回调签名
|
||||||
|
|
||||||
|
```python
|
||||||
|
def on_message(sock: socket.socket, addr: tuple, text: str):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
sock: 发送消息的客户端套接字(用于 reply)
|
||||||
|
addr: (ip, port) 元组
|
||||||
|
text: 解码后的字符串消息
|
||||||
|
"""
|
||||||
|
server.reply(sock, "ack")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pymsg import MsgServer
|
||||||
|
|
||||||
|
def on_message(sock, addr, text):
|
||||||
|
print(f"[{addr}] {text}")
|
||||||
|
server.reply(sock, "ack")
|
||||||
|
|
||||||
|
server = MsgServer("0.0.0.0", 8005)
|
||||||
|
server.start(on_message) # 阻塞
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 异常速查
|
||||||
|
|
||||||
|
| 异常 | 触发条件 |
|
||||||
|
|------|----------|
|
||||||
|
| `pymsg.TimeoutError` | `send()` 或 `recv()` 超过 `timeout` |
|
||||||
|
| `ConnectionError`(内置) | 完整消息接收前 socket 断开 |
|
||||||
|
| `ConnectionRefusedError`(内置) | `MsgClient.call()` 或 `send()` 无法连接目标 |
|
||||||
|
| `RuntimeError` | `MsgClient.listen()` 在 `connect()` 之前调用 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、使用场景
|
||||||
|
|
||||||
|
### 5.1 场景一:数据库服务(db:8005)
|
||||||
|
|
||||||
|
db 服务通过 pymsg 暴露通用 CRUD 接口:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from pymsg import MsgServer
|
||||||
|
|
||||||
|
class DBServer:
|
||||||
|
def __init__(self):
|
||||||
|
self._server = MsgServer("127.0.0.1", 8005)
|
||||||
|
self._routes = {"execute": self._handle_execute}
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._server.start(self._dispatch)
|
||||||
|
|
||||||
|
def _dispatch(self, sock, addr, text):
|
||||||
|
req = json.loads(text)
|
||||||
|
route = req.pop("_route", "")
|
||||||
|
handler = self._routes.get(route)
|
||||||
|
if handler is None:
|
||||||
|
self._server.reply(sock,
|
||||||
|
json.dumps({"success": False, "error": "未知路由"}))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
result = handler(req)
|
||||||
|
self._server.reply(sock,
|
||||||
|
json.dumps({"success": True, "data": result}))
|
||||||
|
except Exception as e:
|
||||||
|
self._server.reply(sock,
|
||||||
|
json.dumps({"success": False, "error": str(e)}))
|
||||||
|
|
||||||
|
def _handle_execute(self, data):
|
||||||
|
# 通过 SQLAlchemy 执行 SQL,返回 dict 列表
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 场景二:RPC 调用(dnfs → db)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from pymsg import MsgClient
|
||||||
|
|
||||||
|
def _call_dbv2(payload):
|
||||||
|
client = MsgClient("127.0.0.1", 8005, "dnfs")
|
||||||
|
resp = client.call(json.dumps(payload), timeout=10.0)
|
||||||
|
return json.loads(resp)
|
||||||
|
|
||||||
|
# 创建笔记
|
||||||
|
result = _call_dbv2({
|
||||||
|
"_route": "execute",
|
||||||
|
"action": "insert",
|
||||||
|
"table": "Notes",
|
||||||
|
"values": {"title": "会议记录", "content": "..."}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 场景三:异步日志推送(log_client)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
from pymsg import MsgClient
|
||||||
|
|
||||||
|
def _send(batch):
|
||||||
|
for entry in batch:
|
||||||
|
client = MsgClient("127.0.0.1", 8005, "gateway-log")
|
||||||
|
client.call(json.dumps({
|
||||||
|
"_route": "execute",
|
||||||
|
"action": "insert",
|
||||||
|
"table": "Logs",
|
||||||
|
"values": {
|
||||||
|
"ts": now,
|
||||||
|
"service": entry["service"],
|
||||||
|
"level": entry["level"],
|
||||||
|
"message": entry["message"],
|
||||||
|
}
|
||||||
|
}), timeout=5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 场景四:日志推送(Go 实现)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// waf/internal/logclient/logclient.go
|
||||||
|
func (c *Client) sendOne(entry Entry, ts string) {
|
||||||
|
req := dbV2Request{
|
||||||
|
Route: "execute",
|
||||||
|
Action: "insert",
|
||||||
|
Table: "Logs",
|
||||||
|
Values: values,
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(req)
|
||||||
|
resp, err := pymsgCall(data) // 短连接 RPC
|
||||||
|
if err != nil {
|
||||||
|
return // fail-safe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pymsgCall(payload []byte) ([]byte, error) {
|
||||||
|
conn, err := net.DialTimeout("tcp", "127.0.0.1:8005", 5*time.Second)
|
||||||
|
// ... 写 4 字节头 + 载荷,读回复
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、跨语言实现对照
|
||||||
|
|
||||||
|
### 6.1 协议层
|
||||||
|
|
||||||
|
| 功能 | Python | C++ | Go |
|
||||||
|
|------|--------|-----|-----|
|
||||||
|
| 4 字节编码 | `struct.Struct("!I")` | `writeUint32BE` (手动位移) | `binary.BigEndian.PutUint32` |
|
||||||
|
| 4 字节解码 | `struct.unpack("!I", header)` | `readUint32BE` (手动位移) | `binary.BigEndian.Uint32` |
|
||||||
|
| 超时设置 | `_timeout` context manager | `setSockTimeout` (setsockopt) | `net.DialTimeout` + `SetDeadline` |
|
||||||
|
| 安全上限 | 无 | 64 MiB | 无 |
|
||||||
|
|
||||||
|
### 6.2 客户端对照
|
||||||
|
|
||||||
|
| 操作 | Python | C++ | Go |
|
||||||
|
|------|--------|-----|-----|
|
||||||
|
| 构造 | `MsgClient(addr, port, name)` | `MsgClient(addr, port, name)` | — |
|
||||||
|
| 长连接 | `connect()` | `connect()` | — |
|
||||||
|
| 发送即忘 | `send(msg)` 短连接 + `name:` 前缀 | `send(msg)` 短连接 + `name:` 前缀 | — |
|
||||||
|
| RPC 调用 | `call(msg, timeout)` 短连接 | `call(msg, timeout)` 短连接 | `pymsgCall(payload)` 短连接 |
|
||||||
|
| 长监听 | `listen(on_message)` | `listen(on_message)` | — |
|
||||||
|
| 停止 | `stop()` | `stop()` | — |
|
||||||
|
|
||||||
|
### 6.3 服务端对照
|
||||||
|
|
||||||
|
| 操作 | Python | C++ | Go |
|
||||||
|
|------|--------|-----|-----|
|
||||||
|
| 启动 | `start(on_message)` | — | — |
|
||||||
|
| 回复 | `reply(sock, msg)` | `pmsg_send(fd, msg)` | — |
|
||||||
|
| 广播 | `broadcast(msg, except_sock)` | — | — |
|
||||||
|
| 停止 | `stop()` | — | — |
|
||||||
|
|
||||||
|
### 6.4 从头实现协议
|
||||||
|
|
||||||
|
仅需四步:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 将字符串编码为 UTF-8 字节
|
||||||
|
2. 将字节长度打包为 4 字节大端无符号整数
|
||||||
|
3. 写入 头 + 载荷 到 socket
|
||||||
|
4. 接收端先读 4 字节解出长度,再读对应字节数,解码为 UTF-8
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、线程安全
|
||||||
|
|
||||||
|
| 场景 | 安全性 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `MsgServer.stop()` 跨线程调用 | ✅ 安全 | 设置原子标志并关闭监听 socket |
|
||||||
|
| `MsgServer.reply()` 在回调内调用 | ✅ 安全 | 同一线程内调用 |
|
||||||
|
| `MsgServer.broadcast()` 在回调内调用 | ✅ 安全 | 同一线程内调用 |
|
||||||
|
| `MsgClient.stop()` 跨线程调用 | ✅ 安全 | 可中断 `listen()` |
|
||||||
|
| `send()`/`recv()` 多线程共享 socket | ⚠️ 不安全 | 调用方需自行串行化 |
|
||||||
|
| `MsgServer` 并发调用 broadcast/reply | ⚠️ 不支持 | 仅在 `on_message` 回调内(同一线程)安全 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、性能特性
|
||||||
|
|
||||||
|
### 8.1 性能特点
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 零拷贝 | 长度前缀帧避免数据复制 |
|
||||||
|
| 短连接复用 | `call()` 每次新建连接,但调用完成立即释放 |
|
||||||
|
| fail-safe | 日志推送失败静默丢弃,不阻塞业务 |
|
||||||
|
| 批量缓冲 | 日志推送:50 条或 2 秒自动 flush |
|
||||||
|
|
||||||
|
### 8.2 连接模型
|
||||||
|
|
||||||
|
| 模式 | 连接数 | 延迟 | 适用场景 |
|
||||||
|
|------|--------|------|---------|
|
||||||
|
| `send()` | 每次 1 个 | 中 | 低频通知 |
|
||||||
|
| `call()` | 每次 1 个 | 中 | RPC 调用 |
|
||||||
|
| `listen()` | 持久 1 个 | 低 | 高频消息流 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、已知限制
|
||||||
|
|
||||||
|
| ID | 描述 | 严重等级 | 影响范围 |
|
||||||
|
|----|------|---------|---------|
|
||||||
|
| L1 | **无消息校验**:协议无 CRC/哈希,传输损坏无法检测 | 🟡 Medium | 所有消息 |
|
||||||
|
| L2 | **无认证**:任何人可连接 8005 端口,无鉴权机制 | 🔴 High | db 服务 |
|
||||||
|
| L3 | **无加密**:明文传输,无 TLS/加密保护 | 🔴 High | 所有服务 |
|
||||||
|
| L4 | **Python 版无安全上限**:C++ 版有 64 MiB 上限,Python 版未检查 | 🟡 Medium | Python 客户端 |
|
||||||
|
| L5 | **无重试机制**:连接失败直接抛异常,无自动重试 | 🟢 Low | 所有调用 |
|
||||||
|
| L6 | **无版本号**:协议升级无兼容路径 | 🟢 Low | 长期维护 |
|
||||||
|
| L7 | **无心跳**:长连接无 keep-alive,可能被防火墙切断 | 🟡 Medium | listen 模式 |
|
||||||
|
| L8 | **全局阻塞**:`MsgServer.start()` 阻塞主线程,需另开线程 | 🟢 Low | 服务端 |
|
||||||
|
|
||||||
|
### 建议改进
|
||||||
|
|
||||||
|
1. **L2/L3**:在防火墙层面限制 8005 端口仅允许本机连接(当前部署已这样做)
|
||||||
|
2. **L4**:在 Python `recv()` 中添加消息大小上限检查
|
||||||
|
3. **L7**:在 `listen()` 循环中添加 ping/pong 机制
|
||||||
|
4. **L5**:在 `call()` 中添加简单重试(1-2 次)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、设计考量
|
||||||
|
|
||||||
|
### 10.1 为什么用长度前缀帧?
|
||||||
|
|
||||||
|
| 方案 | 优点 | 缺点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **长度前缀帧** ✅ | 二进制安全、实现简单、无需流式解析 | 无 |
|
||||||
|
| 换行分隔 | 简单 | JSON 中换行需转义、缓冲区管理复杂 |
|
||||||
|
| HTTP | 成熟、有工具 | 头部开销大、需额外依赖 |
|
||||||
|
| Protobuf | 高效、类型安全 | 需额外依赖、跨语言实现复杂 |
|
||||||
|
|
||||||
|
### 10.2 为什么用短连接?
|
||||||
|
|
||||||
|
| 考虑 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 简单性 | 短连接实现最简洁,无连接管理开销 |
|
||||||
|
| 可靠性 | 每次调用都是独立连接,单个请求失败不影响其他 |
|
||||||
|
| 性能 | 本地回环(`127.0.0.1`)下 TCP 连接建立开销很小 |
|
||||||
|
| 可测试性 | 易于模拟各种网络故障 |
|
||||||
|
|
||||||
|
### 10.3 协议设计取舍
|
||||||
|
|
||||||
|
| 取舍 | 选择 | 原因 |
|
||||||
|
|------|------|------|
|
||||||
|
| 头大小 | 4 字节 | 支持最大 4 GiB 消息,足够使用 |
|
||||||
|
| 字节序 | 大端 | 网络标准序,跨语言一致 |
|
||||||
|
| 编码 | UTF-8 | 支持中文/多语言,与 JSON 兼容 |
|
||||||
|
| 版本号 | 无 | 保持最简,升级时可扩展 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、开发指南
|
||||||
|
|
||||||
|
### 11.1 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式
|
||||||
|
cd pymsg
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# 作为本地依赖
|
||||||
|
# 在其他项目的 requirements.txt 中:
|
||||||
|
# ../pymsg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.2 添加新功能
|
||||||
|
|
||||||
|
#### 添加消息校验(CRC)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
def send_with_crc(sock, data):
|
||||||
|
payload = data.encode("utf-8")
|
||||||
|
crc = zlib.crc32(payload) & 0xFFFFFFFF
|
||||||
|
# [4字节长度][4字节CRC][UTF-8载荷]
|
||||||
|
header = _HEADER.pack(len(payload) + 4)
|
||||||
|
crc_bytes = struct.pack("!I", crc)
|
||||||
|
sock.sendall(header + crc_bytes + payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 添加心跳机制
|
||||||
|
|
||||||
|
```python
|
||||||
|
import time
|
||||||
|
|
||||||
|
def listen_with_heartbeat(self, on_message, interval=30):
|
||||||
|
last_recv = time.time()
|
||||||
|
while self._running:
|
||||||
|
# 发送心跳
|
||||||
|
if time.time() - last_recv > interval:
|
||||||
|
send(self._sock, "PING")
|
||||||
|
# 接收消息
|
||||||
|
text = recv(self._sock, timeout=1.0)
|
||||||
|
if text == "PONG":
|
||||||
|
continue
|
||||||
|
last_recv = time.time()
|
||||||
|
on_message(text)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 扩展协议版本
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 在头中添加版本号
|
||||||
|
_VERSION = 1 # 1 字节版本号
|
||||||
|
|
||||||
|
def send_v2(sock, data):
|
||||||
|
payload = data.encode("utf-8")
|
||||||
|
header = struct.pack("!BI", _VERSION, len(payload))
|
||||||
|
sock.sendall(header + payload)
|
||||||
|
|
||||||
|
def recv_v2(sock):
|
||||||
|
header = _recv_exact(sock, 5)
|
||||||
|
version, length = struct.unpack("!BI", header)
|
||||||
|
if version != _VERSION:
|
||||||
|
raise ValueError(f"Unsupported protocol version: {version}")
|
||||||
|
payload = _recv_exact(sock, length)
|
||||||
|
return payload.decode("utf-8")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.4 构建与测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 测试
|
||||||
|
cd pymsg
|
||||||
|
python -c "
|
||||||
|
from pymsg import MsgClient, MsgServer, send, recv
|
||||||
|
# ... 单元测试
|
||||||
|
"
|
||||||
|
|
||||||
|
# 黑盒测试
|
||||||
|
cd tests/blackbox
|
||||||
|
python test_runner.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、版本历史
|
||||||
|
|
||||||
|
| 版本 | 日期 | 主要变更 |
|
||||||
|
|------|------|---------|
|
||||||
|
| 0.1.0 | 当前 | 初始版本:核心协议、MsgClient、MsgServer、跨语言实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录
|
||||||
|
|
||||||
|
### A. 协议规范(RFC 风格)
|
||||||
|
|
||||||
|
```
|
||||||
|
Protocol: pymsg v0
|
||||||
|
|
||||||
|
Frame Format:
|
||||||
|
┌──────────┬──────────┬──────────────────┐
|
||||||
|
│ 4 bytes │ N bytes │ │
|
||||||
|
├──────────┼──────────┼──────────────────┤
|
||||||
|
│ uint32 │ UTF-8 │ │
|
||||||
|
│ big-end │ string │ │
|
||||||
|
│ payload │ payload │ │
|
||||||
|
│ length │ │ │
|
||||||
|
└──────────┴──────────┴──────────────────┘
|
||||||
|
|
||||||
|
Encoding: UTF-8
|
||||||
|
Byte Order: Big-Endian (network byte order)
|
||||||
|
Max Message Size: 2^32 - 1 bytes (theoretical)
|
||||||
|
Timeout: Optional, specified per operation
|
||||||
|
|
||||||
|
Modes:
|
||||||
|
- Short Connection (send/call): connect → send → recv → close
|
||||||
|
- Long Connection (listen): connect → loop(recv → callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### B. 错误码映射
|
||||||
|
|
||||||
|
| 异常 | 来源 | 场景 |
|
||||||
|
|------|------|------|
|
||||||
|
| `TimeoutError` | pymsg | 操作超过指定超时 |
|
||||||
|
| `ConnectionError` | Python | 连接在接收前断开 |
|
||||||
|
| `ConnectionRefusedError` | Python | 目标服务不可达 |
|
||||||
|
| `RuntimeError` | Python | 在 `connect()` 前调用 `listen()` |
|
||||||
|
| `MsgTimeoutException` | C++ | C++ 版超时 |
|
||||||
|
| `MsgConnectionException` | C++ | C++ 版连接异常 |
|
||||||
|
|
||||||
|
### C. 参考实现清单
|
||||||
|
|
||||||
|
| 语言 | 文件 | 类型 |
|
||||||
|
|------|------|------|
|
||||||
|
| Python | [pymsg/__init__.py](file:///Users/qingsong/micro_servers/pymsg/pymsg/__init__.py) | 主实现 |
|
||||||
|
| Python | [tests/blackbox/helpers.py](file:///Users/qingsong/micro_servers/tests/blackbox/helpers.py) | 测试客户端 |
|
||||||
|
| C++ | [gateway/inc/Msg.hpp](file:///Users/qingsong/micro_servers/gateway/inc/Msg.hpp) | gateway 客户端 |
|
||||||
|
| Go | [waf/internal/logclient/logclient.go](file:///Users/qingsong/micro_servers/waf/internal/logclient/logclient.go) | waf 客户端 |
|
||||||
|
|
||||||
|
### D. 参考资源
|
||||||
|
|
||||||
|
- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) — Socket 编程经典
|
||||||
|
- [The SO_REUSEADDR socket option](https://man7.org/linux/man-pages/man2/socket.2.html)
|
||||||
|
- [Python socket module](https://docs.python.org/3/library/socket.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*本文档基于 pymsg 源码分析生成,供开发团队参考。*
|
||||||
Reference in new issue
Block a user