概念

kratos 为了使http协议的逻辑代码和grpc的逻辑代码使用同一份,选择了基于protobuf的IDL文件使用proto插件生成辅助代码的方式。

protoc http插件的地址为:https://github.com/go-kratos/kratos/tree/main/cmd/protoc-gen-go-http

示例

syntax = "proto3";

package helloworld;

option go_package = "test/helloworld;helloworld";
option java_multiple_files = true;
option java_package = "helloworld";
import "google/api/annotations.proto"; service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/helloworld", // 声明路由
body: "*"
};
}
} message HelloRequest {
string name = 1;
} message HelloReply {
string msg = 1;
}

使用kratos proto client xxx 生成的代码为:

// Code generated by protoc-gen-go-http. DO NOT EDIT.
// versions:
// - protoc-gen-go-http v2.4.0
// - protoc v3.19.4
// source: helloworld/helloworld.proto package helloworld import (
context "context"
http "github.com/go-kratos/kratos/v2/transport/http"
binding "github.com/go-kratos/kratos/v2/transport/http/binding"
) // This is a compile-time assertion to ensure that this generated file
// is compatible with the kratos package it is being compiled against.
var _ = new(context.Context)
var _ = binding.EncodeURL const _ = http.SupportPackageIsVersion1 const OperationGreeterSayHello = "/helloworld.Greeter/SayHello" type GreeterHTTPServer interface {
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
} func RegisterGreeterHTTPServer(s *http.Server, srv GreeterHTTPServer) {
r := s.Route("/")
r.POST("/helloworld", _Greeter_SayHello0_HTTP_Handler(srv))
} func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
return func(ctx http.Context) error {
var in HelloRequest
if err := ctx.Bind(&in); err != nil {
return err
}
http.SetOperation(ctx, OperationGreeterSayHello)
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.SayHello(ctx, req.(*HelloRequest))
})
out, err := h(ctx, &in)
if err != nil {
return err
}
reply := out.(*HelloReply)
return ctx.Result(200, reply)
}
} type GreeterHTTPClient interface {
SayHello(ctx context.Context, req *HelloRequest, opts ...http.CallOption) (rsp *HelloReply, err error)
} type GreeterHTTPClientImpl struct {
cc *http.Client
} func NewGreeterHTTPClient(client *http.Client) GreeterHTTPClient {
return &GreeterHTTPClientImpl{client}
} func (c *GreeterHTTPClientImpl) SayHello(ctx context.Context, in *HelloRequest, opts ...http.CallOption) (*HelloReply, error) {
var out HelloReply
pattern := "/helloworld"
path := binding.EncodeURL(pattern, in, false)
opts = append(opts, http.Operation(OperationGreeterSayHello))
opts = append(opts, http.PathTemplate(pattern))
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...)
if err != nil {
return nil, err
}
return &out, err
}

开启一个grpc及http服务:

package main

import (
"context"
"fmt"
"log"
"test/helloworld" "github.com/go-kratos/kratos/v2"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
) type server struct {
helloworld.UnimplementedGreeterServer
} func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
return &helloworld.HelloReply{Msg: fmt.Sprintf("Hello %+v", in.Name)}, nil
} func main() {
s := &server{}
httpSrv := http.NewServer(
http.Address(":8000"),
http.Middleware(
recovery.Recovery(),
),
)
grpcSrv := grpc.NewServer(
grpc.Address(":9000"),
grpc.Middleware(
recovery.Recovery(),
),
) helloworld.RegisterGreeterServer(grpcSrv, s)
helloworld.RegisterGreeterHTTPServer(httpSrv, s) app := kratos.New(
kratos.Name("test"),
kratos.Server(
httpSrv,
grpcSrv,
),
) if err := app.Run(); err != nil {
log.Fatal(err)
}
}

http client:

package main

import (
"context"
"log"
"test/helloworld" "github.com/go-kratos/kratos/v2/middleware/recovery"
transhttp "github.com/go-kratos/kratos/v2/transport/http"
) func main() {
callHTTP()
} func callHTTP() {
conn, err := transhttp.NewClient(
context.Background(),
transhttp.WithMiddleware(
recovery.Recovery(),
),
transhttp.WithEndpoint("127.0.0.1:8000"),
)
if err != nil {
panic(err)
}
defer conn.Close()
client := helloworld.NewGreeterHTTPClient(conn)
reply, err := client.SayHello(context.Background(), &helloworld.HelloRequest{Name: "kratos"})
if err != nil {
log.Fatal(err)
}
log.Printf("[http] SayHello %s\n", reply.Msg)
}

