golang

Создаём микросервис обработки изображений на Go с gRPC

  • воскресенье, 1 марта 2026 г. в 00:00:11
https://habr.com/ru/articles/1004928/

В этой статье мы рассмотрим создание микросервиса обработки изображений на golang с использованием технологии gRPC. Цель статьи - показать как может выглядеть такой сервис и что он может в себя включать. В результате мы получим полностью рабочий сервис по обработке изображений, который принимает данные, сохраняет исходную картинку, сжимает её, накладывает на неё ватермарку, изменяет размер изображения, и конвертирует его в нужный формат.

Разберём возможные варианты взаимодействия клиента с сервером для обработки больших объектов, в нашем случае это картинки:

  1. HTTP/1.1 (REST)
    Передача изображений в виде текстовых чанков (например, base64) приводит к значительным накладным расходам: бинарные данные увеличиваются на ~33% при кодировании в Base64, а текстовый формат неэффективен для больших объёмов.

  2. WebSocket
    Подходит для долгоживущих сессий и двустороннего обмена, но избыточен, если нам нужно просто «принять изображение → обработать → вернуть результат». Удержание тысяч соединений ради однократных операций — неоптимально.

  3. gRPC использует:
    Protocol Buffers — строго типизированный, компактный бинарный формат,
    HTTP/2 — мультиплексирование, потоки, сжатие заголовков,
    Client-Streaming — идеально подходит для передачи одного большого файла (например, изображения) в одном вызове.

I. Постановка задачи

Сервис должен:

Принимать изображение и параметры (format, compress, watermark, width[], height[]),
Сохранять оригинал с уникальным путём (./download/YYYY/MM/DD/UUID/img/...),
Накладывать водяной знак,
Генерировать версии заданных размеров,
Сохранять полученные изображения,
Возвращать список путей.

Пример:

Запрос с width = [1920, 1280], height = [1080, 720], format = "webp", watermark = "logo.png"
→ Сервис вернёт два пути:
./download/2026/02/08/abc123/img/abc123_1920x1080.webp
./download/2026/02/08/abc124/img/abc124_1280x720.webp

II. Архитектура приложения

Обработка происходит в строгом порядке:

1.Приём → 2. Сохранение исходного файла→ 3. Сжатие → 4. Watermark → 5. Resize → 6. Конвертация → 7. Ответ.

Структура хранения:

./download/2026/02/08/a1b2c3d4-.../img/
├── a1b2c3d4-....jpg ← оригинал
├── a1b2c3d4-..._800x600.png
└── a1b2c3d4-..._1024x768.png

Необходимые инструменты:

Для работы с WebP мы используем библиотеку golang.org/x/image/webp, а исходные утилиты можно скачать на официальной странице Google.

Для генерации protobuff нам нужен protoc-gen-go

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

protoc-gen-go-grpc

Позволяет генерировать определения сервисов Go для буфера протокола, заданного нашим .proto файлом protoc-gen-go-grpc

Также для работы webp нам потребуется работа с CGO, которую мы рассмотрим отдельно.

III. Реализация proto файла

Для начала работы опишем наш proto файл. Это будет сервис с единственным rpc который будет обрабатывать картинки пользователей.

./proto/image.proto

message DownloadImagesRequest {
        ImageInfo info = 1; //параметры обработки изображения
        bytes image = 2; // непосредственно данные изображения
}

message ImageInfo {
       string compress = 1; // сжатие
    string watermark = 2; // вотермарк
    string format = 3; // перевод в формат файла
    repeated int32 width = 4; // ширина обработанной картинки
    repeated int32 height = 5; // высота обработанной картинки
  }
message DownloadImagesResponse {
    repeated string storage_path = 1; // ссылка куда сохранить
    string error=2;
}

/* При передаче файлов больших размеров клиент передает нам данные потоком, сервер дожидается окончания передачи, обрабатывет запрос и отдает пользователю ответ. */
service ImageService {
    rpc DownloadImages(stream DownloadImagesRequest) returns (DownloadImagesResponse);
  }

Мы используем Client-Streaming RPC — клиент может отправить несколько сообщений, сервер — один ответ. Этот способ поможет нам в случае необходимости гибкого расширения и избежания ограничений на размер одного сообщения.

IV. Реализация основных функций

1. Создание сервера

1.1 Генерация go файлов из .proto:

Сгенерируем файлы для реализации сервера с помощью protoc:

protoc --go_out=.  --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative -I . image.proto

