gRPC вперед! Идти! Идти! | 🏆 Второй выпуск технической темы прием статей

Go

Что такое RPC (удаленный вызов процедур)?

Проще говоря, RPC вызывает удаленную службу, как если бы это была локальная служба.

Это протокол для запроса услуг от удаленной компьютерной программы по сети без знания базовой сетевой технологии. Другими словами, два сервера A и B, приложение развернуто на сервере A. Если вы хотите вызвать метод, предоставленный приложением на сервере B, поскольку он не находится в пространстве памяти, его нельзя вызвать напрямую. необходимо выразить семантику вызова и передать вызов по сети.Данные.

Протокол RPC предполагает существование некоторого транспортного протокола, такого как TCP или UDP, для передачи информационных данных между взаимодействующими программами. В модели сетевой связи OSI RPC охватывает транспортный уровень и прикладной уровень. RPC упрощает разработку приложений, включая сетевое распределенное мультипрограммирование. В отрасли существует множество отличных фреймворков RPC с открытым исходным кодом, таких как Spring Cloud, Dubbo, Thrift, gRPC и т. д.

На самом деле RPC не является новой концепцией: концепция RPC впервые была использована в 1980-х годах.Bruce Jay Nelsonв своей диссертацииImplementing Remote Procedure Callsпредложить.

Как работает RPC

  1. Клиент вызывает удаленные службы так же, как и локальные службы;
  2. После того, как клиентская заглушка получает вызов, она сериализует метод и параметры.
  3. Клиент отправляет сообщения на сервер через сокеты
  4. Декодирование серверной заглушки после получения сообщения (десериализация объекта сообщения)
  5. Заглушка сервера вызывает локальную службу в соответствии с результатом декодирования
  6. Локальное выполнение службы (локальное выполнение для сервера) и возврат результата на заглушку сервера
  7. Заглушка сервера упаковывает возвращенный результат в сообщение (сериализует объект сообщения результата)
  8. Сервер отправляет сообщения клиенту через сокеты или другие методы.
  9. Клиентская заглушка получает сообщение результата и декодирует его (десериализует сообщение результата).
  10. Клиент получает конечный результат

В чем разница между RPC и HTTP?

В широком смысле HTTP — это, по сути, реализация концепции RPC, а HTTP — воплощение идеи RPC.

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

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

Зачем использовать RPC вместо HTTP?

Протокол HTTP принадлежит прикладному уровню в иерархии нашей сетевой модели, на уровне 7.

Наш общий протокол http 1.x, формат запроса пост

HTTP/1.0 200 OK 
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84

{
	"name": "hello"
}

В данных всего пакета пара ключ-значение заголовка занимает более 70% байтов. Это сильно расходует ресурсы передачи. При использовании уникального двоичного протокола RPC для передачи использование двоичного кодирования и передачи на основе частного протокола может значительно повысить эффективность передачи, что приведет к некоторым улучшениям производительности в крупномасштабных сетевых службах.

gRPC

Существует много популярных фреймворков RPC, и каждый из них имеет свои особенности, здесь мы выбираем gRPC для практики.

gRPC — это высокопроизводительная инфраструктура RPC общего назначения с открытым исходным кодом, разработанная Google на основе стандарта протокола HTTP/2, разработанная на основе протокола сериализации Protobuf (Protocol Buffers) и поддерживающая множество языков разработки.

Поддержка C++, Java, Go, Python, Ruby, C#, Node.js, Android Java, Objective-C, PHP и других языков программирования.

Nginx поддерживает прокси gRPC в версии 1.13.10.

gRPC использует protubuf в качестве двоичного кодирования

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

Производительность и эффективность Protobuf значительно выше, чем у других форматов структурированных данных, таких как JSON и XML. Protobuf хранится в бинарном виде, что занимает мало места, но также имеет недостаток плохой читабельности.

Протобуф в.protoОпределить структурированные данные, которые необходимо обработать, которые можноprotocинструмент, воля.protoФайл преобразуется в код на C, C++, Golang, Java, Python и других языках с хорошей совместимостью и простотой в использовании.

1. Установите компилятор Protubuf

Перейдите на страницу выпуска protobufРаздел GitHub.com/protocol…

Загрузите последний сжатый пакет для соответствующей платформы.Вот, возьмите Mac в качестве примера, загрузитеРаздел GitHub.com/protocol…

Разархивируйте в указанный вами каталог, затем обновите системную переменную PATH или убедитесь, что protoc находится в каталоге PATH.

2. Установите плагин компилятора Go protoc

