DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,415 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"longheng.io/server/internal/platform/runtimeguard"
|
||||
"longheng.io/server/pkg/contracts"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// ErrTransportClosed 表示 gRPC transport 已关闭或未正确装配。
|
||||
var ErrTransportClosed = errors.New("rpc transport is closed")
|
||||
|
||||
// ErrTransportCredReq 用于 errors.Is 判断生产强制 TLS/mTLS 时仍缺少 gRPC 传输凭据的配置错误。
|
||||
var ErrTransportCredReq = errors.New("rpc transport credentials are required")
|
||||
|
||||
// GRPCTransportOptions 是 gRPC transport 的监听、节点表和安全凭据配置。
|
||||
type GRPCTransportOptions struct {
|
||||
ListenAddress string
|
||||
Environment string
|
||||
Peers map[string]string
|
||||
ServerCredentials credentials.TransportCredentials
|
||||
DialCredentials credentials.TransportCredentials
|
||||
RequireTransportSecurity bool
|
||||
ServerOptions []grpc.ServerOption
|
||||
DialOptions []grpc.DialOption
|
||||
}
|
||||
|
||||
// GRPCTransport 使用 gRPC 在不同节点之间传递 RPC 请求和响应。
|
||||
type GRPCTransport struct {
|
||||
mu sync.RWMutex
|
||||
listener net.Listener
|
||||
server *grpc.Server
|
||||
listenAddress string
|
||||
peers map[string]string
|
||||
requestHandlers map[string]RequestHandler
|
||||
responseHandlers map[string]ResponseHandler
|
||||
conns map[string]*grpc.ClientConn
|
||||
dialCredentials credentials.TransportCredentials
|
||||
dialOptions []grpc.DialOption
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewGRPCTransport 创建并启动一个 gRPC transport。
|
||||
func NewGRPCTransport(options GRPCTransportOptions) (*GRPCTransport, error) {
|
||||
address := strings.TrimSpace(options.ListenAddress)
|
||||
if address == "" {
|
||||
address = "127.0.0.1:0"
|
||||
}
|
||||
listener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
needSecureTransport := options.RequireTransportSecurity || runtimeguard.IsProductionEnvironment(options.Environment)
|
||||
if needSecureTransport && (options.ServerCredentials == nil || options.DialCredentials == nil) {
|
||||
_ = listener.Close()
|
||||
return nil, ErrTransportCredReq
|
||||
}
|
||||
serverOptions := []grpc.ServerOption{grpc.ForceServerCodec(rpcJSONCodec{})}
|
||||
if options.ServerCredentials != nil {
|
||||
serverOptions = append(serverOptions, grpc.Creds(options.ServerCredentials.Clone()))
|
||||
}
|
||||
serverOptions = append(serverOptions, options.ServerOptions...)
|
||||
transport := &GRPCTransport{
|
||||
listener: listener,
|
||||
server: grpc.NewServer(serverOptions...),
|
||||
listenAddress: listener.Addr().String(),
|
||||
peers: cloneStringMap(options.Peers),
|
||||
requestHandlers: make(map[string]RequestHandler),
|
||||
responseHandlers: make(map[string]ResponseHandler),
|
||||
conns: make(map[string]*grpc.ClientConn),
|
||||
dialCredentials: cloneTransportCreds(options.DialCredentials),
|
||||
dialOptions: append([]grpc.DialOption(nil), options.DialOptions...),
|
||||
}
|
||||
regGRPCWire(transport.server, transport)
|
||||
go func() {
|
||||
_ = transport.server.Serve(listener)
|
||||
}()
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// ListenAddress 返回实际监听地址,端口为 0 时可用于发现系统分配端口。
|
||||
func (t *GRPCTransport) ListenAddress() string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.listenAddress
|
||||
}
|
||||
|
||||
// SetPeer 设置或删除目标节点到 gRPC 地址的映射。
|
||||
func (t *GRPCTransport) SetPeer(nodeID, address string) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
address = strings.TrimSpace(address)
|
||||
if nodeID == "" {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
if t.peers == nil {
|
||||
t.peers = make(map[string]string)
|
||||
}
|
||||
if address == "" {
|
||||
delete(t.peers, nodeID)
|
||||
} else {
|
||||
t.peers[nodeID] = address
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
// SubscribeRequests 注册当前节点的请求处理函数。
|
||||
func (t *GRPCTransport) SubscribeRequests(_ context.Context, nodeID string, handler RequestHandler) (TransportSubscription, error) {
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if nodeID == "" {
|
||||
return nil, ErrInvalidNodeID
|
||||
}
|
||||
if handler == nil {
|
||||
return nil, ErrInvalidRoute
|
||||
}
|
||||
if t == nil {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.closed {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
t.requestHandlers[nodeID] = handler
|
||||
return transportSub(func() {
|
||||
t.mu.Lock()
|
||||
delete(t.requestHandlers, nodeID)
|
||||
t.mu.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
// SubscribeResponses 注册当前节点的响应处理函数。
|
||||
func (t *GRPCTransport) SubscribeResponses(_ context.Context, nodeID string, handler ResponseHandler) (TransportSubscription, error) {
|
||||
nodeID = strings.TrimSpace(nodeID)
|
||||
if nodeID == "" {
|
||||
return nil, ErrInvalidNodeID
|
||||
}
|
||||
if handler == nil {
|
||||
return nil, ErrInvalidRoute
|
||||
}
|
||||
if t == nil {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.closed {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
t.responseHandlers[nodeID] = handler
|
||||
return transportSub(func() {
|
||||
t.mu.Lock()
|
||||
delete(t.responseHandlers, nodeID)
|
||||
t.mu.Unlock()
|
||||
}), nil
|
||||
}
|
||||
|
||||
// PublishRequest 通过 gRPC 把请求发送到目标节点。
|
||||
func (t *GRPCTransport) PublishRequest(ctx context.Context, targetNodeID string, request contracts.RPCRequest) error {
|
||||
request.TargetNodeID = strings.TrimSpace(targetNodeID)
|
||||
return t.invoke(ctx, request.TargetNodeID, "/longheng.rpc.Transport/Request", &request, &grpcAck{})
|
||||
}
|
||||
|
||||
// PublishResponse 通过 gRPC 把响应发送到目标节点。
|
||||
func (t *GRPCTransport) PublishResponse(ctx context.Context, targetNodeID string, response contracts.RPCResponse) error {
|
||||
response.TargetNodeID = strings.TrimSpace(targetNodeID)
|
||||
return t.invoke(ctx, response.TargetNodeID, "/longheng.rpc.Transport/Response", &response, &grpcAck{})
|
||||
}
|
||||
|
||||
// Close 停止 gRPC server 并关闭所有已缓存连接。
|
||||
func (t *GRPCTransport) Close() error {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
t.mu.Lock()
|
||||
if t.closed {
|
||||
t.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
t.closed = true
|
||||
server := t.server
|
||||
conns := t.conns
|
||||
t.conns = nil
|
||||
t.requestHandlers = nil
|
||||
t.responseHandlers = nil
|
||||
t.mu.Unlock()
|
||||
if server != nil {
|
||||
server.Stop()
|
||||
}
|
||||
var firstErr error
|
||||
for _, conn := range conns {
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
if err := conn.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (t *GRPCTransport) request(ctx context.Context, request *contracts.RPCRequest) (*grpcAck, error) {
|
||||
if request == nil {
|
||||
return nil, ErrInvalidRequestID
|
||||
}
|
||||
t.mu.RLock()
|
||||
handler := t.requestHandlers[strings.TrimSpace(request.TargetNodeID)]
|
||||
t.mu.RUnlock()
|
||||
if handler == nil {
|
||||
return nil, ErrNoTargetNodes
|
||||
}
|
||||
return &grpcAck{}, handler(ctx, *request)
|
||||
}
|
||||
|
||||
func (t *GRPCTransport) response(ctx context.Context, response *contracts.RPCResponse) (*grpcAck, error) {
|
||||
if response == nil {
|
||||
return nil, ErrInvalidRequestID
|
||||
}
|
||||
t.mu.RLock()
|
||||
handler := t.responseHandlers[strings.TrimSpace(response.TargetNodeID)]
|
||||
t.mu.RUnlock()
|
||||
if handler == nil {
|
||||
return nil, ErrNoTargetNodes
|
||||
}
|
||||
return &grpcAck{}, handler(ctx, *response)
|
||||
}
|
||||
|
||||
func (t *GRPCTransport) invoke(ctx context.Context, nodeID, method string, in any, out any) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if strings.TrimSpace(nodeID) == "" {
|
||||
return ErrInvalidTarget
|
||||
}
|
||||
conn, err := t.clientConn(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.Invoke(ctx, method, in, out, grpc.ForceCodec(rpcJSONCodec{}))
|
||||
}
|
||||
|
||||
func (t *GRPCTransport) clientConn(nodeID string) (*grpc.ClientConn, error) {
|
||||
if t == nil {
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
t.mu.RLock()
|
||||
if t.closed {
|
||||
t.mu.RUnlock()
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
address := strings.TrimSpace(t.peers[strings.TrimSpace(nodeID)])
|
||||
if address == "" {
|
||||
t.mu.RUnlock()
|
||||
return nil, ErrNoTargetNodes
|
||||
}
|
||||
if conn := t.conns[address]; conn != nil {
|
||||
t.mu.RUnlock()
|
||||
return conn, nil
|
||||
}
|
||||
t.mu.RUnlock()
|
||||
|
||||
options := []grpc.DialOption{
|
||||
grpc.WithDefaultCallOptions(grpc.ForceCodec(rpcJSONCodec{})),
|
||||
}
|
||||
if t.dialCredentials != nil {
|
||||
options = append(options, grpc.WithTransportCredentials(t.dialCredentials.Clone()))
|
||||
} else {
|
||||
options = append(options, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
}
|
||||
t.mu.RLock()
|
||||
options = append(options, t.dialOptions...)
|
||||
t.mu.RUnlock()
|
||||
conn, err := grpc.NewClient(grpcTarget(address), options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.mu.Lock()
|
||||
if t.closed {
|
||||
t.mu.Unlock()
|
||||
_ = conn.Close()
|
||||
return nil, ErrTransportClosed
|
||||
}
|
||||
if existing := t.conns[address]; existing != nil {
|
||||
t.mu.Unlock()
|
||||
_ = conn.Close()
|
||||
return existing, nil
|
||||
}
|
||||
t.conns[address] = conn
|
||||
t.mu.Unlock()
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func cloneTransportCreds(creds credentials.TransportCredentials) credentials.TransportCredentials {
|
||||
if creds == nil {
|
||||
return nil
|
||||
}
|
||||
return creds.Clone()
|
||||
}
|
||||
|
||||
type transportSub func()
|
||||
|
||||
// Close 注销 transport 订阅。
|
||||
func (s transportSub) Close() error {
|
||||
if s != nil {
|
||||
s()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rpcJSONCodec struct{}
|
||||
|
||||
// Name 返回 gRPC codec 名称。
|
||||
func (rpcJSONCodec) Name() string {
|
||||
return "json"
|
||||
}
|
||||
|
||||
// Marshal 把 RPC 结构编码为 JSON 字节。
|
||||
func (rpcJSONCodec) Marshal(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// Unmarshal 把 JSON 字节解码为 RPC 结构。
|
||||
func (rpcJSONCodec) Unmarshal(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
type grpcAck struct{}
|
||||
|
||||
type grpcTransportService interface {
|
||||
request(context.Context, *contracts.RPCRequest) (*grpcAck, error)
|
||||
response(context.Context, *contracts.RPCResponse) (*grpcAck, error)
|
||||
}
|
||||
|
||||
var grpcServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "longheng.rpc.Transport",
|
||||
HandlerType: (*grpcTransportService)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{MethodName: "Request", Handler: grpcRequestHandler},
|
||||
{MethodName: "Response", Handler: grpcResponseHandler},
|
||||
},
|
||||
}
|
||||
|
||||
func grpcRequestHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||
in := new(contracts.RPCRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(grpcTransportService).request(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: "/longheng.rpc.Transport/Request"}
|
||||
handler := func(ctx context.Context, req any) (any, error) {
|
||||
return srv.(grpcTransportService).request(ctx, req.(*contracts.RPCRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func grpcResponseHandler(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
|
||||
in := new(contracts.RPCResponse)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(grpcTransportService).response(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: "/longheng.rpc.Transport/Response"}
|
||||
handler := func(ctx context.Context, req any) (any, error) {
|
||||
return srv.(grpcTransportService).response(ctx, req.(*contracts.RPCResponse))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func regGRPCWire(server *grpc.Server, transport *GRPCTransport) {
|
||||
server.RegisterService(&grpcServiceDesc, transport)
|
||||
}
|
||||
|
||||
func grpcTarget(address string) string {
|
||||
address = strings.TrimSpace(address)
|
||||
if strings.Contains(address, "://") {
|
||||
return address
|
||||
}
|
||||
return "passthrough:///" + address
|
||||
}
|
||||
|
||||
func cloneStringMap(values map[string]string) map[string]string {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(values))
|
||||
for key, value := range values {
|
||||
key = strings.TrimSpace(key)
|
||||
value = strings.TrimSpace(value)
|
||||
if key != "" && value != "" {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user