У нас должно получиться 2 файла image.pb.go и image_grpc.pb.go в директории proto

1.2 Реализуем конструктор сервера и interceptor (аналог middleware в grpc) восстановления после паники:

./internal/app/server.go

package app

import (
	pb "image-converter/proto"
	"log/slog"
	"net"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type GrpcServer struct {
	Server *grpc.Server
}

func recoveryFn(p any) (err error) {
	return status.Errorf(codes.Unknown, "panic triggered: %v", p)
}

func NewGrpcServer() *GrpcServer {
	return &GrpcServer{
		Server: grpc.NewServer(

			grpc.ChainStreamInterceptor(
				recovery.StreamServerInterceptor(recovery.WithRecoveryHandler(recoveryFn)),
			)),
	}
}

Теперь реализуем функцию запуска нашего grpc сервера:

./internal/app/server.go

...
func (s *GrpcServer) GrpcServeServer(a ImageServer, adress string) error {
	lis, err := net.Listen("tcp", adress)
	if err != nil {
		slog.Error("address for grpc server not found, attempting graceful shutdown")
		s.Server.GracefulStop()
		return err
	}

	pb.RegisterImageServiceServer(s.Server, a)

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		<-sigCh
		slog.Error("got signal 1, attempting graceful shutdown")
		s.Server.GracefulStop()
		wg.Done()
	}()

	slog.Info("starting grpc server", "address", adress)
	if err := s.Server.Serve(lis); err != nil {
		slog.Error("grpc server error", "error", err.Error())
		s.Server.GracefulStop()
		return err
	}
	wg.Wait()

	return nil
}

Мы создали наш grpc сервер, но пока нет никакой реализации ImageServiceServer который сгенерировал нам protoc, это просто интерфейс который нам и нужно реализовать.

2. Реализация ImageServiceServer

Нам необходимо создать структуру ImageServer которая реализует интерфейс ImageServiceServer с его методом DownloadImage, также создадим конструктор для него :

./internal/app/image.go

package app

import (
	"errors"
	pb "image-converter/proto"
	"io"
	"strconv"
)

type ImageServer struct {
	pb.ImageServiceServer
}

func NewImageServer() ImageServer {
	i := ImageServer{}
	return i
}

// создадим структуру OriginalImage которая будет хранить метаданные о наших картинках
type OriginalImage struct {
	Path      string
	Lenght    []int32
	Width     []int32
	Format    string
	Folder    string
	Watermark string
	UUID      string
}

const defaultWatermark = "watermark.png"

func (img ImageServer) DownloadImages(stream pb.ImageService_DownloadImagesServer) error {
	var images []*pb.DownloadImagesRequest
	//принимаем поток от клиента и проверяем, что он дошёл до нас без сбоев
	for {
		image, err := stream.Recv()
		if err == io.EOF {
			break
		}
		if err != nil { // возвращаем ошибку в случае неудачного приема
			return stream.SendAndClose(&pb.DownloadImagesResponse{
				Error: err.Error(),
			})
		}
		images = append(images, image)
	}
	/*Проверяем , чтобы клиент при передаче длины и ширины картинок которые
	  необходимо создать передал одинаковое их количество, иначе будет непонятно
	  по каким размерам её нужно будет обрабатывать, и сохраняем сами картинки,
	   также проверяем чтобы в ватермарке присутствовало хотя бы значение
	   по умолчанию, а не пустое */
	var paths []OriginalImage
	for i := range images {
		if len(images[i].Info.Height) != len(images[i].Info.Width) {
			return stream.SendAndClose(&pb.DownloadImagesResponse{
				Error: "different len of lenght and width for picture " + strconv.Itoa(i),
			})
		}
		if images[i].Info.Watermark == "" { // если нам не передали делаем по умолчанию
			images[i].Info.Watermark = defaultWatermark
		}
	}

	/*наше сохранение исходных файлов, полученных от клиента,
	а также помещение всех метаданных в слайс наших структур для
	удобства работы с ними */
	paths = saveSourceFiles(images)
	if len(images) == 0 {
		return stream.SendAndClose(&pb.DownloadImagesResponse{
			Error: "no images in request",
		})
	}

	//наложение watermark на наши изображения
	err := watermark(paths)
	if err != nil {
		return stream.SendAndClose(&pb.DownloadImagesResponse{
			Error: errors.New("path for watermark is invalid").Error(),
		})
	}
	//изменение размера картинки и сохранение уже обработанной версии
	uploadPath := resizeAndSave(paths)

	res := &pb.DownloadImagesResponse{
		StoragePath: uploadPath,
	}
	//возврат полученных путей хранения картинок для возможности просмотра
	err = stream.SendAndClose(res)
	if err != nil {
		return stream.SendAndClose(&pb.DownloadImagesResponse{
			Error: "error when receive an responce",
		})
	}

	return nil
}

Нам осталось реализовать ключевые функции для нашего сервиса, а именно: saveSourceFiles: отвечает за сохранение исходных файлов и их сжатие, watermark: наложение ватермарки , resizeAndSave: изменения размера картинки и его конвертация в нужный формат изображения.

3. Сохранение оригинала

Мы создаем путь для сохранения картинки, сохраняем её с необходимым для клиента уровнем сжатия и потом для удобства всю метаинформацию об картинке помещаем уже в нашу структуру и работаем в дальнейшем только с ней:

./internal/app/save.go

package app

import (
	"bytes"
	"fmt"
	"image"
	pb "image-converter/proto"
	"image/jpeg"
	"image/png"
	"log/slog"
	"os"
	sync "sync"
	"time"

	"github.com/chai2010/webp"
	"github.com/google/uuid"
)

// вспомогательная функция для получения года, месяца и дня
func getTimeData() (string, string, string) {
	year := time.Now().Year()
	month := time.Now().Month()
	day := time.Now().Day()
	y := fmt.Sprintf("%v", year)
	m2 := int(month)
	m := fmt.Sprintf("%v", m2)
	d := fmt.Sprintf("%v", day)
	return y, m, d
}

// вспомогательные функции для определения уровня сжатия картинок
func setLevelCompressionPNG(level string) png.CompressionLevel {
	var compessInt png.CompressionLevel
	switch level {
	case "low":
		compessInt = -3
	case "medium":
		compessInt = -2
	case "max":
		compessInt = -1
	}
	return compessInt
}

func setLevelCompressionJPG(level string) int {
	var compessInt int
	switch level {
	case "low":
		compessInt = 10
	case "medium":
		compessInt = 50
	case "max":
		compessInt = 100
	}
	return compessInt
}

func setLevelCompressionWEBP(level string) float32 {
	var compessInt float32
	switch level {
	case "low":
		compessInt = 10
	case "medium":
		compessInt = 50
	case "max":
		compessInt = 100
	}
	return compessInt
}

func saveSourceFiles(images []*pb.DownloadImagesRequest) []OriginalImage {
	var wg sync.WaitGroup
	var mu sync.Mutex
	y, m, d := getTimeData()
	imagesNew := make([]OriginalImage, 0)
	for i := range images {
		//применяем WaitGroup чтобы дождаться сохранения всех картинок переданных пользователем
		wg.Add(1)
		//используем горутины чтобы конкурентно обработать наши картинки
		go func(i int) {
			defer wg.Done()
			extension := images[i].Info.Format
			//генерируем уникальное имя и путь сохранения
			uuid := uuid.New().String()
			newFileName := uuid + "." + extension
			newFilePath := "./download/" + y + "/" + m + "/" + d + "/" + uuid + "/" + "img/"
			//создаём папку для сохранения файлов
			err := os.MkdirAll(newFilePath, 0755)
			if err != nil {
				slog.Error("failed to create directory", "error", err.Error())
			}
			// декодируем изображение
			img, _, err := image.Decode(bytes.NewReader(images[i].Image))
			if err != nil {
				slog.Error("failed to decode image", "error", err.Error())
				return
			}
			//собираем полный путь до файла с его названием и расширением
			path := newFilePath + newFileName
			//создаём файл для последующего сохранения туда картинки
			out, _ := os.Create(path)
			defer out.Close()
			//теперь обрабатываем картинку в зависимости от полученного формата изображения
			switch extension {
			case "png":
				var enc png.Encoder
				//сжимаем в соответствии с полученными параметрами от клиента
				level := setLevelCompressionPNG(images[i].Info.Compress)
				enc.CompressionLevel = level
				//сохраняем изображение
				err = enc.Encode(out, img)
				if err != nil {
					slog.Error("failed to encode PNG", "error", err.Error())
				}
			case "jpeg", "jpg":
				var opts jpeg.Options
				level := setLevelCompressionJPG(images[i].Info.Compress)
				opts.Quality = level
				err = jpeg.Encode(out, img, &opts)
				if err != nil {
					slog.Error("failed to encode JPEG", "error", err.Error())
				}
			case "webp":
				var data []byte
				level := setLevelCompressionWEBP(images[i].Info.Compress)
				data, err = webp.EncodeRGB(img, level)
				if err != nil {
					slog.Error("failed to encode WEBP", "error", err.Error())
				}
				if err = os.WriteFile(path, data, 0666); err != nil {
					slog.Error("failed to write WEBP file", "error", err.Error())
				}
			default:
				slog.Error("unknown file format, please provide a file with extension png,jpg,webp")
			}
			//создание структуры с метаданными для последующей работы
			imageNew := OriginalImage{
				Path:      path,
				Lenght:    images[i].Info.Height,
				Width:     images[i].Info.Width,
				Format:    images[i].Info.Format,
				Folder:    newFilePath,
				Watermark: images[i].Info.Watermark,
				UUID:      uuid,
			}
			// берём мьютекс для корректного добавления в слайс нашего изображения
			mu.Lock()
			imagesNew = append(imagesNew, imageNew)
			mu.Unlock()
		}(i)
		wg.Wait()
	}
	return imagesNew
}

Для ускорения нашего процесса обработки все этапы мы будем выполнять конкурентно с помощью горутин.

4. Водяной знак

Следующим этапом идёт наложение водяного знака на нашу картинку. Процесс наложения: мы передаем каждой горутине по изображению, они его обрабатывают, а для того чтобы убедиться что они все обработались мы применяем sync.WaitGroup. Сам процесс наложения водяного знака это задание параметров для установки его на исходную картинку, в нашем случае мы делаем её полупрозрачную с небольшим углом поворота и в случайном месте и сохраняем её:

./internal/app/watermark.go

package app

import (
	"os"
	sync "sync"

	"github.com/disintegration/imaging"
	"github.com/filipenevs/go-imagewatermark"
)

func watermark(paths []OriginalImage) error {
	var wg sync.WaitGroup
	var mu sync.Mutex
	var err error
	//получение текущего каталога для последующего открытия
	//и работы с watermark
	currDir, err := os.Getwd()
	if err != nil {
		return err
	}
	for i := range paths {
		//применяем WaitGroup чтобы дождаться наложения watermark на все картинки
		wg.Add(1)
		//используем горутины чтобы конкурентно обработать наши картинки
		go func(i int) {
			defer wg.Done()
			watermarkS := ""
			//проверяем если все таки нет конкретного указания на необходимую watermark
			// используем default
			switch paths[i].Watermark {
			case defaultWatermark:
				watermarkS = defaultWatermark
			case "":
				watermarkS = defaultWatermark
			default:
				watermarkS = paths[i].Watermark
			}

			watermarkPath := currDir + "\\" + watermarkS
			//сама функция наложения watermark
			funcErr := addWaterMark(paths[i].Path, watermarkPath)
			//так как обработка конкурентна возьмем мьютекс для того чтобы если
			//произошла хоть 1 ошибка то она точно вернется нам
			if funcErr != nil {
				mu.Lock()
				if err == nil {
					err = funcErr
				}
				mu.Unlock()
			}
		}(i)
	}
	wg.Wait()
	return err
}

func addWaterMark(bgImg, watermark string) error {
	/*наложение картинки на картину, так же здесь можно указывать
	её прозрачность, угол поворота, пропорцию, положение по горизонтали/вертикали
	*/
	result, err := imagewatermark.ProcessImageWithWatermark(imagewatermark.WatermarkConfig{
		InputPath:             bgImg,
		WatermarkPath:         watermark,
		OpacityAlpha:          0.5,
		WatermarkWidthPercent: 40,
		VerticalAlign:         imagewatermark.VerticalRandom,
		HorizontalAlign:       imagewatermark.HorizontalRandom,
		Spacing:               10,
		RotationDegrees:       20,
	})

	if err != nil {
		return err
	}
	//сохранение на диск
	err = imaging.Save(result, bgImg)
	return nil
}

Остаётся только изменить размер и сохранить в нужном формате наши обработанные изображения.

5. Resize и конвертация.

Теперь создадим файл resize.go и реализуем функцию изменения размера изображении и сохранения в нужном нам формате:

./internal/app/resize.go

package app

import (
	"bytes"
	"image/jpeg"
	"image/png"
	"io"
	"log/slog"
	"os"
	"strconv"
	sync "sync"

	"github.com/chai2010/webp"
	"github.com/nfnt/resize"
)

func resizeAndSave(paths []OriginalImage) []string {
	var wg sync.WaitGroup
	var mu sync.Mutex
	//создаём результирующий слайс путей наших файлов чтобы потом отдать клиенту
	uploadPaths := make([]string, 0)
	for i := range paths {
		// используем wg чтобы дождаться обработки все�� картинок
		wg.Add(1)
		// используем горутины для распараллеливания процесса
		go func(i int) {
			defer wg.Done()
			//основная функция изменения размера
			uploadPath := resizeImage(paths[i])
			if len(uploadPath) != 0 {
				mu.Lock()
				uploadPaths = append(uploadPaths, uploadPath...)
				mu.Unlock()
			} else {
				mu.Lock()
				uploadPaths = append(uploadPaths, paths[i].Path)
				mu.Unlock()
			}
		}(i)
	}
	wg.Wait()
	return uploadPaths
}

// наша основная функция на этом этапе обработки
func resizeImage(path OriginalImage) []string {
	var mu sync.Mutex
	// проверка что длина и ширина изображения имеются
	for i := range path.Lenght {
		if path.Lenght[i] == 0 || path.Width[i] == 0 {
			return []string{}
		}
	}
	//проверяем что у нас одинаковое количество
	// параметров длины и ширины картинки для ее создания
	if len(path.Lenght) != len(path.Width) {
		return []string{}
	}
	//результирующий слайс путей наших файлов
	var uploadPaths []string
	switch path.Format {
	case "png":
		for i := range path.Lenght {
			//открытие файла уже сжатого и с ватермаркой
			imgIn, err := os.Open(path.Path)
			if err != nil {
				slog.Error("failed to open PNG file", "error", err.Error())
				return []string{}
			}
			//его считывание
			imgPng, err := png.Decode(imgIn)
			if err != nil {
				slog.Error("failed to decode PNG", "error", err.Error())
				return []string{}
			}
			// и закрытие
			err = imgIn.Close()
			if err != nil {
				slog.Error("failed to close PNG file", "error", err.Error())
				return []string{}
			}
			// генерация нового изображения с измененным размером
			imgPng = resize.Resize(uint(path.Lenght[i]), uint(path.Width[i]), imgPng, resize.Bilinear)
			upPath := path.Folder + path.UUID + "_" + strconv.FormatUint(uint64(path.Lenght[i]), 10) + "x" + strconv.FormatUint(uint64(path.Width[i]), 10) + "." + path.Format
			buf := new(bytes.Buffer)
			// его запись в нужный нам формат
			err = png.Encode(buf, imgPng)
			if err != nil {
				slog.Error("failed to encode PNG", "error", err.Error())
				return []string{}
			}
			imgSave := buf.Bytes()
			//сохранение нового изображения по сгенерированному пути
			err = os.WriteFile(upPath, imgSave, 0666)
			if err != nil {
				slog.Error("failed to save PNG file", "error", err.Error())
				return []string{}
			}
			// добавление полученного пути к общему слайсу
			mu.Lock()
			uploadPaths = append(uploadPaths, upPath)
			mu.Unlock()
		}
		// по аналогии с кейсом png
	case "jpg", "jpeg":
		for i := range path.Lenght {
			imgIn, err := os.Open(path.Path)
			if err != nil {
				slog.Error("failed to open JPEG file", "error", err.Error())
				return []string{}
			}
			imgJpeg, err := jpeg.Decode(imgIn)
			if err != nil {
				slog.Error("failed to decode JPEG", "error", err.Error())
				return []string{}
			}
			err = imgIn.Close()
			if err != nil {
				slog.Error("failed to close JPEG file", "error", err.Error())
				return []string{}
			}
			imgJpeg = resize.Resize(uint(path.Lenght[i]), uint(path.Width[i]), imgJpeg, resize.Bilinear)
			upPath := path.Folder + path.UUID + "_" + strconv.FormatUint(uint64(path.Lenght[i]), 10) + "x" + strconv.FormatUint(uint64(path.Width[i]), 10) + "." + path.Format
			buf := new(bytes.Buffer)
			err = jpeg.Encode(buf, imgJpeg, &jpeg.Options{Quality: 100})
			if err != nil {
				slog.Error("failed to encode JPEG", "error", err.Error())
				return []string{}
			}
			imgSave := buf.Bytes()
			err = os.WriteFile(upPath, imgSave, 0666)
			if err != nil {
				slog.Error("failed to save JPEG file", "error", err.Error())
				return []string{}
			}
			mu.Lock()
			uploadPaths = append(uploadPaths, upPath)
			mu.Unlock()
		}
		// по аналогии с кейсом png
	case "webp":
		for i := range path.Lenght {
			imgIn, err := os.Open(path.Path)
			if err != nil {
				slog.Error("failed to open WEBP file", "error", err.Error())
				return []string{}
			}
			// Decode webp
			mg, err := io.ReadAll(imgIn)
			if err != nil {
				slog.Error("failed to read WEBP file", "error", err.Error())
				return []string{}
			}
			m, err := webp.DecodeRGB(mg)
			if err != nil {
				slog.Error("failed to decode WEBP", "error", err.Error())
				return []string{}
			}
			err = imgIn.Close()
			if err != nil {
				slog.Error("failed to close WEBP file", "error", err.Error())
				return []string{}
			}
			imgWebp := resize.Resize(uint(path.Lenght[i]), uint(path.Width[i]), m, resize.Bilinear)
			upPath := path.Folder + path.UUID + "_" + strconv.FormatUint(uint64(path.Lenght[i]), 10) + "x" + strconv.FormatUint(uint64(path.Width[i]), 10) + "." + path.Format
			imgSave, err := webp.EncodeRGB(imgWebp, 100)
			if err != nil {
				slog.Error("failed to encode WEBP", "error", err.Error())
				return []string{}
			}
			err = os.WriteFile(upPath, imgSave, 0666)
			if err != nil {
				slog.Error("failed to save WEBP file", "error", err.Error())
				return []string{}
			}
			mu.Lock()
			uploadPaths = append(uploadPaths, upPath)
			mu.Unlock()
		}
	default:
		slog.Error("unknown file format, please provide a file with extension png,jpg,webp")
	}
	return uploadPaths
}

Теперь реализуем main.go в котором создадим и вызовем наш сервис обработки изображений.

./internal/cmd/main.go

package main


import (
    "image-converter/internal/app"
    "log/slog"
)


func main() {
    imageServer := app.NewImageServer()
    grpcServer := app.NewGrpcServer()
    err := grpcServer.GrpcServeServer(imageServer, ":8086")
    if err != nil {
        slog.Warn("Server shutdown with error", "error", err.Error())
    }
}

VI. CGO

Наше приложение полностью готово, но теперь нужно удостовериться, что у нас работает поддержка CGO. Для начала нужно убедиться что мы скачали и установили webp по ссылке официальная страница Google. Далее нам необходимо установить GCC, без него go не может распознать импортированные C файлы, скачиваем и устанавливаем официальные зеркала GNU. Теперь нужно проверить, что переменная CGO_ENABLED=1, для этого используем команду go env и проверяем, если она равна 0, то используем go set CGO_ENABLED=1 и проверяем ,что она применилась, если нет то перезагружаем нашу систему и проверяем. Теперь мы готовы собрать наше приложение и приступить к тестированию.

VII. Тестирование и оптимизация

Тестирование с Bruno

Здесь вы можете тестировать удобными для вас средствами такими как Postman, Bruno, Yaak, grpccurl и т.д., главное чтобы они поддерживали тестирование grpc методов. Рассмотрим пример с Bruno:
1.Конвертируйте изображение в Base64: Image to Base64 Converter

2.Откройте Bruno создайте новый grpc запрос

  1. Выбираем метод grpc:

4.Уберите ползунок с reflection и укажите .proto файл

5.Выберите метод DownloadImage

6.В поле message заполните поле в соответствии с параметрами, например:

если появляются проблемы при подключении к сервису, то не выносите текстовое представление картинки в переменную
если появляются проблемы при подключении к сервису, то не выносите текстовое представление картинки в переменную

В поле image нужно скопировать текстовую строку ,что идёт после запятой, сгенерированную на шаге 1

7.Далее нужно запустить наше приложение и подключиться к нему с помощью кнопки →

В случае успеха должен появиться статус streaming

8.Отправляем наше сообщение/сообщения нажав на кнопку "Send message" один или несколько раз и завершаем нашу передачу сообщений с помощью кнопки → (если не нажать то приложение будет ожидать приёма сообщений). Результатом будет возврат путей наших обработанных сообщений

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

Подведем итоги, мы создали сервис который:

  • Использует gRPC для эффективной передачи данных,

  • Поддерживает WebP, JPEG, PNG,

  • Безопасен в конкурентной среде,

  • Можно легко масштабировать.

В результате получился сервис обработки изображений. Этот подход применим не только к изображениям, но и к любым бинарным данным.

Исходный код доступен по ссылке

Спасибо за внимание!