gRPC, большая похвала

Go gRPC

Оригинальная ссылка: gRPC, большая похвала

Технология gRPC действительно великолепна, со строгими ограничениями интерфейса и высокой производительностью, она используется в k8s и многих микросервисных фреймворках.

Как программист, изучите это правильно.

Раньше я писал несколько сервисов gRPC на Python, а теперь собираюсь использовать Go, чтобы попробовать настоящую разработку программы gRPC.

Особенность этой статьи в том, чтобы говорить напрямую с кодом, через полный код из коробки, чтобы представить различные методы использования gRPC.

Код загружен наGitHub, официально начинается следующее.

вводить

gRPC — это межъязыковая среда RPC с открытым исходным кодом, разработанная Google на основе Protobuf. gRPC разработан на основе протокола HTTP/2 и может предоставлять несколько служб на основе ссылки HTTP/2, что делает его более удобным для мобильных устройств.

начиная

Давайте сначала рассмотрим простейшую службу gRPC. Первым шагом является определение прото-файла. Поскольку gRPC также является архитектурой C/S, этот шаг эквивалентен уточнению спецификации интерфейса.

proto

syntax = "proto3";

package proto;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

Сгенерируйте код gRPC с помощью подключаемого модуля gRPC, встроенного в protoc-gen-go:

protoc --go_out=plugins=grpc:. helloworld.proto

После выполнения этой команды в текущей директории будет сгенерирован файл helloworld.pb.go, определяющий интерфейсы сервера и клиента:

// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
	// Sends a greeting
	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
	// Sends a greeting
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

Следующим шагом является написание кода сервера и клиента для реализации соответствующих интерфейсов соответственно.

server

package main

import (
	"context"
	"fmt"
	"grpc-server/proto"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

type greeter struct {
}

func (*greeter) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
	fmt.Println(req)
	reply := &proto.HelloReply{Message: "hello"}
	return reply, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	server := grpc.NewServer()
	// 注册 grpcurl 所需的 reflection 服务
	reflection.Register(server)
	// 注册业务服务
	proto.RegisterGreeterServer(server, &greeter{})

	fmt.Println("grpc server start ...")
	if err := server.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

client

package main

import (
	"context"
	"fmt"
	"grpc-client/proto"
	"log"

	"google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := proto.NewGreeterClient(conn)
	reply, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "zhangsan"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply.Message)
}

На этом разработка самой базовой службы gRPC завершена. Далее мы продолжим обогащать этот «базовый шаблон» и изучать дополнительные функции.

режим потока

Далее давайте посмотрим на способ потоковой передачи.Как следует из названия, данные можно отправлять и получать непрерывно.

Поток делится на односторонний поток и двусторонний поток.Здесь мы непосредственно используем двусторонний поток в качестве примера.

proto

service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    // Sends stream message
    rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}

Добавить функцию потокаSayHelloStream,пройти черезstreamключевое слово для указания свойств потока.

Файл helloworld.pb.go необходимо перегенерировать, поэтому я не буду здесь больше говорить.

server

func (*greeter) SayHelloStream(stream proto.Greeter_SayHelloStreamServer) error {
	for {
		args, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}

		fmt.Println("Recv: " + args.Name)
		reply := &proto.HelloReply{Message: "hi " + args.Name}

		err = stream.Send(reply)
		if err != nil {
			return err
		}
	}
}

Добавлено в "Основной шаблон"SayHelloStreamфункции, больше ничего менять не нужно.

client

client := proto.NewGreeterClient(conn)

// 流处理
stream, err := client.SayHelloStream(context.Background())
if err != nil {
	log.Fatal(err)
}

// 发送消息
go func() {
	for {
		if err := stream.Send(&proto.HelloRequest{Name: "zhangsan"}); err != nil {
			log.Fatal(err)
		}
		time.Sleep(time.Second)
	}
}()

// 接收消息
for {
	reply, err := stream.Recv()
	if err != nil {
		if err == io.EOF {
			break
		}
		log.Fatal(err)
	}
	fmt.Println(reply.Message)
}

Отправляйте сообщения через горутину, основную программуforПолучайте сообщения в цикле.

Выполнение программы обнаружит, что и сервер, и клиент имеют постоянные распечатки.

валидатор

Следующим идет валидатор.Это требование естественно продумано.Поскольку оно включает запросы между интерфейсами, необходимо должным образом проверить параметры.

