<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Dmitry</title>
    <description>The latest articles on Forem by Dmitry (@dimk00z).</description>
    <link>https://forem.com/dimk00z</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F474983%2F1ba4b61c-323f-4e1a-8579-d7d620299674.jpg</url>
      <title>Forem: Dmitry</title>
      <link>https://forem.com/dimk00z</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dimk00z"/>
    <language>en</language>
    <item>
      <title>gRPC file transfer with GO</title>
      <dc:creator>Dmitry</dc:creator>
      <pubDate>Sat, 24 Dec 2022 14:17:21 +0000</pubDate>
      <link>https://forem.com/dimk00z/grpc-file-transfer-with-go-1nb2</link>
      <guid>https://forem.com/dimk00z/grpc-file-transfer-with-go-1nb2</guid>
      <description>&lt;h2&gt;
  
  
  Task
&lt;/h2&gt;

&lt;p&gt;Not long ago I was looking for an additional work project as GO-developer and found a vacancy of a no-name company with the test task to write a simple client-server app for uploading large files with gRPC connection.&lt;/p&gt;

&lt;p&gt;I thought: OK, why not.&lt;/p&gt;

&lt;p&gt;Spoiler: I got an offer but declined it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Preparation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://grpc.io/docs/what-is-grpc/core-concepts/"&gt;Official docs&lt;/a&gt; of gRPC protocol says that there are two ways of communication: unary and streaming.&lt;/p&gt;

&lt;p&gt;For uploading big files we could use streaming some bytes of a part of a file from a user to the server.&lt;/p&gt;

&lt;p&gt;Let's write simple proto file for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#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);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you see, &lt;code&gt;FileUploadRequest&lt;/code&gt; contains &lt;code&gt;file_name&lt;/code&gt; and &lt;code&gt;file_chunk&lt;/code&gt;, &lt;code&gt;FileUploadResponse&lt;/code&gt; - simple response after correct uploading. The service has the only method &lt;code&gt;Upload&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's generate go-file in your proto dir: &lt;code&gt;protoc --go_out=. --go_opt=paths=source_relative upload.proto&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ok, we have generated go code and can start to write our services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server side
&lt;/h3&gt;

&lt;p&gt;Server side can read its config, listen to incoming gRPC clients by &lt;code&gt;Upload&lt;/code&gt; procedure and write files parts and answers after clients streams ends.&lt;/p&gt;

&lt;p&gt;The server should embed &lt;code&gt;UnimplementedFileServiceServer&lt;/code&gt; that has been generated by protoc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type FileServiceServer struct {
    uploadpb.UnimplementedFileServiceServer
    l   *logger.Logger
    cfg *config.Config
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and implements &lt;code&gt;Upload&lt;/code&gt; method that takes stream as argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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(&amp;amp;uploadpb.FileUploadResponse{FileName: fileName, Size: fileSize})
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used simple &lt;code&gt;File&lt;/code&gt; stuct that has three  methods for files operation: &lt;code&gt;SetFile&lt;/code&gt;, &lt;code&gt;Write&lt;/code&gt; and &lt;code&gt;Close&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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()
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server writes file parts to the hard drive right away as soon as they are received from the client. That is why the file size doesn't matter, it depends only on file system.&lt;/p&gt;

&lt;p&gt;I know that using &lt;code&gt;log.Fatal&lt;/code&gt; isn't a good idea so don't do that in your production apps.&lt;/p&gt;

&lt;p&gt;Now we have a fully written server side. As I didn't put here the whole code, you can check it on my &lt;a href="https://github.com/dimk00z/grpc-filetransfer"&gt;github&lt;/a&gt; &lt;/p&gt;

&lt;h3&gt;
  
  
  Client side
&lt;/h3&gt;

&lt;p&gt;Our client app is a simple CLI with two required options: a gRPC server address and a path for uploading file.&lt;/p&gt;

&lt;p&gt;For CLI interface I chose &lt;code&gt;cobra&lt;/code&gt; framework just because it's simple to use and shows that I know it=) But it's overhead for two params app. &lt;br&gt;
An example of the client app usage:&lt;br&gt;
&lt;code&gt;./grpc-filetransfer-client -a=':9000' -f=8GB.bin&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Let's write client uploading logic. The app should connect to server, upload a file and close connection after transferring it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type ClientService struct {
    addr      string
    filePath  string
    batchSize int
    client    uploadpb.FileServiceClient
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client reads the file by chuck size==batchSize and sends it to gRPC steam.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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(&amp;amp;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
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;We wrote the client-server gRPC file transfer app that could upload a file of any size. The speed of uploading depends on a batch size, you can try to find the best value for that.&lt;/p&gt;

&lt;p&gt;Full code on my &lt;a href="https://github.com/dimk00z/grpc-filetransfer"&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used some code from those tutorials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/techschoolguru/upload-file-in-chunks-with-client-streaming-grpc-golang-4loc"&gt;Upload file in chunks with client-streaming gRPC - Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://www.inanzzz.com/index.php/post/152g/transferring-files-with-grpc-client-side-streams-using-golang"&gt;Transferring files with gRPC client-side streams using Golang&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Got cover image from &lt;a href="https://entgo.io/blog/2021/03/18/generating-a-grpc-server-with-ent/"&gt;that place&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It was my first article, so any comments would be welcomed.&lt;/p&gt;

</description>
      <category>go</category>
      <category>grpc</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
