作为一个文件服务器,文件就分为大文件和小文件,小文件嘛,好处理,毕竟不大,主要关注的点就是,细碎,需要集中管理,比如将其塞到一个文件中合并存储,当然,这玩意不是我现在关注的点。这里关注的是大文件的操作。

以下大多文字信息均来自:服务器端文件分片合并的思考和实践

大文件的需求

文件上传是个很常见的需求。尽管HTTP是基于TCP上层的协议,但是HTTP协议本身并不适合处理超大的请求体,文件上传有很大的稳定性问题,如果中途断开了,将前功尽弃。为了改善用户体验或者缓解服务器压力,通常会考虑将文件分成小片,将小片一个个上传,如果中途断开了也能从某个失败的小片开始继续上传。

在前端的处理上,对于Web页面,可以采用plupload作为上传组件,该组件支持html5、flash、sl等多种上传方式,因此,可以提供较好的浏览器兼容性。七牛云存储的js-sdk就是基于这个组件开发的。不过本文的重点并不是讨论前端技术,关于前端就到此为止。


文件下载

大文件上传时普遍都采取分片的方式进行上传,那服务器接收的分片也会有一个整合的过程,当然,博客里也提到一些整合的方式介绍,我这里是比较中意其说的不合并的方式,有几个原因:

一、合并占用IO操作,消耗性能和时间;

二、文件分片有利于数据加密;

这是我想的两点,所以,我并没有使得文件合并这个操作出现在我的服务中,我参考了博客中说的一个操作,HTTP流。

为什么非要合并!

再思考下去,如果文件系统无法做到将分片直接连接起来的的话,那么从用户接口层(HTTP)是否能做到呢?试想,通过HTTP的方式提供文件的访问,如果HTTP服务器能够知道这个文件是由多个小文件按何种顺序组成的,那么就可以按照顺序将分片依次放在同一个HTTP流中返回,对用户来说一次请求还是得到一个文件,好像文件是合并好的一样,但实际上文件在文件系统并不存在。

这样做需要单独将分片的顺序维护好,每次都要读出分片的顺序和位置,然后依次一个个写入HTTP流中。但是高层的Web编程框架似乎无法支持这种做法。

代码

这里的代码要修改HTTP库中的http.serveFile函数。

serveContent函数我砍掉了一些操作,比如支持HTTP的Range,针对于实际的业务场景,客户端断点续传,多线程下载那肯定是要支持的,我这里是处于简化,直接砍掉了这个操作。

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
func ServeMyFile(w http.ResponseWriter, r *http.Request, name string) {
dir, baseName := filepath.Split(name)
serveFile(w, r, http.Dir(dir), baseName)
}

func toHTTPError(err error) (msg string, httpStatus int) {
if os.IsNotExist(err) {
return "404 page not found", http.StatusNotFound
}
if os.IsPermission(err) {
return "403 Forbidden", http.StatusForbidden
}
// Default:
return "500 Internal Server Error", http.StatusInternalServerError
}

// name is '/'-separated, not filepath.Separator.
func serveFile(w http.ResponseWriter, r *http.Request, fs http.FileSystem, name string) {
f, err := fs.Open(name)
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}
defer f.Close()

d, err := f.Stat()
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}

buffer := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(buffer)
var size int64

// serveContent will check modification time
sizeFunc := func() (int64, error) { return size, nil }

if d.IsDir() {
//dir := fs.(http.Dir)

dirs, err := f.Readdir(-1)
if err != nil {
http.Error(w, "Error reading directory", http.StatusInternalServerError)
return
}

// a, b, c ..., 保存文件的时候即以字母做顺序标记了,根据个人修改
sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })

for _, ff := range dirs {
if !ff.IsDir() {
// 以Dir为相对路径,接上ff.Name(),也就是[Dir/name]
fi, err := fs.Open(path.Join(name, ff.Name()))
if err != nil {
msg, code := toHTTPError(err)
http.Error(w, msg, code)
return
}

io.Copy(bw, fi)
size += ff.Size()
}
}

serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, bytes.NewReader(buffer.Bytes()))
} else {
size = d.Size()

serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f)
}
}

var unixEpochTime = time.Unix(0, 0)

// isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
func isZeroTime(t time.Time) bool {
return t.IsZero() || t.Equal(unixEpochTime)
}

func setLastModified(w http.ResponseWriter, modtime time.Time) {
if !isZeroTime(modtime) {
w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat))
}
}

func serveContent(w http.ResponseWriter, r *http.Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) {
setLastModified(w, modtime)

size, err := sizeFunc()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

sendSize := size
var sendContent io.Reader = content
if size >= 0 {
w.Header().Set("Accept-Ranges", "bytes")
if w.Header().Get("Content-Encoding") == "" {
w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10))
}
}

w.WriteHeader(http.StatusOK)
if r.Method != "HEAD" {
io.CopyN(w, sendContent, sendSize)
}
}