Введение в микросервисную архитектуру
Часть 1 из 10
Адаптация статей Ewan Valentine.
Это серия из десяти частей, я постараюсь раз в месяц писать про построение микросервисов на Golang. Я буду использовать protobuf и gRPC в качестве основного транспортного протокола.
Стек, который я использовал: golang, mongodb, grpc, docker, Google Cloud, Kubernetes, NATS, CircleCI, Terraform и go-micro.
Зачем мне это? Поскольку мне потребовалось много времени, чтобы разобраться в этом и решить накопившиеся проблемы. Так же я хотел поделиться с вами тем, что я узнал о создании, тестировании и развертывании микросервисов на Go и другие новые технологии.
В этой части, я хочу показать основные концепции и технологии для построения микросервисов. Напишем простую реализацию. В проекте будут следующие сущности:
- грузы
- инвентарь
- суда
- пользователи
- роли
- аутентификация
Что бы идти далее, вам нужно установить Golang и необходимые библиотек, а так же создать git репозиторий.
Теория
Что такое микросервисная архитектура?
Микросервисы изолируют отдельный функционал в сервис, самодостаточный с точки зрения выполняемой этим сервисом функции. Для совместимости с другими сервисами, он имеет известный и определённый заранее интерфейс.
Микросервисы общаются между собой с помощью сообщений, передаваемых через некоторого посредника, брокера сообщений.
Благодаря микросервисной архитектуре, приложение можно масштабировать не целиком, а частями. Например если сервис авторизации "дёргаеться" чаще остальных, мы можем увеличить количество его инстансов. Эта концепция отвечаем концепции облачных вычислений и контенерезации в целом.
Почему Golang?
Микросервисы поддерживаются практически всеми языками, в конце концов, микросервисы представляют собой концепцию, а не определенную структуру или инструмент. При этом некоторые языки лучше подходят и, кроме того, имеют лучшую поддержку для микросервисов, чем другие. Одним языком с большой поддержкой является Golang.
Познакомимся с protobuf/gRPC
Как сказано ранее, микросервисы разделены на отдельные кодовые базы, одной из важных проблем связных с микросервисами является связь. Если у вас монолит, то вы просто можете вызвать код непосредственно из другого места в вашей программе.
Что бы решить проблему связи, мы можем использовать традиционный подход REST и передавать данные в формате JSON или XML через HTTP. Но у такого подхода есть свои минусы, например то что перед отправкой сообщения придётся кодировать свои данные, а на стороне принимающей декодировать обратно. А это накладные расходы и увеличивает сложность кода.
Есть решение! Это протокол gRPC — лёгкий, основанный на двоичном коде, что исключает передачу заголовков HTTP, и это сэкономит нам некоторое количество байт. Так же будущий протокол HTTP2 подразумевает использование двоичных данных, что снова говорит в пользу gRPC. HTTP2 позволяет вести двунаправленную связь, и это круто!
Так же gRPC позволяет определить интерфейс к вашему сервису в дружеском формате — это > protobuf.
Практика
Создадим файл /project/consigment.proto.
Официальная документация protobuf
//consigment.proto
syntax = "proto3";
package go.micro.srv.consignment;
service ShippingService {
rpc CreateConsignment(Consignment) returns (Response) {}
}
message Consignment {
string id = 1;
string description = 2;
int32 weight = 3;
repeated Container containers = 4;
string vessel_id = 5;
}
message Container {
string id = 1;
string customer_id = 2;
string origin = 3;
string user_id = 4;
}
message Response {
bool created = 1;
Consignment consignment = 2;
}
Это простой пример, который содержит сервис который вы хотите предоставить другим сервисам: service ShippingService, затем мы определим свои сообщения. Protobuf статически типизированный протокол, и мы можем создавать пользовательские типы (похоже на структуры в golang). Здесь контейнер, вложен в партию.
Установим библиотеки, компилятор и скомпилируем наш протокол:
$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go
$ sudo apt install protobuf-compiler
$ mkdir consignment && cd consignment
$ protoc -I=. --go_out=plugins=grpc:. consignment.proto
На выходе должен получиться файл:
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: consignment.proto
package consignment
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
context "golang.org/x/net/context"
grpc "google.golang.org/grpc"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Consignment struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
Weight int32 `protobuf:"varint,3,opt,name=weight,proto3" json:"weight,omitempty"`
Containers []*Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"`
VesselId string `protobuf:"bytes,5,opt,name=vessel_id,json=vesselId,proto3" json:"vessel_id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Consignment) Reset() { *m = Consignment{} }
func (m *Consignment) String() string { return proto.CompactTextString(m) }
func (*Consignment) ProtoMessage() {}
func (*Consignment) Descriptor() ([]byte, []int) {
return fileDescriptor_3804bf87090b51a9, []int{0}
}
func (m *Consignment) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Consignment.Unmarshal(m, b)
}
func (m *Consignment) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Consignment.Marshal(b, m, deterministic)
}
func (m *Consignment) XXX_Merge(src proto.Message) {
xxx_messageInfo_Consignment.Merge(m, src)
}
func (m *Consignment) XXX_Size() int {
return xxx_messageInfo_Consignment.Size(m)
}
func (m *Consignment) XXX_DiscardUnknown() {
xxx_messageInfo_Consignment.DiscardUnknown(m)
}
var xxx_messageInfo_Consignment proto.InternalMessageInfo
func (m *Consignment) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *Consignment) GetDescription() string {
if m != nil {
return m.Description
}
return ""
}
func (m *Consignment) GetWeight() int32 {
if m != nil {
return m.Weight
}
return 0
}
func (m *Consignment) GetContainers() []*Container {
if m != nil {
return m.Containers
}
return nil
}
func (m *Consignment) GetVesselId() string {
if m != nil {
return m.VesselId
}
return ""
}
type Container struct {
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CustomerId string `protobuf:"bytes,2,opt,name=customer_id,json=customerId,proto3" json:"customer_id,omitempty"`
Origin string `protobuf:"bytes,3,opt,name=origin,proto3" json:"origin,omitempty"`
UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Container) Reset() { *m = Container{} }
func (m *Container) String() string { return proto.CompactTextString(m) }
func (*Container) ProtoMessage() {}
func (*Container) Descriptor() ([]byte, []int) {
return fileDescriptor_3804bf87090b51a9, []int{1}
}
func (m *Container) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Container.Unmarshal(m, b)
}
func (m *Container) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Container.Marshal(b, m, deterministic)
}
func (m *Container) XXX_Merge(src proto.Message) {
xxx_messageInfo_Container.Merge(m, src)
}
func (m *Container) XXX_Size() int {
return xxx_messageInfo_Container.Size(m)
}
func (m *Container) XXX_DiscardUnknown() {
xxx_messageInfo_Container.DiscardUnknown(m)
}
var xxx_messageInfo_Container proto.InternalMessageInfo
func (m *Container) GetId() int32 {
if m != nil {
return m.Id
}
return 0
}
func (m *Container) GetCustomerId() string {
if m != nil {
return m.CustomerId
}
return ""
}
func (m *Container) GetOrigin() string {
if m != nil {
return m.Origin
}
return ""
}
func (m *Container) GetUserId() string {
if m != nil {
return m.UserId
}
return ""
}
type Response struct {
Created bool `protobuf:"varint,1,opt,name=created,proto3" json:"created,omitempty"`
Consignment *Consignment `protobuf:"bytes,2,opt,name=consignment,proto3" json:"consignment,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Response) Reset() { *m = Response{} }
func (m *Response) String() string { return proto.CompactTextString(m) }
func (*Response) ProtoMessage() {}
func (*Response) Descriptor() ([]byte, []int) {
return fileDescriptor_3804bf87090b51a9, []int{2}
}
func (m *Response) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Response.Unmarshal(m, b)
}
func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Response.Marshal(b, m, deterministic)
}
func (m *Response) XXX_Merge(src proto.Message) {
xxx_messageInfo_Response.Merge(m, src)
}
func (m *Response) XXX_Size() int {
return xxx_messageInfo_Response.Size(m)
}
func (m *Response) XXX_DiscardUnknown() {
xxx_messageInfo_Response.DiscardUnknown(m)
}
var xxx_messageInfo_Response proto.InternalMessageInfo
func (m *Response) GetCreated() bool {
if m != nil {
return m.Created
}
return false
}
func (m *Response) GetConsignment() *Consignment {
if m != nil {
return m.Consignment
}
return nil
}
func init() {
proto.RegisterType((*Consignment)(nil), "Consignment")
proto.RegisterType((*Container)(nil), "Container")
proto.RegisterType((*Response)(nil), "Response")
}
func init() { proto.RegisterFile("consignment.proto", fileDescriptor_3804bf87090b51a9) }
var fileDescriptor_3804bf87090b51a9 = []byte{
// 281 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xbf, 0x4e, 0x33, 0x31,
0x10, 0xc4, 0xbf, 0xcb, 0xff, 0x5b, 0x7f, 0x02, 0xc5, 0x05, 0x58, 0x50, 0x70, 0xba, 0x2a, 0xa2,
0x70, 0x11, 0x9e, 0x00, 0xa5, 0x4a, 0xeb, 0xd0, 0xa3, 0x60, 0xaf, 0x2e, 0x2b, 0x11, 0xfb, 0x64,
0x3b, 0xe1, 0x75, 0x78, 0x54, 0x74, 0xbe, 0x1c, 0x18, 0x51, 0xce, 0xac, 0x67, 0xf7, 0xa7, 0x31,
0x2c, 0xb5, 0xb3, 0x81, 0x1a, 0x7b, 0x44, 0x1b, 0x65, 0xeb, 0x5d, 0x74, 0xf5, 0x67, 0x01, 0x6c,
0xf3, 0xe3, 0xf2, 0x2b, 0x18, 0x91, 0x11, 0x45, 0x55, 0xac, 0xa6, 0x6a, 0x44, 0x86, 0x57, 0xc0,
0x0c, 0x06, 0xed, 0xa9, 0x8d, 0xe4, 0xac, 0x18, 0x55, 0xc5, 0xaa, 0x54, 0xb9, 0xc5, 0x6f, 0x60,
0xf6, 0x81, 0xd4, 0x1c, 0xa2, 0x18, 0xa7, 0xd4, 0x45, 0xf1, 0x47, 0x00, 0xed, 0x6c, 0xdc, 0x93,
0x45, 0x1f, 0xc4, 0xa4, 0x1a, 0xaf, 0xd8, 0x1a, 0xe4, 0x66, 0xb0, 0x54, 0x36, 0xe5, 0xf7, 0x50,
0x9e, 0x31, 0x04, 0x7c, 0x7f, 0x25, 0x23, 0xa6, 0xe9, 0xc6, 0xa2, 0x37, 0xb6, 0xa6, 0x3e, 0x42,
0xf9, 0x9d, 0xfa, 0xc3, 0xf7, 0x00, 0x4c, 0x9f, 0x42, 0x74, 0x47, 0xf4, 0x5d, 0xb6, 0xe7, 0x83,
0xc1, 0xda, 0x9a, 0x0e, 0xcf, 0x79, 0x6a, 0xc8, 0x26, 0xbc, 0x52, 0x5d, 0x14, 0xbf, 0x85, 0xf9,
0x29, 0xf4, 0xa1, 0x49, 0x3f, 0xe8, 0xe4, 0xd6, 0xd4, 0x2f, 0xb0, 0x50, 0x18, 0x5a, 0x67, 0x03,
0x72, 0x01, 0x73, 0xed, 0x71, 0x1f, 0xb1, 0x3f, 0xb9, 0x50, 0x83, 0xe4, 0x12, 0x58, 0x56, 0x66,
0xba, 0xcb, 0xd6, 0xff, 0x65, 0x56, 0xa5, 0xca, 0x1f, 0xac, 0x9f, 0xe1, 0x7a, 0x77, 0xa0, 0xb6,
0x25, 0xdb, 0xec, 0xd0, 0x9f, 0x49, 0x23, 0x97, 0xb0, 0xdc, 0xa4, 0x6d, 0x79, 0xff, 0xbf, 0x56,
0xdc, 0x95, 0x72, 0x40, 0xa9, 0xff, 0xbd, 0xcd, 0xd2, 0x8f, 0x3d, 0x7d, 0x05, 0x00, 0x00, 0xff,
0xff, 0x84, 0x5c, 0xa4, 0x06, 0xc6, 0x01, 0x00, 0x00,
}
// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4
// ShippingServiceClient is the client API for ShippingService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ShippingServiceClient interface {
CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error)
}
type shippingServiceClient struct {
cc *grpc.ClientConn
}
func NewShippingServiceClient(cc *grpc.ClientConn) ShippingServiceClient {
return &shippingServiceClient{cc}
}
func (c *shippingServiceClient) CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) {
out := new(Response)
err := c.cc.Invoke(ctx, "/ShippingService/CreateConsignment", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// ShippingServiceServer is the server API for ShippingService service.
type ShippingServiceServer interface {
CreateConsignment(context.Context, *Consignment) (*Response, error)
}
func RegisterShippingServiceServer(s *grpc.Server, srv ShippingServiceServer) {
s.RegisterService(&_ShippingService_serviceDesc, srv)
}
func _ShippingService_CreateConsignment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Consignment)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ShippingServiceServer).CreateConsignment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/ShippingService/CreateConsignment",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ShippingServiceServer).CreateConsignment(ctx, req.(*Consignment))
}
return interceptor(ctx, in, info, handler)
}
var _ShippingService_serviceDesc = grpc.ServiceDesc{
ServiceName: "ShippingService",
HandlerType: (*ShippingServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CreateConsignment",
Handler: _ShippingService_CreateConsignment_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "consignment.proto",
}
Если, что то пошло не так. Обратите внимание на аргументы -I путь где компилятор ищет фалйлы, --go_out где будет создан новый файл. Всегда есть справка
$ protoc -h
Это код, который автоматически генерируется библиотеками gRPC / protobuf, чтобы вы могли связать свое определение protobuf с вашим собственным кодом.
Напишем main.go
package seaport
import (
"log"
"net"
// Импотртируем код протобуфера
pbf "seaport/consignment"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
const (
port = ":50051"
)
//IRepository - интерфейс хранилища
type IRepository interface {
Create(*pbf.Consignment) (*pbf.Consignment, error)
}
// Repository - структура для эмитации хранилища,
// после мы заменим её на настоящие хранилищем
type Repository struct {
consignments []*pbf.Consignment
}
//Create - создаём новое хранилище
func (repo *Repository) Create(consignment *pbf.Consignment) (*pbf.Consignment, error) {
updated := append(repo.consignments, consignment)
repo.consignments = updated
return consignment, nil
}
// Служба должна реализовать все методы для удовлетворения сервиса
// которые мы определили в нашем определении протобуфа. Вы можете проверить интерфейсы
// в сгенерированном коде для точных сигнатур методов и т. д.
type service struct {
repo IRepository
}
// CreateConsignment - мы создали только один метод для нашего сервиса,
// который является методом create, который принимает контекст и запрос
// потом они обрабатываются сервером gRPC.
func (s *service) CreateConsignment(ctx context.Context, req *pbf.Consignment) (*pbf.Response, error) {
// Сохраним нашу партию в хранидище
consignment, err := s.repo.Create(req)
if err != nil {
return nil, err
}
// Возвращаем сообщение `Response`,
// которое мы создали в нашем определнии пробуфа
return &pbf.Response{Created: true, Consignment: consignment}, nil
}
func main() {
repo := &Repository{}
// Стартуем наш gRPC сервер для прослушивания tcp
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
// Зарегистрируйте нашу службу на сервере gRPC, это свяжет нашу
// реализацию с кодом автогенерированного интерфейса для нашего
// сообщения `Response`, которое мы создали в нашем протобуфе
pbf.RegisterShippingServiceServer(s, &service{repo})
// Регистрация службы ответов на сервере gRPC.
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Пожалуйста, внимательно прочитайте комментарии, оставленные в коде. Судя по всему, здесь мы создаем логику реализации, в которой наши методы gRPC взаимодействуют, используя сгенерированные форматы, создавая новый сервер gRPC на порту 50051. Теперь там будет жить наш gRPC сервис.
Вы можете запустить это с помощью $ go run main.go, но вы ничего не увидите, и вы не сможете его использовать… Итак, давайте создадим клиента, чтобы увидеть его в действии.
Давайте создадим интерфейс командной строки, который возьмет файл JSON и будет взаимодействовать с нашей службой gRPC.
В корневом каталоге создайте новый подкаталог $ mkdir consignment-cli. В этом каталоге создайте файл cli.go со следующим содержимым:
package main
import (
"encoding/json"
"io/ioutil"
"log"
"os"
pbf "seaport/consignment"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultFilename = "consignment.json"
)
//Функция парсит переданный фаил
func parseFile(file string) (*pbf.Consignment, error) {
var consignment *pbf.Consignment
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
json.Unmarshal(data, &consignment)
return consignment, err
}
func main() {
// Установим соединение с сервером
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("Не могу подключиться: %v", err)
}
defer conn.Close()
client := pbf.NewShippingServiceClient(conn)
// Передадим в обработку consignment.json,
// иначе возьмём путь к файлу из аргументов командной строки
file := defaultFilename
if len(os.Args) > 1 {
file = os.Args[1]
}
consignment, err := parseFile(file)
if err != nil {
log.Fatalf("Не возможно распарсить фаил: %v", err)
}
r, err := client.CreateConsignment(context.Background(), consignment)
if err != nil {
log.Fatalf("Не удалось создать: %v", err)
}
log.Printf("Создан: %t", r.Created)
}
Теперь создайте партию (consignment-cli / consignment.json):
{
"description": "Тестовая партия груза",
"weight": 100,
"containers": [
{
"customer_id": "Заказчик_001",
"user_id": "Пользователь_001",
"origin": "Порт Находка"
}
],
"vessel_id": "судно_001"
}
Теперь, если вы запустите $ go run main.go из пакета seaport, а затем в отдельной панели терминала запустите $ go run cli.go. Вы должны увидеть сообщение «Created: true».
Но как мы можем проверить, что именно было создано? Давайте обновим нашу службу с помощью метода GetConsignments, чтобы мы могли просматривать все наши созданные партии.
//consigment.proto
syntax = "proto3";
service ShippingService{
rpc CreateConsignment(Consignment) returns (Response) {}
// Создадим новый метод
rpc GetConsignments(GetRequest) returns (Response) {}
}
message Consignment {
int32 id = 1;
string description = 2;
int32 weight = 3;
repeated Container containers = 4;
string vessel_id = 5;
}
message Container {
int32 id =1;
string customer_id =2;
string origin = 3;
string user_id = 4;
}
// Создадим пустой запрос
message GetRequest {}
message Response {
bool created = 1;
Consignment consignment = 2;
// Добавим повторяющиюся запись партий
// в наше ответное сообщение
repeated Consignment consignments = 3;
}
Итак, здесь мы создали новый метод на нашем сервисе под названием GetConsignments, мы также создали новый GetRequest, который пока не содержит ничего. Мы также добавили поле отправленных партий в наше ответное сообщение. Вы заметите, что тип здесь имеет ключевое слово repeated до типа. Это, как вы, наверное, догадались, просто означает рассматривать это поле как массив этих типов.
Не спешите запускать программу, реализация наших методов gRPC основана на сопоставлении интерфейса, созданного библиотекой protobuf, нам необходимо убедиться, что наша реализация соответствует нашему proto определению.
//seaport/main.go
//IRepository - интерфейс хранилища
type IRepository interface {
Create(*pbf.Consignment) (*pbf.Consignment, error)
GetAll() []*pbf.Consignment
}
//GetAll - метод получения всех партий из хранилища
func (repo *Repository) GetAll() []*pbf.Consignment {
return repo.consignments
}
//GetConsignments - метод для получения всех партий из ответа сервера
func (s *service) GetConsignments(ctx context.Context, req *pbf.GetRequest) (*pbf.Response, error) {
consignments := s.repo.GetAll()
return &pbf.Response{Consignments: consignments}, nil
}
Здесь мы включили наш новый метод GetConsignments, обновили наше хранилище и интерфейс, соответственно созданным в определении consignments.proto. Если вы снова запустите $ go run main.go, то программа должно снова заработать.
Давайте обновим наш инструмент cli, чтобы включить возможность вызова этого метода и появилась возможность перечислить наши партии:
package main
import (
"encoding/json"
"io/ioutil"
"log"
"os"
pbf "seaport/consignment"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
defaultFilename = "consignment.json"
)
//Функция парсит переданный фаил
func parseFile(file string) (*pbf.Consignment, error) {
var consignment *pbf.Consignment
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
json.Unmarshal(data, &consignment)
return consignment, err
}
func main() {
// Установим соединение с сервером
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("Не могу подключиться: %v", err)
}
defer conn.Close()
client := pbf.NewShippingServiceClient(conn)
// Передадим в обработку consignment.json,
// иначе возьмём путь к файлу из аргументов командной строки
file := defaultFilename
if len(os.Args) > 1 {
file = os.Args[1]
}
consignment, err := parseFile(file)
if err != nil {
log.Fatalf("Не возможно распарсить фаил: %v", err)
}
r, err := client.CreateConsignment(context.Background(), consignment)
if err != nil {
log.Fatalf("Не удалось создать: %v", err)
}
log.Printf("Создан: %t", r.Created)
getAll, err := client.GetConsignments(context.Background(), &pbf.GetRequest{})
if err != nil {
log.Fatalf("Не возможно получить список партий: %v", err)
}
for _, cns := range getAll.Consignments {
fmt.Printf("Id: %v\n", cns.GetId())
fmt.Printf("Description: %v\n", cns.GetDescription())
fmt.Printf("Weight: %d\n", cns.GetWeight())
fmt.Printf("VesselId: %v\n", cns.GetVesselId())
for _, cnt := range cns.GetContainers() {
fmt.Printf("\tId: %v\n", cnt.GetId())
fmt.Printf("\tUserId: %v\n", cnt.GetUserId())
fmt.Printf("\tCustomerId: %v\n", cnt.GetCustomerId())
fmt.Printf("\tOrigin: %v\n", cnt.GetOrigin())
}
}
}
Добавим код выше в cli.go и снова запустите $ go run cli.go. Клиент запустит CreateConsignment, а затем вызовет GetConsignments. И вы должны увидеть, что в ответе список содержит состав партии.
Таким образом, у нас есть первый микросервис и клиент, чтобы взаимодействовать с ним, используя protobuf и gRPC.
Следующая часть этой серии будет включать интеграцию go-micro, которая является мощной основой для создания микросервисов на основе gRPC. Мы также создадим наш второй сервис. Рассмотрим работу наших сервисов в контейнерах Docker, в следующей части этой серии статей.