Здесь мы используем protoc-gen-govalidators и go-grpc-middleware для достижения этой цели.

Сначала установите:

go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators

go get github.com/grpc-ecosystem/go-grpc-middleware

Затем измените файл proto:

proto

import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";

message HelloRequest {
    string name = 1 [
        (validator.field) = {regex: "^[z]{2,5}$"}
    ];
}

Прямо здесьnameПараметры проверяются, и обычный запрос должен быть выполнен до того, как будет сделан обычный запрос.

Существуют и другие правила проверки, такие как проверка размера чисел и т. д., которые здесь не рассматриваются.

Затем сгенерируйте файл *.pb.go:

protoc  \
    --proto_path=${GOPATH}/pkg/mod \
    --proto_path=${GOPATH}/pkg/mod/github.com/gogo/protobuf@v1.3.2 \
    --proto_path=. \
    --govalidators_out=. --go_out=plugins=grpc:.\
    *.proto

После успешного выполнения в каталоге появится дополнительный файл helloworld.validator.pb.go.

Здесь необходимо обратить особое внимание, нельзя использовать предыдущую простую команду, и нужно использовать несколькоproto_pathПараметр указывает каталог, в который импортируются прото-файлы.

Официально даны две зависимости, одна — google protobuf, а другая — gogo protobuf. Я использую второй здесь.

Даже с приведенной выше командой вы можете столкнуться с этой ошибкой:

Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors

Но не паникуйте, есть большая вероятность, что это проблема с эталонным путем, вы должны быть оптимистичны в отношении вашей установленной версии, иGOPATHконкретный путь в .

Наконец, модификация кода на стороне сервера:

Пакет импорта:

grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"

Затем добавьте функцию валидатора во время инициализации:

server := grpc.NewServer(
	grpc.UnaryInterceptor(
		grpc_middleware.ChainUnaryServer(
			grpc_validator.UnaryServerInterceptor(),
		),
	),
	grpc.StreamInterceptor(
		grpc_middleware.ChainStreamServer(
			grpc_validator.StreamServerInterceptor(),
		),
	),
)

После запуска программы мы используем предыдущий код клиента для запроса, и мы получим ошибку:

2021/10/11 18:32:59 rpc error: code = InvalidArgument desc = invalid field Name: value 'zhangsan' must be a string conforming to regex "^[z]{2,5}$"
exit status 1

так какname: zhangsanНе соответствует штатным требованиям сервера, но если параметр переданname: zzz, вы можете вернуться в обычном режиме.

Токен аутентификации

Наконец, ссылка для аутентификации достигнута. Давайте сначала рассмотрим метод аутентификации Token, а затем представим аутентификацию сертификата.

Сначала трансформируйте сервер.Имея опыт валидатора выше, вы можете использовать тот же метод для написания перехватчика, а затем внедрить его при инициализации сервера.

Функция аутентификации:

func Auth(ctx context.Context) error {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return fmt.Errorf("missing credentials")
	}

	var user string
	var password string

	if val, ok := md["user"]; ok {
		user = val[0]
	}
	if val, ok := md["password"]; ok {
		password = val[0]
	}

	if user != "admin" || password != "admin" {
		return grpc.Errorf(codes.Unauthenticated, "invalid token")
	}

	return nil
}

metadata.FromIncomingContextСчитайте имя пользователя и пароль из контекста, а затем сравните их с фактическими данными, чтобы определить, пройдена ли проверка подлинности.

Перехватчик:

var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
	ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
	//拦截普通方法请求,验证 Token
	err = Auth(ctx)
	if err != nil {
		return
	}
	// 继续处理请求
	return handler(ctx, req)
}

инициализация:

server := grpc.NewServer(
	grpc.UnaryInterceptor(
		grpc_middleware.ChainUnaryServer(
			authInterceptor,
			grpc_validator.UnaryServerInterceptor(),
		),
	),
	grpc.StreamInterceptor(
		grpc_middleware.ChainStreamServer(
			grpc_validator.StreamServerInterceptor(),
		),
	),
)

В дополнение к вышеуказанным валидаторам есть еще перехватчики аутентификации Token.authInterceptor.

Наконец, клиент трансформируется, и ему нужно реализоватьPerRPCCredentialsинтерфейс.