go get -u github.com/golang/protobuf/protoc-gen-go

3. Установите пакет Go gRPC

go get google.golang.org/grpc

Практика gRPC и сравнение с HTTP

Весь приведенный выше код размещен в моем репозитории Github, и заинтересованные друзья могут его попробовать.

GitHub.com/3inch time/Боюсь…

Дизайн этой демонстрации выглядит следующим образом: HTTP и gRPC используются для одновременного доступа к внутреннему серверу для добавления, удаления, изменения и запроса базы данных.

Общий дизайн представляет собой видео-сайт, добавляющий и запрашивающий данные.

Есть две услуги:

  • домен против открытого интерфейса HTTP

  • Backend Backstage Service, домен использует GRPC или HTTP и связь с Backend

Для части HTTP мы используем gin в качестве фрейма.

Мы определяем два интерфейса:

  • get_video Получить информацию о видео
  • create_video Создать информацию о видео

1. Подготовьте

Структура данных выглядит следующим образом:

значение поле
ID id
заглавие title
Введение note
обложка pic
Адрес видео video

Сначала создайте оператор таблицы

CREATE TABLE `video` (
  `id` char(64) NOT NULL,
  `title` varchar(256) NOT NULL,
  `note` varchar(256) NOT NULL,
  `pic_path` char(64) NOT NULL,
  `video_path` char(64) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `video_id_uindex` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Создайте каталог проекта следующим образом

|pilipili
|
├─backend
│  ├─dao
│  ├─internal
│  │  ├─server
│  │  └─service
│  └─model
├─domain
│  └─internal
│      ├─server
│      └─service
└─proto

Управление пакетами рекомендует использовать мод go, поэтому я не буду вдаваться в подробности.

2. Создайте прото-файл

syntax = "proto3";

package proto;

service Pilipili {
  rpc RpcGetVideoInfo(GetVideoRequest) returns (VideoInfo) {}
  rpc RpcCreateVideoInfo(VideoInfo) returns (CreateVideoReplay) {}
}

message GetVideoRequest {
  string id = 1;
}

message CreateVideoReplay {
  string status = 1;
}

message VideoInfo {
  string id = 1;
  string title = 2;
  string note = 3;
  string pic = 4;
  string video = 5;
}
  • Есть 2 версии protobuf с разным синтаксисом.Версия по умолчанию - proto2.Если требуется proto3, то нужно использовать синтаксис="proto3" для указания версии в первой строке непустого не комментария. Теперь рекомендуется версия proto3.

  • Мы определяем данные с помощью трех структур:GetVideoRequestПользователь запрашивает детали видео, где id — это запрошенный контент.CreateVideoReplayЭто ответ при создании видеосообщения.VideoInfoЭто наше видеообращение.

  • service PilipiliОпределяет два интерфейса, которые наш protobuf предоставляет сервисы, в том числеRpcGetVideoInfo,RpcCreateVideoInfo.

  • RpcGetVideoInfoспособ отправкиGetVideoRequestполучитьVideoInfo.

  • RpcCreateVideoInfoспособ отправкиVideoInfoполучитьCreateVideoReplay.

3. Создайте файл Go protobuf

 protoc -I . --go_out=plugins=grpc:. ./pilipili.proto

Таким образом, вpilipili.protoсоздается в том же каталогеpilipili.pb.goфайл, этот файл содержит все методы, которые нам нужны в gRPC.

4. Разработка серверного кода

4.1 Подключение к базе данных

Сначала создайте соединение с базой данных для GO

backend/dao/dao.go

package dao

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
)

type Dao struct {
	DB *sql.DB
}

func NewDao() *Dao {
	return &Dao{
		DB: NewDB(),
	}
}

func NewDB() *sql.DB {
	Mysql, err := sql.Open("mysql", "root:123456@tcp(localhost:3306)/pilipili?charset=utf8mb4")
	if err != nil {
		panic(err)
	}
	Mysql.SetMaxOpenConns(10)
	return Mysql
}

Мы не будем вдаваться в подробности о той части Go, которая управляет MySQL.

4.2 Операции с базой данных

backend/model/model.go

package model

type Video struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Note      string `json:"note"`
	PicPath   string `json:"pic_path"`
	VideoPath string `json:"video_path"`
}

Создайте структуру видео.

backend/dao/info.go

package dao

import (
	"database/sql"
	"github.com/google/uuid"
	"github.com/sirupsen/logrus"
	"pilipili/backend/model"

	_ "github.com/go-sql-driver/mysql"
)

