Привет, Хабр! представляю вашему вниманию перевод статьи «Go, gRPC and Docker» автора Mat Evans.
Существует множество статей о совместном использовании Go и Docker. Создавать контейнеры, способные взаимодействовать с клиентами и между собой, очень легко. Далее следует небольшой пример того, как это делается на базовом уровне.
Мы будем создавать очень простые клиент и сервер, взаимодействующие между собой при помощи gRPC. Сервер при этом будет находиться внутри Docker-контейнера, чтобы его можно было легко развернуть.
Предположим, что нам нужен сервис, который принимает от клиента строку и возвращает в ответ строку с обращенным порядком символов. Например, посылаем «кот» и получаем в ответ «ток».
.proto-файл описысывает, какие операции наш сервис будет осуществлять и какими данными он при этом будет обмениваться. Создаем в проекте папку proto, а в ней — файл reverse.proto
Функция, которая вызывается удаленно на сервере и возвращает данные клиенту, помечается как rpc. Структуры данных, служащие для обмена информацией между взаимодействующими узлами, помечаются как message. Каждому полю сообщения необходимо присвоить порядковый номер. В данном случае наша функция принимает от клиента сообщения типа Request и возвращает сообщения типа Response.
Как только мы создали .proto-файл, необходимо получить .go-файл нашего сервиса. Для этого нужно выполнить седующую консольную команду в папке proto:
Разумеется, сначала вам нужно выполнить сборку gRPC.
Выполнение вышеприведенной команды создаст новый .go-файл, содержащий методы для создания клиента, сервера и сообщений, которыми они обмениваются. Если мы вызовем godoc, то увидим следующее:
Было бы неплохо, если бы наш клиент работал вот так:
Вот код, который создает gRPC-клиент, используя структуры данных, сгенерированные из .proto-файла:
Сервер испозует тот же самый сгенерированный .go-файл. Однако он определяет только интерфейс сервера, логику же нам придется реализовать самостоятельно:
Я предполагаю, что вы знаете, что такое Docker и для чего он нужен. Вот наш Dockerfile:
Здесь прописан код сборки Docker-образа. Разберем его построчно.
Это команда обозначает, что мы хотим создать образ нашего приложения на основе заранее созданного образа, а именно golang. Это Docker-образ с уже настроенной средой для сборки и запуска программ, написанных на Go.
Эта команда копирует исходный код нашего приложения в GOPATH/src контейнера.
Эта команда собирает наше приложение из скопированных в контейнер исходников и устанавливает его в папку контейнера GOPATH/bin.
Эта команда конфигурирует контейнер таким образом, чтобы он работал как исполнимая программа. В ней мы указываем путь к исполнимому файлу приложения и, при необходимости, аргументы командной строки.
Этой командой мы сообщаем контейнеру, какие порты должны быть доступны извне.
Нам нужно запустить контейнер с нашим серверным приложением.
Сначала необходимо построить образ на основе инструкций из Dockerfile:
Теперь мы можем увидеть данный образ в списке:
Отлично! У нас есть образ нашего серверного приложения, при помощи которого можно запустить его контейнер с помошью следующей команды:
В данном случае выполняется т.н. проброс портов. Заметьте, что для него нам нужны как инструкция EXPOSE, так и аргумент -p.
Контейнеризация клиента не даст больших преимуществ, так что запустим его обычным способом:
Спасибо за прочтение!
Существует множество статей о совместном использовании Go и Docker. Создавать контейнеры, способные взаимодействовать с клиентами и между собой, очень легко. Далее следует небольшой пример того, как это делается на базовом уровне.
Что мы создаем?
Мы будем создавать очень простые клиент и сервер, взаимодействующие между собой при помощи gRPC. Сервер при этом будет находиться внутри Docker-контейнера, чтобы его можно было легко развернуть.
Предположим, что нам нужен сервис, который принимает от клиента строку и возвращает в ответ строку с обращенным порядком символов. Например, посылаем «кот» и получаем в ответ «ток».
.proto-файл
.proto-файл описысывает, какие операции наш сервис будет осуществлять и какими данными он при этом будет обмениваться. Создаем в проекте папку proto, а в ней — файл reverse.proto
syntax = "proto3";
package reverse;
service Reverse {
rpc Do(Request) returns (Response) {}
}
message Request {
string message = 1;
}
message Response {
string message = 1;
}
Функция, которая вызывается удаленно на сервере и возвращает данные клиенту, помечается как rpc. Структуры данных, служащие для обмена информацией между взаимодействующими узлами, помечаются как message. Каждому полю сообщения необходимо присвоить порядковый номер. В данном случае наша функция принимает от клиента сообщения типа Request и возвращает сообщения типа Response.
Как только мы создали .proto-файл, необходимо получить .go-файл нашего сервиса. Для этого нужно выполнить седующую консольную команду в папке proto:
$ protoc -I . reverse.proto --go_out=plugins=grpc:.
Разумеется, сначала вам нужно выполнить сборку gRPC.
Выполнение вышеприведенной команды создаст новый .go-файл, содержащий методы для создания клиента, сервера и сообщений, которыми они обмениваются. Если мы вызовем godoc, то увидим следующее:
$ godoc .
PACKAGE DOCUMENTATION
package reverse
import "."
Package reverse is a generated protocol buffer package.
It is generated from these files:
reverse.proto
It has these top-level messages:
Request
Response
....
Клиент
Было бы неплохо, если бы наш клиент работал вот так:
reverse "this is a test"
tset a si siht
Вот код, который создает gRPC-клиент, используя структуры данных, сгенерированные из .proto-файла:
package main
import (
"context"
"fmt"
"os"
pb "github.com/matzhouse/go-grpc/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
func main() {
opts := []grpc.DialOption{
grpc.WithInsecure(),
}
args := os.Args
conn, err := grpc.Dial("127.0.0.1:5300", opts...)
if err != nil {
grpclog.Fatalf("fail to dial: %v", err)
}
defer conn.Close()
client := pb.NewReverseClient(conn)
request := &pb.Request{
Message: args[1],
}
response, err := client.Do(context.Background(), request)
if err != nil {
grpclog.Fatalf("fail to dial: %v", err)
}
fmt.Println(response.Message)
}
Сервер
Сервер испозует тот же самый сгенерированный .go-файл. Однако он определяет только интерфейс сервера, логику же нам придется реализовать самостоятельно:
package main
import (
"net"
pb "github.com/matzhouse/go-grpc/proto"
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
func main() {
listener, err := net.Listen("tcp", ":5300")
if err != nil {
grpclog.Fatalf("failed to listen: %v", err)
}
opts := []grpc.ServerOption{}
grpcServer := grpc.NewServer(opts...)
pb.RegisterReverseServer(grpcServer, &server{})
grpcServer.Serve(listener)
}
type server struct{}
func (s *server) Do(c context.Context, request *pb.Request)
(response *pb.Response, err error) {
n := 0
// Сreate an array of runes to safely reverse a string.
rune := make([]rune, len(request.Message))
for _, r := range request.Message {
rune[n] = r
n++
}
// Reverse using runes.
rune = rune[0:n]
for i := 0; i < n/2; i++ {
rune[i], rune[n-1-i] = rune[n-1-i], rune[i]
}
output := string(rune)
response = &pb.Response{
Message: output,
}
return response, nil
}
Docker
Я предполагаю, что вы знаете, что такое Docker и для чего он нужен. Вот наш Dockerfile:
FROM golang:1.12
ADD . /go/src/github.com/matzhouse/go-grpc/server
RUN go install github.com/matzhouse/go-grpc/server
ENTRYPOINT ["/go/bin/server"]
EXPOSE 5300
Здесь прописан код сборки Docker-образа. Разберем его построчно.
FROM golang:1.12
Это команда обозначает, что мы хотим создать образ нашего приложения на основе заранее созданного образа, а именно golang. Это Docker-образ с уже настроенной средой для сборки и запуска программ, написанных на Go.
ADD . /go/src/github.com/matzhouse/go-grpc/server
Эта команда копирует исходный код нашего приложения в GOPATH/src контейнера.
RUN go install github.com/matzhouse/go-grpc/server
Эта команда собирает наше приложение из скопированных в контейнер исходников и устанавливает его в папку контейнера GOPATH/bin.
ENTRYPOINT ["/go/bin/server"]
Эта команда конфигурирует контейнер таким образом, чтобы он работал как исполнимая программа. В ней мы указываем путь к исполнимому файлу приложения и, при необходимости, аргументы командной строки.
EXPOSE 5300
Этой командой мы сообщаем контейнеру, какие порты должны быть доступны извне.
Запуск сервера
Нам нужно запустить контейнер с нашим серверным приложением.
Сначала необходимо построить образ на основе инструкций из Dockerfile:
$ sudo docker build -t matzhouse/grpc-server .
Sending build context to Docker daemon 31.76 MB
Step 1/5 : FROM golang
---> a0c61f0b0796
Step 2/5 : ADD . /go/src/github.com/matzhouse/go-grpc
---> 9508be6501c1
Removing intermediate container 94dc6e3a9a20
Step 3/5 : RUN go install github.com/matzhouse/go-grpc/server
---> Running in f3e0b993a420
---> f7a0370b7f7d
Removing intermediate container f3e0b993a420
Step 4/5 : ENTRYPOINT /go/bin/server
---> Running in 9c9619e45df4
---> fb34dfe1c0ea
Removing intermediate container 9c9619e45df4
Step 5/5 : EXPOSE 5300
---> Running in 0403390af135
---> 008e09b9aebd
Removing intermediate container 0403390af135
Successfully built 008e09b9aebd
Теперь мы можем увидеть данный образ в списке:
$ docker images
REPOSITORY TAG IMAGE ID
...
matzhouse/grpc-server latest 008e09b9aebd
...
Отлично! У нас есть образ нашего серверного приложения, при помощи которого можно запустить его контейнер с помошью следующей команды:
$ docker run -it -p 5300:5300 matzhouse/grpc-server
В данном случае выполняется т.н. проброс портов. Заметьте, что для него нам нужны как инструкция EXPOSE, так и аргумент -p.
Запуск клиента
Контейнеризация клиента не даст больших преимуществ, так что запустим его обычным способом:
$ go build -o reverse
$ ./reverse "this is a test"
tset a si siht
Спасибо за прочтение!