我们的框架代码地址是 gorpc

一、server 的结构

要搭建 server 层,首先我们要明确 server 层需要支持哪些能力,其实 server 的核心就是提供服务请求的处理能力。server 侧定义服务,发布服务,接收到服务的请求后,根据服务名和请求的方法名去路由到一个 handler 处理器,然后由 handler 处理请求,得到响应,并且把响应数据发送给 client。

按照这个思路,我们可以先定义出 server 的结构,如下:

type Server struct {
   opts *ServerOptions
   services map[string]Service
}

ServerOptions 是通过选项模式来透传业务自己指定的一些参数,比如服务监听的地址 address,网络类型 network 是 tcp 还是 udp,后端服务的超时时间 timeout 等。

type ServerOptions struct {
   address string  // listening address, e.g. :( ip://127.0.0.1:8080、 dns://www.google.com)
   network string  // network type, e.g. : tcp、udp
   protocol string  // protocol typpe, e.g. : proto、json
   timeout time.Duration       // timeout
   serializationType string   // serialization type, default: proto

   selectorSvrAddr string       // service discovery server address, required when using the third-party service discovery plugin
   tracingSvrAddr  string         // tracing plugin server address, required when using the third-party tracing plugin
   tracingSpanName string       // tracing span name, required when using the third-party tracing plugin
   pluginNames []string         // plugin name
   interceptors []interceptor.ServerInterceptor
}

services 是一个 Service 的 map,每个 Service 表示一个服务,一个 server 可以发布多个服务,用服务名 serviceName 作 map 的 key。我们来看一下 Service 的定义和结构,Service 的接口定义了每个服务需要提供的通用能力,包括 Register (处理函数 Handler 的注册)、提供服务 Serve,服务关闭 Close 等方法

// Service 定义了某个具体服务的通用实现接口
type Service interface {
   Register(string, Handler)
   Serve(*ServerOptions)
   Close()
}

type service struct{
   svr interface{}          // server
   ctx context.Context       // 每一个 service 一个上下文进行管理
   cancel context.CancelFunc   // context 的控制器
   serviceName string        // 服务名
   handlers map[string]Handler
   opts *ServerOptions       // 参数选项
}

顺便说一下 service 这个结构,它是 Service 接口的具体实现。它的核心是 handlers 这个 map,每一类请求会分配一个 Handler 进行处理

二、server 提供的能力

1、server 创建

server 的创建比较简单,主要是 ServerOptions 和 service map 的初始化,这里通过选项模式对 opts 进行赋值

func NewServer(opt ...ServerOption) *Server{
   s := &Server {
      opts : &ServerOptions{},
      services: make(map[string]Service),
   }
   for _, o := range opt {
      o(s.opts)
   }
   return s
}

2、服务注册

服务注册主要是将 Service 添加到 server 的 service map 里面,这里就牵涉到服务的定义了,定义一个服务有两种方式

  • 通过 go struct 定义

    这种定义方式是按照协议 go struct 的方式,去申明一个服务,例如以下是一个服务的定义:

    package helloworld
    
    import "context"
    
    type Service struct {
    
    }
    
    type HelloRequest struct {
       Msg string
    }
    
    type HelloReply struct {
       Msg string
    }
    
    func (s *Service) SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
       rsp := &HelloReply{
          Msg : "world",
       }
    
       return rsp, nil
    }
    

    定义了一个服务之后,通过调用 s.RegisterService 去进行服务注册

    s.RegisterService("/helloworld.Greeter", new(helloworld.Service))
    

    这里会通过反射的方式,通过 service 引用去获取 service 的每一个方法生成一个 handler,然后添加到 service 的 handler map 里面。当接收到 client 发过来的请求是,通过服务的方法名获取到这个服务的 handler,进而处理请求,得到响应,发送给客户端。反射调用的 demo 可以参考:helloworld

  • 通过 proto 文件定义

    这种定义方式是按照 protobuf 的标准去定义一个 Service,然后使用框架自带的代码生成工具生成 service的描述文件,然后调用 server 的 RegisterService 方法进行服务注册。例如以下是一个服务的定义

    syntax = "proto3";
    
    package helloworld;
    
    service Greeter {
      rpc SayHello (HelloRequest) returns (HelloReply) {}
    }
    
    message HelloRequest {
      string msg = 1;
    }
    
    message HelloReply {
      string msg = 1;
    }
    

    定义了一个服务之后,通过框架自带的 proto 生成工具插件,会根据 proto 文件去生成 service 的描述信息,包括服务名、服务每一个方法的方法名和方法的 handler

    var _Greeter_serviceDesc = &gorpc.ServiceDesc{
       ServiceName: "helloworld.Greeter",
       HandlerType: (*GreeterService)(nil),
       Methods : []*gorpc.MethodDesc{
          {
             MethodName: "SayHello",
             Handler:    GreeterService_SayHello_Handler,
          },
       },
    }
    

    同样会暴露出一个注册服务的 RegisterService 接口供 server 进行调用。跟反射的方式不一样的是,这里是在编译时就已经生成好了调用代码,而反射是在程序运行时去动态生成的。代码生成调用方式的 demo 可参考:helloworld2

