GRPC文件传输与GO
#初学者 #编程 #go #grpc

任务

不久前,我一直在寻找一个额外的工作项目作为Go-Developer,并找到了一家具有测试任务的无名公司的空缺,以编写一个简单的客户端服务器应用程序,用于使用GRPC连接上传大型文件。

我想:好的,为什么不。

剧透:我得到了一个要约,但拒绝了。

解决方案

准备

grpc协议的Official docs说,有两种交流方式:一元和流。

用于上传大文件,我们可以使用从用户到服务器的文件的某些字节。

让我们写简单的原始文件:

#grpc-filetransfer/pkg/proto
syntax = "proto3";
package proto;
option go_package = "./;uploadpb";
message FileUploadRequest {
    string file_name = 1;
    bytes chunk = 2;
}
message FileUploadResponse {
  string file_name = 1;
  uint32 size = 2;
}
service FileService {
   rpc Upload(stream FileUploadRequest) returns(FileUploadResponse);
}

如您所见,FileUploadRequest包含file_namefile_chunkFileUploadResponse-正确上传后简单响应。该服务是唯一的方法Upload

让我们在您的proto dir:protoc --go_out=. --go_opt=paths=source_relative upload.proto

中生成文件

好吧,我们已经生成了GO代码,可以开始编写我们的服务。

服务器端

服务器端可以读取其配置,通过Upload过程收听传入的GRPC客户端,然后在客户端结束后编写文件和答案。

服务器应嵌入protoc生成的UnimplementedFileServiceServer

type FileServiceServer struct {
    uploadpb.UnimplementedFileServiceServer
    l   *logger.Logger
    cfg *config.Config
}

并实现Upload方法,该方法将流作为参数:

func (g *FileServiceServer) Upload(stream uploadpb.FileService_UploadServer) error {
    file := NewFile()
    var fileSize uint32
    fileSize = 0
    defer func() {
        if err := file.OutputFile.Close(); err != nil {
            g.l.Error(err)
        }
    }()
    for {
        req, err := stream.Recv()
        if file.FilePath == "" {
            file.SetFile(req.GetFileName(), g.cfg.FilesStorage.Location)
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return g.logError(status.Error(codes.Internal, err.Error()))
        }
        chunk := req.GetChunk()
        fileSize += uint32(len(chunk))
        g.l.Debug("received a chunk with size: %d", fileSize)
        if err := file.Write(chunk); err != nil {
            return g.logError(status.Error(codes.Internal, err.Error()))
        }
    }
    fileName := filepath.Base(file.FilePath)
    g.l.Debug("saved file: %s, size: %d", fileName, fileSize)
    return stream.SendAndClose(&uploadpb.FileUploadResponse{FileName: fileName, Size: fileSize})
}

我使用了简单的File stuct,它具有三种用于文件操作的方法:SetFileWriteClose

type File struct {
    FilePath   string
    buffer     *bytes.Buffer
    OutputFile *os.File
}

func (f *File) SetFile(fileName, path string) error {
    err := os.MkdirAll(path, os.ModePerm)
    if err != nil {
        log.Fatal(err)
    }
    f.FilePath = filepath.Join(path, fileName)
    file, err := os.Create(f.FilePath)
    if err != nil {
        return err
    }
    f.OutputFile = file
    return nil
}

func (f *File) Write(chunk []byte) error {
    if f.OutputFile == nil {
        return nil
    }
    _, err := f.OutputFile.Write(chunk)
    return err
}

func (f *File) Close() error {
    return f.OutputFile.Close()
}

服务器从客户端收到后立即将文件零件写入硬盘驱动器。这就是为什么文件大小无关紧要的原因,仅取决于文件系统。

我知道使用log.Fatal不是一个好主意,所以不要在您的生产应用中这样做。

现在我们有一个完全编写的服务器端。由于我没有将整个代码放在这里,因此您可以在我的github

上检查它

客户端

我们的客户端应用程序是一个简单的CLI,具有两个必需的选项:GRPC服务器地址和上传文件的路径。

对于CLI接口,我选择了cobra框架,因为它易于使用并显示我知道=),但它是两个参数应用程序的开销。
客户端应用程序用法的示例:
./grpc-filetransfer-client -a=':9000' -f=8GB.bin

让我们写客户上传逻辑。该应用应该连接到服务器,上传文件并在传输后关闭连接。

type ClientService struct {
    addr      string
    filePath  string
    batchSize int
    client    uploadpb.FileServiceClient
}

客户端通过Chuck size读取文件==批次尺寸并将其发送到GRPC Steam。

func (s *ClientService) upload(ctx context.Context, cancel context.CancelFunc) error {
    stream, err := s.client.Upload(ctx)
    if err != nil {
        return err
    }
    file, err := os.Open(s.filePath)
    if err != nil {
        return err
    }
    buf := make([]byte, s.batchSize)
    batchNumber := 1
    for {
        num, err := file.Read(buf)
        if err == io.EOF {
            break
        }
        if err != nil {
            return err
        }
        chunk := buf[:num]

        if err := stream.Send(&uploadpb.FileUploadRequest{FileName: s.filePath, Chunk: chunk}); err != nil {
            return err
        }
        log.Printf("Sent - batch #%v - size - %v\n", batchNumber, len(chunk))
        batchNumber += 1

    }
    res, err := stream.CloseAndRecv()
    if err != nil {
        return err
    }
    log.Printf("Sent - %v bytes - %s\n", res.GetSize(), res.GetFileName())
    cancel()
    return nil
}

结论

我们编写了客户服务器GRPC文件传输应用程序,该应用程序可以上传任何大小的文件。上传的速度取决于批量的大小,您可以尝试为此找到最佳价值。

我的github上的完整代码

我使用了这些教程中的一些代码:

that place获得封面图像。

这是我的第一篇文章,因此任何评论都将受到欢迎。