Ya tenemos la Navidad, aquí a la vuelta de la esquina, pero Friends of Go no descansa, y es que ya llevamos dos interesantes artículos sobre gRPC, vimos cómo empezar a usar gRPC y vimos cómo crear y utilizar interceptors, todo esto sobre un proyecto muy útil para estas fechas, un wishlist.
Pero… claro, ¿cómo podemos mostrar nuestra lista al mundo? ¿cómo podemos hacer que otras personas o Santa o incluso los Reyes Magos vean lo que nosotros queremos, mediante un frontal o un app móvil? ¿volvemos a api REST de toda la vida?
GRPC Gateway
Obviamente no íbamos a escribir este artículo sin tener una buena solución al problema, aunque dicha solución no sea nuestra sino de Google, y se trata de añadir un plugin a nuestro compilador de protocol buffers.
Como no iba a ser de otro modo, instalar dicho plugin es tan sencillo como ejecutar un go get
, recordad que debemos de realizarlo fuera de cualquier directorio con go.mod
o utilizando GO111MODULE=off
, de lo contrario nos añadirá una dependencia en nuestro proyecto que tendremos que remover, utilizando go mod tidy
. En principio, a partir de go 1.14
esto debería cambiar con la inclusión del flag -g
, pero eso es historia para otro día.
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
Dicho plugin leerá la definicion de nuestros ficheros profobuf y generará un proxy reverso el cual traduce de RESTful HTTP API a gRPC. Para ello nos valdremos de la anotación google.api.http
en nuestras definiciones.
Esto nos permitirá, con un sólo protobuf
generar tanto nuestra API en gRPC cono en RESTful.
Configurando nuestros ficheros .proto
Como hemos dicho para poder hacer la magia de que nuestro fichero .proto
genere tanto la parte gRPC
como la RESTful
deberemos utilizas las anotaciones de google.api.http
, así que veamos como modificar nuestro proyecto wishlist para ello.
Si echamos la mirada atrás, recordaremos que tenemos un fichero proto con los siguientes servicios:
service WishListService {
rpc Create(CreateWishListReq) returns (CreateWishListResp);
rpc Add(AddItemReq) returns (AddItemResp);
rpc List(ListWishListReq) returns (ListWishListResp);
}
Bien, tenemos tres métodos, Create
, Add
y List
, si esto lo traducimos a RESTful
, podemos decir que tenemos un POST
, un POST
y un GET
respectivamente. Así que veremos como hacer un ejemplo con POST
y un ejemplo con GET
, los demás verbos, PUT
, DELETE
, etc. se comportarán de manera muy similar.
No podemos añadir items a una whislist que no existe, así que vamos a empezar a crear el POST
para nuestro método Create
.
service WishListService {
rpc Create(CreateWishListReq) returns (CreateWishListResp){
option (google.api.http) = {
post: "/v1/wishlist"
body: "wish_list"
}
};
rpc Add(AddItemReq) returns (AddItemResp);
rpc List(ListWishListReq) returns (ListWishListResp);
}
¿Qué hemos hecho? hemos añadido a nuestro método Create
un nuevo endpoint
(/v1/wishlist/
) y le hemos dicho que tendrá un body request
que cumplirá con la propuedad wish_list
dentro de nuestro CreateWishListReq
.
Veamos como quedan entonces, el Add
y el List
:
service WishListService {
rpc Create (CreateWishListReq) returns (CreateWishListResp) {
option (google.api.http) = {
post: "/v1/wishlist"
body: "wish_list"
};
}
rpc Add (AddItemReq) returns (AddItemResp) {
option (google.api.http) = {
post: "/v1/wishlist/{wishList_id}/item"
body: "item"
};
}
rpc List (ListWishListReq) returns (ListWishListResp) {
option (google.api.http) = {
get: "/v1/wishlist/{wishList_id}"
};
}
}
Ahora deberemos generar nuestro nuevo fichero .pb.gw.go
, para ello modificaremos el comando que utilizamos en el primer artículo:
$ protoc -I $(PROTO_FILES_PATH) -I $(GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out=plugins=grpc:$(PROTO_OUT) --grpc-gateway_out=logtostderr=true:$(PROTO_OUT) $(PROTO_FILES_PATH)/*.proto
Las variables de entorno utilizadas son:
Como podemos ver es un churro de comando, pero realmente hemos añadido poca cosa, primero de todo hemos añadido la nueva dependencia, ya que para poder usar las anotaciones que nos da el plugin de grpc-gateway
tenemos que incluirlas a la hora de compilar, -I $(GOPATH)/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis
, seguidamente, hemos indicado cual será el directorio de salida de nuestro nuevo fichero, --grpc-gateway_out=logtostderr=true:$(PROTO_OUT)
que en este caso hemos dicho de ponerlo al mismo nivel que nuestros autogenerados, eso acabara creándonos como hemos dicho un nuevo fichero .pb.gw.go
que tendrá todo lo necesario para correr nuestro servidor http.
Pero por desgracia, no hemos terminado aún, al igual que hicimos con el servidor gRPC
, tenemos que levantar nuestro servidor HTTP
, pero, ¿cómo lo hacemos?
Levantar nuestro servidor HTTP autogenerado
Realmente no será una tarea muy complicada, pero tendrémos que hacer algunas modificaciones adicionales a nuestro código, primeramente deberemos de crear nuestro servidor HTTP
, para ello crearemos un nuevo directorio http
dentro de server
y a su vez un nuevo server.go
type Server struct {
httpAddr string
}
func NewServer(httpAddr string) *Server {
return &Server{httpAddr: httpAddr}
}
Con esto ya tendremos un nuevo servidor con todo lo necesario, pero claro, debemos de arrancar algo ¿no?
func (s *Server) Serve(ctx context.Context) error {
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
err := wishgrpc.RegisterWishListServiceHandlerFromEndpoint(ctx, mux, s.httpAddr, opts)
if err != nil {
return err
}
return http.ListenAndServe(s.httpAddr, mux)
}
Vamos por partes, primeramente crearemos un servidor mux
, este servidor viene dado por la propia librería de grpc-gateway
que se nos habrá descargado al bajar el plugin que vimos al principio, que nos ofrece un paquete runtime
, no confundir con el paquete runtime
de la librería estándar. Muy importante que reviséis que estáis en la última versión de dicha librería, actualmente v1.12.1
ya que en versiones muy anteriores, esto no existía y nos dará problemas.
Luego podemos añadir opciones, igual que hacemos con el cliente, para conectarnos a nuestro servidor, en este caso como no usamos un servidor HTTPS
, le indicamos que será con la opción WithInsecure
.
A continuación, llamaremos al nuevo método que se nos ha generado con el fichero .pb.gw.go
, que suele tener la terminación ServiceHandlerFromEndpoint
, y espera, un context
, un runtime.ServerMux
, la dirección donde será levantado el servidor y las opciones de conexión.
Si no se ha producido ningún error, lo siguiente que haremos es, ahora sí levantar nuestro servidor.
Pues ya tenemos todo lo necesario, ahora simplemente cambiaremos nuestro main.go
si queremos levantar dichos servidores a la vez, por unas rutinas, y !voilá¡
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
srvCfg := server.Config{Protocol: protocol, Host: host, Port: port}
srv := grpc.NewServer(srvCfg, creatingService, addingService, listingService)
log.Printf("gRPC server running at %s://%s:%s ...\n", protocol, host, port)
return srv.Serve()
})
g.Go(func() error {
httpAddr := fmt.Sprintf(":%s", port)
httpSrv := http.NewServer(httpAddr)
log.Printf("HTTP server running at %s ...\n", httpAddr)
return httpSrv.Serve(ctx)
})
log.Fatal(g.Wait())
Para facilitarnos la tarea hemos utilizado la libería errgroup, la cual es realmente útil, para tratar con este tipo de inicializaciones.
Conclusión
Hoy hemos aprendido, como crear una completa API RESTful a partir de nuestros ficheros .proto
, para ello hemos partido desde la v0.2 de nuestro proyecto Wishlist, viendo todas las modificaciones que tenemos que realizar para llega a nuestra API RESTful deseada.
Y ahora es tu turno de seguir experimentando, puedes encontrar todo el ejemplo de código así como el código funcionando en nuestro repositorio, en la release v0.3
Recuerda que si tienes cualquier duda o sugerencia, puedes dejarlo en los comentarios o vía nuestro twitter FriendsOfGo