简单的helloworld
// 首先看最基本的new app run
return kratos.New(
kratos.ID(id),
kratos.Name(Name),
kratos.Version(Version),
kratos.Metadata(map[string]string{}),
kratos.Logger(logger),
kratos.Server(
gs,
hs,
),
)
}
那么生成这个kratos的应用时候,大概工作有哪些呢?
// /home/soda/go/pkg/mod/github.com/go-kratos/kratos/v2@v2.4.1/app.go
func New(opts ...Option) *App {
......
// 省去上面一堆配置项,关键在于下面,kratos生成了一个专属自己的cancel contex,这个context在后面管理 // http和grpc等服务时候起了关键的作用
ctx, cancel := context.WithCancel(o.ctx)
return &App{
ctx: ctx,
cancel: cancel,
opts: o,
}
}
然后到后面关键的启动时候,程序做了哪些动作呢?
func (a *App) Run() error {
....
// 省去配置中心等代码
// 整个程序的的不同server管理,主要通过go内置的errgroup管理,而errgroup的ctx衍生于前面app初始化时候的ctx.简单说,app的ctx被cancel的时候,整个errgroup的ctx也会被cancel
eg, ctx := errgroup.WithContext(NewContext(a.ctx, a))
wg := sync.WaitGroup{}
for _, srv := range a.opts.servers {
// 下面是个注意点,在go的for循环中,闭包中有用到循环变量的话,用下面的用法可以防止错误
srv := srv
eg.Go(func() error {
<-ctx.Done() // 当app被cancel或者errgroup启动的协程函数返回错误时候,堵塞在此处的代码开始启动.运行各个server的Stop函数.注意,这里衍生的并不是app的ctx,而是初始化的ctx.
stopCtx, cancel := context.WithTimeout(NewContext(a.opts.ctx, a), a.opts.stopTimeout)
defer cancel()
return srv.Stop(stopCtx)
})
// 值得注意的是,下面需要利用WaigGroup来保证各个server启动后才继续进行下一步
wg.Add(1)
eg.Go(func() error {
wg.Done()
// 任意一个server启动失败的时候,都会返回err.导致errgroup的ctx被取消
return srv.Start(NewContext(a.opts.ctx, a))
})
}
wg.Wait()
......
c := make(chan os.Signal, 1)
signal.Notify(c, a.opts.sigs...)
eg.Go(func() error {
// 关键的地方,下面有两种情况会返回.一个是errgroup的ctx被结束时候.这个时候已经不需要再返回啥err
select {
case <-ctx.Done():
return nil
// 一个是收到系统信号时候,app的Stop函数会被调用.而Stop函数的主要内容有两个
// 一是在注册中心Deregister,二是调用app的Cancel函数,cancel掉app的ctx
// 当app的ctx被cancel后,整个errgroup的ctx也会被取消
// 接下里,前面的<-ctx.Done()将会停止阻塞.然后直接调用server的Stop函数.
// 值得注意的是,srv.Start传入的不是app的ctx或者衍生的errgroup的ctx,所以Start方法并不会第一时间被cancel.而是调用了Stop后才会被取消.这样Stop就可以传入timeout这个超时参数,来等待srv的返回
case <-c:
return a.Stop()
}
})
if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) {
return err
}
return nil
}
transport结构解析
kratos.New中,需要传入对应的grpc或者http服务.而在app的Run()方法中,就是遍历app的servers []transport.Server来实现的.
所以不管是提供http服务还是grpc服务,都是属于kratos的transport模块下.而krtaos的trans模块,主要抽象了下面两个接口.
// Server is transport server.
// http或者grpc的server,都必须实现下面接口,让app执行Run方法后服务启动或者后续的停止服务.
type Server interface {
Start(context.Context) error
Stop(context.Context) error
}
// Transporter is transport context value interface.
// Transporter的主要作用,就是起码当前请求的各种信息,比如是grpc还是http,路径和操作,请求带的header等等.把grpc和http两种类型的服务的请求的各种信息,统一成一种接口返回.
// 通常情况下,Transporter会放在请求的context里面,通过FromServerContext/FromClientContext来返回.
type Transporter interface {
Kind() Kind
Endpoint() string
Operation() string
RequestHeader() Header
ReplyHeader() Header
}
transport对应服务实现-grpc-server
krtaos提供的grpc-server关键代码如下:
type Server struct {
//内嵌了一个grpc的自身的server
*grpc.Server
//下面一系列补充属性,后面讲到会补充说明
...
}
// 下面的kratos grpc包里实现Transport接口的Transport结构体
type Transport struct {
endpoint string
operation string
reqHeader headerCarrier
replyHeader headerCarrier
filters []selector.Filter
}
// 对grpc的MD类型的扩展,使得实现transport.Header接口
type headerCarrier metadata.MD
// 初始化grpc-server方法
func NewServer(opts ...ServerOption) *Server {
// 下面两行代码是关键,初始化两个grpc拦截器数组,而这个数组在最开始就分别添加了unaryServerInterceptor和StreamServerInterceptor这两个拦截器.
unaryInts := []grpc.UnaryServerInterceptor{
srv.unaryServerInterceptor(),
}
streamInts := []grpc.StreamServerInterceptor{
srv.streamServerInterceptor(),
}
// 下面才是把我们自己需要的拦截器append到这两个数组
if len(srv.unaryInts) > 0 {
unaryInts = append(unaryInts, srv.unaryInts...)
}
if len(srv.streamInts) > 0 {
streamInts = append(streamInts, srv.streamInts...)
}
// 最终传给嵌入grpc server初始化的数组
grpcOpts := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(unaryInts...),
grpc.ChainStreamInterceptor(streamInts...),
}
....
// 配置项完成后,才开始
srv.Server = grpc.NewServer(grpcOpts...)
srv.metadata = apimd.NewServer(srv.Server)
// internal register
grpc_health_v1.RegisterHealthServer(srv.Server, srv.health)
apimd.RegisterMetadataServer(srv.Server, srv.metadata)
reflection.Register(srv.Server)
return srv
}
kratos是通过unaryServerInterceptor/ChainStreamInterceptor拦截器,用请求和服务器信息生成对应的transport,传入到请求的context中的,我们来看其中一个unaryServerInterceptor方法
func (s *Server) unaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// kratos的一个工具方法,把服务器的contex和请求的context合并成一个.
ctx, cancel := ic.Merge(ctx, s.baseCtx)
defer cancel()
// 取出grpc请求中对于的metadata
md, _ := grpcmd.FromIncomingContext(ctx)
replyHeader := grpcmd.MD{}
tr := &Transport{
operation: info.FullMethod,
// 把md转化成实现transport.Header接口的headerCarrier
reqHeader: headerCarrier(md),
replyHeader: headerCarrier(replyHeader),
}
if s.endpoint != nil {
tr.endpoint = s.endpoint.String()
}
ctx = transport.NewServerContext(ctx, tr)
if s.timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, s.timeout)
defer cancel()
}
h := func(ctx context.Context, req interface{}) (interface{}, error) {
return handler(ctx, req)
}
if next := s.middleware.Match(tr.Operation()); len(next) > 0 {
h = middleware.Chain(next...)(h)
}
reply, err := h(ctx, req)
if len(replyHeader) > 0 {
_ = grpc.SetHeader(ctx, replyHeader)
}
return reply, err
}
}
上面可以看到这个拦截器做了两个关键的动作:
- 通过一个工具方法ic.Merge,使得kratos-grpc-server的context和每个grpc请求的context合并为一个请求.
- 通过拦截器,首先把transport放在贯穿整个service的context里面.而把grpc的metadata和路由方法等信息放在统一transport里面.使得无论是grpc或者是后面要讲的http都能统一通过获取transport获取这些信息.
1.ic.Merge简单了解
// 合并后实现了Context接口的mergeCtx结构体
type mergeCtx struct {
parent1, parent2 context.Context
done chan struct{}
doneMark uint32
doneOnce sync.Once
doneErr error
cancelCh chan struct{}
cancelOnce sync.Once
}
// mergeCtx结构体把两个成员都作为自己的属性(双亲contex).在新建的时候就做好了其中一个已经被cancel的准备了
func Merge(parent1, parent2 context.Context) (context.Context, context.CancelFunc) {
mc := &mergeCtx{
parent1: parent1,
parent2: parent2,
done: make(chan struct{}),
cancelCh: make(chan struct{}),
}
select {
case <-parent1.Done():
_ = mc.finish(parent1.Err())
case <-parent2.Done():
_ = mc.finish(parent2.Err())
default:
go mc.wait()
}
return mc, mc.cancel
}
// 利用sync.Once只执行一次的特性,结束自身的done channel同时,
// 只保留第一个结束的子ctx的err
func (mc *mergeCtx) finish(err error) error {
mc.doneOnce.Do(func() {
mc.doneErr = err
atomic.StoreUint32(&mc.doneMark, 1)
close(mc.done)
})
return mc.doneErr
}
// 在任一双亲ctx结束时候,调用finish方法结束自身
func (mc *mergeCtx) wait() {
var err error
select {
case <-mc.parent1.Done():
err = mc.parent1.Err()
case <-mc.parent2.Done():
err = mc.parent2.Err()
case <-mc.cancelCh:
err = context.Canceled
}
_ = mc.finish(err)
}
// 下面两个是关键方法,很好的阐述明白了golang里面ctx的特性:
// 当父ctx(现在是任一双亲ctx)被结束时,自身也会被终结.
func (mc *mergeCtx) Done() <-chan struct{} {
return mc.done
}
// 注意的是,如果已经被结束了(doneMark已经被标记),就不必在去判断双亲节点结束与否
func (mc *mergeCtx) Err() error {
if atomic.LoadUint32(&mc.doneMark) != 0 {
return mc.doneErr
}
var err error
select {
case <-mc.parent1.Done():
err = mc.parent1.Err()
case <-mc.parent2.Done():
err = mc.parent2.Err()
case <-mc.cancelCh:
err = context.Canceled
default:
return nil
}
return mc.finish(err)
}
// 总结上面就是,自身监控着双亲context是否结束
// 一旦双亲结束或者自身被取消,则调用finish方法close自身done通道,且标记doneMark
2.kratos的中间件设计
kratos提供了一个通用的Middleware类型.使得一个这样类型的中间件可以同时用在grpc和http服务中使用.这部分内容可以后面再详细讲讲
// Middleware is HTTP/gRPC transport middleware.
type Middleware func(Handler) Handler