背景有天打开nas发现存储空间损毁，有块盘找不到了，于是想重启重新插拔一下硬盘。结果无法开机（）于是接了显示器，发现引导损坏（进不去rr）修复引导其实算不上修复，重新烧录一份引导就好。参考rr官方的烧录教程，然后把引导u盘插到nas上开机，成功进入rr界面。会提示Found an installed DSM system and restored it. Please rebuild and boot。此时只要选择尝试恢复已安装的DSM系统就行，会自动读取系统配置（包括型号、插件、模块信息等）。随后编译并启动即可。RAID1降级原本是2块4t sata ssd组成的raid1，既然坏了一块硬盘，那就索性降级到basic，后续使用冷备份或者上云等手段保证数据安全。ssh登录到群晖，并sudo -i切换到root用户cat /proc/mdstat查看分区情况root@GK_NAS:~# cat /proc/mdstat Personalities : [raid1] md3 : active raid1 sata2p3[0] 7803302208 blocks super 1.2 [1/1] [U] md2 : active raid1 sata1p3[1] 3740013888 blocks super 1.2 [2/1] [_U] md1 : active raid1 sata1p2[1] sata2p2[2] 2097088 blocks [4/2] [_UU_] md0 : active raid1 sata1p1[1] sata2p1[2] 8388544 blocks [4/2] [_UU_] unused devices: <none>可以看到，损坏的raid1分区为md2（显示[2/1]）md0为群晖系统分区，md1为群晖交换分区，其余的应该就是存储分区。mdadm --grow --raid-devices=1 --force /dev/md2进行强制降级到basicmdadm：这是一个用于管理软件 RAID 阵列的 Linux 程序。--grow：这个选项用于修改现有的 RAID 阵列，比如更改设备数量、布局或其他属性。--raid-devices=1：这个选项指定 RAID 阵列应该具有的新设备数量。在当前情况下，将其设置为 1，这实际上意味着将 RAID 阵列缩小为单个设备，即降级为Basic模式。此时群晖存储空间里就能看到降级成功了
上一篇中，我们梳理了server.Server启动、运行、退出的全流程。我们知道server.Server通过调用remotesvr.Server提供的接口进行数据传输，而不需要关心连接具体如何建立、数据如何封装的问题。这篇我们就来分析一下remotesvr.Server是如何与客户端建立连接并交互的。remote.ServerTransHandler与客户端建立连接、传输数据等工作，remotesvr.Serve是调用了remote.TransServer提供的方法实现的：// remotesvr.Server type server struct { opt *remote.ServerOption listener net.Listener transSvr remote.TransServer sync.Mutex }而remote.TransServer又是依赖ServerTransHandler实现的这些功能：// remote.TransServer type TransServerFactory interface { NewTransServer(opt *ServerOption, transHdlr ServerTransHandler) TransServer } // NewTransServer implements the remote.TransServerFactory interface. func (f *gonetTransServerFactory) NewTransServer(opt *remote.ServerOption, transHdlr remote.ServerTransHandler) remote.TransServer { return &transServer{ opt: opt, transHdlr: transHdlr, lncfg: trans.NewListenConfig(opt), } }以下是ServerTransHandler提供的方法：// TransHandler is similar to the handler role in netty // Transport can be refactored to support pipeline, and then is able to support other extensions at conn level. type TransHandler interface { // read() write() TransReadWriter // 关闭连接 OnInactive(ctx context.Context, conn net.Conn) // 处理请求出错 OnError(ctx context.Context, err error, conn net.Conn) // 请求处理完成后调用，调用先前构建的中间件调用链进行进一步处理 OnMessage(ctx context.Context, args, result Message) (context.Context, error) SetPipeline(pipeline *TransPipeline) } // ServerTransHandler have some new functions. type ServerTransHandler interface { TransHandler // 新建连接 OnActive(ctx context.Context, conn net.Conn) (context.Context, error) // 收到请求 OnRead(ctx context.Context, conn net.Conn) error }可见remote.ServerTransHandler是整个流程的核心，与客户端的连接基本是由它来负责的，那接下来就看一下remote.ServerTransHandler创建的过程：func (s *server) newSvrTransHandler() (handler remote.ServerTransHandler, err error) { // 工厂模式创建 transHdlrFactory := s.opt.RemoteOpt.SvrHandlerFactory transHdlr, err := transHdlrFactory.NewTransHandler(s.opt.RemoteOpt) if err != nil { return nil, err } // 判断是否实现 InvokeHandleFuncSetter 接口 if setter, ok := transHdlr.(remote.InvokeHandleFuncSetter); ok { // 将服务器已经构建好的、包含所有中间件的最终处理端点 s.eps 设置给传输处理器 setter.SetInvokeHandleFunc(s.eps) } // 创建代理类，并将核心处理器 transHdlr 作为管道的基础 transPl := remote.NewTransPipeline(transHdlr) for _, ib := range s.opt.RemoteOpt.Inbounds { transPl.AddInboundHandler(ib) } for _, ob := range s.opt.RemoteOpt.Outbounds { transPl.AddOutboundHandler(ob) } return transPl, nil }newSvrTransHandler()返回的是一个代理类TransPipeline。类似装饰器模式，TransPipeline包装了ServerTransHandler，并添加了inboundHdrls与outboundHdrls两个组件，这样可以在不修改核心处理器transHdlr的情况下，动态的为请求处理流程增加功能，包括限流、连接数控制、日志记录等功能。TransPipeline类的核心ServerTransHandler是通过工厂模式创建的，我们顺着server初始化的过程一路查下去（newServer() -> NewOptions() -> newServerRemoteOption() -> newServerRemoteOption()），最后可以看到SvrHandlerFactory是通过detection.NewSvrTransHandlerFactory()创建的：func newServerRemoteOption() *remote.ServerOption { return &remote.ServerOption{ ... SvrHandlerFactory: detection.NewSvrTransHandlerFactory(gonet.NewSvrTransHandlerFactory(), nphttp2.NewSvrTransHandlerFactory(), ttstream.NewSvrTransHandlerFactory()), ... } } // detection.NewSvrTransHandlerFactory func NewSvrTransHandlerFactory(defaultHandlerFactory remote.ServerTransHandlerFactory, detectableHandlerFactory ...remote.ServerTransHandlerFactory, ) remote.ServerTransHandlerFactory { return &svrTransHandlerFactory{ defaultHandlerFactory: defaultHandlerFactory, detectableHandlerFactory: detectableHandlerFactory, } }kitex将gonet.NewSvrTransHandlerFactory()设置为默认的处理器工厂，而nphttp2与ttstream协议的处理器工厂则被指定为可检测协议的处理器（detectableHandler）工厂。detection.NewSvrTransHandler的作用是在请求到来时，依次使用可检测协议的处理器工厂匹配协议，如果所有detectableHandler都无法识别当前的连接协议，则会用默认的gonet处理器进行处理。这就实现了对多种传输协议的支持。但这仍然不是具体的实现，我们看gonet.NewSvrTransHandler相关的代码:type svrTransHandlerFactory struct{} // NewSvrTransHandlerFactory creates a default go net server transport handler factory. func NewSvrTransHandlerFactory() remote.ServerTransHandlerFactory { return &svrTransHandlerFactory{} } // NewTransHandler implements the remote.ServerTransHandlerFactory interface. func (f *svrTransHandlerFactory) NewTransHandler(opt *remote.ServerOption) (remote.ServerTransHandler, error) { return newSvrTransHandler(opt) } func newSvrTransHandler(opt *remote.ServerOption) (remote.ServerTransHandler, error) { return trans.NewDefaultSvrTransHandler(opt, NewGonetExtension()) }svrTransHandlerFactory实现了remote.ServerTransHandlerFactory接口，用于创建ServerTransHandler实例。可以看到gonet复用了trans.svrTransHandler，而不像nphttp2与ttstream实现了自己的svrTransHandler。接下来就详细看看trans.svrTransHandler的实现。trans.svrTransHandler上一篇中我们提到，s.transSvr.BootstrapServer(ln)是真正用于处理请求的循环，而BootstrapServer()又把处理请求的职责交给了serveConn(ctx context.Context, conn net.Conn) (err error)func (ts *transServer) serveConn(ctx context.Context, conn net.Conn) (err error) { ... // 初始化 ctx, err = ts.transHdlr.OnActive(ctx, bc) if err != nil { klog.CtxErrorf(ctx, "KITEX: OnActive error=%s", err) return err } for { ... // FIXME: for gRPC transHandler, OnRead should execute only once. err = ts.transHdlr.OnRead(ctx, bc) if err != nil { return err } } }可以看到serveConn()中先使用ts.transHdlr.OnActive(ctx, bc)进行初始化，然后调用了transHdlr的OnRead()方法进行处理。// OnActive implements the remote.ServerTransHandler interface. func (t *svrTransHandler) OnActive(ctx context.Context, conn net.Conn) (context.Context, error) { ctx = remote.WithServiceSearcher(ctx, t.svcSearcher) // init rpcinfo ri := t.opt.InitOrResetRPCInfoFunc(nil, conn.RemoteAddr()) return rpcinfo.NewCtxWithRPCInfo(ctx, ri), nil }OnActive 方法在每个新连接建立时执行一次，其主要任务是初始化一个与该连接绑定的 RPCInfo 对象（RPCInfo 是 Kitex 框架中用于存储单次 RPC 调用所有相关信息的核心结构，包括配置、统计数据、对端信息等），并将其与 ServiceSearcher一同存入 context 中，确保链路信息能够在整个请求处理周期中传递，为后续在该连接上处理具体的 RPC 请求做好准备。OnRead()方法是Kitex服务端处理网络连接上新数据到达的核心方法// OnRead implements the remote.ServerTransHandler interface. // The connection should be closed after returning error. func (t *svrTransHandler) OnRead(ctx context.Context, conn net.Conn) (err error) { ctx, ri := t.newCtxWithRPCInfo(ctx, conn) t.ext.SetReadTimeout(ctx, conn, ri.Config(), remote.Server) var recvMsg remote.Message var sendMsg remote.Message closeConnOutsideIfErr := true // 资源回收与错误处理 defer func() { var panicErr error // 捕获可能发生的panic // 关闭本次请求的链路追踪和性能分析 // 资源回收 // 重置RPCInfo对象，以便复用，减少gc压力 ... if rpcinfo.PoolEnabled() { t.opt.InitOrResetRPCInfoFunc(ri, conn.RemoteAddr()) } if err != nil && !closeConnOutsideIfErr { // when error is not nil, outside will close conn, // set err to nil to indicate that this kind of error does not require closing the connection err = nil } }() // 开始链路追踪与性能分析 ctx = t.startTracer(ctx, ri) ctx = t.startProfiler(ctx) recvMsg = remote.NewMessage(nil, ri, remote.Call, remote.Server) // 设置解码器 recvMsg.SetPayloadCodec(t.opt.PayloadCodec) // 从conn中读取请求信息 ctx, err = t.transPipe.Read(ctx, conn, recvMsg) if err != nil { t.writeErrorReplyIfNeeded(ctx, recvMsg, conn, err, ri, true, true) // t.OnError(ctx, err, conn) will be executed at outer function when transServer close the conn return err } // 判断是否为心跳，是的话就创建心跳回复信息，并跳过业务逻辑处理 if recvMsg.MessageType() == remote.Heartbeat { sendMsg = remote.NewMessage(nil, ri, remote.Heartbeat, remote.Server) } else { // reply processing methodInfo := ri.Invocation().MethodInfo() // 判断是否为单向调用，不是的话会生成一个空的result结构体 if methodInfo.OneWay() { sendMsg = remote.NewMessage(nil, ri, remote.Reply, remote.Server) } else { sendMsg = remote.NewMessage(methodInfo.NewResult(), ri, remote.Reply, remote.Server) } // 进入处理流程 ctx, err = t.transPipe.OnMessage(ctx, recvMsg, sendMsg) if err != nil { // error cannot be wrapped to print here, so it must exec before NewTransError t.OnError(ctx, err, conn) err = remote.NewTransError(remote.InternalError, err) if closeConn := t.writeErrorReplyIfNeeded(ctx, recvMsg, conn, err, ri, false, false); closeConn { return err } // connection don't need to be closed when the error is return by the server handler closeConnOutsideIfErr = false return } } // 设置编码器 sendMsg.SetPayloadCodec(t.opt.PayloadCodec) // 写回响应 if ctx, err = t.transPipe.Write(ctx, conn, sendMsg); err != nil { // t.OnError(ctx, err, conn) will be executed at outer function when transServer close the conn return err } return }OnRead()方法的总体流程大概是从连接中读取数据并解码成一个请求消息 (recvMsg)。处理该请求（如果是心跳则准备心跳回复，如果是普通请求则调用业务逻辑）。将业务逻辑的执行结果或错误编码成一个响应消息 (sendMsg)。将响应消息写回连接。在整个过程中进行 tracing（链路追踪）、profiling（性能分析）、错误处理和资源回收。Read()OnRead()方法调用了Read()去从连接中读取并解码// Read implements the remote.ServerTransHandler interface. func (t *svrTransHandler) Read(ctx context.Context, conn net.Conn, recvMsg remote.Message) (nctx context.Context, err error) { var bufReader remote.ByteBuffer ri := recvMsg.RPCInfo() defer func() { // 错误处理 ... // 记录结束事件 rpcinfo.Record(ctx, ri, stats.ReadFinish, err) }() // 记录一个 ReadStart 事件。这用于链路追踪和性能统计，标记着从连接读取数据的操作正式开始 rpcinfo.Record(ctx, ri, stats.ReadStart, nil) bufReader = t.ext.NewReadByteBuffer(ctx, conn, recvMsg) // 检查是否实现了 remote.MetaDecoder 接口 if codec, ok := t.codec.(remote.MetaDecoder); ok { // 先解码消息头 if err = codec.DecodeMeta(ctx, recvMsg, bufReader); err == nil { if t.opt.Profiler != nil && t.opt.ProfilerTransInfoTagging != nil && recvMsg.TransInfo() != nil { var tags []string ctx, tags = t.opt.ProfilerTransInfoTagging(ctx, recvMsg) ctx = t.opt.Profiler.Tag(ctx, tags...) } // 解码消息体 err = codec.DecodePayload(ctx, recvMsg, bufReader) } } else { // 不支持MetaDecoder则进行单阶段解码 err = t.codec.Decode(ctx, recvMsg, bufReader) } if err != nil { // 出现错误会给recvMsg打上一个ReadFail的标签，用于后续的错误处理 recvMsg.Tags()[remote.ReadFailed] = true return ctx, err } return ctx, nil }Read()方法会先从 Extension (t.ext)获取一个用于读取数据的 ByteBuffer，随后会判断t.codec是否实现了remote.MetaDecode接口，这是一种更优的解码方式，支持将消息头（Meta）和消息体（Payload）分开解码。实现了则使用 DecodeMeta 和 DecodePayload 进行两阶段解码，先解析消息头，再解析消息体。如果 Codec 不支持，则回退到使用 Decode 方法一次性解码整个消息。注意到在获取buffer之前，Read()先调用了rpcinfo.Record()方法，而在用于处理异常的buffer块中也调用了同样的方法，这个方法会记录读取开始与结束事件，用于链路追踪与性能统计。// Record records the event to RPCStats. func Record(ctx context.Context, ri RPCInfo, event stats.Event, err error) { if ctx == nil { return } st := ri.Stats() if st == nil { return } if err != nil { st.Record(ctx, event, stats.StatusError, err.Error()) } else { st.Record(ctx, event, stats.StatusInfo, "") } } // Record implements the RPCStats interface. // It only record once for each event, it will ignore follow events with the same Index() func (r *rpcStats) Record(ctx context.Context, e stats.Event, status stats.Status, info string) { if e.Level() > r.level { return } idx := e.Index() p := &r.eventStatus[idx] if atomic.CompareAndSwapUint32(p, eventUnset, eventUpdating) || (r.copied && atomic.CompareAndSwapUint32(p, eventStale, eventUpdating)) { r.eventMap[idx] = event{event: e, status: status, info: info, time: time.Now()} atomic.StoreUint32(p, eventRecorded) // done, make it visible to GetEvent } else { // eventRecorded? panic? } }这个辅助函数最终会调用 RPCStats 接口的 Record()方法。这个方法会使用 atomic.CompareAndSwapUint32 确保对于同一种 event（由 event.Index() 标识），只记录一次。这可以防止在重试等复杂场景下重复记录同一个事件点。当成功获取记录权限后，它会创建一个 event 结构体，包含事件类型、状态、信息和当前时间戳 time.Now()，并将其存储在 rpcStats 的 eventMap 数组中。关于atomic.CompareAndSwapUint32：这是一个“比较并交换”操作，是实现无锁并发编程的核心原语之一。它的行为可以分解为以下三个步骤，但这三个步骤是作为一个不可分割的原子操作完成的：读取:：读取 addr 指针指向的内存地址的当前值。比较：将读取到的当前值与 old 参数进行比较。交换：如果 当前值等于 old，那么就将 new 的值写入到 addr 指向的地址，并返回 true。如果 当前值不等于 old（意味着在当前 goroutine 读取和尝试写入的间隙，有其他 goroutine 修改了这个值），那么什么也不做，直接返回 false。 在Record()代码中，atomic.CompareAndSwapUint32是为了防止在重试等复杂场景下重复记录同一个事件点。if atomic.CompareAndSwapUint32(p, eventUnset, eventUpdating) || ...当一个 goroutine 调用 Record 时，它会尝试将事件状态从 eventUnset 原子地更新为 eventUpdating。如果 CompareAndSwapUint32 返回 true，说明它是第一个成功修改此事件状态的 goroutine。它就获得了“记录权”，可以继续执行 if 块内的逻辑来记录事件的详细信息。如果返回 false，说明在它执行 CAS 操作之前，另一个 goroutine 已经将状态从 eventUnset 修改掉了。当前 goroutine 就知道这个事件已经被记录过了，不会进入 if 块，从而避免了重复记录(r.copied && atomic.CompareAndSwapUint32(p, eventStale, eventUpdating)这个判断逻辑用于 RPC 重试场景：先通过r.copied检查当前 rpcStats 对象是否是从上一次失败的请求中复制过来的，如果是复制的，那么某些事件（如 RPCStart）可能从上一次请求中被带了过来，其状态会被标记为 eventStale（陈旧的）。atomic.CompareAndSwapUint32(p, eventStale, eventUpdating)这个 CAS 操作尝试将“陈旧”状态更新为“正在更新”。如果操作成功，说明这是一个重试请求，并且我们现在要用本次重试的新事件数据覆盖掉上次请求的旧数据。goroutine 同样获得了记录权。OnMessage()OnMessage()方法负责进行消息处理// OnMessage implements the InboundHandler interface. func (p *TransPipeline) OnMessage(ctx context.Context, args, result Message) (context.Context, error) { var err error for _, h := range p.inboundHdrls { ctx, err = h.OnMessage(ctx, args, result) if err != nil { return ctx, err } } if result.MessageType() == Exception { return ctx, nil } return p.netHdlr.OnMessage(ctx, args, result) }一个请求消息被成功从网络连接中读取并解码后，OnMessage()会对其进行处理。它通过调用链，将消息依次传递给一系列注册的入站处理器（Inbound Handlers），最终再交给核心的网络处理器（netHdlr）来执行业务逻辑。在执行完所有前置的 inboundHdrls 后，代码会检查 result 消息的类型。如果某个前置处理器（比如认证处理器）发现请求不合法，它不会返回 error（因为error通常表示连接级错误），而是会将 result 消息的类型设置为 Exception 并填充错误信息。这个检查就是为了处理这种情况：如果 result已经被标记为异常，就意味着请求已经被处理（判定为失败），无需再执行后续的业务逻辑。Write()最后是Write()方法，负责写回响应// Write implements the OutboundHandler interface. func (p *TransPipeline) Write(ctx context.Context, conn net.Conn, sendMsg Message) (nctx context.Context, err error) { for _, h := range p.outboundHdrls { ctx, err = h.Write(ctx, conn, sendMsg) if err != nil { return ctx, err } } return p.netHdlr.Write(ctx, conn, sendMsg) } // Write implements the remote.ServerTransHandler interface. func (t *svrTransHandler) Write(ctx context.Context, conn net.Conn, sendMsg remote.Message) (nctx context.Context, err error) { var bufWriter remote.ByteBuffer ri := sendMsg.RPCInfo() rpcinfo.Record(ctx, ri, stats.WriteStart, nil) defer func() { t.ext.ReleaseBuffer(bufWriter, err) rpcinfo.Record(ctx, ri, stats.WriteFinish, err) }() // 判断是否是单向调用 if methodInfo := ri.Invocation().MethodInfo(); methodInfo != nil && methodInfo.OneWay() { return ctx, nil } bufWriter = t.ext.NewWriteByteBuffer(ctx, conn, sendMsg) err = t.codec.Encode(ctx, sendMsg, bufWriter) if err != nil { return ctx, err } return ctx, bufWriter.Flush() }大概流程是先记录写入开始事件 (WriteStart)，然后判断是否为单向调用。单向调用意味着客户端只管发送请求，不关心也不等待服务端的响应。如果是单向调用，服务端则无需执行任何写入操作，直接成功返回，从而节省了编码和网络 I/O 的开销。如果不是则从 Extension 获取一个优化的 ByteBuffer 用于写入，并使用 Codec 将 Message 对象编码成字节流，存入 ByteBuffer。最后调用 ByteBuffer的 Flush 方法，将编码后的数据通过网络连接发送给客户端。
我们可以从官方的示例kitex-example中的hello入手服务初始化main函数不出意外应该是这样的：package main import ( "log" api "github.com/cloudwego/kitex-examples/hello/kitex_gen/api/hello" ) func main() { svr := api.NewServer(new(HelloImpl)) err := svr.Run() if err != nil { log.Println(err.Error()) } }先创建Server对象，再调用svr.Run()运行程序。让我们看一下api.NewServer创建Server对象的过程// NewServer creates a server.Server with the given handler and options. func NewServer(handler api.Hello, opts ...server.Option) server.Server { var options []server.Option options = append(options, opts...) options = append(options, server.WithCompatibleMiddlewareForUnary()) svr := server.NewServer(options...) if err := svr.RegisterService(serviceInfo(), handler); err != nil { panic(err) } return svr } func RegisterService(svr server.Server, handler api.Hello, opts ...server.RegisterOption) error { return svr.RegisterService(serviceInfo(), handler, opts...) }这是kitex框架生成的代码，它先创建了一个server对象，然后调用RegisterService()在对象上注册相应的服务。这个服务可以看作一个回调对象：当有请求时，kitex框架负责接受数据并解码，封装成请求体，再调用相应的服务与方法进行业务逻辑处理。RegisterService()会调用s.svcs.addService()方法，将服务添加到server.svcs属性中。svcs是一个services对象，通过一个knownSvcMap map[string]*service存储相应的服务：func (s *services) addService(svcInfo *serviceinfo.ServiceInfo, handler interface{}, registerOpts *RegisterOptions) error { // unknown service // 处理未知服务，确保只能有一个未知服务 if registerOpts.IsUnknownService { if s.unknownSvc != nil { return errors.New("multiple unknown services cannot be registered") } s.unknownSvc = &unknownService{svcs: map[string]*service{}, handler: handler} return nil } svc := newService(svcInfo, handler) // 处理回退服务，确保只能有一个回退服务 if registerOpts.IsFallbackService { if s.fallbackSvc != nil { return fmt.Errorf("multiple fallback services cannot be registered. [%s] is already registered as a fallback service", s.fallbackSvc.svcInfo.ServiceName) } s.fallbackSvc = svc } // 处理普通服务 // 防止重复注册 if _, ok := s.knownSvcMap[svcInfo.ServiceName]; ok { return fmt.Errorf("service [%s] has already been registered", svcInfo.ServiceName) } s.knownSvcMap[svcInfo.ServiceName] = svc // 记录非回退服务 if !registerOpts.IsFallbackService { s.nonFallbackSvcs = append(s.nonFallbackSvcs, svc) } return nil }启动服务初始化完成后，就可以通过svr.Run()启动服务// Run runs the server. func (s *server) Run() (err error) { s.Lock() s.isRun = true s.Unlock() // 初始化中间件调用链 (buildInvokeChain)、调试服务、本地会话备份等 s.init() if err = s.check(); err != nil { return err } // 获取远程通信相关的配置和监听地址 svrCfg := s.opt.RemoteOpt addr := svrCfg.Address // should not be nil ... s.registerDebugInfo() // 添加一些内置的 BoundHandler，例如用于元信息处理、限流、连接数限制的处理器 s.richRemoteOption() // 创建传输处理器 transHdlr, err := s.newSvrTransHandler() if err != nil { return err } // 实例化 remotesvr.Server。负责网络监听、接收连接和读写数据，利用上文的TransHandler进行数据处理 svr, err := remotesvr.NewServer(s.opt.RemoteOpt, transHdlr) if err != nil { return err } ... // 开始异步监听（非阻塞） errCh := svr.Start() ... // 进入阻塞状态，并将服务注册到服务发现中心 if err = s.waitExit(errCh); err != nil { klog.Errorf("KITEX: received error and exit: error=%s", err.Error()) } // 退出 if e := s.Stop(); e != nil && err == nil { err = e klog.Errorf("KITEX: stop server error: error=%s", e.Error()) } return }可以看到，启动的流程大致是以下几步：初始化：执行 init()，构建中间件调用链。配置检查：检查已注册的服务是否符合要求。准备传输层：确定监听地址，并创建包含限流、元信息处理等逻辑的传输处理器 (TransHandler)。创建底层 Server：实例化一个 remotesvr.Server，它封装了网络监听和数据收发的逻辑。开始监听：调用 remotesvr.Server.Start()，服务器开始在网络端口上接收连接。服务注册与等待：构建服务注册信息，然后阻塞等待退出信号。在阻塞期间，它会向服务发现中心注册自己，宣告服务可用。退出：收到退出信号后，执行 Stop()，从服务中心反注册并关闭服务器。中间件调用链func (s *server) buildInvokeChain(ctx context.Context) { mws := s.buildMiddlewares(ctx) s.eps = endpoint.Chain(mws...)(s.unaryOrStreamEndpoint(ctx)) }初始化过程中调用了buildInvokeChain()，构建中间件调用链。func (s *server) buildMiddlewares(ctx context.Context) []endpoint.Middleware { ... // 3. universal middleware var mws []endpoint.Middleware // 分发中间件，判断是一元调用还是流式调用，并分发给相应的中间件链进行处理 mws = append(mws, s.wrapStreamMiddleware()) if s.opt.EnableContextTimeout { mws = append(mws, serverTimeoutMW) } // register server middlewares // 添加用户自定义的中间件 for i := range s.opt.MWBs { if mw := s.opt.MWBs[i](ctx); mw != nil { mws = append(mws, mw) } } // register core middleware, // core middleware MUST be the last middleware mws = append(mws, s.buildCoreMiddleware()) return mws } buildMiddlewares会先向中间件链中加入一个分发中间件wrapStreamMiddleware，负责识别请求类型（一元或流式）并分发到正确的调用链。随后添加用户自定义的中间件。最末端则是一个coreMiddleware，负责执行ACL与错误处理等框架级别的逻辑。随后的Chain()会按下标逆序逐个包裹mws中的中间件，最内层则是s.unaryOrStreamEndpoint(ctx)创建的分发器，将请求交给对应的业务逻辑处理链。这样我们就得到了一个完整的中间件调用链。检查同名方法冲突server在启动时会调用service的check方法，确保至少注册了一个服务且多个服务之间没有同名方法冲突// 检查服务是否已注册，以及是否存在多个服务但未指定回退服务（fallback service）等配置问题 func (s *services) check(refuseTrafficWithoutServiceName bool) error { // 检查是否至少注册了一个服务（无论是普通服务 knownSvcMap 还是泛化调用服务 unknownSvc） if len(s.knownSvcMap) == 0 && s.unknownSvc == nil { return errors.New("run: no service. Use RegisterService to set one") } for _, svc := range s.knownSvcMap { // binary thrift generic v1无法与其他服务一起注册 if svc.svcInfo.ServiceName == serviceinfo.GenericService { s.binaryThriftGenericV1SvcInfo = svc.svcInfo if len(s.knownSvcMap) > 1 { return error } if s.unknownSvc != nil { return error } return nil } // 同上，combine service合并服务也无法与其他服务一起注册 if isCombineService, _ := svc.svcInfo.Extra[serviceinfo.CombineServiceKey].(bool); isCombineService { ... } } // 配置泛化服务（用于处理未明确注册的服务或方法的泛化调用）降级方案，会遍历所有已知的普通服务，注入泛化方法作为降级方案 if s.unknownSvc != nil { for _, svc := range s.knownSvcMap { // 若该服务没有对应的方法，则会被转交给unknownSvb的handler处理 } } ... // 检测同名方法冲突 fallbackCheckingMap := make(map[string]int) // 遍历服务与方法 for _, svc := range s.knownSvcMap { for method := range svc.svcInfo.Methods { // 若为回退服务，则该方法标记-1 if svc == s.fallbackSvc { fallbackCheckingMap[method] = -1 } else if num := fallbackCheckingMap[method]; num >= 0 { fallbackCheckingMap[method] = num + 1 } } } // 若某方法在多个服务中存在则报错 for methodName, serviceNum := range fallbackCheckingMap { if serviceNum > 1 { return fmt.Errorf("method name [%s] is conflicted between services but no fallback service is specified", methodName) } } return nil }首先，check会检查是否至少注册了一个服务（无论是普通服务 knownSvcMap 还是泛化调用服务 unknownSvc）。处理特殊服务（互斥检查）：检查是否存在旧版 Thrift 泛化服务 (binary thrift generic v1)与合并服务 (combine service)，这两类服务不能与其他服务一起注册随后配置泛化调用降级：如果存在 unknownSvc（用于处理未明确注册的服务或方法的泛化调用），则遍历所有已知的普通服务。它会为每个普通服务动态地注入一个作为降级方案。如果请求的某个服务没有对应的方法，请求不会立即失败，而是会被转交给 unknownSvc 的处理器来尝试进行泛化处理。检查方法名冲突：这是最关键的一步。当服务器注册了多个服务时，不同的服务可能会有同名的方法（例如，两个服务都有一个叫 GetUser 的方法）。为了解决这种冲突，Kitex 允许指定一个 fallbackSvc（回退服务）。check函数会遍历所有已注册服务的所有方法并用一个 map 来统计每个方法在非回退服务中出现了多少次。最后检查结果。如果发现任何一个方法在超过一个非回退服务中出现（即 serviceNum > 1），并且没有指定回退服务来解决这个冲突，函数就会返回一个错误指出哪个方法名冲突了。监听s.richRemoteOption()这个方法会添加各种绑定处理器，从而实现性能分析、流控等功能。随后实例化一个remotesvr.Server并开始异步监听。// Start starts the server and return chan, the chan receive means server shutdown or err happen func (s *server) Start() chan error { errCh := make(chan error, 1) ln, err := s.buildListener() if err != nil { errCh <- err return errCh } s.Lock() s.listener = ln s.Unlock() // 调用BootstrapServer处理连接 gofunc.GoFunc(context.Background(), func() { errCh <- s.transSvr.BootstrapServer(ln) }) return errCh }可以看到Start()方法是一个非阻塞方法，会立即返回一个error类型的channel。这个channel用于接受真正的服务循环s.transSvr.BootstrapServer(ln)的返回值，当从这个channel中接收到值时就意味着服务已经停止运行。gofunc.GoFunc()是 Kitex 提供的一个 goroutine 包装器。它类似于 Go 语言原生的 go func() {}，但增加了一个关键功能：panic 恢复。如果内部的函数执行时发生 panic，gofunc 会捕获它并将其作为错误处理，从而防止整个服务进程因未捕获的 panic 而崩溃，增强了服务的稳定性。而BootstrapServer方法有两个实现，先看一下标准库gonet的实现：func (ts *transServer) BootstrapServer(ln net.Listener) error { ... for { conn, err := ln.Accept() if err != nil { ... klog.Errorf("KITEX: BootstrapServer accept failed, err=%s", err.Error()) return err } go ts.serveConn(context.Background(), conn) } }conn, err := ln.Accept()是循环的核心。Accept() 是一个阻塞调用，它会一直等待，直到有新的客户端连接请求到达。当一个连接被接受后，它会返回代表该连接的 net.Conn 对象和一个 error。如果error为nil，则连接正常，会启动一个新的goroutine处理这个连接。func (ts *transServer) serveConn(ctx context.Context, conn net.Conn) (err error) { // 兜底 panic恢复，防止单个连接的崩溃导致整个server进程推出 defer transRecover(ctx, conn, "serveConn") ... // 初始化 ctx, err = ts.transHdlr.OnActive(ctx, bc) if err != nil { klog.CtxErrorf(ctx, "KITEX: OnActive error=%s", err) return err } for { // 设置连接空闲超时 ts.refreshIdleDeadline(bc) _, err = bc.r.Peek(1) if err != nil { return err } // 设置请求读取超时，确保服务器不会因为缓慢或恶意的客户端一直不发送完整数据而被永久阻塞 ts.refreshReadDeadline(rpcinfo.GetRPCInfo(ctx), bc) // FIXME: for gRPC transHandler, OnRead should execute only once. // 开始读取 err = ts.transHdlr.OnRead(ctx, bc) if err != nil { return err } } }serveConn()的逻辑大致是先初始化连接，成功后开始循环进行数据读取。transServer设置了连接空闲超时与请求读取超时，防止连接空闲时间过长以及读取超时阻塞导致的资源浪费与泄露。读取循环会一直运行，直到出现错误或数据读取完毕（连接空闲超时）该方法和go标准库中的ListenAndServe方法很像，都是让一个goroutine去等待连接建立，再交给其他goroutine接收并处理数据服务注册开启监听后，Run()方法会调用waitExit()方法阻塞主线程，直到退出func (s *server) waitExit(errCh chan error) error { exitSignal := s.opt.ExitSignal() // service may not be available as soon as startup. delayRegister := time.After(1 * time.Second) for { select { case err := <-exitSignal: return err case err := <-errCh: return err case <-delayRegister: s.Lock() if err := s.opt.Registry.Register(s.opt.RegistryInfo); err != nil { s.Unlock() return err } s.Unlock() } } }它主要监听以下三个信号：外部退出信号 (exitSignal): 用于接收操作系统的信号（如 SIGINT, SIGTERM），当这个 channel 收到信号时，方法会返回收到的 error，从而触发服务停止流程。内部服务器错误 (errCh): 这个 channel 是从底层的网络服务器（remotesvr.Server）传递过来的。如果在监听、接受连接或处理数据时发生不可恢复的错误，该错误会被发送到这个 channel。waitExit 接收到错误后会立即返回，同样触发服务停止。延迟注册定时器 (delayRegister): 这是一个通过 time.After(1 * time.Second) 创建的定时器。它会延迟1秒钟再执行服务注册。这样做的目的是为了确保服务器已经完全准备好处理请求，然后再将自己注册到服务发现中心（如 Nacos、etcd），避免客户端过早地发现一个尚未完全就绪的服务实例。退出Stop()方法用于在服务结束后进行收尾工作// Stop stops the server gracefully. func (s *server) Stop() (err error) { s.stopped.Do(func() { s.Lock() defer s.Unlock() // 执行关闭相关钩子函数 muShutdownHooks.Lock() for i := range onShutdown { onShutdown[i]() } muShutdownHooks.Unlock() // 服务注册中心取消注册 if s.opt.RegistryInfo != nil { err = s.opt.Registry.Deregister(s.opt.RegistryInfo) s.opt.RegistryInfo = nil } // 停止监听 if s.svr != nil { if e := s.svr.Stop(); e != nil { err = e } s.svr = nil } }) return }在server结构体的定义中，stopped的类型是sync.Oncetype server struct { // ... stopped sync.Once // ... }它使得 Stop()方法是幂等且线程安全的。无论你调用 Stop() 一次还是多次，效果都是相同的——服务器只会被关闭一次。后续的调用会立即返回，不会产生任何副作用，也不会报错。并且sync.Once 可以保证即使有多个协程并发调用 Stop()，包裹在 Do 方法里的实际关闭代码（服务反注册、停止网络监听等）也只会被其中一个协程执行一次，从而避免了竞态条件和重复关闭导致的混乱。
PVE使用群晖网络ups服务器实现停电自动关机
背景目前有一台山特ups，ups只有一个typeb口，但需要同时给多台设备（两台nas一台pve软路由）供电，且家里停电时ups能够通知所有设备自动关机。由于群晖的网络ups协议用的就是nut，所以将群晖作为nutServer，在pve上安装nut进行配置即可。配置PVE中nut客户端nut在各类linux系统中都可以用默认的包管理系统进行安装apt install nutnut配置修改/etc/nut/nut.conf，将MODE设置为netclientMODE=netclient修改/etc/nut/upsmon.conf中的MONITOR等配置项MONITOR <system> <powervalue> <username> <password> ("master"|"slave") #<system>设置为ups@服务器地址或域名，这里是ups@192.168.1.107 #<powervalue>设置为1 #<username>群晖UPS服务器的默认用户名是monuser #<password>群晖UPS服务器的默认密码是secret #("master"|"slave")，客户机设置为slaveMONITOR ups@192.168.1.107 1 monuser secret slave有可能需要设置RUN_AS_USER选项以解决权限问题RUN_AS_USER root通过upsc ups@<UPS_IP>测试连接情况启动nutClientsystemctl start nut-client #启动网络ups客户端服务 systemctl status nut-client #检查服务状态，如有错误再根据提示进行排查 systemctl enable nut-client #设置网络ups客户端服务自启动编写自动关机脚本默认情况下nut会在收到LOWBATT时执行关机操作，即在ups低电量时关机，相关的配置在/etc/nut/upsmon.conf中POWERDOWNFLAG /etc/killpower SHUTDOWNCMD "/sbin/shutdown -h +0"该配置文件中还有message相关配置# -------------------------------------------------------------------------- # NOTIFYMSG - change messages sent by upsmon when certain events occur # # You can change the default messages to something else if you like. # # NOTIFYMSG <notify type> "message"而我希望软路由会在停电3分钟后自动关机，这就需要使用upssched自定义。在upsmon中设置触发条件通知upssched，并由upssched完成后续工作。配置upsmon首先在upsmon.conf中添加NOTIFYCMD /sbin/upssched，即在发生事件时运行upssched服务。设置触发条件：NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC触发条件各个选项的含义在upsmon.conf的注释中有详细描述配置upssched随后在/etc/nut/upssched.conf中实现所需功能：CMDSCRIPT /etc/nut/upssched-cmd PIPEFN /etc/nut/upssched.pipe LOCKFN /etc/nut/upssched.lock # 断电使用电池开始180秒后，执行upssched-cmd脚本，带shutdown_self参数 AT ONBATT * START-TIMER shutdown_self 180 # 来电后，停止shutdown_all计时 AT ONLINE * CANCEL-TIMER shutdown_self # 来电后，执行脚本，带start_all参数 AT ONLINE * EXECUTE start_all # 低电量，执行脚本，带shutdown_self参数 AT LOWBATT * EXECUTE shutdown_selfCMDSCRIPT指定需要执行的脚本，随后我们设置触发条件以及传入脚本的参数。AT语句需要在前三行之后，且需要保证PIPEFN及LOCKFN命令中的文件NUT应有读取写入及操作的权限，若权限不正确则会报错Failed to connect to parent and failed to create parent: No such file or directory编写脚本脚本放在上文中CMDSCRIPT指定的位置#! /bin/sh case $1 in shutdown_self) #关闭所有 /usr/sbin/upsmon -c fsd ;; shutdown_all) pvesh create /nodes/pve/qemu/100/status/stop pvesh create /nodes/pve/qemu/101/status/stop ;; start_all) #开启所有 pvesh create /nodes/pve/qemu/100/status/start pvesh create /nodes/pve/qemu/101/status/start ;; *) logger -t upssched-cmd "Unrecognized command: $1" ;; esacnut需要有执行该脚本的权限，否则会报错exec_cmd(your-path your-event) returned 126最后重启服务，就可以断电测试了。
自建根服务器对于 zerotier 根服务器在国外，高延迟且连接不稳定的问题，此前尝试过自己部署 moon 节点，不过虚拟局域网内的设备想要通过 moon 节点直连还是需要先通过根服务器与 moon 节点取得联系（仅仅是取得联系即可，对带宽延迟都没有太大要求，所以之前处在勉强可用的状态）然而最近 zerotier 的官方根服务器似乎彻底被墙死，moon 节点也几乎不可用。为了继续正常使用 zerotier，开始考虑自建 zerotier 根服务器的方案。最终找到了基于 ztncui 优化而来的 zerotier-planet 项目，简单而言就是用 docker 部署一个独立的 zerotier 虚拟网络。部署这个项目的全部准备只有在你的公网服务器上安装好 docker 和 docker-composejonnyan404 已经写好了 zerotier-planet 的 docker 配置脚本，我们只需要从 gitee 上 clone 下来即可git clone https://gitee.com/Jonnyan404/zerotier-planet然后进入相应文件夹cd zerotier-planet修改配置文件vi docker-compose.yml填写你的服务器的公网 ip 地址然后就可以通过 docker-compose 启动 dockerdocker-compose up -d此时可以通过 http://ip:4000 访问后台页面（记得开放服务器 4000 端口）后面的操作就和 zerotier 部署一样了替换 planet 文件此时你自建的根服务器已经可以正常运行了，但是如果你使用 zerotier-cli peers 查看虚拟局域网中的设备，你会发现你自己的根服务器是以leaf的身份运行的，而官方的根服务器依旧在设备列表中占据了 planet 的位置。虽然完全不影响使用，但是看着终归有些不爽。这时候你需要重新编译并替换局域网中所有设备的 planet 文件查看认证信息进入 docker 挂载的 zerotier-one 目录查看 identity.public 和 authtoken.secret 中记录的认证信息，记录下来编译 planet 文件下面的步骤可以在任意一台机子上完成，只需要最后编译好的 planet 文件如果不想在云服务器上编译，完全可以在自己的 linux 虚拟机中进行先下载 zerotier 源码git clone https://github.com/zerotier/ZeroTierOne进入源码目录[root@cloud zerotier-one]# ls authtoken.secret controller.d identity.public identity.secret networks.d planet zerotier-cli zerotier-idtool zerotier-one ZeroTierOne zerotier-one.pid zerotier-one.port zerotier-one.te进入目录ZeroTierOne[root@cloud zerotier-one]# cd ./ZeroTierOne [root@cloud ZeroTierOne]# ls artwork controller Dockerfile.ci java make-mac.mk one.cpp RELEASE-NOTES.md tcp-proxy zeroidc attic ...进入attic/world[root@cloud ZeroTierOne]# cd attic/world/ [root@cloud world]# ls build.sh current.c25519 mkworld mkworld.cpp planet previous.c25519 README.md world.c修改 mkworld.cpp[root@cloud ZeroTierOne]# vim mkworld.cpp找到其中的 roots.push_back，这里的四个就是默认的根服务器 将他们都注释掉，添加你自己的：将之前记下的 identity.public 中的字符串写入 Identity 中InetAddress写服务器公网ip，端口为9993 roots.push_back(World::Root()); roots.back().identity = Identity("identity.public中的字符串"); roots.back().stableEndpoints.push_back(InetAddress("公网IP/9993"));回到 world 目录下，执行：source ./build.sh ./mkworld mv ./world.bin ./planet你就会得到新的 planet 文件将虚拟局域网内设备 zerotier 根目录下的 planet 文件用这份新的 planet 文件替换后重启服务Windows下planet位置：C:\ProgramData\ZeroTier\OneLinux下planet位置：/var/lib/zerotier-one再使用 zerotier-cli peers 命令，就会发现 planet 变为你自己的服务器了
相关命令查询节点zerotier-cli peers加入网络sudo zerotier-cli join xxxxxxxxxxxxxxxx离开网络zerotier-cli leave xxxxxxxxxxx加入 moonzerotier-cli orbit xxxxxxx yyyyyyyy离开 moonzerotier-cli deorbit xxxxxxxxZerotier Moon服务器搭建zerotier的体验也符合木桶效应，直连的长板很长，走中转的短板很短，在一些时候会带来很多不好的体验。zerotier允许用户自定义中转服务器，让被迫走中转时的速度和延迟都比较让人可以接受。同时，购买的VPS通常都是BGP网络，也就是同时接入多个运营商，使用中转服务器可以一定程度上提高跨运营商组网的体验。什么是Moon服务器Moon服务器就是上面说的中转服务器，在zerotier中，每个客户端叫做LEAF，官方的节点叫做PLANET，个人的节点叫做MOON。搭建Moon服务器首先购买一台vps可以想办法开一个阿里云/腾讯云/华为云学生机（或者新用户优惠）VPS推荐安装zerotier执行官方一键安装脚本curl -s https://install.zerotier.com | sudo bash安装完成后执行zerotier-cli检查是否安装成功接着先把vps加入zerotier网络，xxxxx为zerotier的Network ID，可以在后台看到。zerotier-cli join xxxxx加入以后去后台允许一下，接着进行moon相关配置，先导出moon.jsoncd /var/lib/zerotier-one zerotier-idtool initmoon identity.public > moon.json使用nano编辑文件nano moon.json生成的文件大概是这样的，只需要修改stableEndpoints，格式为”vps的公网IP/端口号”，如果vps有v6地址也可以一起写上："stableEndpoints": ["47.xx.xx.xx/9993","2004:xxxx:xxxx::xxx/9993"]编辑完成后按ctrl+X退出并选择Y保存设置。下一步生成moon文件：zerotier-idtool genmoon moon.json输入命令后会在该目录下面生成一个类似于 000000xxxxx. moon 的文件，接着我们让这个文件生效：# 创建一个存放刚刚生成文件的目录 mkdir moons.d # 把文件移动到该目录 mv 000000xxxxx.moon moons.d/重新启动 zerotier 的服务器, 由于使用命令安装时会自动注册为服务，所以使用该命令重启:service zerotier-one restart这一步做完还需要修改防火墙，打开9993端口。如果使用的是aliyun的机器还需要在aliyun的控制台放行9993端口。客户端连接Moon服务器不管是哪个平台首先都需要知道Moon节点的id，这个id可以通过在vps执行zerotier-cli info获得。下面简称id。群辉在 Zerotier 的根目录下新建文件夹 moons.d，将上文中生成的 moon 文件复制到该目录下。或者假设已经接入了zerotier局域网。在控制面板打开ssh，连接ssh以后执行zerotier-cli orbit id id后面两个 id 输入 moon 节点的 id，要输入两次！显示 200 orbit OK 说明加入成功。（不过这种方法不推荐，因为依赖于planet根服务器，如果无法连接根服务器则无法缓存moon文件）PC 端首先下载zerotier客户端并安装，打开后输入网络ID并Join Network之后与群晖类似首先运行 services. msc, 找到服务 ZeroTier One，并且在属性内找到其对应的可执行文件路径，如 C:\ProgramData\ZeroTier\One\zerotier-one_x64.exe，在该目录下新建文件夹 moons.d，将 moon 文件复制到该目录下。或者在命令行中输入：zerotier-cli orbit id id，其中 id 为私有根服务器的 ID （不推荐，理由同上）移动端由于官方提供的 app 不支持 moon，这里使用kaaass的ZerotierFixkaaass/ZerotierFix: An unofficial Zerotier Android client patched from official client (github.com)
规范分支管理main 分支： 主分支，最终的、稳定的、经过测试没有 bug 的、可部署于生产环境的分支 只能由 release 和 hotfix 分支合并，任何情况下都不能直接修改代码feature/hotfix 分支： 功能模块开发分支，对应于一个特定的功能模块 该分支以 dev 分支为基线，完成开发工作后再合并到 main 分支 命名规则：feature/NAME 或 feature-NAME 热修复分支，当 main 分支部署到生产环境后发生紧急状况，需要及时处理时，该分支负责热修复，即在保证应用不下线的条件下，对 bug 进行紧急修复 该分支以 main 分支为基线，修复 bug 后，合并到 main 分支部署上线，同时也合并到 dev 分支保持最新进度 命名规则： hotfix/NAME 或 hotfix-NAMErelease 分支： 预发布分支，在发布正式版本前进行全面测试和修复 该分支以 main 分支为基线，将需要发布的新功能分支合并到该分支，并进行测试 经过测试没有问题之后，合并到 main 分支部署上线，同时也合并到 dev 分支保持最新进度 命名规则：release/NAME 或 release-NAME分支名称分支职责基线分支合并分支main主分支--feature/hotfix功能模块开发/热修复分支mainmain,releaserelease预发布分支mainmainmain 分支贯穿于整个项目的生命周期hotfix/feature、release 分支都是临时分支，分别负责热修复、功能模块开发、预发布多人协作任务划分主仓库 A：管理 main 分支，review 提交的代码，处理冲突开发者仓库 B：承担主要开发任务初始状态新建主仓库 A，开发人员 fork 仓库 A使用 git clone 将远程仓库 clone 到本地git clone https://github.com/Grosser-Kurfuerst/Cesium_Demo.git和 fork 的主仓库地址进行关联git remote add upstream https://github.com/MisakaBryant/Cesium_Demo.git//源项目地址，upstream是远程仓库别名，可以为任意名字在本地仓库中创建需要的分支，例如 feature 分支，并与远程分支绑定（可以省略绑定，push时会自动创建）git checkout -b feature origin/feature此时可以使用 git branch 查看本地分支与主仓库版本保持同步在开始新的工作前需要拉取当前分支最新的版本使用 git remote -v 命令查看本地仓库连接的远程仓库origin https://github.com/Grosser-Kurfuerst/Cesium_Demo.git (fetch) origin https://github.com/Grosser-Kurfuerst/Cesium_Demo.gitt (push) upstream https://github.com/MisakaBryant/Cesium_Demo.git (fetch) upstream https://github.com/MisakaBryant/Cesium_Demo.git (push)切换到 dev 分支，获取并合并 upstream 代码git checkout dev git pull upstream devgit pull 命令相当于 fetch 之后再 merge，与以下命令等价git fetch upstream dev git merge upstream/dev在本地完成合并之后记得push到自己的远程仓库开发功能模块在开发阶段，应当尽量避免发生冲突，同时保持 commit 历史的干净整洁，善用 git rebase 命令可以保证 commit 历史更加整洁在 main 的基础上创建 feature-measureDistance 分支，并且切换到该分支git checkout -b feature-measureDistance将该分支 push 到远程仓库，若远程仓库没有该分支则自动创建git push origin feature-measureDistance:feature-measureDistance开发完成后进行 commitgit commit -m "measureDistance finished"push 到远程仓库git push origin feature-measureDistance提交 pull request，与主仓库合并将 feature 分支合并到 main 分支，提交 pull request，请求合并到主仓的 main 分支git checkout main git merge feature也可以使用 rebase 命令git checkout feature //注意是在feature分支下rebase git rebase main git checkout main git merge feature 最后 push 到远程仓库 随后就可以在 github 中 open pull request 按要求填写 title 与 comment 之后即可提交此时主仓库管理员会收到 request，review 代码之后确认没有问题并且没有冲突就可以允许 mergeMerge pull request 有三个选项：Create a merge commit ：表示把这个 PR 作为一个分支合并，并保留分支上的所有提交记录（不能保持 main 分支干净，但是保留了所有的 commit history，当 PR 中 commit 次数较多时不推荐此方式）Squash and merge ：表示只为这次合并保留一个提交记录（也可以保持 main 分支干净，但是 main 中 author 都是 maintainer，而不是原 author）Rebase and merge ：找到两个分支共同的祖先，然后在当前分支上合并从共同祖先到现在的所有 commit（可以尽可能保持 main 分支干净整洁，并且易于识别 author）建议使用 rebase 主仓库管理员更新本地仓库，拉取 pull 分支最新代码git pull origin main解决冲突两个分支都修改了同一文件（不管什么地方）两个分支都修改了同一文件的名称解决冲突之后需要 add 以及 commitgit add conflict_file git commit -m "fix confilict"若是分支后出现了 REBASE 或是 MERGING 在解决冲突之后需要 continue rebasegit rebase --continue版本回退使用 git reset 将当前分支回退到之前某个 commit 节点git reset HEAD^HEAD^ 意为 head 指向的版本的前一个版本，HEAD^^ 即为前两个版本，而 HEAD~10 为前 10 个版本 同样，也可以将 HEAD 更改为分支名称，如 master^ 代表回退到 master 指向的版本的前一个版本 也可以使用版本号进行回退git reset 版本号使用 git log --oneline 查看版本号此外，git reset 有三个选项 ^1bd57b--soft : 仅回退本地库中的内容，相当于撤销 git commit--mixed: （默认方式）同时回退本地库和暂存区内容，相当于进一步撤销 git add--hard : 同时回退本地库、暂存区和工作区内容，相当于进一步撤销本地仓库文件夹中的改动返回命令git 命令1. 新建代码库在当前目录新建一个Git代码库git init 新建一个目录，将其初始化为Git代码库git init [project-name] # 下载一个项目和它的整个代码历史 git clone [url]2. 配置显示当前的Git配置git config --list 编辑Git配置文件git config -e [--global] # 设置提交代码时的用户信息 git config [--global] user.name "[name]" git config [--global] user.email "[email address]"3. 增加/删除文件添加指定文件到暂存区git add [file1] [file2] ... # 添加指定目录到暂存区，包括子目录 git add [dir] # 添加当前目录的所有文件到暂存区 git add . # 添加每个变化前，都会要求确认 # 对于同一个文件的多处变化，可以实现分次提交 git add -p 删除工作区文件，并且将这次删除放入暂存区git rm [file1] [file2] ... # 停止追踪指定文件，但该文件会保留在工作区 git rm --cached [file] # 改名文件，并且将这个改名放入暂存区 git mv [file-original] [file-renamed]4. 代码提交提交暂存区到仓库区git commit -m [message] # 提交暂存区的指定文件到仓库区 git commit [file1] [file2] ... -m [message] # 提交工作区自上次commit之后的变化，直接到仓库区 git commit -a 提交时显示所有diff信息git commit -v 使用一次新的 commit，替代上一次提交如果代码没有任何新变化，则用来改写上一次commit的提交信息git commit --amend -m [message] # 重做上一次commit，并包括指定文件的新变化 git commit --amend [file1] [file2] ...5. 分支列出所有本地分支git branch 列出所有远程分支git branch -r 列出所有本地分支和远程分支git branch -a 新建一个分支git branch [branch-name] git checkout -b [branch] // 新建一个分支，并切换到该分支 git branch [branch] [commit] // 新建一个分支，指向指定commit git branch --track [branch] [remote-branch] // 新建一个分支，与指定的远程分支建立追踪关系切换分支git checkout [branch-name] // 切换到指定分支，并更新工作区 git checkout - // 切换到上一个分支合并git branch --set-upstream [branch] [remote-branch] // 建立追踪关系，在现有分支与指定的远程分支之间 git merge [branch] // 合并指定分支到当前分支 git cherry-pick [commit] // 选择一个commit，合并进当前分支删除git branch -d [branch-name] // 删除分支 git push origin --delete [branch-name] // 删除远程分支 6. 查看信息显示有变更的文件git status显示当前分支的版本历史git log 显示commit历史，以及每次commit发生变更的文件git log --stat 搜索提交历史，根据关键词git log -S [keyword] 显示某个commit之后的所有变动，每个commit占据一行git log [tag] HEAD --pretty=format:%s 7. 远程同步下载远程仓库的所有变动git fetch [remote] 显示所有远程仓库git remote -v 显示某个远程仓库的信息git remote show [remote] 增加一个新的远程仓库，并命名git remote add [shortname] [url] 拉取远程仓库的变化，并与本地分支合并git pull [remote] [branch] 上传本地指定分支到远程仓库git push [remote] [branch] 强行推送当前分支到远程仓库，即使有冲突git push [remote] --force 推送所有分支到远程仓库git push [remote] --all8. 撤销重置暂存区的指定文件，与上一次commit保持一致，但工作区不变git reset [file]重置暂存区与工作区，与上一次commit保持一致git reset --hard重置当前分支的指针为指定commit，同时重置暂存区，但工作区不变git reset [commit] reset选项见这里 ^382508若reset后需要回到最新版本，可以使用git reflog查看reset前的版本号，再使用reset回到最新版本即可新建一个commit，用来撤销指定commit # 后者的所有变化都将被前者抵消，并且应用到当前分支git revert [commit]
跨域是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的，是浏览器对 JavaScript 实施的安全限制vite 跨域代理把对某一段 URL 的访问直接转换成另一个真实的后端 API 地址，这样前后端就视为使用了相同的域名、协议和端口，就避免了跨域的问题，还能避免繁琐的反复编写域名server. proxy 为开发服务器配置自定义代理规则官方示例export default defineConfig({ server: { proxy: { // 字符串简写写法 '/foo': 'http://localhost:4567', // 选项写法 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') }, // 正则表达式写法 '^/fallback/.*': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/fallback/, '') }, // 使用 proxy 实例 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, configure: (proxy, options) => { // proxy 是 'http-proxy' 的实例 } }, // Proxying websockets or socket.io '/socket.io': { target: 'ws://localhost:3000', ws: true } } } })选择使用 proxy 解决：server: { base: './', proxy: { '/api': { target: 'http://172.26.19.244:5000/', // 代理目标地址 ws: true, // 是否开启websocket支持 changeOrigin: true, // 是否允许跨域 rewrite: (path) => path.replace(/^\/api/, ''), //路径重写规则 } } }如果需要向 http://172.26.19.244:5000发起请求 ，则 target 就填 http://172.26.19.244:5000 ，在所有请求的地方用“/api”替换 http://172.26.19.244:5000 例如：url: "/api/3dtiles"
配置 dockerdocker 中下载 latest 镜像 下载完成后在【映像】标签页找到下载好的nginx，双击创建容器 网络就使用桥接即可，设置一个未被占用的本地端口 将需要共享的文件夹挂载到 /home 目录，并且 x 需要共享的文件夹必须要有 everyone 的权限，不然到时候 web 打开是 403 错误 配置完成后运行容器修改 nginx 配置因为 docker 的终端机里没有 vim，所以选择 ssh 连到 nas 进行修改先用 docker ps 命令查看一下 nginx 容器有没有在运行 如图第一个就是把 nginx 的配置文件复制到群晖里，修改完再复制进去docker cp nginx1:/etc/nginx/conf.d/default.conf . // 复制到当前目录 vi default.conf 在对应位置添加charset utf-8; autoindex on; autoindex_exact_size off; autoindex_localtime on;然后将 location 内的 root 与 index 注释掉，添加 root /home; 复制回去 docker cp default.conf nginx-CesiumData:/etc/nginx/conf.d/default.conf 最后重启容器 docker restart nginx-CesiumData现在就可以通过nas的ip地址与对应端口访问文件了
0. 引言0.1 语法结构程序设计语言源程序的构成：语法结构一个实例0.2 文法一种用于描述程序设计语言语法的表示方法，能够自然地描述程序设计语言构造的层次化语法结构 1. 语法分析器输入：词法分析器输出的此法单元序列输出：语法树表示语法分析器功能： 语法分析器的类型： 类型检查，语义分析，翻译生成中间代码等往往和语法分析过程交错完成，实践中往往和语法分析放入一个模块，图上用“前端的其余部分”表示上述活动2. 上下文无关法 (Context Free Grammar, CFG)上下文无关文法G是一个四元组G=(T,N,P,S)G=(T,N,P,S):1. T是**终结符号 Terminal**集合, 对应于词法分析器产生的词法单元 2. N是**非终结符号 Non-terminal**集合 3. P 是**产生式 Production**集合 终结符：不可以再进行推导，是不可拆分的最小元素非终结符：可以进行拆分的元素2.1 什么是上下文文法的分类上下文有关文法上下文无关文法参考2.2 推导产生式又可称为重写规则（Rewriting rule）推导即是将某个产生式的左边替换成它的右边每一步推导需要选择替换哪个非终结符号，以及使用哪个产生式 推导的一般性定义 2.2.1 句型/句子/语言2.2.2 推导的问题推导：从上层视角判断输入内容是否符合文法从推导的角度看，语法分析的任务：推导过程中可能遇到两个问题：每一步替换哪个非终结符号？若以这个非终结符号为头的产生式有多个，用哪个产生式的右部替换？非终结符号的替换顺序2.2.3 语法分析树推导的图形表示形式 有时允许树根不是开始符号（对应于某个短语）树的叶子组成的序列四根的文法符号的句型 2.2.4 推导和语法树对于推导中的每一个句型α1，都能构造出一个以α1 为结果的语法树2.3 二义性 程序设计语言的文法通常都应该是无二义性的但有些二义性的情况可以方便文法或语法分析器的设计2.4 文法及其生成的语言语言是由闻法得开始符号出发，能够推导得到的所有句子的集合 2.5 上下文无关文法和正则表达式上下文无关文法的表达能力更强每个正则表达式都可以用一个上下文无关文法来描述，反之不成立每个正则语言都是一个上下文无关语言， 反之不成立 3. 文法的设计在进行高效的语法分析之前，需要对文法做以下处理： 3.1 消除二义性二义性的例子： 一些二义性文法可以被改成等价的无二义性的文法 二义性的消除方法没有规律可循通常并不是通过改变文法来消除二义性3.2 消除左递归左递归的定义： 3.2.1 立即左递归的消除示例：3.2.2 消除多步左递归消除立即左递归的方法并不能消除因为两步或多步推导而产生的左递归 消除算法不能含有回路（P→P）不能含有ε为右部的产生式 内层循环的每一次迭代保证了Ai的产生式的右部首符号都比Aj更加靠后当内层循环结束时，Ai的产生式的右部首符号不先于Ai消除立即左递归就保证了Ai的产生式的右部首符号必然比Ai后。（不能有环和ε产生式）当外层循环结束时，所有的产生式都是如此。使用这些产生式当然不会产生左递归的情形示例：3.3 提取左公因子在推导的时候，不知道该如何选择 方法：例子：4. 自顶向下分析技术自顶向下分析可以被看作是为输入串构造语法分析树的问题，也可以看作一个寻找输入串的最左推导的过程最左推导：总是选择每个句型的最左非终结符进行替换问题：对于非终结符号 A，选择哪一个产生式 根据输入流中的下一个终结符，选择最左非终结符4.1 预测分析法处理 LL 文法时需要考虑是否有左递归、是否需要提取公因子预测分析技术：一种确定的、无回溯的分析技术，在每一步选择正确的产生式 比递归下降分析效率更高、准确性更高 可以向前选择多个产生式一般情况下只需要向前一个符号 LL (1)提取左公因子后不会有两个相同的符号LL (2)、LL (n) 效率会降低很多4.1.1 First 和 Follow 定义 First 和 Follow： 4.1.1.1 First函数First 函数的意义： First函数的计算：对于文法符号 X 的 FIRST (X)，通过不断应用下列规则，直到没有新的终结符号或者ε可以加入到任何 FIRST 集合为止 所有的 First (x) 中都有ε时才将ε加入 First (x1x2x3... xn) 中示例： 4.1.1.2 Follow 函数Follow 的计算： 示例：4.2 LL (1) 文法LL (1) 文法，第一个 L 表示自左向右扫描，第二个 L 指产生最左推导，而 1 则表示向前看一个输入符号 对于 LL (1) 文法，可以在自顶向下分析过程中，根据当前输入符号来确定使用的产生式 4.3 LL (1) 文法的预测分析技术4.3.1 预测分析程序构成 总控程序：根据现行栈顶符号和当前输入符号，执行动作分析表分析栈：用于存放文法符号总控程序对于任何一种语言是通用的——数据与控制分离预测分析表将 First 和 Follow 集合中的信息放入一个预测分析表 M[A, a]，该预测表告诉我们当非终结符号为 A，当前输入符号为 a 时，要选择哪条产生式 4.3.1 预测分析表的构造输入：文法 G输出：预测分析表 M 例子：4.3.2 非递归的预测分析 4.3.3 分析表驱动的预测分析器 5. 自底向上语法分析采用最左规约，即反向构造最右推导在每一步的归约中，一个与某产生式体相匹配的特定子串被替换为该产生式头部的非终结符号，一次归约实质上是一个推导的反向操作自底向上分析的过程 规约的问题：选择哪部分进行归约?应用哪个产生式进行归约?5.1 短语、直接短语和句柄短语：直接短语：短语代表了可规约的串直接短语代表了课立即规约的串句柄：一个句型的最左直接短语称为该句型的句柄句柄是最右推导的反向过程中被规约的那些部分 5.1.1 句柄剪枝用句柄进行规约 201772174326675199915.2 移入-规约 使用一个栈来保存归约/扫描移入的文法符号栈中符号（从底向上）和待扫描的符号组成了一个最右句型开始时刻：栈中只包含$，而输入为w$成功结束时刻：栈中$S，而输入$在分析过程中，不断地移入符号，并在识别到句型时进行归约主要分析动作5.2.1 移进/规约冲突移入-归约技术并不能处理所有上下文无关文法 5.3 LR 分析法优点：自底向上分析的关键是正确的识别句柄句柄是逐步形成的，用“状态”表示句柄识别的进展程度 LR 分析器基于这些状态来构造自动机进行句柄识别5.3.1 LR 分析器的结构包含一个与符号栈平行的状态栈分析表的结构： 每一行对应一个状态ACTION 表每一列对应一个终结符与$，每一项表示一个动作sn：表示移入，将当前输入符号移入分析栈，并进入到状态nrn：表示规约，使用第n个产生式规约5.3.2 LR (0) 项 项目描述了句柄识别的状态5.3.3 LR (0) 自动机规范LR(0)项集族提供构建LR(0)自动机的基础LR(0)自动机可用于做出语法分析决定LR (0) 自动机中每个状态代表了 LR (0) 项集族中的一个项集 5.3.3.1 增广文法5.3.3.2 文法中的项 红色方框中的为规约项目，点在末尾 后继项：同属于一个产生式，但是圆点的位置只相差一个符号 CLOSURE (I) 的构造算法把所有项目都作为自动机的状态，会使得自动机的状态过多，可以寻找等价的项目来减少自动机的项目 把所有等价的项目组成一个项目集，称为项目集闭包，每个项目及闭包对应着自动机的一个状态 例子： 内核项与非内核项GOTO 函数5.3.3.3 求 LR (0) 相集规范族的算法与构造自动机自动机的构造方法 例子： 相应的分析表： 1 号状态为接收状态，遇到$后完成接受4 号状态为规约状态，对应第三条产生式，采用第三条产生式进行规约5 号 6 号类似 4 号5.3.3.5 LR (0) 自动机的作用