用Nodejs实现文件服务器(一)

来源:互联网 发布:安禄山 杨贵妃 知乎 编辑:程序博客网 时间:2024/06/05 01:03

服务器中,用户的个人数据包含用户名、密码甚至好友关系等等,这些数据直接储存在数据库中即可。但有时,用户数据还包括图片、视频甚至文件,此时应该使用一个单独的服务器用于用户文件的存储与发送。当然,对于web,文件服务器亦用于储存网页文件。

这份服务器代码仅实现了一个简单的文件服务器,接收来自API服务器的请求,储存或发送用户的照片。该服务器没有考虑当存在大量请求时的性能问题,本身对于项目来说亦功能不完善。这两个问题将在后续中一一解决。

该文件服务器使用nodejs实现。若不了解甚至没使用过nodejs可以前去http://nodejs.cn/下载,语法与javascript类似。(博主本身也仅初步接触nodejs,内容中如有不对欢迎 指正)

首先应用express模块与fs模块,这是处理url请求与文件读写必不可少的一部分。如果对这两个模块的使用不熟悉请自行查阅官方文档或一些教程。

var express = require('express');var fs = require('fs');var app = express();
该文件服务器逻辑如下,对于uri为/upload的post请求返回一个上传的id;对于uri为/upload/:upload_id的put请求接收上传文件并命名存储;对于uri未/upload/:upload_id的get请求认为该id对应的上传操作已完成,返回这个id对应的上传文件的文件名,该id失效;对于uri为/download/:filename的get请求返回对应的文件。

对于id的合法性判断,可以使用id为键创建数组。此处id初始值为0,每生效一个id就讲id值加1。而对于id对应的文件名,以id为键存在另一个数组中。

var upload_ids = [];var next_id = 0;var file_names = [];app.post('/upload', function (req, res) {    upload_ids[next_id] = true;    res.set("upload_id", (next_id++).toString());    res.send();});
app.get('/upload/:upload_id', function (req, res) {    if (upload_ids[req.params.upload_id] == true) {        fs.stat(file_names[req.params.upload_id], function(err, states) {            res.set('origin_size', states.size.toString());            res.set('origin_url', file_names[req.params.upload_id]);            upload_ids[req.params.upload_id] = false;            res.send();        });    } else res.send();});
结束上传时,返回文件的大小和文件名。此处暂时将两个参数设置在response的报文中,其实应该生成一个json文件返回,将在后续处理。

很显然这是个很“粗暴”的做法。一方面,将id单纯的设为数字的累加,当APP运行较长时间,历史上传次数很多时,这个数字很可能变成一个过大的数字,也许可以考虑用当前时间加标记的方法作为id避免这种问题;另一方面,当id失效时两个数组中的对应项其实就不再有使用意义,然而因为nodejs的数组不提供删除操作这项数据依然储存在内存中,同样当历史上传次数极多时,将会造成很大的浪费。后续将提出整改方案。

接下来是文件上传部分,代码如下。

app.put('/upload/:upload_id', function (req, res) {    if (upload_ids[parseInt(req.params.upload_id)] == true) {        var filename = req.params.upload_id+Date.now();        file_names[req.params.upload_id] = filename;        var filepath = "./upload/"+filename;        req.pipe(fs.createWriteStream(filepath));
    }    res.send();});
代码较短,逻辑方面没啥好说的,文件保存在nodejs文件的父目录的upload目录中。需要注意的是关于数据传输是使用了pipe和stream,而不是常见的

req.on("data", function(postDataChunk) {    putData += postDataChunk;});
数据较小时,使用这种方法当然没问题。但对于文件,一般往往以MB为单位,putData这一nodejs中的临时变量根本无法容纳如此庞大的数据量,使用这种方法只会获得残缺的文件。那或许有人问,直接把putData用append添加到文件中不行吗?答案是,不行。还是文件大小问题,对于以MB为单位的数据,是用多个http数据包发送的。即便是博主本机测试,也存在数据重发冗余、乱序、数据出错等网络问题(若缺乏计算机网络知识请自行前去了解)。除非你顺便打算熟悉熟悉计算机网络,自行实现一个与TCP协议有相同功能的程序,否则不要考虑去直接操作这些原始的http的数据,nodejs的pipe与stream可以帮助我们完成对冗余数据包的丢弃以及对数据的排序。
文件的下载,代码如下

app.get('/download/:filename', function (req, res) {    var filepath = "./upload/"+req.params.filename;    fs.exists(filepath, function(exist) {        if (!exist) {            res.send(req.params.filename+" is not found");        } else {            fs.createReadStream("./upload/"+req.params.filename).pipe(res);        }    });});
逻辑方面同样没啥好说的,就是注意使用管道流的方式发送文件。

最后,关于服务器的启动

var server = app.listen(8080, function () {    console.log("listen at 8080");});
这样,一份支持文件上传下载的服务器代码也算是初步完成了。

下面考虑一些应该添加的功能。上传与下载文件时,有时会中途被中断,可能是APP临时关闭,可能是网络信号不好,可能是一些其他原因。当恢复操作时,我们显然不希望一切从头再来。这里就需要添加一下对于断点的上传和下载。

对于断点上传,客户端请求上传时http报头包含offset参数,若非0则表示是继续上传文件,而offset的值由客户端先向服务器发送请求,服务器返回目前文件的大小。对于断点下载,客户端请求下载时http报头包含range参数,服务器根据range值返回对应部分内容。

添加这些功能后,完整代码如下

var express = require('express');var fs = require('fs');var app = express();var upload_ids = [];var next_id = 0;var file_names = [];app.post('/upload', function (req, res) {    upload_ids[next_id] = true;    res.set("upload_id", (next_id++).toString());    res.send();});app.put('/upload/:upload_id', function (req, res) {    if (upload_ids[parseInt(req.params.upload_id)] == true) {        var filename = req.params.upload_id+Date.now();        if (parseInt(req.headers['offset']) == 0) file_names[req.params.upload_id] = filename;        else filename = file_names[req.params.upload_id];        var filepath = "./upload/"+filename;        if (parseInt(req.headers['offset']) == 0) req.pipe(fs.createWriteStream(filepath));        else req.pipe(fs.createWriteStream(filepath, {'flags': 'a'}));    }    res.send();});app.get('/upload/:upload_id', function (req, res) {    if (upload_ids[req.params.upload_id] == true) {        if (req.headers['content-length'] != undefined) {            fs.stat(file_names[req.params.upload_id], function(err, states) {                res.set('offset', states.size.toString());                res.send();            });        } else {            fs.stat(file_names[req.params.upload_id], function(err, states) {                res.set('origin_size', states.size.toString());                res.set('origin_url', file_names[req.params.upload_id]);                upload_ids[req.params.upload_id] = false;                res.send();            });        }    } else res.send();});app.get('/download/:filename', function (req, res) {    var filepath = "./upload/"+req.params.filename;    fs.exists(filepath, function(exist) {        if (!exist) {            res.send(req.params.filename+" is not found");        } else {            if (req.headers['range'] == undefined) {                fs.createReadStream("./upload/"+req.params.filename).pipe(res);            } else {                var arg = req.headers['range'].split(",");                fs.createReadStream(filepath, {start: parseInt(arg[0]), end: parseInt(arg[1])}).pipe(res);            }        }    });});var server = app.listen(8080, function () {    console.log("listen at 8080");});
计划是打算做一款图片相关的APP,所以文件均是图片。实际上还需要加入处理算法生成一份压缩图片,将压缩图片与原图关系保存并将两份图片一并存储。

这个坑,待我日后有空再填。。。。。。




0 0
原创粉丝点击