http server端实现原理

核心流程为下图 :

首先新建一个struct 并实现 http_pb.go种 GreeterHTTPServer interface  的方法,GreeterHTTPServer的命名方式为protobuf文件中的 service+HTTPServer,interface的方法为protobuf中使用google.api.http生命http路由所有的method。

然后使用RegisterGreeterHTTPServer方法把服务注册进去。大体的流程如下:

const OperationGreeterSayHello = "/helloworld.Greeter/SayHello"

func RegisterGreeterHTTPServer(s *http.Server, srv GreeterHTTPServer) {
r := s.Route("/")
r.POST("/helloworld", _Greeter_SayHello0_HTTP_Handler(srv)) // 注册路由
} func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
return func(ctx http.Context) error {
var in HelloRequest // protobuf 中声明的request
if err := ctx.Bind(&in); err != nil { // 把http的参数绑定到 in
return err
}
http.SetOperation(ctx, OperationGreeterSayHello) // 设置Operation 和grpc一值,用于middleware select 等
h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.SayHello(ctx, req.(*HelloRequest)) // 这个方法也就是上文提到的GreeterHTTPServer接口的方法,也就是我们自己实现的struct server里的SayHello方法
}) // 使用责任链模式middleware 这里没有任何中间件
out, err := h(ctx, &in) // 执行
if err != nil {
return err
}
reply := out.(*HelloReply)
return ctx.Result(200, reply)
}
}

什么事责任链模式?

https://haiyux.cc/post/designmode/behavioral/#责任链模式

上段代码中的POST方法为:

代码在https://github.com/go-kratos/kratos/blob/main/transport/http/router.go#L76

func (r *Router) POST(path string, h HandlerFunc, m ...FilterFunc) {
r.Handle(http.MethodPost, path, h, m...) // MethodPost = POST net/http下的常量
} // h 为上段xxx_http_pb.go代码中_Greeter_SayHello0_HTTP_Handler的返回值
func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) {
next := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
ctx := r.pool.Get().(Context)
ctx.Reset(res, req) // 把 net/http的http.ResponseWriter 和*http.Request 设置ctx中
if err := h(ctx); err != nil { // 执行h
r.srv.ene(res, req, err) // 如果出错了 执行 ene(EncodeErrorFunc)
}
ctx.Reset(nil, nil)
r.pool.Put(ctx)
}))
next = FilterChain(filters...)(next)
next = FilterChain(r.filters...)(next) // 添加filter 责任链模式
r.srv.router.Handle(path.Join(r.prefix, relativePath), next).Methods(method) // router 为 mux的router 把方法注册到路由中
}

当我们访问 path.Join(r.prefix, relativePath)也就是/helloworld 时,会执行上段代码中的next方法,next是一个责任链。

核心为会执行_Greeter_SayHello0_HTTP_Handler方法,

如果没发生错误,执行ctx.Result(200, reply)

type wrapper struct {
router *Router
req *http.Request
res http.ResponseWriter
w responseWriter
} func (c *wrapper) Result(code int, v interface{}) error {
c.w.WriteHeader(code)
return c.router.srv.enc(&c.w, c.req, v)
}

enc也就是EncodeResponseFunc, 为kratos预留的返回值函数

type EncodeResponseFunc func(http.ResponseWriter, *http.Request, interface{}) error

kratos提供了默认的EncodeResponseFunc

func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
if v == nil {
return nil
}
if rd, ok := v.(Redirector); ok { // 检查有无Redirect方法,如果实现了interface 为跳转路由 也就是http的301 302等
url, code := rd.Redirect()
http.Redirect(w, r, url, code) // 跳转
return nil
}
codec, _ := CodecForRequest(r, "Accept") // 查看需要返回的参数类型 比如json
data, err := codec.Marshal(v) // 把数据Marshal成[]byte
if err != nil {
return err
}
w.Header().Set("Content-Type", httputil.ContentType(codec.Name())) // 设置header
_, err = w.Write(data) // 写数据
if err != nil {
return err
}
return nil
}

