go-micro中的client实现

809 阅读3分钟

Client是一个用于请求服务的接口,支持通过Transport进行Request/Response,以及通过Broker进行发布订阅的功能。同时也支持全双工的steam请求。

type Client interface {
	Init(...Option) error
	Options() Options
	NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
	NewRequest(service, endpoint string, req interface{}, reqOpts ...RequestOption) Request
        // 创建一个同步调用
	Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
	Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
	Publish(ctx context.Context, msg Message, opts ...PublishOption) error
	String() string
}

接下来我们就一起看看默认的实现

var (
	// DefaultClient is a default client to use out of the box
	DefaultClient Client = newRpcClient()
	// DefaultBackoff is the default backoff function for retries
	DefaultBackoff = exponentialBackoff
	// DefaultRetry is the default check-for-retry function for retries
	DefaultRetry = RetryOnError
	// DefaultRetries is the default number of times a request is tried
	DefaultRetries = 1
	// DefaultRequestTimeout is the default request timeout
	DefaultRequestTimeout = time.Second * 5
	// DefaultPoolSize sets the connection pool size
	DefaultPoolSize = 100
	// DefaultPoolTTL sets the connection pool ttl
	DefaultPoolTTL = time.Minute

	// NewClient returns a new client
	NewClient func(...Option) Client = newRpcClient
)

使用newRpcClient实例化一个rpcClient实例

type rpcClient struct {
	seq  uint64
	once atomic.Value
	opts Options
	pool pool.Pool
}

func newRpcClient(opt ...Option) Client {
	opts := NewOptions(opt...)

	p := pool.NewPool(
		pool.Size(opts.PoolSize),
		pool.TTL(opts.PoolTTL),
		pool.Transport(opts.Transport),
	)

	rc := &rpcClient{
		opts: opts,
		pool: p,
		seq:  0,
	}
	rc.once.Store(false)

	c := Client(rc)

	// wrap in reverse
	for i := len(opts.Wrappers); i > 0; i-- {
		c = opts.Wrappers[i-1](c)
	}

	return c
}

rpcClient实例中,我们一起来看看他是如何实现的同步请求的,由于该方法内容较多,我们摘选部分重点代码进行阅读

  1. 首先复制一份CallOptions
callOpts := r.opts.CallOptions
	for _, opt := range opts {
		opt(&callOpts)
	}

2.负载均衡一个发起请求调用的节点。

next, err := r.next(request, callOpts)
	if err != nil {
		return err
	}

3.检查是否进行了超时请求配置

d, ok := ctx.Deadline()
	if !ok {
		// no deadline so we create a new one
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
		defer cancel()
	} else {
		// got a deadline so no need to setup context
		// but we need to set the timeout we pass along
		opt := WithRequestTimeout(time.Until(d))
		opt(&callOpts)
	}

4,复制一份call方法,并以逆序的方式包装上的调用中间件。

rcall := r.call
for i := len(callOpts.CallWrappers); i > 0; i-- {
        rcall = callOpts.CallWrappers[i-1](rcall)
}

5.定义一个内部的函数变量在请求重试的时候进行调用,该函数变量进行真正意义上的方法调用就是我们上个步骤中的call方法拷贝。

call := func(i int) error {
		// call backoff first. Someone may want an initial start delay
		t, err := callOpts.Backoff(ctx, request, i)
		if err != nil {
			return errors.InternalServerError("go.micro.client", "backoff error: %v", err.Error())
		}

		// only sleep if greater than 0
		if t.Seconds() > 0 {
			time.Sleep(t)
		}

		// select next node
		node, err := next()
		service := request.Service()
		if err != nil {
			if err == selector.ErrNotFound {
				return errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
			}
			return errors.InternalServerError("go.micro.client", "error getting next %s node: %s", service, err.Error())
		}

		// make the call
		err = rcall(ctx, node, request, response, callOpts)
		r.opts.Selector.Mark(service, node, err)
		return err
	}

如果我们请求service使用的是代理的话,就不会进行重试retires置位0.

在重试的逻辑中,

我们首先进行第一次方法调用,结果返回值放到chan error类型的管道中,

如果我们从chan error取出的err为空,说明调用正常,退出Call方法,并返回nil,否则进行重新调用rcall,重试次数限定为我们opts中的Retries配置。

在第四步中,进行了的call方法复制时,复制的是rpcClient实例上的call方法,具体我们来看看这个方法的实现逻辑,

codec := newRpcCodec(msg, c, cf, "")

	rsp := &rpcResponse{
		socket: c,
		codec:  codec,
	}

构建一个rpcResponse,并使用该rsp进行实例化一个rpcStream

stream := &rpcStream{
		id:       fmt.Sprintf("%v", seq),
		context:  ctx,
		request:  req,
		response: rsp,
		codec:    codec,
		closed:   make(chan bool),
		release:  func(err error) { r.pool.Release(c, err) },
		sendEOS:  false,
	}
	// close the stream on exiting this function
	defer stream.Close()

新起一个协程,用于全双工的发送请求以及接收响应

go func() {
		defer func() {
			if r := recover(); r != nil {
				ch <- errors.InternalServerError("go.micro.client", "panic recovered: %v", r)
			}
		}()

		// send request
		if err := stream.Send(req.Body()); err != nil {
			ch <- err
			return
		}

		// recv request
		if err := stream.Recv(resp); err != nil {
			ch <- err
			return
		}

		// success
		ch <- nil
	}()

最后根据结果来控制方法的异常处理

var grr error

select {
case err := <-ch:
        return err
case <-ctx.Done():
        grr = errors.Timeout("go.micro.client", fmt.Sprintf("%v", ctx.Err()))
}

// set the stream error
if grr != nil {
        stream.Lock()
        stream.err = grr
        stream.Unlock()

        return grr
}

以上就是整个请求的处理过程。我们通过一个示例来看看如何实现一个client

func call(i int, c client.Client) {
	// Create new request to service go.micro.srv.example, method Example.Call
	req := c.NewRequest("go.micro.srv.example", "Example.Call", &example.Request{
		Name: "John",
	})

	// create context with metadata
	ctx := metadata.NewContext(context.Background(), map[string]string{
		"X-User-Id": "john",
		"X-From-Id": "script",
	})

	rsp := &example.Response{}

	// Call service
	if err := c.Call(ctx, req, rsp); err != nil {
		fmt.Println("call err: ", err, rsp)
		return
	}

	fmt.Println("Call:", i, "rsp:", rsp.Msg)
}

func main() {
        // 这个是使用micro框架启动的一个服务
	service := micro.NewService()
	service.Init()


	fmt.Println("\n--- Call example ---")
	for i := 0; i < 10; i++ {
		call(i, service.Client())
	}
}