Skip to content

服务注册表

sing-box 使用 Go 的 context.Context 作为服务容器进行依赖注入。这消除了全局单例,使依赖图更加明确。

源码: github.com/sagernet/sing/service, box.go, include/

工作原理

sing/service 包提供了类型化的服务注册:

go
// 在 context 中注册服务
func ContextWith[T any](ctx context.Context, service T) context.Context

// 从 context 中获取服务
func FromContext[T any](ctx context.Context) T

// 注册时若重复则 panic
func MustRegister[T any](ctx context.Context, service T)

服务以其接口类型而非具体类型作为键。这意味着:

go
// 注册 NetworkManager
service.MustRegister[adapter.NetworkManager](ctx, networkManager)

// 任何持有 context 的代码都可以获取它
nm := service.FromContext[adapter.NetworkManager](ctx)

在 Box.New() 中注册

Box.New() 期间,所有管理器都会被注册:

go
// 创建管理器
endpointManager := endpoint.NewManager(...)
inboundManager := inbound.NewManager(...)
outboundManager := outbound.NewManager(...)
dnsTransportManager := dns.NewTransportManager(...)
serviceManager := boxService.NewManager(...)

// 在 context 中注册
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)

// 同样注册路由器、网络管理器、DNS 路由器、连接管理器
service.MustRegister[adapter.Router](ctx, router)
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)

注册表模式

协议类型通过类型化注册表进行注册:

go
type InboundRegistry interface {
    option.InboundOptionsRegistry
    Create(ctx, router, logger, tag, inboundType string, options any) (Inbound, error)
}

type OutboundRegistry interface {
    option.OutboundOptionsRegistry
    CreateOutbound(ctx, router, logger, tag, outboundType string, options any) (Outbound, error)
}

注册表如何填充

include/ 包使用构建标签来注册协议类型:

go
// include/inbound.go
func InboundRegistry() *inbound.Registry {
    registry := inbound.NewRegistry()
    tun.RegisterInbound(registry)
    socks.RegisterInbound(registry)
    http.RegisterInbound(registry)
    mixed.RegisterInbound(registry)
    direct.RegisterInbound(registry)
    // ... 所有协议类型
    return registry
}

每个协议自行注册:

go
// protocol/vless/inbound.go
func RegisterInbound(registry adapter.InboundRegistry) {
    inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound)
}

泛型 Register 函数建立映射:类型字符串 -> 选项类型 -> 工厂函数。

Context 初始化

box.Context() 函数使用注册表准备 context:

go
func Context(
    ctx context.Context,
    inboundRegistry adapter.InboundRegistry,
    outboundRegistry adapter.OutboundRegistry,
    endpointRegistry adapter.EndpointRegistry,
    dnsTransportRegistry adapter.DNSTransportRegistry,
    serviceRegistry adapter.ServiceRegistry,
) context.Context {
    ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
    ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
    // ... 等等
    return ctx
}

这在 Box.New() 之前调用,通常在 cmd/sing-box/main.go 中:

go
ctx = box.Context(ctx,
    include.InboundRegistry(),
    include.OutboundRegistry(),
    include.EndpointRegistry(),
    include.DNSTransportRegistry(),
    include.ServiceRegistry(),
)
instance, err := box.New(box.Options{
    Context: ctx,
    Options: options,
})

双重注册

选项注册表和适配器注册表都会被注册:

go
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)

选项注册表在 JSON 解析期间用于确定正确的选项结构体类型。适配器注册表在 Box.New() 期间用于从选项创建实例。

在组件中使用

任何拥有 context 的组件都可以获取服务:

go
// 在 Router 构造函数中
func NewRouter(ctx context.Context, ...) *Router {
    return &Router{
        inbound:  service.FromContext[adapter.InboundManager](ctx),
        outbound: service.FromContext[adapter.OutboundManager](ctx),
        dns:      service.FromContext[adapter.DNSRouter](ctx),
        network:  service.FromContext[adapter.NetworkManager](ctx),
        // ...
    }
}

与 Xray-core 的对比

方面Xray-coresing-box
依赖注入模式RequireFeatures + 反射基于 context 的类型化查找
注册方式Instance 上的全局 feature 注册表每个 context 独立的服务注册表
解析方式延迟解析(所有依赖就绪时)即时解析(创建时立即解析)
类型安全运行时类型断言编译时泛型
生命周期Feature.Start() 由 Instance 调用多阶段 Start(stage)