golang的http上传文件的实现细节严格遵循了multipart/form-data的RFC1867规范,细节网上已经分析了很多了
本篇只是讨论golang上如何使用官方库实现的框架
客户端上传很简单,利用"mime/multipart"官方库即可完成上传
可以看到这里上传文件时全部加载在内存的bodyBuf字符数组中,所以这里只是上传小文件,大文件这么上传会写爆内存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 func postFile (filename, targetUrl, token string ) error { var ( bodyBuf *bytes.Buffer bodyWriter *multipart.Writer file *os.File err error contentType string client http.Client req *http.Request resp *http.Response respBody []byte ) bodyBuf = &bytes.Buffer{} bodyWriter = multipart.NewWriter(bodyBuf) fileWriter, err := bodyWriter.CreateFormFile("PolkaFile" ,filename) if err != nil { return err } file, err = os.Open(filename) defer file.Close() if err != nil { return err } _, err = io.Copy(fileWriter, file) if err != nil { return err } err = bodyWriter.Close(); if err != nil { return err } contentType = bodyWriter.FormDataContentType() req, err = http.NewRequest("POST" , targetUrl, bodyBuf) if err != nil { return err } req.Header.Set("Content-Type" ,contentType) req.Header.Set("Authorization" ,token) resp, err = client.Do(req) if err != nil { return err } defer resp.Body.Close() respBody, err = ioutil.ReadAll(resp.Body) if err != nil { return err } log .Infof("resp status: %s,resp body: %s" ,resp.Status, string (respBody)) return nil } ``` ------------------------------------------- 服务端的上传也很简单 在golang http框架用mux的HandleFunc方法绑定的回调函数中写入几行代码即可 ```c func (this *proxy) upload(wr http.ResponseWriter, r *http.Request) { file, handle, err := r.FormFile("PolkaFile" ) defer file.Close() if err != nil { log .Errorf("%v" ,err) } return } ``` 这里的handle的Filename字段对应了客户端上传时"PolkaFile" 这个字符串绑定的文件名 这里的file是一个接口 ```c type File interface { io.Reader io.ReaderAt io.Seeker io.Closer } ``` 为什么要这么做呢,因为文件有可能存储在内存里或者是磁盘上 ```c 按照http.Request.FormFile()->http.Request.ParseMultipartForm()->multipart.Reader.ReadForm()这个调用链来看 在multipart.Reader.ReadForm()中有这么一段 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 { file, err := ioutil.TempFile("" , "multipart-" ) if err != nil { return nil, err } defer file.Close() _, err = io.Copy(file, io.MultiReader(&b, p)) if err != nil { os.Remove(file.Name()) return nil, err } fh.tmpfile = file.Name() } else { fh.content = b.Bytes() maxMemory -= n }
数据从p中读出,如果大于最大内存(默认值32MB),那么会用MultiReader重新读取,并且写入到TempFile中
这部分文件会在multipart.Reader.RemoveAll()中被销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func (f *Form) RemoveAll() error { var err error for _, fhs := range f.File { for _, fh := range fhs { if fh.tmpfile != "" { e := os.Remove(fh.tmpfile) if e != nil && err == nil { err = e } } } } return err }
RemoveAll()函数会在http.response.finishRequest()这个函数中被调用,也就是结束请求后被销毁
我们可以通过handle来获取这个结构体,但是无法操作content和tmpfile这两个未导出字段
1 2 3 4 5 6 7 type FileHeader struct { Filename string Header textproto.MIMEHeader content []byte tmpfile string }
只能通过这个File接口,所以数据必须通过io.Copy等方式进行一次复制,这样效率会大大降低
假如是一个[]byte数组,那么这个数组可以直接被发送数据,执行一次复制会加大内存的开销和GC的负担
加入是一个tmp文件,那么这个文件可以直接使用sendfile调用发送出去,执行一次复制再写到其他文件或者写到内存,也会增加内存的开销
要做到零拷贝,就只能通过unsafe来直接操作未导出字段
我写了些测试代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package common import ( "fmt" "net/textproto" ) type FileHeader struct { Filename string Header textproto.MIMEHeader content []byte tmpfile string } func (this *FileHeader) Set() { this.Filename = "123" this.content = []byte{1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 } this.tmpfile = "456" } func (this *FileHeader) Get() { fmt.Println(this.Filename) fmt.Println(this.content) fmt.Println(this.tmpfile) }
这是这个山寨FileHeader,用于进行简单的GetSet
然后是UnSafeMultipart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package main import ( "unsafe" "fmt" "coding.net/tedcy/Polka/common" ) type UnsafeMultipart struct { common.FileHeader } func (this *UnsafeMultipart) GetContent() []byte{ contentPtr := (*[]byte)(unsafe.Pointer(uintptr(unsafe.Pointer(&this.Header)) + uintptr(unsafe.Sizeof(this.Header)))) return *contentPtr } func (this *UnsafeMultipart) GetTmpfileName() string { tmpfileNamePtr := (*string )( unsafe.Pointer(uintptr(unsafe.Pointer(&this.Header)) + uintptr(unsafe.Sizeof(this.Header)) + uintptr(unsafe.Sizeof([]byte(nil))))) return *tmpfileNamePtr } func main() { u := &UnsafeMultipart{} u.Set() u.Get() fmt.Println(u.GetContent()) fmt.Println(u.GetTmpfileName()) }
输出
1 2 3 4 5 123 [1 2 3 4 5 6 7 8 9 10] 456 [1 2 3 4 5 6 7 8 9 10] 456
可以看到,已经取得了我要的未导出字段
unsafe有几个小细节
1 unsafe.Pointer()传入的只能是指针
2 unsafe.Pointer要参与运算只能转换为uintptr
3 uintptr得到的地址只能转换为unsafe.Pointer
4
最终转换的unsafe.Pointer也只能转换成某个类型的指针,例如[]byte, string
5 加一段测试代码
1 2 3 4 fmt.Println(uintptr(unsafe.Pointer(u))) fmt.Println(uintptr(unsafe.Pointer(&u.FileHeader))) fmt.Println(uintptr(unsafe.Pointer(&u.Filename))) fmt.Println(uintptr(unsafe.Pointer(&u.Header)))
输出是
1 2 3 4 859530421504 859530421504 859530421504 859530421520
可以看到u的地址和u封装的FileHeader是同一个地址,和FileHeader内的Filename也是同一个地址
然后Filename虽然是string类型,但是实际占用是固定长度的大小
完毕
补充:
后来发现以上全是考虑太多了,go的http库有两种解析multipart上传的方式,另外一种可以满足需求
利用http
request的MultipartReader()即可,这个函数是和ParseMultipartForm互斥的,无论哪个被调用,另外一个都不能再调用了
ParseMultipartForm是将表单的form全部解析好,而MultipartReader则是将表单解析成单独的Part内容,以供解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 reader, err := r.MultipartReader() if err != nil { ... } for { part, err := reader.NextPart() if err == io.EOF { break } if part.FileName() != "" { //filecontent continue } data, err := ioutil.ReadAll(part) if err != nil { ... } // form data, key = part.FormName(),value = string(data) }
可以看到,解析完全依赖自己的处理逻辑,也就是避免了二次拷贝