# 根据id查询视频数据
func (d *Dao) GetVideoInfo(id string) *model.Video {
	querySQL, err := d.DB.Prepare("SELECT title, note, pic_path, video_path FROM video WHERE id = ?")
	if err != nil {
		logrus.Errorf("Prepare Select SQL Error: %s", err.Error())
		return nil
	}

	defer querySQL.Close()
	v := new(model.Video)
	err = querySQL.QueryRow(id).Scan(&v.Title, &v.Note, &v.PicPath, &v.VideoPath)
	if err != nil && err != sql.ErrNoRows {
		logrus.Errorf("Query Video Error: %s", err.Error())
	}
	return v
}

# 创建视频数据,id使用UUID
func (d *Dao) CreateNewVideo(v *model.Video) {
	id := UUID()
	title := v.Title
	note := v.Note
	picPath := v.PicPath
	videoPath := v.VideoPath

	insertSql, err := d.DB.Prepare("INSERT INTO video (id, title, note, pic_path, video_path) VALUES (?, ?, ?, ?, ?)")
	if err != nil {
		logrus.Errorf("Prepare Insert SQL Error: %s", err.Error())
	}

	_, err = insertSql.Exec(id, title, note, picPath, videoPath)
	if err != nil {
		logrus.Errorf("Insert Video Info SQL Error: %s", err.Error())
	}
	defer insertSql.Close()
	return
}

func UUID() string {
	id, err := uuid.NewUUID()
	if err != nil {
		logrus.Infof("UUID Error: %s", err.Error())
	}
	return id.String()
}

Таким образом, мы сначала инкапсулируем код операции нашей базы данных, а затем начинаем писать часть gRPC.

4.3 gRPC-сервер

backend/internal/service/grpc.go

package service

import (
	"context"
	"pilipili/backend/model"
	pb "pilipili/proto"
)

func (s *Service) RpcGetVideoInfo(ctx context.Context, request *pb.GetVideoRequest) (*pb.VideoInfo, error) {
	id := request.Id
	v := s.dao.GetVideoInfo(id)

	# 直接使用proto生成的go代码,里面带有所有我们定义好的数据结构
	resp := new(pb.VideoInfo)

	resp.Id = id
	resp.Title = v.Title
	resp.Note = v.Note
	resp.Pic = v.PicPath
	resp.Video = v.VideoPath

	return resp, nil
}

func (s *Service) RpcCreateVideoInfo(ctx context.Context, info *pb.VideoInfo) (*pb.CreateVideoReplay, error) {
	title := info.Title
	note := info.Note
	pic := info.Pic
	video := info.Video

	v := new(model.Video)
	v.Title = title
	v.Note = note
	v.PicPath = pic
	v.VideoPath = video
    
	s.dao.CreateNewVideo(v)
    
	# 直接使用proto生成的go代码,里面带有所有我们定义好的数据结构
	resp := new(pb.CreateVideoReplay)
	resp.Status = "OK"
	return resp, nil
}

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

RpcGetVideoInfoПолучите GetVideoRequest и верните VideoInfo

RpcCreateVideoInfoПолучает VideoInfo и возвращает CreateVideoReplay

4.4 HTTP-сервер

backend/internal/service/http.go

package service

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"pilipili/backend/model"
)

func (s *Service) InitRoutes() {
	s.Router.GET("/video", s.HttpGetVideoInfo)
	s.Router.POST("/video", s.HttpCreateVideo)
}

func (s *Service) HttpGetVideoInfo(ctx *gin.Context) {
	id := ctx.Query("id")
	v := s.dao.GetVideoInfo(id)
	ctx.JSON(http.StatusOK, gin.H{
		"id":    id,
		"title": v.Title,
		"note":  v.Note,
		"pic":   v.PicPath,
		"video": v.VideoPath,
	})
}

func (s *Service) HttpCreateVideo(ctx *gin.Context) {
	title := ctx.PostForm("title")
	note := ctx.PostForm("note")
	pic := ctx.PostForm("pic")
	video := ctx.PostForm("video")

	v := new(model.Video)
	v.Title = title
	v.Note = note
	v.PicPath = pic
	v.VideoPath = video

	s.dao.CreateNewVideo(v)

	ctx.String(http.StatusOK, "OK")
}

Здесь я определяю интерфейс в стиле Restful для службы HTTP:

GET /videoПолучить информацию о видеоPOST /videoСоздать видео сообщение

4.5 Строительство Backend Services

Теперь мы интегрируем сервисы gRPC и HTTP в один сервис.

backend/internal/service/service.go

package service