type PerRPCCredentials interface {
    // GetRequestMetadata gets the current request metadata, refreshing
    // tokens if required. This should be called by the transport layer on
    // each request, and the data should be populated in headers or other
    // context. If a status code is returned, it will be used as the status
    // for the RPC. uri is the URI of the entry point for the request.
    // When supported by the underlying implementation, ctx can be used for
    // timeout and cancellation.
    // TODO(zhaoq): Define the set of the qualified keys instead of leaving
    // it as an arbitrary string.
    GetRequestMetadata(ctx context.Context, uri ...string) (
        map[string]string,    error,
    )
    // RequireTransportSecurity indicates whether the credentials requires
    // transport security.
    RequireTransportSecurity() bool
}

GetRequestMetadataМетод возвращает необходимую информацию, необходимую для аутентификации,RequireTransportSecurityМетод указывает, следует ли включать безопасную ссылку, в производственной среде она обычно включена, но для удобства тестирования здесь временно отключена.

Реализовать интерфейс:

type Authentication struct {
	User     string
	Password string
}

func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
	map[string]string, error,
) {
	return map[string]string{"user": a.User, "password": a.Password}, nil
}

func (a *Authentication) RequireTransportSecurity() bool {
	return false
}

соединять:

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))

Что ж, теперь в нашем сервисе есть функция аутентификации по токену. Если имя пользователя или пароль неверны, клиент получит:

2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token
exit status 1

Если имя пользователя и пароль верны, он возвращается нормально.

Односторонняя аутентификация сертификата

Существует два типа проверки подлинности сертификата:

  1. Односторонняя аутентификация
  2. Двусторонняя аутентификация

Давайте сначала рассмотрим метод односторонней аутентификации:

создать сертификат

Сначала создайте самозаверяющий сертификат SSL с помощью инструмента openssl.

1. Сгенерируйте закрытый ключ:

openssl genrsa -des3 -out server.pass.key 2048

2. Снимите пароль с закрытого ключа:

openssl rsa -in server.pass.key -out server.key

3. Создайте файл csr:

openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=beijing/L=beijing/O=grpcdev/OU=grpcdev/CN=example.grpcdev.cn"

4. Создайте сертификат:

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

Еще одно слово, давайте представим три файла, содержащиеся в сертификате X.509: ключ, csr и crt.

  • ключ:Файл закрытого ключа на сервере, используемый для шифрования данных, отправляемых клиенту, и расшифровки данных, полученных от клиента.
  • csr:Файл запроса на подпись сертификата, который отправляется в центр сертификации (ЦС) для подписи сертификата.
  • ЭЛТ:Сертификат, подписанный центром сертификации (ЦС), или сертификат, подписанный разработчиком самостоятельно, содержит информацию о владельце сертификата, открытом ключе держателя и подписи лица, подписавшего сертификат.

код gRPC

После получения сертификата, остальное - программа трансформации, первое - серверный код.

// 证书认证-单向认证
creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
if err != nil {
	log.Fatal(err)
	return
}

server := grpc.NewServer(grpc.Creds(creds))

Нужно изменить всего несколько строк кода, достаточно просто, клиент следующий.

Поскольку это односторонняя аутентификация, нет необходимости генерировать отдельный сертификат для клиента, просто скопируйте crt-файл сервера в соответствующий каталог клиента.

// 证书认证-单向认证
creds, err := credentials.NewClientTLSFromFile("keys/server.crt", "example.grpcdev.cn")
if err != nil {
	log.Fatal(err)
	return
}
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))

Что ж, теперь наш сервис поддерживает одностороннюю аутентификацию по сертификату.

Но это еще не конец, здесь может быть проблема:

2021/10/11 21:32:37 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
exit status 1

Причина в том, что с Go 1.15CommonName устарел, рекомендуется сертификат SAN. Если вы хотите быть совместимым с предыдущим методом, вы можете поддержать его, установив переменные среды следующим образом:

export GODEBUG="x509ignoreCN=0"

Однако следует отметить, что начиная с Go 1.17 переменные среды больше не действуют, и их необходимо выполнять через SAN. Поэтому, чтобы обновить последующую версию Go, лучше как можно скорее ее поддержать.

Двусторонняя аутентификация сертификата

Наконец, давайте рассмотрим двустороннюю аутентификацию сертификата.

Генерация сертификата с SAN

Опять же, сначала создайте сертификат, но на этот раз немного по-другому, нам нужно создать сертификат с расширением SAN.

