任务
不久前,我一直在寻找一个额外的工作项目作为Go-Developer,并找到了一家具有测试任务的无名公司的空缺,以编写一个简单的客户端服务器应用程序,用于使用GRPC连接上传大型文件。 p>
我想:好的,为什么不。
剧透:我得到了一个要约,但拒绝了。
解决方案
准备
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_name
和file_chunk
,FileUploadResponse
-正确上传后简单响应。该服务是唯一的方法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,它具有三种用于文件操作的方法:SetFile
,Write
和Close
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上的完整代码
我使用了这些教程中的一些代码:
- Upload file in chunks with client-streaming gRPC - Go
- Transferring files with gRPC client-side streams using Golang
从that place获得封面图像。
这是我的第一篇文章,因此任何评论都将受到欢迎。