3、服务运行

服务运行的核心方法是 Serve(),它的逻辑非常简单,就是会遍历 service map 里面所有的 service,然后运行 service 的 Serve 方法

func (s *Server) Serve() {

   for _, service := range s.services {
      go service.Serve(s.opts)
   }

   ch := make(chan os.Signal, 1)
   signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGSEGV)
   <-ch

   s.Close()
}

service 的 Serve 方法会去构建 transport ,监听客户端请求,根据请求的服务名 serviceName 和请求的方法名 methodName 调用相应的 handler 去处理请求,然后进行回包。

4、服务参数的传递 —— 选项模式

这里想单独介绍下选项 (Option) 模式,选项模式的实现方式基本会贯穿整个框架。几乎所有开源组件对于参数选项的传递都是使用这个模式

现在有一个场景,你创建一个 server 的时候,要设置若干不同类型的参数,例如有下列参数:

type ServerOptions struct {
   address string  // listening address, e.g. :( ip://127.0.0.1:8080、 dns://www.google.com)
   network string  // network type, e.g. : tcp、udp
   protocol string  // protocol typpe, e.g. : proto、json
   timeout time.Duration       // timeout
   serializationType string   // serialization type, default: proto

   selectorSvrAddr string       // service discovery server address, required when using the third-party service discovery plugin
   tracingSvrAddr  string         // tracing plugin server address, required when using the third-party tracing plugin
   tracingSpanName string       // tracing span name, required when using the third-party tracing plugin
   pluginNames []string         // plugin name
   interceptors []interceptor.ServerInterceptor
}

一般情况下,很多人的实现方式可能是:

func main() {
   opts := &ServerOptions{
      address : "ip://127.0.0.1:8080",
      network : "tcp",
      protocol : "proto",
   }
   NewServer(opts)
}

func NewServer(opts *ServerOptions) *Server {
   return &Server{
      opts: opts,
   }
}

type Server struct {
   opts *ServerOptions
}

这种方式要求 ServerOptions 里面的属性 address、network 等必须和调用函数 main 在同一个包下,假如是不同包的话,address、network 要被定义成公共变量 Address 和 Network ,否则无法访问(go 里面首字母小写代表这个变量是私有的,只能同一个包下访问,首字母大写是共有变量,才能在不同包下访问)。一般情况下,考虑到面向对象的封装性,当前包下访问的变量建议定义为私有变量。

一种更为优雅的实现方式即用选项模式实现,它的实现方式就如下面这段代码:

type ServerOptions struct {
   address string  // listening address, e.g. :( ip://127.0.0.1:8080、 dns://www.google.com)
   network string  // network type, e.g. : tcp、udp
   protocol string  // protocol typpe, e.g. : proto、json
}

type ServerOption func(*ServerOptions)

func WithAddress(address string) ServerOption{
   return func(o *ServerOptions) {
      o.address = address
   }
}

func WithNetwork(network string) ServerOption {
   return func(o *ServerOptions) {
      o.network = network
   }
}

在创建 Server 的时候我们可能需要指定一些配置信息,比如监听地址 address、网络类型 network,我们是这样传递的

opts := []gorpc.ServerOption{
   gorpc.WithAddress("127.0.0.1:8000"),
   gorpc.WithNetwork("tcp"),
   gorpc.WithProtocol("proto"),
   gorpc.WithTimeout(time.Millisecond * 2000),
}
s := gorpc.NewServer(opts ...)

NewServer 里面设置这些参数时会遍历所有的 ServerOption ,进行变量设置,如下:

func NewServer(opt ...ServerOption) *Server{
	s := &Server {
		opts : &ServerOptions{},
		services: make(map[string]Service),
	}

	for _, o := range opt {
		o(s.opts)
	}
  ...
}

这样既实现了参数的完美透传,又满足了面向对象的封装性。

小结

本章内容主要介绍了 server 的结构和 server 的创建、服务的注册和运行。可以有两种服务定义的方式,参数的透传使用选项模式。下一章我们将会介绍 client