转载地址:https://mp.weixin.qq.com/s/OHzXxfcBaf5RNT4dA38LCQ

1. Form简介

Form(中文译为表单)[1],是HTML标记语言中的重要语法元素。一个Form不仅包含正常的文本内容、标记等,还包含被称为控件的特殊元素。用户通常通过修改控件(比如:输入文本、选择菜单项等)来“完成”表单,然后将表单数据以HTTP Get或Post请求的形式提交(submit)给Web服务器。

很多初学者总是混淆HTML和HTTP。其实,http通常作为html传输的承载体,打个比方,html就像乘客,http就像出租车,将乘客从一个地方运输到另外一个地方。但显然http这辆出租车可不仅仅只拉html这一个乘客,很多格式均可作为http这辆出租车的乘客,比如json(over http)、xml(over http)。

在一个HTML文档中,一个表单的标准格式如下:

<form action="http://localhost:8080/repositories" method="get"><input type="text" name="language" value="go" /><input type="text" name="since" value="monthly" /><input type="submit" />
</form>

这样的一个Form被加载到浏览器中后会呈现为一个表单的样式,当在两个文本框中分别输入文本(或以默认的文本作为输入)后,点击“提交(submit)”,浏览器会向http://localhost:8080发出一个HTTP请求,由于Form的method属性为get,因此该HTTP请求会将表单的输入文本作为查询字符串参数(Query String Parameter,在这里即是?language=go&since=monthly)。服务器端处理完该请求后,会返回一个HTTP承载的应答,该应答被浏览器接收后会按特定样式呈现在浏览器窗口中。上述这个过程可以用总结为下面这幅示意图:

Form中的method也可以使用post,就像下面这样:

<form action="http://localhost:8080/repositories" method="post"><input type="text" name="language" value="go" /><input type="text" name="since" value="monthly" /><input type="submit" />
</form>

改为post的Form表单在点击提交后发出的http请求与method=get时的请求有何不同呢?不同之处就在于在method=post的情况下,表单的参数不会再以查询字符串参数的形式放在请求的URL中,而是会被写入HTTP的BODY中。我们也将这一过程用一幅示意图的形式总结一下:

由于表单参数被放置在HTTP Body中传输(body中的数据为:language=go&since=monthly),因此在该HTTP请求的headers中我们会发现新增一个header字段:Content-Type,在这里例子中,它的值为application/x-www-form-urlencoded。我们可以在Form中使用enctype属性改变Form传输数据的内容编码类型,该属性的默认值就是application/x-www-form-urlencoded(即key1=value1&key2=value2&...的形式)。enctype的其它可选值还包括:

  • text/plain

  • multipart/form-data

采用method=get的Form的表单参数以查询字符串参数的形式放入http请求,这使得其应用场景相对局限,比如:

  • 当参数值很多,参数值很长时,可能会超出URL最大长度限制;

  • 传递敏感数据时,参数值以明文放在HTTP请求头是不安全的;

  • 无法胜任传递二进制数据(比如一个文件内容)的情形。

因此,在面对上述这些情形时,method=post的表单更有优势。当enctype为不同值时,method=post的表单在http Body中传输的数据形式如下图:

我们看到:enctype=application/x-www-urlencoded时,Body中的数据呈现为key1=value1&key2=value2&...的形式,好似URL的查询字符串参数的组合呈现形式;当enctype=text/plain时,这种编码格式也称为raw,即将数据内容原封不动的放入Body中传输,保持数据的原先的编码方式(通常为utf-8);而当enctype=multipart/form-data时,HTTP Body中的数据以多段(part)的形式呈现,段与段之间使用指定的随机字符串分隔,该随机字符串也会随着HTTP Post请求一并传给服务端(放在Header中的Content-Type的值中,与multipart/form-data使用分号相隔),如:

Content-Type: multipart/form-data; boundary=--------------------------399501358433894470769897

我们来看一个稍微复杂些的enctype=multipart/form-data的例子的示意图:

我们用Postman模拟了一个包含5个分段(part)的Post请求,其中包含两个文本分段(text)和三个文件分段,并且这三个文件是不同格式的文件,分别是txt,png和json。针对文件分段,Postman使用每个分段中的Content-Type来指明这个分段的数据内容类型。当服务端接收到这些数据时,根据分段Content-Type的指示,便可以有针对性的对分段数据进行解析了。文件分段的默认Content-Type为text/plain;对于无法识别的文件类型(比如:没有扩展名),文件分段的Content-Type通常会设置为application/octet-stream

通过Form上传文件是RFC1867规范[2]赋予html的一种能力,并且该能力已被证明非常有用,并被广泛使用,甚至我们可以直接将multipart/form-data作为HTTP Post body的一种数据承载协议在两个端之间传输文件数据。

2. 支持以multipart/form-data格式上传文件的Go服务器

http.Request提供了ParseMultipartForm的方法对以multipart/form-data格式传输的数据进行解析,解析即是将数据映射为Request结构的MultipartForm字段的过程:

// $GOROOT/src/net/http/request.gotype Request struct {... ...// MultipartForm is the parsed multipart form, including file uploads.// This field is only available after ParseMultipartForm is called.// The HTTP client ignores MultipartForm and uses Body instead.MultipartForm *multipart.Form... ...
}

multipart.Form代表了一个解析后的multipart/form-data的Body,其结构如下:

// $GOROOT/src/mime/multipart/formdata.go// Form is a parsed multipart form.
// Its File parts are stored either in memory or on disk,
// and are accessible via the *FileHeader's Open method.
// Its Value parts are stored as strings.
// Both are keyed by field name.
type Form struct {Value map[string][]stringFile  map[string][]*FileHeader
}

我们看到这个Form结构由两个map组成,一个map中存放了所有的value part(就像前面的name、age),另外一个map存放了所有的file part(就像前面的part1.txt、part2.png和part3.json)。value part集合没什么可说的,map的key就是每个值分段中的"name";我们的重点在file part上。每个file part对应一组FileHeader,FileHeader的结构如下:

// $GOROOT/src/mime/multipart/formdata.go
type FileHeader struct {Filename stringHeader   textproto.MIMEHeaderSize     int64content []bytetmpfile string
}

每个file part的FileHeader包含五个字段:

  • Filename - 上传文件的原始文件名

  • Size - 上传文件的大小(单位:字节)

  • content - 内存中存储的上传文件的(部分或全部)数据内容

  • tmpfile - 在服务器本地的临时文件中存储的部分上传文件的数据内容(如果上传的文件大小大于传给ParseMultipartForm的参数maxMemory,剩余部分存储在临时文件中)

  • Header - file part的header内容,它亦是一个map,其结构如下:

// $GOROOT/src/net/textproto/header.go// A MIMEHeader represents a MIME-style header mapping
// keys to sets of values.
type MIMEHeader map[string][]string

我们可以将ParseMultipartForm方法实现的数据映射过程表述为下面这张示意图,这样看起来更为直观:

有了上述对通过multipart/form-data格式上传文件的原理的拆解,我们就可以很容易地利用Go http包实现一个简单的支持以multipart/form-data格式上传文件的Go服务器:

// github.com/bigwhite/experiments/multipart-formdata/server/file_server1.go
package mainimport ("fmt""io""net/http""os"
)const uploadPath = "./upload"func handleUploadFile(w http.ResponseWriter, r *http.Request) {r.ParseMultipartForm(100)mForm := r.MultipartFormfor k, _ := range mForm.File {// k is the key of file partfile, fileHeader, err := r.FormFile(k)if err != nil {fmt.Println("inovke FormFile error:", err)return}defer file.Close()fmt.Printf("the uploaded file: name[%s], size[%d], header[%#v]\n",fileHeader.Filename, fileHeader.Size, fileHeader.Header)// store uploaded file into local pathlocalFileName := uploadPath + "/" + fileHeader.Filenameout, err := os.Create(localFileName)if err != nil {fmt.Printf("failed to open the file %s for writing", localFileName)return}defer out.Close()_, err = io.Copy(out, file)if err != nil {fmt.Printf("copy file err:%s\n", err)return}fmt.Printf("file %s uploaded ok\n", fileHeader.Filename)}
}func main() {http.HandleFunc("/upload", handleUploadFile)http.ListenAndServe(":8080", nil)
}

我们可以用Postman或下面curl命令向上述文件服务器同时上传两个文件part1.txt和part3.json:

curl --location --request POST ':8080/upload' \
--form 'name="tony bai"' \
--form 'age="23"' \
--form 'file1=@"/your_local_path/part1.txt"' \
--form 'file3=@"/your_local_path/part3.json"'

文件上传服务器的运行输出日志如下:

$go run file_server1.go
the uploaded file: name[part3.json], size[130], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file3\"; filename=\"part3.json\""}, "Content-Type":[]string{"application/json"}}]
file part3.json uploaded ok
the uploaded file: name[part1.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"part1.txt\""}, "Content-Type":[]string{"text/plain"}}]
file part1.txt uploaded ok

之后我们可以看到:文件上传服务器成功地将接收到的part1.txt和part3.json存储到了当前路径下的upload目录中了!

3. 支持以multipart/form-data格式上传文件的Go客户端

前面进行文件上传的客户端要么是浏览器,要么是Postman,要么是curl,如果我们自己构要造一个支持以multipart/form-data格式上传文件的客户端,应该如何做呢?我们需要按照multipart/form-data的格式构造HTTP请求的包体(Body),还好通过Go标准库提供的mime/multipart包,我们可以很容易地构建出满足要求的包体:

// github.com/bigwhite/experiments/multipart-formdata/client/client1.go... ...
var (filePath stringaddr     string
)func init() {flag.StringVar(&filePath, "file", "", "the file to upload")flag.StringVar(&addr, "addr", "localhost:8080", "the addr of file server")flag.Parse()
}func main() {if filePath == "" {fmt.Println("file must not be empty")return}err := doUpload(addr, filePath)if err != nil {fmt.Printf("upload file [%s] error: %s", filePath, err)return}fmt.Printf("upload file [%s] ok\n", filePath)
}func createReqBody(filePath string) (string, io.Reader, error) {var err errorbuf := new(bytes.Buffer)bw := multipart.NewWriter(buf) // body writerf, err := os.Open(filePath)if err != nil {return "", nil, err}defer f.Close()// text part1p1w, _ := bw.CreateFormField("name")p1w.Write([]byte("Tony Bai"))// text part2p2w, _ := bw.CreateFormField("age")p2w.Write([]byte("15"))// file part1_, fileName := filepath.Split(filePath)fw1, _ := bw.CreateFormFile("file1", fileName)io.Copy(fw1, f)bw.Close() //write the tail boundryreturn bw.FormDataContentType(), buf, nil
}func doUpload(addr, filePath string) error {// create bodycontType, reader, err := createReqBody(filePath)if err != nil {return err}url := fmt.Sprintf("http://%s/upload", addr)req, err := http.NewRequest("POST", url, reader)// add headersreq.Header.Add("Content-Type", contType)client := &http.Client{}resp, err := client.Do(req)if err != nil {fmt.Println("request send error:", err)return err}resp.Body.Close()return nil
}

显然上面这个client端的代码的核心是createReqBody函数:

  • 该client在body中创建了三个分段,前两个分段仅仅是我为了演示如何创建text part而故意加入的,真正的上传文件客户端是不需要创建这两个分段(part)的;

  • createReqBody使用bytes.Buffer作为http body的临时存储;

  • 构建完body内容后,不要忘记调用multipart.Writer的Close方法以写入结尾的boundary标记。

我们使用这个客户端向前面的支持以multipart/form-data格式上传文件的服务器上传一个文件:

// 客户端
$go run client1.go -file hello.txt
upload file [hello.txt] ok// 服务端
$go run file_server1.gohttp request: http.Request{Method:"POST", URL:(*url.URL)(0xc00016e100), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Length":[]string{"492"}, "Content-Type":[]string{"multipart/form-data; boundary=b55090594eaa1aaac1abad1d89a77ae689130d79d6f66af82590036bd8ba"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000146380), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:492, TransferEncoding:[]string(nil), Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc000110d50), Trailer:http.Header(nil), RemoteAddr:"[::1]:58569", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc0001463c0)}
the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"application/octet-stream"}}]
file hello.txt uploaded ok

我们看到hello.txt这个文本文件被成功上传!

4. 自定义file分段中的header

从上面file_server1的输出来看,client1这个客户端上传文件时在file分段(part)中设置的Content-Type为默认的application/octet-stream。有时候,服务端可能会需要根据这个Content-Type做分类处理,需要客户端给出准确的值。上面的client1实现中,我们使用了multipart.Writer.CreateFormFile这个方法来创建file part:

// file part1
_, fileName := filepath.Split(filePath)
fw1, _ := bw.CreateFormFile("file1", fileName)
io.Copy(fw1, f)

下面是标准库中CreateFormFile方法的实现代码:

// $GOROOT/mime/multipart/writer.go
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {h := make(textproto.MIMEHeader)h.Set("Content-Disposition",fmt.Sprintf(`form-data; name="%s"; filename="%s"`,escapeQuotes(fieldname), escapeQuotes(filename)))h.Set("Content-Type", "application/octet-stream")return w.CreatePart(h)
}

我们看到无论待上传的文件是什么类型,CreateFormFile均将Content-Type置为application/octet-stream这一默认值。如果我们要自定义file part中Header字段Content-Type的值,我们就不能直接使用CreateFormFile,不过我们可以参考其实现:

// github.com/bigwhite/experiments/multipart-formdata/client/client2.govar quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")func escapeQuotes(s string) string {return quoteEscaper.Replace(s)
}func createReqBody(filePath string) (string, io.Reader, error) {var err errorbuf := new(bytes.Buffer)bw := multipart.NewWriter(buf) // body writerf, err := os.Open(filePath)if err != nil {return "", nil, err}defer f.Close()// text part1p1w, _ := bw.CreateFormField("name")p1w.Write([]byte("Tony Bai"))// text part2p2w, _ := bw.CreateFormField("age")p2w.Write([]byte("15"))// file part1_, fileName := filepath.Split(filePath)h := make(textproto.MIMEHeader)h.Set("Content-Disposition",fmt.Sprintf(`form-data; name="%s"; filename="%s"`,escapeQuotes("file1"), escapeQuotes(fileName)))h.Set("Content-Type", "text/plain")fw1, _ := bw.CreatePart(h)io.Copy(fw1, f)bw.Close() //write the tail boundryreturn bw.FormDataContentType(), buf, nil
}

我们通过textproto.MIMEHeader实例来自定义file part的header部分,然后基于该实例调用CreatePart创建file part,之后将hello.txt的文件内容写到该part的header后面。

我们运行client2来上传hello.txt文件,在file_server侧,我们就能看到如下日志:

the uploaded file: name[hello.txt], size[15], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"hello.txt\""}, "Content-Type":[]string{"text/plain"}}]
file hello.txt uploaded ok

我们看到file part的Content-Type的值已经变为我们设定的text/plain了。

5. 解决上传大文件的问题

在上面的客户端中存在一个问题,那就是我们在构建http body的时候,使用了一个bytes.Buffer加载了待上传文件的所有内容,这样一来,如果待上传的文件很大的话,内存空间消耗势必过大。那么如何将每次上传内存文件时对内存的使用限制在一个适当的范围,或者说上传文件所消耗的内存空间不因待传文件的变大而变大呢?我们来看下面的这个解决方案:

// github.com/bigwhite/experiments/multipart-formdata/client/client3.go
... ...
func createReqBody(filePath string) (string, io.Reader, error) {var err errorpr, pw := io.Pipe()bw := multipart.NewWriter(pw) // body writerf, err := os.Open(filePath)if err != nil {return "", nil, err}go func() {defer f.Close()// text part1p1w, _ := bw.CreateFormField("name")p1w.Write([]byte("Tony Bai"))// text part2p2w, _ := bw.CreateFormField("age")p2w.Write([]byte("15"))// file part1_, fileName := filepath.Split(filePath)h := make(textproto.MIMEHeader)h.Set("Content-Disposition",fmt.Sprintf(`form-data; name="%s"; filename="%s"`,escapeQuotes("file1"), escapeQuotes(fileName)))h.Set("Content-Type", "application/pdf")fw1, _ := bw.CreatePart(h)cnt, _ := io.Copy(fw1, f)log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)bw.Close() //write the tail boundrypw.Close()}()return bw.FormDataContentType(), pr, nil
}func doUpload(addr, filePath string) error {// create bodycontType, reader, err := createReqBody(filePath)if err != nil {return err}log.Printf("createReqBody ok\n")url := fmt.Sprintf("http://%s/upload", addr)req, err := http.NewRequest("POST", url, reader)//add headersreq.Header.Add("Content-Type", contType)client := &http.Client{}log.Printf("upload %s...\n", filePath)resp, err := client.Do(req)if err != nil {fmt.Println("request send error:", err)return err}resp.Body.Close()log.Printf("upload %s ok\n", filePath)return nil
}

在这个方案中,我们通过io.Pipe函数创建了一个读写管道,其写端作为io.Writer实例传给multipart.NewWriter,读端返回给调用者,用于构建http request时使用。io.Pipe基于channel实现,其内部不维护任何内存缓存:

// $GOROOT/src/io/pipe.go
func Pipe() (*PipeReader, *PipeWriter) {p := &pipe{wrCh: make(chan []byte),rdCh: make(chan int),done: make(chan struct{}),}return &PipeReader{p}, &PipeWriter{p}
}

通过Pipe返回的读端读取管道中数据时,如果尚未有数据写入管道,那么读端会像读取channel那样阻塞在那里。由于http request在被发送时(client.Do(req))才会真正基于构建req时传入的reader对Body数据进行读取,因此client会阻塞在对管道的read上。显然我们不能将读写两端的操作放在一个goroutine中,那样会因所有goroutine都挂起而导致panic。在上面的client3.go代码中,函数createReqBody内部创建了一个新goroutine,将真正构建multipart/form-data body的工作放在了新goroutine中。新goroutine最终会将待上传文件的数据通过管道写端写入管道:

cnt, _ := io.Copy(fw1, f)

而这些数据也会被client读取并通过网络连接传输出去。io.Copy的实现如下:

// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader) (written int64, err error) {return copyBuffer(dst, src, nil)
}

io.copyBuffer内部维护了一个默认32k的小buffer,它每次从src尝试最大读取32k的数据,并写入到dst中,直到读完为止。这样无论待上传的文件有多大,我们实际上每次上传所分配的内存仅有32k。

下面就是我们用client3.go上传一个大小为252M的文件的日志:

$go run client3.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
2021/01/10 12:56:45 createReqBody ok
2021/01/10 12:56:45 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
2021/01/10 12:56:46 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
2021/01/10 12:56:46 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok$go run file_server1.go
http request: http.Request{Method:"POST", URL:(*url.URL)(0xc000078200), Proto:"HTTP/1.1", ProtoMajor:1, ProtoMinor:1, Header:http.Header{"Accept-Encoding":[]string{"gzip"}, "Content-Type":[]string{"multipart/form-data; boundary=4470ba3867218f1130878713da88b5bd79f33dfbed65566e4fd76a1ae58d"}, "User-Agent":[]string{"Go-http-client/1.1"}}, Body:(*http.body)(0xc000026240), GetBody:(func() (io.ReadCloser, error))(nil), ContentLength:-1, TransferEncoding:[]string{"chunked"}, Close:false, Host:"localhost:8080", Form:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, PostForm:url.Values{"age":[]string{"15"}, "name":[]string{"Tony Bai"}}, MultipartForm:(*multipart.Form)(0xc0000122a0), Trailer:http.Header(nil), RemoteAddr:"[::1]:54899", RequestURI:"/upload", TLS:(*tls.ConnectionState)(nil), Cancel:(<-chan struct {})(nil), Response:(*http.Response)(nil), ctx:(*context.cancelCtx)(0xc000026280)}
the uploaded file: name[ICME-2019-Tutorial-final.pdf], size[264517032], header[textproto.MIMEHeader{"Content-Disposition":[]string{"form-data; name=\"file1\"; filename=\"ICME-2019-Tutorial-final.pdf\""}, "Content-Type":[]string{"application/pdf"}}]
file ICME-2019-Tutorial-final.pdf uploaded ok$ls -l upload
-rw-r--r--  1 tonybai  staff  264517032  1 14 12:56 ICME-2019-Tutorial-final.pdf

如果你觉得32k仍然很大,每次上传要使用更小的buffer,你可以用io.CopyBuffer替代io.Copy:

// github.com/bigwhite/experiments/multipart-formdata/client/client4.gofunc createReqBody(filePath string) (string, io.Reader, error) {var err errorpr, pw := io.Pipe()bw := multipart.NewWriter(pw) // body writerf, err := os.Open(filePath)if err != nil {return "", nil, err}go func() {defer f.Close()// text part1p1w, _ := bw.CreateFormField("name")p1w.Write([]byte("Tony Bai"))// text part2p2w, _ := bw.CreateFormField("age")p2w.Write([]byte("15"))// file part1_, fileName := filepath.Split(filePath)h := make(textproto.MIMEHeader)h.Set("Content-Disposition",fmt.Sprintf(`form-data; name="%s"; filename="%s"`,escapeQuotes("file1"), escapeQuotes(fileName)))h.Set("Content-Type", "application/pdf")fw1, _ := bw.CreatePart(h)var buf = make([]byte, 1024)cnt, _ := io.CopyBuffer(fw1, f, buf)log.Printf("copy %d bytes from file %s in total\n", cnt, fileName)bw.Close() //write the tail boundrypw.Close()}()return bw.FormDataContentType(), pr, nil
}

运行这个client4:

$go run client4.go -file /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf
2021/01/10 13:39:06 createReqBody ok
2021/01/10 13:39:06 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf...
2021/01/10 13:39:09 copy 264517032 bytes from file ICME-2019-Tutorial-final.pdf in total
2021/01/10 13:39:09 upload /Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf ok
upload file [/Users/tonybai/Downloads/ICME-2019-Tutorial-final.pdf] ok

你会看到虽然上传成功了,但由于每次read仅能读1k数据,对于大文件来说,其上传的时间消耗增加了不少。