如果没发生错误,执行ene,也就是EncodeErrorFunc, 为kratos预留的错误返回值删除

type EncodeErrorFunc func(http.ResponseWriter, *http.Request, error)

kratos提供了默认的EncodeErrorFunc

func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {
se := errors.FromError(err) // 把error变成自定义的实现error的结构体
codec, _ := CodecForRequest(r, "Accept") // 查看需要返回的参数类型 比如json
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
w.WriteHeader(int(se.Code)) // 写入 error中的code
_, _ = w.Write(body) // 返回错误信息
}

http client端实现原理

在上传的代码中http client的部分为

type GreeterHTTPClient interface {
SayHello(ctx context.Context, req *HelloRequest, opts ...http.CallOption) (rsp *HelloReply, err error)
} type GreeterHTTPClientImpl struct { // 实现 GreeterHTTPClient 接口
cc *http.Client
} func NewGreeterHTTPClient(client *http.Client) GreeterHTTPClient {
return &GreeterHTTPClientImpl{client}
} func (c *GreeterHTTPClientImpl) SayHello(ctx context.Context, in *HelloRequest, opts ...http.CallOption) (*HelloReply, error) {
var out HelloReply // 返回值
pattern := "/helloworld"
path := binding.EncodeURL(pattern, in, false) // 整理path 传入in 是由于可能有path参数或者query
opts = append(opts, http.Operation(OperationGreeterSayHello))
opts = append(opts, http.PathTemplate(pattern))
err := c.cc.Invoke(ctx, "POST", path, in, &out, opts...) // 访问接口
if err != nil {
return nil, err
}
return &out, err
}

上段代码中的Invoke方法为:

代码在https://github.com/go-kratos/kratos/blob/main/transport/http/client.go#L192

func (client *Client) Invoke(ctx context.Context, method, path string, args interface{}, reply interface{}, opts ...CallOption) error {
var (
contentType string
body io.Reader
)
c := defaultCallInfo(path)
for _, o := range opts {
if err := o.before(&c); err != nil {
return err
}
}
if args != nil {
data, err := client.opts.encoder(ctx, c.contentType, args)
if err != nil {
return err
}
contentType = c.contentType
body = bytes.NewReader(data)
}
url := fmt.Sprintf("%s://%s%s", client.target.Scheme, client.target.Authority, path)
req, err := http.NewRequest(method, url, body)
if err != nil {
return err
}
if contentType != "" {
req.Header.Set("Content-Type", c.contentType)
}
if client.opts.userAgent != "" {
req.Header.Set("User-Agent", client.opts.userAgent)
}
ctx = transport.NewClientContext(ctx, &Transport{
endpoint: client.opts.endpoint,
reqHeader: headerCarrier(req.Header),
operation: c.operation,
request: req,
pathTemplate: c.pathTemplate,
})
return client.invoke(ctx, req, args, reply, c, opts...)
} func (client *Client) invoke(ctx context.Context, req *http.Request, args interface{}, reply interface{}, c callInfo, opts ...CallOption) error {
h := func(ctx context.Context, in interface{}) (interface{}, error) {
res, err := client.do(req.WithContext(ctx))
if res != nil {
cs := csAttempt{res: res}
for _, o := range opts {
o.after(&c, &cs)
}
}
if err != nil {
return nil, err
}
defer res.Body.Close()
if err := client.opts.decoder(ctx, res, reply); err != nil {
return nil, err
}
return reply, nil
}
var p selector.Peer
ctx = selector.NewPeerContext(ctx, &p)
if len(client.opts.middleware) > 0 {
h = middleware.Chain(client.opts.middleware...)(h)
}
_, err := h(ctx, args)
return err
}

