前面已经讲述了构建服务端和客户端并进行简单的数据交换,本文将实现从客户端上报图片到服务端并保存。

直接对前文的客户端进行改造后如下:

package main

import (
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strings"
    "sync"
    "time"
)

var wc sync.WaitGroup

//SendData sends data to server.
func SendData(c *http.Client, url string, method string, filePath string) {
    defer wc.Done()

    if c == nil {
        log.Fatalln("client is nil")
    }
    if method == "POST" {
        boundary := "ASSDFWDFBFWEFWWDF" //可以自己设定,需要比较复杂的字符串作
        var data []byte
        if _, err := os.Lstat(filePath); err == nil {
            file, _ := os.Open(filePath)
            defer file.Close()

            data, _ = ioutil.ReadAll(file)
        } else {
            log.Fatal("file not exist")
        }

        picData := "--" + boundary + "\n"
        picData = picData + "Content-Disposition: form-data; name=\"userfile\"; filename=" + filePath + "\n"
        picData = picData + "Content-Type: application/octet-stream\n\n"
        picData = picData + string(data) + "\n"
        picData = picData + "--" + boundary + "\n"
        picData = picData + "Content-Disposition: form-data; name=\"text\";filename=\"1.txt\"\n\n"
        picData = picData + string("data=ali") + "\n"
        picData = picData + "--" + boundary + "--"

        req, err := http.NewRequest(method, url, strings.NewReader(picData))
        req.Header.Set("Content-Type", "multipart/form-data; boundary=" + boundary)
        if err == nil {
            if rep, err := c.Do(req); err == nil {
                content, _ := ioutil.ReadAll(rep.Body)
                log.Println("get response: " + string(content))
                rep.Body.Close()
            }
        }
    } else if method == "GET" {
        //TODO get data from server
    }
}

func main() {
    client := &http.Client{
        Timeout: time.Second * 3,
    }
    postImgPath := "1.png"
    method := "POST"
    url := "http://127.0.0.1:8000/postdata"
    wc.Add(1)

    go SendData(client, url, method, postImgPath)

    wc.Wait()
}

在客户端代码中,有几点需要说明一下:

multipart/form-data:在请求头中的Content-Type字段应该设置成multipart/form-data,利用表单的形式上传文件。同时还应该加上boundary的内容,boundary可以自己指定。

boundary:如何大家和本文一样自己构建请求体,需要利用boundary进行数据分隔,分隔的格式有严格的要求,下面给出一个范例。其中name字段是表单中该元素的名词,filename是内容的名称(可以认为是文件名),二者是有区别的;另外具体的内容,如1.png的内容与前一行必须有一个空行(回车);boundary加上字符"--"作为分隔符,而结束的boundary还应该在末尾加上字符"--",

--boundary  //分割符 
Content-Disposition: form-data; name="userfile"; filename="1.png"  
Content-Type: application/octet-stream  

1.png的内容  
--${bound}  
Content-Disposition: form-data; name="text"; filename="username"  

name=Tom
--boundary--

随后再编写server端,代码如下所示

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "time"
)

//DownloadFile download file from client to local.
func DownloadFile(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
        fmt.Println("GET")
        w.Write([]byte(string("hi, get successful")))
    case "POST":
        fmt.Println("POST")
        r.ParseForm() //解析表单
        imgFile, _, err := r.FormFile("userfile")//获取文件内容
        if err != nil {
            log.Fatal(err)
        }
        defer imgFile.Close()

        imgName := ""
        files := r.MultipartForm.File //获取表单中的信息
        for k, v := range files {
            for _, vv := range v {
                fmt.Println(k + ":" + vv.Filename)//获取文件名
                if strings.Index(vv.Filename, ".png") > 0 {
                    imgName = vv.Filename
                }
            }
        }

        saveFile, _ := os.Create(imgName)
        defer saveFile.Close()
        io.Copy(saveFile, imgFile) //保存

        w.Write([]byte("successfully saved"))
    default:
        fmt.Println("default")
    }
}

func main() {
    server := &http.Server{
        Addr:         "127.0.0.1:8000",
        ReadTimeout:  2 * time.Second,
        WriteTimeout: 2 * time.Second,
    }
    mux := http.NewServeMux()
    mux.HandleFunc("/postdata", DownloadFile)
    server.Handler = mux
    server.ListenAndServe()
}

server端主要是响应请求并存储文件,相关注释见代码。理论上可以上传任意格式、任意大小的文件,例如我在本机测试成功上传40M的mp4文件,然而由于网络、传输效率、成功率等考虑,大文件可以分多块上传,大家可以自行探究,后期如果有时间,我将再进行详细讨论。

目前的很多golang框架都能够方便的实现文件的上传下载等功能,本文主要想通过实现http的文件传输了解具体的实现过程和其中的网络知识,而在实际项目中,大家可以基于各类成熟框架进行开发。