Go 如何利用multipart/form-data实现文件的上传与下载相关推荐

  1. 利用WebService实现远程服务器文件的上传和下载

    有时候我们通常需要把上传的图片或其他文件放在其他服务器上,以便和网站服务器分开,这时候ASP.NET的WebService就派上大用场 了.我们可以在文件服务器上运行一个WebService,提供上传 ...

  2. 利用 WebService实现远程服务器文件的上传和下载

    有时候我们通常需要把上传的图片或其他文件放在其他服务器上,以便和网站服务器分开,这时候ASP.NET的WebService就派上大用场了.我们可以在文件服务器上运行一个WebService,提供上传和 ...

  3. java图片上传下载_java实现文件的上传和下载

    1. servlet 如何实现文件的上传和下载? 1.1上传文件 参考自:http://blog.csdn.net/hzc543806053/article/details/7524491 通过前台选 ...

  4. SpringMVC实现文件的上传和下载

    SpringMVC实现文件的上传和下载http://www.bieryun.com/1120.html 前些天一位江苏经贸的学弟跟我留言问了我这样一个问题:"用什么技术来实现一般网页上文件的 ...

  5. Akka实战:HTTP大文件断点上传、下载,秒传

    2019独角兽企业重金招聘Python工程师标准>>> 访问:https://github.com/yangbajing/scala-applications/tree/master ...

  6. 初学Java Web(7)——文件的上传和下载

    文件上传 文件上传前的准备 在表单中必须有一个上传的控件 <input type="file" name="testImg"/> 因为 GET 方式 ...

  7. SpringMVC实现文件的上传与下载

    文件的上传与下载可以说是工作中经常使用的功能,现在整理一下,希望能够给大家一个参考.这里以 Maven 的形式来创建项目,相关的配置文件会把主要的内容列出来,其他头文件信息不再一一全部的列出.最后会把 ...

  8. 文件的上传和下载---学习笔记

    文件上传原理 在TCP/IP中,最早出现的文件上传机制是FTP.它是将文件由客户端发送到服务器的标准机制. 但是JSP编程中不能使用FTP方法来上传文件,这是由JSP运行机制所决定的. JSP中上传文 ...

  9. jsp+servlet实现文件的上传和下载

    实现文件的上传和下载首先需要理解几个知识,这样才可以很好的完成文件的上传和下载: (1):上传文件是上传到服务器上,而保存到数据库是文件名 (2):上传文件是以文件转换为二进制流的形式上传的 (3): ...

  10. SpringMVC与JSON传值,取值,使用SpringMVC实现文件的上传与下载,SpringMVC拦截器

    一. JSON 1.1 什么是JSON 在实际开发中,通常需要和别的系统交换数据,数据交换的格式通常有XML和JSON等: JSON(JavaScript Object Notation:JavaSc ...

最新文章

  1. 玩转高性能超猛防火墙nf-HiPAC
  2. java中用于选择按钮的语句_java程序员考试套题1
  3. 周一 周五 记录_6.12日独家提示买入的 民丰特纸,属于突破年线的股票当天,所以我们买入后小幅盈利 于今天周一6月月15日冲高逢高获利卖出...
  4. 面试精讲之面试考点及大厂真题 - 分布式专栏 20 降级组件Hystrix的功能特性
  5. 改善CSS的10种最佳做法,帮助你从样式中获得最大的收益
  6. 身份证过期了银行卡还能用吗?
  7. JS 中引号多级嵌套
  8. asp.net 路径
  9. WDS+MDT网络部署操作系统
  10. List转JSON格式方法
  11. python淘宝cookies抢购_Python爬虫利用cookie抓取淘宝商品比价
  12. Introducing Heka
  13. 常用的评论/帖子/文章排序算法四(牛顿冷却定律)
  14. 校园网络设备巡检的准备工作
  15. Python可视化打包神器,绝了!
  16. ISO 8601时间格式时间创建
  17. 小说作者推荐:休屠城合集
  18. KeyError(‘metric mAP is not supported‘.format(metric))
  19. 路由器 dhcp服务器的作用,你可知道在路由器中DHCP是什么吗
  20. Google Payments?

热门文章

  1. php gif 切成一帧,GIF动画帧提取器 如何截取gif的每一帧图片
  2. java异常应用_Java异常处理机制 —— 深入理解与开发应用
  3. PostgreSQL统计信息的几个重要视图
  4. web端功能自动化定位元素(暂不更新)
  5. Nodejs事件循环
  6. Centos7 下定义MariaDB源Yum安装最新版本的MariaDB
  7. 结对开发——电梯调度问题需求分析
  8. 使用Powershell远程管理Windows Server(WinRM)
  9. Linux下TCP/IP优化
  10. Delphi使用server组件控制excel的一点经验