kratos http原理的更多相关文章

  1. 通过 layout 探索 kratos 运行原理

    创建项目 首先需要安装好对应的依赖环境,以及工具: go 下载 protoc go install google.golang.org/protobuf/cmd/protoc-gen-go@lates ...

  2. kratos

    技术文章 日志库的使用姿势 通过 layout 探索 kratos 运行原理 发版日志 发布日志 - kratos v2.0.5 版本发布 发布日志 - kratos v2.0.4 版本发布

  3. go微服务框架kratos学习笔记八 (kratos的依赖注入)

    目录 go微服务框架kratos学习笔记八(kratos的依赖注入) 什么是依赖注入 google wire kratos中的wire Providers injector(注入器) Binding ...

  4. 从kratos分析BBR限流源码实现

    什么是自适应限流 自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load.CPU 使用率.总体平均 RT.入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流 ...

  5. 奇异值分解(SVD)原理与在降维中的应用

    奇异值分解(Singular Value Decomposition,以下简称SVD)是在机器学习领域广泛应用的算法,它不光可以用于降维算法中的特征分解,还可以用于推荐系统,以及自然语言处理等领域.是 ...

  6. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

  7. 线性判别分析LDA原理总结

    在主成分分析(PCA)原理总结中,我们对降维算法PCA做了总结.这里我们就对另外一种经典的降维方法线性判别分析(Linear Discriminant Analysis, 以下简称LDA)做一个总结. ...

  8. [原] KVM 虚拟化原理探究(1)— overview

    KVM 虚拟化原理探究- overview 标签(空格分隔): KVM 写在前面的话 本文不介绍kvm和qemu的基本安装操作,希望读者具有一定的KVM实践经验.同时希望借此系列博客,能够对KVM底层 ...

  9. H5单页面手势滑屏切换原理

    H5单页面手势滑屏切换是采用HTML5 触摸事件(Touch) 和 CSS3动画(Transform,Transition)来实现的,效果图如下所示,本文简单说一下其实现原理和主要思路. 1.实现原理 ...

  10. .NET Core中间件的注册和管道的构建(1)---- 注册和构建原理

    .NET Core中间件的注册和管道的构建(1)---- 注册和构建原理 0x00 问题的产生 管道是.NET Core中非常关键的一个概念,很多重要的组件都以中间件的形式存在,包括权限管理.会话管理 ...

随机推荐

  1. Zabbix“专家坐诊”第182期问答汇总

    问题一: Q:像烽火.浪潮这种没有ilo的设备怎么监控他们的硬件状态呢? A:如果没有ilo,可以使用其他硬件监控软件,例如HP Insight Manager.IBM Director.Dell O ...

  2. [VueJsDev] 基础知识 - Node.js常用函数

    [VueJsDev] 目录列表 https://www.cnblogs.com/pengchenggang/p/17037320.html Node.js 常用函数 总结常用 node 函数 用的 E ...

  3. 【数据结构】哈夫曼树与哈夫曼编码(Huffman Encoding)

    一.背景 编码是信息处理的基础(重新表示信息). 普通的编码是等长编码,例如7位的ASCIL编码,对出现频率不同的字符都使用相同的编码长度.但其在传输和存储等情况下编码效率不高. 可使用不等长编码,来 ...

  4. 几个有用的svn命令

    1.检验authz文件是否合法;svnauthz-validate /root/.svn/webservice/conf/authz 2.杀死svn服务:killall svnserve 3.查看sv ...

  5. vite中配置less,vue3中配置less

    前言 如果赶时间请直接使用目录跳到解决问题的部分. 使用的项目使用vue脚手架生成. npm init vue@latest 版本如下 "@vitejs/plugin-vue": ...

  6. windows 远程桌面 复制粘贴 无效

    rdpclip.exe进程没有运行或运行异常. rdpclip 是让rdp协议(远程桌面协议)可以通过远程复制文件的,如果你使用rdp(3389)远程连接别人或者被别人连接,通常这个进程都会启动,他的 ...

  7. multisim的操作回顾

    multisim的操作回顾 1.写在前面 multisim的仿真功能还是强大的,能够有效地实现对电路功能的验证.但是,不能全局搜索器件是个大问题.对于不熟悉器件的基本分类的人来说,一排的分类足以浪费大 ...

  8. kingbase ES group by 语句优化

    1.group by 分组语句 在SQL中group by主要用来进行分组统计,分组字段放在group by的后面:分组结果一般需要借助聚合函数实现. group by语法结构 1.常用语法 语法结构 ...

  9. KingbaseES 原生XML系列三--XML数据查询函数

    KingbaseES 原生XML系列三--XML数据查询函数(EXTRACT,EXTRACTVALUE,EXISTSNODE,XPATH,XPATH_EXISTS,XMLEXISTS) XML的简单使 ...

  10. .net core ECDsa

    ECDsa(Elliptic Curve Digital Signature Algorithm)是一种基于椭圆曲线密码学的数字签名算法.在.NET Core中,System.Security.Cry ...