Что такое САН?

SAN (альтернативное имя субъекта) — это расширение, определенное в стандарте SSL x509. SSL-сертификат, использующий поле SAN, может расширить доменные имена, поддерживаемые сертификатом, так что один сертификат может поддерживать разрешение нескольких разных доменных имен.

Скопируйте файл конфигурации OpenSSL по умолчанию в текущий каталог.

Системы Linux находятся по адресу:

/etc/pki/tls/openssl.cnf

Системы Mac находятся по адресу:

/System/Library/OpenSSL/openssl.cnf

Измените временный файл конфигурации и найдите[ req ]абзац, затем раскомментируйте следующее утверждение.

req_extensions = v3_req # The extensions to add to a certificate request

Затем добавьте следующую конфигурацию:

[ v3_req ]
# Extensions to add to a certificate request

basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = www.example.grpcdev.cn

[ alt_names ]Расположение может быть настроено с несколькими доменными именами, такими как:

[ alt_names ]
DNS.1 = www.example.grpcdev.cn
DNS.2 = www.test.grpcdev.cn

Для удобства тестирования здесь настроено только одно доменное имя.

1. Создайте сертификат ЦС:

openssl genrsa -out ca.key 2048

openssl req -x509 -new -nodes -key ca.key -subj "/CN=example.grpcdev.com" -days 5000 -out ca.pem

2. Сгенерируйте сертификат сервера:

# 生成证书
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout server.key \
    -out server.csr
    
# 签名证书
openssl x509 -req -days 365000 \
    -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out server.pem

3. Сгенерируйте клиентский сертификат:

# 生成证书
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout client.key \
    -out client.csr

# 签名证书
openssl x509 -req -days 365000 \
    -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out client.pem

код gRPC

Далее приступаем к модификации кода, сначала смотрим на сервер:

// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
	// 设置证书链,允许包含一个或多个
	Certificates: []tls.Certificate{cert},
	// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
	ClientAuth: tls.RequireAndVerifyClientCert,
	// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
	ClientCAs: certPool,
})

Посмотрите на клиента еще раз:

// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
	// 设置证书链,允许包含一个或多个
	Certificates: []tls.Certificate{cert},
	// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
	ServerName: "www.example.grpcdev.cn",
	RootCAs:    certPool,
})

Готово.

Python-клиент

Как упоминалось ранее, gRPC является кросс-язычным, поэтому в конце этой статьи мы напишем клиент на Python для запроса сервера Go.

Используйте самый простой способ сделать это:

В прото-файле используется исходный прото-файл «базового шаблона»:

syntax = "proto3";

package proto;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    // Sends stream message
    rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
 message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

Точно так же вам также необходимо сгенерировать файл pb.py через командную строку:

python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./*.proto

После успешного выполнения в каталоге будут сгенерированы два файла helloworld_pb2.py и helloworld_pb2_grpc.py.

Этот процесс также может сообщить об ошибке:

ModuleNotFoundError: No module named 'grpc_tools'

Не паникуйте, пакет отсутствует, просто установите его:

pip3 install grpcio
pip3 install grpcio-tools

Последний взгляд на клиентский код Python:

import grpc

import helloworld_pb2
import helloworld_pb2_grpc


def main():
    channel = grpc.insecure_channel("127.0.0.1:50051")
    stub = helloworld_pb2_grpc.GreeterStub(channel)
    response = stub.SayHello(helloworld_pb2.HelloRequest(name="zhangsan"))
    print(response.message)


if __name__ == '__main__':
    main()

Таким образом, серверная служба с поддержкой Go может быть запрошена через клиент Python.

Суммировать

В этой статье объясняются некоторые приложения gRPC с точки зрения реальных боевых действий, и речь идет непосредственно о коде.

Контент включает в себя простую службу gRPC, режим потоковой обработки, валидатор, аутентификацию по токену и аутентификацию по сертификату.

В дополнение к этому, есть и другие вещи, на которые стоит обратить внимание, такие как контроль времени ожидания, интерфейсы REST и балансировка нагрузки. В будущем я займу время, чтобы продолжить улучшать оставшуюся часть контента.

Код в этой статье был протестирован и проверен, может выполняться напрямую и загружен наGitHub, друзья могут снова и снова читать исходный код и учиться на основе содержания статьи.


Адрес источника:

Рекомендуемое чтение:

Справочная статья: