对于微服务架构,将图像处理服务独立为单独的代码仓库是推荐的做法。这样可以实现更好的解耦、独立部署和团队自治。以下是具体实现方案:
1. 项目结构建议
主服务仓库 (main-service)
cmd/
└── main-server/
└── main.go
internal/
└── imageclient/
└── client.go
proto/
└── image_service.proto
图像服务仓库 (image-service)
cmd/
└── image-server/
└── main.go
internal/
└── handler/
└── image_handler.go
proto/
└── image_service.proto
2. Protobuf 定义
// proto/image_service.proto
syntax = "proto3";
package image;
option go_package = "github.com/yourorg/image-service/proto";
service ImageService {
rpc ProcessImage(ImageRequest) returns (ImageResponse);
}
message ImageRequest {
bytes image_data = 1;
string text_to_add = 2;
bool return_path = 3;
}
message ImageResponse {
string image_url = 1;
string status = 2;
string error_message = 3;
}
3. 图像服务实现
// image-service/internal/handler/image_handler.go
package handler
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"image"
"image/draw"
"image/jpeg"
"image/png"
"bytes"
"strings"
)
type ImageService struct {
s3Client *s3.S3
bucket string
}
func NewImageService(bucket string) *ImageService {
sess := session.Must(session.NewSession())
return &ImageService{
s3Client: s3.New(sess),
bucket: bucket,
}
}
func (s *ImageService) ProcessImage(ctx context.Context, req *proto.ImageRequest) (*proto.ImageResponse, error) {
// 解码图像
img, format, err := image.Decode(bytes.NewReader(req.ImageData))
if err != nil {
return nil, fmt.Errorf("failed to decode image: %v", err)
}
// 添加文本到图像(简化示例)
imgWithText := s.addTextToImage(img, req.TextToAdd)
// 编码图像
var buf bytes.Buffer
switch strings.ToLower(format) {
case "jpeg", "jpg":
err = jpeg.Encode(&buf, imgWithText, nil)
case "png":
err = png.Encode(&buf, imgWithText)
default:
return nil, fmt.Errorf("unsupported image format: %s", format)
}
// 上传到S3
key := fmt.Sprintf("images/%s-processed.%s", generateUUID(), format)
_, err = s.s3Client.PutObjectWithContext(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(buf.Bytes()),
})
if err != nil {
return nil, fmt.Errorf("failed to upload to S3: %v", err)
}
// 构建响应
resp := &proto.ImageResponse{
Status: "success",
}
if req.ReturnPath {
resp.ImageUrl = fmt.Sprintf("https://%s.s3.amazonaws.com/%s", s.bucket, key)
}
return resp, nil
}
func (s *ImageService) addTextToImage(img image.Image, text string) image.Image {
// 实现文本添加逻辑
return img
}
4. 图像服务服务器
// image-service/cmd/image-server/main.go
package main
import (
"log"
"net"
"image-service/internal/handler"
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
imageService := handler.NewImageService("your-s3-bucket-name")
proto.RegisterImageServiceServer(grpcServer, imageService)
log.Println("Image service listening on port 50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
5. 主服务客户端
// main-service/internal/imageclient/client.go
package imageclient
import (
"context"
"google.golang.org/grpc"
"time"
)
type Client struct {
conn *grpc.ClientConn
client proto.ImageServiceClient
}
func NewClient(addr string) (*Client, error) {
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
return nil, err
}
return &Client{
conn: conn,
client: proto.NewImageServiceClient(conn),
}, nil
}
func (c *Client) ProcessImage(imageData []byte, text string, returnPath bool) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req := &proto.ImageRequest{
ImageData: imageData,
TextToAdd: text,
ReturnPath: returnPath,
}
resp, err := c.client.ProcessImage(ctx, req)
if err != nil {
return "", err
}
return resp.ImageUrl, nil
}
func (c *Client) Close() error {
return c.conn.Close()
}
6. 主服务调用示例
// main-service/cmd/main-server/main.go
package main
import (
"log"
"main-service/internal/imageclient"
)
func main() {
// 初始化图像服务客户端
imgClient, err := imageclient.NewClient("image-service:50051")
if err != nil {
log.Fatal(err)
}
defer imgClient.Close()
// 读取图像数据
imageData := readImageData("input.jpg")
// 调用图像服务
imageURL, err := imgClient.ProcessImage(imageData, "Sample Text", true)
if err != nil {
log.Printf("Failed to process image: %v", err)
return
}
log.Printf("Processed image URL: %s", imageURL)
}
7. Docker 部署配置
# image-service/Dockerfile
FROM golang:1.19-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o image-server ./cmd/image-server
FROM alpine:latest
COPY --from=builder /app/image-server /image-server
EXPOSE 50051
CMD ["/image-server"]
8. 服务发现与通信
使用环境变量或配置中心进行服务发现:
// 通过环境变量获取服务地址
func getImageServiceAddr() string {
if addr := os.Getenv("IMAGE_SERVICE_ADDR"); addr != "" {
return addr
}
return "localhost:50051" // 默认地址
}
这种架构允许两个服务独立开发、测试和部署。gRPC 提供了高效的二进制通信,适合微服务间的内部调用。