feat(gomsg): pymsg/libmsg 协议的 Go 实现,waf logclient 抽取接入
- 新增 gomsg/:帧协议(4B 大端长度 + UTF-8,64MiB 上限)、MsgClient/MsgServer、 RpcClient/RpcServer(_route 分发、错误脱敏、panic 恢复、json.Number 保精度)、 AsyncQueue[T]、LogClient(集中式日志,级别过滤 + fail-safe),线协议与 Python/C++ 字节级兼容 - waf/internal/logclient 改为 gomsg 薄适配层(336→20 行),main/engine/proxy 零改动 - gomsg 26 项单测全绿(-race);waf 编译通过
This commit is contained in:
1 parent
0ed03ac095
commit
c4154d4db9
16 files changed
+1624
-317
No files matched your search
+224
@@ -0,0 +1,224 @@
|
||||
package gomsg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// JSON RPC 约定层(与 Python pymsg.rpc / C++ libmsg RpcClient/RpcServer 兼容):
|
||||
//
|
||||
// 请求: {"_route": "<路由名>", ...业务字段}
|
||||
// 响应: {"success": true, "data": ...}
|
||||
// {"success": false, "error": "..."}
|
||||
//
|
||||
// 数字一律以 json.Number 保留原始精度(避免 int64 经 float64 丢失)。
|
||||
|
||||
// RpcError 远端返回 success=false 的业务错误,或响应不符合 RPC 约定。
|
||||
type RpcError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *RpcError) Error() string { return e.msg }
|
||||
|
||||
// RpcHandler 处理一条 RPC 请求,返回值放入 data,错误包装为 success=false。
|
||||
type RpcHandler func(req map[string]any) (any, error)
|
||||
|
||||
// RpcClient JSON RPC 客户端(复用 MsgClient 长连接)。
|
||||
type RpcClient struct {
|
||||
client *MsgClient
|
||||
}
|
||||
|
||||
// NewRpcClient 创建客户端。timeout 为 0 时使用 DefaultTimeout。
|
||||
func NewRpcClient(addr string, timeout time.Duration) *RpcClient {
|
||||
return &RpcClient{client: NewMsgClient(addr, timeout)}
|
||||
}
|
||||
|
||||
// Call 调用指定路由,返回响应中的 data 字段(可能为 nil)。
|
||||
// success=false 或非法响应时返回 *RpcError。
|
||||
func (c *RpcClient) Call(route string, payload map[string]any) (any, error) {
|
||||
req := make(map[string]any, len(payload)+1)
|
||||
for k, v := range payload {
|
||||
req[k] = v
|
||||
}
|
||||
req["_route"] = route
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.client.Call(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var obj map[string]any
|
||||
dec := json.NewDecoder(bytes.NewReader(resp))
|
||||
dec.UseNumber()
|
||||
if err := dec.Decode(&obj); err != nil || obj == nil {
|
||||
return nil, &RpcError{msg: fmt.Sprintf("响应不是合法 JSON: %v", err)}
|
||||
}
|
||||
success, ok := obj["success"].(bool)
|
||||
if !ok {
|
||||
return nil, &RpcError{msg: fmt.Sprintf("非法 RPC 响应: %s", truncate(resp))}
|
||||
}
|
||||
if !success {
|
||||
errMsg, _ := obj["error"].(string)
|
||||
return nil, &RpcError{msg: errMsg}
|
||||
}
|
||||
return obj["data"], nil
|
||||
}
|
||||
|
||||
// Close 关闭底层连接。
|
||||
func (c *RpcClient) Close() error { return c.client.Close() }
|
||||
|
||||
// RpcServer JSON RPC 服务端:基于 MsgServer 的路由分发与响应包装。
|
||||
type RpcServer struct {
|
||||
*MsgServer
|
||||
routes map[string]RpcHandler
|
||||
mu sync.RWMutex
|
||||
// OnError 可选钩子:handler 抛错时调用(用于记日志)。
|
||||
OnError func(route string, err error)
|
||||
// JSONMarshal 响应序列化函数,默认 json.Marshal(对应 Python 的 json_default)。
|
||||
JSONMarshal func(v any) ([]byte, error)
|
||||
}
|
||||
|
||||
// NewRpcServer 创建 RPC 服务端(不监听,需调用 Start)。
|
||||
func NewRpcServer(addr string) *RpcServer {
|
||||
s := &RpcServer{routes: make(map[string]RpcHandler)}
|
||||
s.MsgServer = NewMsgServer(addr, s.dispatch)
|
||||
return s
|
||||
}
|
||||
|
||||
// AddRoute 注册路由处理函数。
|
||||
func (s *RpcServer) AddRoute(name string, fn RpcHandler) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.routes[name] = fn
|
||||
}
|
||||
|
||||
// dispatch 解析请求、查找路由、包装响应;任何输入错误都不向客户端回显内部细节。
|
||||
// handler 内 panic 会被恢复并包装为 success=false(对应 Python 的 except Exception)。
|
||||
func (s *RpcServer) dispatch(payload []byte) (reply []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err := fmt.Errorf("handler panic: %v", r)
|
||||
if s.OnError != nil {
|
||||
s.OnError("", err)
|
||||
}
|
||||
reply = s.reply(false, nil, sanitizeError(err.Error()))
|
||||
}
|
||||
}()
|
||||
|
||||
req, err := decodeRequest(payload)
|
||||
if err != nil {
|
||||
return s.reply(false, nil, "invalid JSON request")
|
||||
}
|
||||
|
||||
routeVal, has := req["_route"]
|
||||
delete(req, "_route")
|
||||
route := ""
|
||||
switch r := routeVal.(type) {
|
||||
case string:
|
||||
route = r
|
||||
case nil:
|
||||
// 保持 Python 行为:缺省 _route 时按空路由查找(未知路由)
|
||||
default:
|
||||
if has {
|
||||
route = fmt.Sprintf("%v", r)
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
handler := s.routes[route]
|
||||
s.mu.RUnlock()
|
||||
if handler == nil {
|
||||
return s.reply(false, nil, "未知路由: "+route)
|
||||
}
|
||||
|
||||
data, err := handler(req)
|
||||
if err != nil {
|
||||
if s.OnError != nil {
|
||||
s.OnError(route, err)
|
||||
}
|
||||
return s.reply(false, nil, sanitizeError(err.Error()))
|
||||
}
|
||||
return s.reply(true, data, "")
|
||||
}
|
||||
|
||||
func (s *RpcServer) reply(success bool, data any, errMsg string) []byte {
|
||||
resp := map[string]any{"success": success}
|
||||
if data != nil {
|
||||
resp["data"] = data
|
||||
}
|
||||
if errMsg != "" {
|
||||
resp["error"] = errMsg
|
||||
}
|
||||
marshal := s.JSONMarshal
|
||||
if marshal == nil {
|
||||
marshal = json.Marshal
|
||||
}
|
||||
out, err := marshal(resp)
|
||||
if err != nil {
|
||||
out, _ = json.Marshal(map[string]any{
|
||||
"success": false,
|
||||
"error": sanitizeError(err.Error()),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// decodeRequest 解析请求 JSON:UseNumber 保留整数精度;
|
||||
// Go 的 encoding/json 内置 10000 层嵌套上限,数字位数字符串化保存,
|
||||
// 输入大小又受帧协议 64 MiB 上限约束,天然规避深度/超大数字 DoS。
|
||||
func decodeRequest(payload []byte) (map[string]any, error) {
|
||||
dec := json.NewDecoder(bytes.NewReader(payload))
|
||||
dec.UseNumber()
|
||||
var req map[string]any
|
||||
if err := dec.Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("empty request")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// pathRe 匹配绝对路径(如 /opt/micro_servers/db/xxx.py),用于脱敏异常信息。
|
||||
var pathRe = regexp.MustCompile(`/[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)*`)
|
||||
|
||||
// sanitizeError 脱敏异常信息中的绝对路径等内部细节,保留业务错误语义
|
||||
// (对齐 Python pymsg._sanitize_error:前导字符为单词字符时不匹配)。
|
||||
func sanitizeError(msg string) string {
|
||||
if msg == "" {
|
||||
return msg
|
||||
}
|
||||
var b strings.Builder
|
||||
last := 0
|
||||
for _, idx := range pathRe.FindAllStringIndex(msg, -1) {
|
||||
start, end := idx[0], idx[1]
|
||||
if start > 0 && isWordChar(msg[start-1]) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(msg[last:start])
|
||||
b.WriteString("<redacted>")
|
||||
last = end
|
||||
}
|
||||
b.WriteString(msg[last:])
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func isWordChar(c byte) bool {
|
||||
return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
}
|
||||
|
||||
func truncate(b []byte) string {
|
||||
const limit = 200
|
||||
if len(b) <= limit {
|
||||
return string(b)
|
||||
}
|
||||
return string(b[:limit]) + "..."
|
||||
}
|
||||
Reference in new issue
Block a user