import (
	"github.com/gin-gonic/gin"
	"google.golang.org/grpc"
	"pilipili/backend/dao"
)

type Service struct {
	dao    *dao.Dao
	Router *gin.Engine
	RPC    *grpc.Server
}

func NewGRPCServer() *grpc.Server {
	s := grpc.NewServer()
	return s
}

func NewService() *Service {
	s := &Service{
		dao:    dao.NewDao(),
		Router: gin.Default(),
		RPC:    NewGRPCServer(),
	}
	return s
}

Таким образом, мы интегрируем HTTP и gRPC в нашу структуру службы.

Далее идет код для инициализации сервиса

backend/internal/server/server.go

package server

import (
	"github.com/sirupsen/logrus"
	"net"
	"net/http"
	"pilipili/backend/internal/service"
	pb "pilipili/proto"
)

func NewHttpServer(s *service.Service) {
	s.InitRoutes()
	server := http.Server{
		Addr:    ":23333",
		Handler: s.Router,
	}
	go func() {
		if err := server.ListenAndServe(); err != nil {
			logrus.Errorf("HTTP Server Error: %s", err.Error())
		}
	}()
}

func NewGRPCServer(s *service.Service) {
	Address := "127.0.0.1:23332"
	listen, err := net.Listen("tcp", Address)
	if err != nil {
		logrus.Errorf("Listen Error: %s", err.Error())
		panic(err)
	}
    
    # 这里也是proto替我们生成好的方法,监听了我们的gRPC服务
	pb.RegisterPilipiliServer(s.RPC, s)
	s.RPC.Serve(listen)
}

4.6 Сервер main.go

backend/main.go

package main

import (
	"os"
	"os/signal"
	"pilipili/backend/internal/server"
	"pilipili/backend/internal/service"
	"syscall"
)

func main() {
	s := service.NewService()
	server.NewHttpServer(s)
	server.NewGRPCServer(s)
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
	for {
		switch <-c {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			return
		case syscall.SIGHUP:
		}
	}
}

5. Развитие связи

5.1 Серверная часть кодов HTTP-запросов

domain/internal/service/http.go

package service

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"io/ioutil"
	"net/http"
	"net/url"
)

const address = "http://0.0.0.0:23333/video"

func (s *Service) HttpGetVideoInfo(ctx *gin.Context) {
	id := ctx.Query("id")
	resp, err := http.Get(address + fmt.Sprintf("?id=%s", id))
	if err != nil {
		logrus.Errorf("HTTP Get Video Error: %s", err.Error())
	}
	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
		}
		ctx.String(http.StatusOK, string(body))
	} else {
		ctx.String(http.StatusInternalServerError, "Failed")
	}
}

func (s *Service) HttpCreateVideo(ctx *gin.Context) {
	title := ctx.PostForm("title")
	note := ctx.PostForm("note")
	pic := ctx.PostForm("pic")
	video := ctx.PostForm("video")

	resp, err := http.PostForm(address, url.Values{
		"title": {title},
		"note":  {note},
		"pic":   {pic},
		"video": {video},
	})
	if err != nil {
		logrus.Errorf("Post Form Error: %s", err.Error())
	}

	defer resp.Body.Close()

	if resp.StatusCode == 200 {
		ctx.String(http.StatusOK, "OK")
	} else {
		ctx.String(http.StatusInternalServerError, "Failed")
	}
}

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

5.2 Внутренний код запроса gRPC

domain/internal/service/grpc.go


package service

import (
	"context"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"net/http"
	pb "pilipili/proto"
)

func (s *Service) GRPCGetVideoInfo(ctx *gin.Context) {
	rpcRequest := new(pb.GetVideoRequest)
	rpcRequest.Id = ctx.Query("id")
	v, err := s.RPC.RpcGetVideoInfo(context.Background(), rpcRequest)
	if err != nil {
		logrus.Errorf("RPC Server Error: %s", err.Error())
	}
	if v != nil {
		ctx.JSON(http.StatusOK, gin.H{"id": v.Id, "title": v.Title, "note": v.Note, "pic": v.Pic, "video": v.Video})
	} else {
		ctx.String(http.StatusInternalServerError, "Failed")
	}
}


func (s *Service) GRPCCreateVideo(ctx *gin.Context) {
	rpcRequest := new(pb.VideoInfo)
	rpcRequest.Title = ctx.PostForm("title")
	rpcRequest.Note = ctx.PostForm("note")
	rpcRequest.Pic = ctx.PostForm("pic")
	rpcRequest.Video = ctx.PostForm("video")

	v, err := s.RPC.RpcCreateVideoInfo(context.Background(), rpcRequest)
	if err != nil {
		logrus.Errorf("RPC Server Error: %s", err.Error())
	}
	if v != nil {
		ctx.String(http.StatusOK, v.Status)
	} else {
		ctx.String(http.StatusInternalServerError, "Failed")
	}
}

