概念

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. Elasticsearch 如何保证写入过程中不丢失数据的

    丢失数据的本质 在本文开始前,首先明白一个点,平时我们说的组件数据不丢失究竟是在指什么,如果你往ES写入数据,ES返回给你写入错误,这个不算数据丢失.如果你往ES写入数据,ES返回给你成功,但是后续因 ...

  2. obsidian 日记本倒序汇总 获取标题显示 插件dataviewjs list

    obsidian 日记本倒序汇总 获取标题显示 插件dataviewjs list // dataviewjs function removeDuplicate(arr) { return arr.f ...

  3. 基于BES2500芯片的低功耗蓝牙BLE游戏手柄解决方案源码解析

    一 往事    寒冬腊月,在一个寂静的天空飘着碎银雪花的夜晚.我接到这么一个电话:"朋友,能否帮忙开发一个游戏手柄的案子?我们遇到了一些问题,迟迟无法解决.",喔,这边我陷入了沉思 ...

  4. [置顶] tomcat处理请求导致页面出现ERR_CONNECTION_RESET错误解决方案

    现象: 浏览器发送请求到servlet,servlet处理时间太久,所以导致chrome浏览器出现ERR_CONNECTION_RESET错误 解决方案: 在相应servlet执行最后添加一句代码: ...

  5. 谈谈Redis五种数据结构及真实应用场景

    前言 如果问你redis有哪些数据结构,你肯定可以一口气说出五种基本数据结构: String(字符串).Hash(哈希).List(列表).Set(集合).zset(有序集合) 你或许还知道它还有三种 ...

  6. Java8的核心功能就是Lambda和Streaming API

    Java8的核心功能就是Lambda和Streaming API

  7. 常用命令rsyncscp-1

    常用命令:rsync/scp scp scp命令文件传输 scp命令用于在Linux下进行远程拷贝文件的命令,和它类似的命令有cp,不过cp只是在本机进行拷贝不能跨服务器,而且scp传输是加密的.可能 ...

  8. 【个人笔记】2023年搭建基于webpack5与typescript的react项目

    写在前面 由于我在另外的一些文章所讨论或分析的内容可能基于一个已经初始化好的项目,为了避免每一个文章都重复的描述如何搭建项目,我在本文会统一记录下来,今后相关的文章直接引用文本,方便读者阅读.此文主要 ...

  9. 工作记录:TypeScript从入门到项目实战(基础篇)

    前言 TypeScript是什么? 引用官方原话 TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript.TypeScript可以在任何浏览器.任何计算机和任何操作系 ...

  10. Kafka之Producer网络传输

    一.背景 在Kafka的组成部分(Broker.Consumer.Producer)中,设计理念迥异,每个部分都有自己独特的思考.而把这些部分有机地组织起来,使其成为一个整体的便是「网络传输」.区别于 ...