上传一个10M的文件, 真的会用10M的内存吗?

442 阅读3分钟
先直接给答案: 是也不是(取决于你的配置和实现方式)

 
> 今天看到社区有人问了一个问题:
>
> 为什么`PHP`文件上传是直接用`move_uploaded_file`移动一个上传好的文件,而不是从`HTTP Body`中读取出文件内容.

* 我也对这个问题很感兴趣. 查阅了资料, 找到一篇鸟哥关联的[PHP文件上传源码分析(RFC1867)](https://www.laruence.com/2009/09/26/1103.html)
* 但也没有说明具体原因, 于是看了一下`Go`的文件上传的实现.(不过基本可以确定是因为内存问题)

### Go

* `Go`中获取上传的文件方式很简单, 只要通过`http.Request.FormFile`方法即可拿到上传的文件
```go
package main

import (
   "log"
   "net/http"
)

func main() {

   http.HandleFunc("/files", func(writer http.ResponseWriter, request *http.Request) {

      // 32M
      err := request.ParseMultipartForm(32 << 20)
      if err != nil {
         log.Println(err)
         return
      }
      
      // 获取上传的文件
      file, handler, err := request.FormFile("file_key")
      log.Println(file, handler, err)
   })
   if err := http.ListenAndServe(":8000", nil); err != nil {
      log.Println(err)
   }
}
```

* `http.Request.FormFile`的实现也比较简单, 直接从一个`map`里拿到想要的数据
* 所以上传的逻辑, 我们还是要看`http.Request.ParseMultipartForm`

```go
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
   if r.MultipartForm == multipartByReader {
      return nil, nil, errors.New("http: multipart handled by MultipartReader")
   }
   if r.MultipartForm == nil {
      err := r.ParseMultipartForm(defaultMaxMemory)
      if err != nil {
         return nil, nil, err
      }
   }
   if r.MultipartForm != nil && r.MultipartForm.File != nil {
      if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
         f, err := fhs[0].Open()
         return f, fhs[0], err
      }
   }
   return nil, nil, ErrMissingFile
}
```

* `http.Request.ParseMultipartForm`方法解析参数, 其中又调用了`multipart.Reader.ReadForm`去读取`Body`中的内容
* 观察此方法不难发现,上传的文件是存储到磁盘还是内存, 取决于给定的`maxMemory`参数是否大于上传的文件大小(多个文件合计计算)
* 注意的是,表单参数值也受`maxMemory`限制,不过给了`10M`.意思是我们如果设置`maxMemory=32M`, 那么提交的`Body`最大只能`42M`(上传文件还是`32M`)
* 如果`Body`小于`maxMemory`那么就直接把上传的文件读取到内存中操作,否则写入到临时文件夹(写入临时文件这个和`PHP`操作一致)
```go
func (r *Reader) ReadForm(maxMemory int64) (*Form, error) {
   return r.readForm(maxMemory)
}

func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
   form := &Form{make(map[string][]string), make(map[string][]*FileHeader)}
   defer func() {
      if err != nil {
         form.RemoveAll()
      }
   }()

   // Reserve an additional 10 MB for non-file parts.
   maxValueBytes := maxMemory + int64(10<<20)
   if maxValueBytes <= 0 {
      if maxMemory < 0 {
         maxValueBytes = 0
      } else {
         maxValueBytes = math.MaxInt64
      }
   }

   for {
      p, err := r.NextPart()
      if err == io.EOF {
         break
      }
      if err != nil {
         return nil, err
      }

      name := p.FormName()
      if name == "" {
         continue
      }
      filename := p.FileName()

      var b bytes.Buffer

      // 如果有没有文件名,就是普通的 form 提交表单支
      if filename == "" {
         // value, store as string in memory
         n, err := io.CopyN(&b, p, maxValueBytes+1)
         if err != nil && err != io.EOF {
            return nil, err
         }
         maxValueBytes -= n
         if maxValueBytes < 0 {
            return nil, ErrMessageTooLarge
         }
         form.Value[name] = append(form.Value[name], b.String())
         continue
      }

      // 否则就是上传文件
      // file, store in memory or on disk
      fh := &FileHeader{
         Filename: filename,
         Header:   p.Header,
      }
      n, err := io.CopyN(&b, p, maxMemory+1)
      if err != nil && err != io.EOF {
         return nil, err
      }

      // 这里判断读取的内容是否大于给定的最大字节
      if n > maxMemory {
         // too big, write to disk and flush buffer
         file, err := os.CreateTemp("", "multipart-")
         if err != nil {
            return nil, err
         }
         size, err := io.Copy(file, io.MultiReader(&b, p))
         if cerr := file.Close(); err == nil {
            err = cerr
         }
         if err != nil {
            os.Remove(file.Name())
            return nil, err
         }
         fh.tmpfile = file.Name()
         fh.Size = size
      } else {
         fh.content = b.Bytes()
         fh.Size = int64(len(fh.content))
         maxMemory -= n
         maxValueBytes -= n
      }
      form.File[name] = append(form.File[name], fh)
   }

   return form, nil
}
```

* 问题到此就结束了, 答案前面说了, 取决于你的配置和实现方式.

****

* 当文件大于给定的最大字节数时, 是怎么实现复制的功能
* 上面的代码中`io.Copy(file, io.MultiReader(&b, p))`, 我们来查看`p`和`b`的来源
* 首先`b`比较简单,就是从`p`中`copy`出来`maxValueBytes+1`个字节, 所以它是来源于`p`
* 而`p`的来源如下
    * 来源于前面的`multipart.Reader`
    * 而`multipart.Reader`来源于`http.request.Body`
    * `http.request.Body`来源于`http.readTransfer`方法,然后从`http.conn.bufr`读取出来
    * `c.bufr`的来源是如下代码, 实际上还是连接`c`只不过封装了好几层
    
 ```go
c.r = &connReader{conn: c}
c.bufr = newBufioReader(c.r)
c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10) 
```

* 上传文件的请求连接可以认为就是一个`io.Reader`接口, 可以不断从请求中读取出数据.

## More

* 如果每次请求都附加大文件, 就会导致总是解析文件上传,为什么不跳过文件上传,直接解析其它`Body`数据呢?
    * 因为读取`Body`的内容肯定是从上到下,文件可能在最前面,可能在最后面
    * 代码只能一行一行的读取`Body`,如果第一个部分是文件, 并且太大的话只能先写到临时文件夹
    * 读取完这一个部分,才能读取接下来的内容
PS: `Go`中的`Request Body`只能读取一次