Приведенный выше код является частью использования gRPC для доступа к серверной части.Вы можете видеть, что здесь реализована концепция PRC, вызывая удаленный метод, как локальный метод.

5.3 Сравнение кода HTTP и gRPC

В приведенном выше коде мы видим явную разницу.

В HTTP-запросе, который мы передаем, необходимо указать метод GET или POST... и ряд операций.

resp, err := http.PostForm(address, url.Values{
		"title": {title},
		"note":  {note},
		"pic":   {pic},
		"video": {video},
	})

А gRPC вызывает удаленные методы точно так же, как и локальные, что, несомненно, более элегантно.

	v, err := s.RPC.RpcCreateVideoInfo(context.Background(), rpcRequest)

5.4 Прокладка на стороне соединения

domain/internal/servide/routes.go

package service

func (s *Service) InitRoutes() {
	httpRoutes := s.Router.Group("/http")
	{
		httpRoutes.GET("/get_video", s.HttpGetVideoInfo)
		httpRoutes.POST("/create_video", s.HttpCreateVideo)
	}
	grpcRoutes := s.Router.Group("/grpc")
	{
		grpcRoutes.GET("/get_video", s.GRPCGetVideoInfo)
		grpcRoutes.POST("/create_video", s.GRPCCreateVideo)
	}
}

Здесь используется метод написания группы HTTP-маршрутизации gin, который является более интуитивным.

Здесь проводится различие между HTTP-запросами и gRPC.

5.5 Служба на стороне соединения

domain/internal/service/service.go


package service

import (
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	pb "pilipili/proto"
)

type Service struct {
	Router *gin.Engine
	RPC    pb.PilipiliClient
}

func NewService() *Service {
	s := &Service{
		Router: gin.Default(),
		RPC:    NewGRPC(),
	}
	return s
}

func NewGRPC() pb.PilipiliClient {
	Address := "127.0.0.1:23332"
	conn, err := grpc.Dial(Address, grpc.WithInsecure())

	if err != nil {
		grpclog.Fatal(err)
		logrus.Errorf("Start GRPC Error: %s", err.Error())
	}

	c := pb.NewPilipiliClient(conn)
	return c
}

5.5 Служба инициализации завершения соединения

domain/internal/server/server.go


package service

import (
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc"
	"google.golang.org/grpc/grpclog"
	pb "pilipili/proto"
)

type Service struct {
	Router *gin.Engine
	RPC    pb.PilipiliClient
}

func NewService() *Service {
	s := &Service{
		Router: gin.Default(),
		RPC:    NewGRPC(),
	}
	return s
}

# 创建gRPC连接
func NewGRPC() pb.PilipiliClient {
	Address := "127.0.0.1:23332"
	conn, err := grpc.Dial(Address, grpc.WithInsecure())

	if err != nil {
		grpclog.Fatal(err)
		logrus.Errorf("Start GRPC Error: %s", err.Error())
	}

	c := pb.NewPilipiliClient(conn)
	return c
}

5.6 Сторона подключения main.go


package main

import (
	"os"
	"os/signal"
	"pilipili/domain/internal/server"
	"pilipili/domain/internal/service"
	"syscall"
)

func main() {
	s := service.NewService()
	server.NewServer(s)
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
	for {
		switch <-c {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			return
		case syscall.SIGHUP:
		}
	}
}

6. Тест

Сейчас мы завершили разработку кода серверной части и стороны подключения.

Запустите службу сервера и стороны подключения соответственно.

Наш конец соединения предоставляет 4 интерфейса HTTP:

/http/get_video

/http/create_video

/grpc/get_video

/grpc/create_video

http использует службу серверной части HTTP-запроса

grpc использует grpc для запроса серверных служб

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

7. Заключение

Этот учебник является относительно базовым, и он поможет вам разработать более практичный сервис gRPC, более практичный, чем простой Hello World.

Спасибо за чтение моей документации, пожалуйста, укажите на любые ошибки.

Весь приведенный выше код размещен в моем репозитории Github, и заинтересованные друзья могут его попробовать.

GitHub.com/3inch time/Боюсь…

Еще раз спасибо всем ~~~

Иди ~ Иди ~ Иди!

🏆 Технический спецвыпуск 2 | То, что я сделал с Go...