Оригинальная ссылка: 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
Если имя пользователя и пароль верны, он возвращается нормально.
Односторонняя аутентификация сертификата
Существует два типа проверки подлинности сертификата:
- Односторонняя аутентификация
- Двусторонняя аутентификация
Давайте сначала рассмотрим метод односторонней аутентификации:
создать сертификат
Сначала создайте самозаверяющий сертификат 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, друзья могут снова и снова читать исходный код и учиться на основе содержания статьи.
Адрес источника:
Рекомендуемое чтение:
- Доступ к службам gRPC из командной строки с помощью grpcurl
- Порекомендуйте три практических инструмента разработки Go
- Застрял в журналах Docker
- Вы знаете эту проблему TCP: невозможно назначить запрошенный адрес
Справочная статья: