从零开始学习单片机_从零开始: nodejs 搭建文件索引服务器(Part 1)

在考虑要用nodejs搭建文件索引服务器来替代我原来那个笨重的Apache服务器(采用省心的bitnami lampstack搭建,然后配了背景图片和图标)之前,说实话我连javascript是什么都不清楚。所以,从语言上讲,确实是从零开始。

但另一方面,我大致知道“菜鸟教程”上有各种语言、环境的入门教程,我还学过C语言,用C语言写过五子棋(课程大作业),用C语言写过单片机外设的驱动,用verilog写过硬件逻辑,用Matlab写过简单的数字信号处理程序、语音合成、图像分析等。没错,都是电子信息工程的必修课程。这些任务大多数需要边做任务边学习(复习),特别是Matlab作业,需要在完全不知道相关概念的情况下自行查阅网上资料现学现用,因此极大锻炼了我编程处理问题的能力,同时也让我有勇气在完全不知道javascript和nodejs特性的情况下、仅凭网上“用nodejs搭建简单的文件服务器”的帖子就敢开始这个项目。

废话就说到这,下面我们开始。

0 下载nodejs

我手头的环境是wsl1, 这年头不会一点linux的估计也不会看这篇文档的吧。相较于windows,将服务器部署在linux显然是更为方便的,但在轻薄本上测试。。。好吧,wsl上。下载安装包我们选择官网http://nodejs.cn/download/,选择linux x86_64下载即可。下好后解压,运行bin/node即可。当然,也可以选择用aptyumpacman安装。打开node之后,就可以看到node解释终端,和python解释终端别无二致。

1 用nodejs搭建简单的文件服务器

就像网上的帖子,在这里,你只需要复制粘贴代码,初步感受一下简单的nodejs服务器是什么样的。

代码:

var http = require('http');
http.createServer(function (request, response) {
 response.writeHead(200, {'Content-Type': 'text/plain'});
 response.end('Hello Worldn');
}).listen(8888);
console.log('Server running at http://127.0.0.1:8888/');

好了,现在将其保存为“hello.js”, 然后用nodejs运行它: node hello.js, 打开浏览器,输入localhost:8888, 你就可以看见“Hello World”的字样了。所以,挺神奇的吧,原来要好大的lampstack才能实现的东西,原来在nodejs里几行代码就能初步实现。而且,代码是可见的,这意味着DIY起来将会很方便,也不需要看很多特定的文档,就像当初手动配置LAMP环境一样,除了安装linux, 没有一点是容易的。

下面,我们来逐句分析一下代码:

var http = require('http'); //载入http模块,具体查阅 nodejs 载入模块 相关内容
http.createServer(...) // 调用http属性createServer, 创建服务器
.listen(8888) // 其实应该写作http.listen(8888), 至于为啥省略了http, 是因为creatServer方法返回值就是http. 刚入门时对对象、方法、属性很难理解,查看了“Node.js模块系统”有关内容会好很多。
console.log('Server running at localhost:8888); // 这句最好理解:终端打印日志...

上面确实是逐句分析的,当然参数没有分析进去。注意到createServer函数的参数有点特别,是一个“无名函数”:

function (request, response) {
 response.writeHead(200, {'Content-Type': 'text/plain'});
 response.end('Hello Worldn');
}

这个函数需要两个参数,一个是request 一个是response, 之后在正式着手写服务器时会经常见到它们。这个函数做了两件事,一个是response.writeHead(), 调用response的writeHead方法写个http响应头, 还有一个是.end(), 调用end方法写点数据,并结个尾。

我们习惯上吧没有名字的函数称为匿名函数,匿名函数还有一种写法就是

(request, response) => {}

看着就像 给什么“()”, 然后“=>”,做什么“{}”,这种感觉。两种写法具体区别自行查阅。

2 目标

我们的目标肯定不是写一个hello world, 而是要写一个像apache文件索引服务那样的东西。具体来说,肯定需要这些功能:

A 请求啥文件,发送啥文件

B 请求是个目录,目录下没有index.html, 上传目录结构

C 请求是个目录,目录下有index.html,自行判断要不要跳转到目录下的index.html

另外,好不容易能DIY,应该也需要写一些额外的功能:

D 利用HTML5 audio标签 和已知的目录文件列表(B中实现)实现简单的音乐播放器

E 上传文件 重命名文件 新建文件夹等

F 系统监视器(利用child_process中的exec调用shell命令,或者用fs模块读取/proc内文件并显示)

3 发送文件

首先我们要解决的当然是发送Index.html的问题。最简单的方法:

var htmlFile = “../*这里写入你的html文本*/...”
...
Response.write(htmlFile);
Response.end();
...

简单易懂,但要是换一个文档呢?或者这个文档很大呢?或者这个文档压根就不是文本文档而是二进制文档呢?显然,这种方法是行不通的。

如果是C语言,我们便可以使用fread等文件读写相关接口来读写硬盘文件。因此我们猜想,nodejs也一定存在这样的模块或方法来实现这个功能。通过查阅资料我们可以找到这个十分有用的模块fs, 同时还找到了它诸多的属性和方法。假设我们现在已经学会了fs读取文件的操作(暂时称为fs.read(path)),那我们就可以这么写:

var htmlFile = fs.read(“./index.html”);
...
Response.write(htmlFile);
Response.end();
...

仔细查阅相关资料,我们会发现更过有用的工具,例如pipe, fs.createReadStream, fs.readdir, fs.rename, fs.mkdir, fs.stat 等一系列与file system相关的东西。有了这个重要的模块,好像我们离目标不远了。使用了fs后,文件读取变成了下面的模样:

function sendFile_0(realPath, Request, Response, callback) {
    try {
        var stats = fs.statSync(realPath);
        if (stats.isDirectory()) {
            callback("error: isDirectory");
            return;
        }
        Response.statusCode = 200;
        Response.setHeader("Content-Length", stats.size);
        fs.createReadStream(realPath).pipe(Response);
    } catch (err) {
        callback(err);
    }
}
 
var http = require("http");
var fs = require("fs");
http.createServer((Request, Response) => {
    sendFile_0("/mnt/d/test/01._Take_Me_Away.mp3", Request, Response, console.log);
}).listen(7777);

现在开始,可以不妨享受以下自己做出的成果了,来听一段音乐吧。

a37bffb583224d3446264675bba3e6e7.png

3.1 parseRange

可是,这音乐怎么没法跳转进度呢?这和Apache服务器上不一样啊。查找资料,我们找到了http Header 中Range这一选项。查看浏览器发出的header, 果然有着一条:

GET / HTTP/1.1
...
Range: bytes=0-

但我们发出的header中确没有对其做回应——甚至我们压根忽视了它的存在。查阅资料可以发现Range的格式一般有以下几种,同时也涉及到HTTP状态码。其中206 Particial Content 便是我们需要的状态。几种例子在下面都已经展现,这里参考的内容为csdn的博客https://blog.csdn.net/thewindkee/article/details/80189434。

// Examples: 1.Range: bytes=1-499 (1-499 Bytes) 2.Range: bytes=-500 (last 500 Bytes)
// 3. Range: bytes=500- (500-end Bytes) 4.Range: bytes=500-600,601-999
// Res: Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity length]
// Examples: Content-Range: bytes 1-499/22400
// HTTP/1.1 200 Ok (No using resume from break point) 
// HTTP/1.1 206 Partial Content (Using resume from break point)

现在,我们已经知道请求头和响应头中Range相关的内容。想要给出响应头及对应的响应内容,自然的,首先需要解析请求头中的Range属性。通过Request.headers["range"]可以得到Range属性值,即诸如“bytes=500-1000”的内容,接下来要做的就是对这个内容做分析。

首先,想到找到“=”的位置,取得字符串“500-1000”,然后利用split(“-”)对字符串分割得到500 和 1000, 这两个就是请求范围的start 和 end。很简单吧? 可要是其它情况呢?

全盘考虑所有情况,首先我们应当排除情况4“500-600,601-999”,这个情况的特点是多个Range,Range之间使用“,”隔开。因此我们利用“,”做分隔便可得到一个表示请求范围的字符串数组。逐个处理即可。

接着我们处理单个字符串,其情况有三种“1-499”“-500” “500-”,利用“-”分割后,共有三种情况[1,499] [NaN,500] [500,NaN],记为[start,end] 因此 ,通过isNaN函数分别判断start和end便可得到正确的Range信息。

上述过程代码如下:

function parseRange(rangeStr, filesize) {
    if (rangeStr.indexOf("=") == -1 || filesize <= 0) return;
    var rangeStr = rangeStr.substring(rangeStr.indexOf("=") + 1, rangeStr.length);
    var rangeList = rangeStr.split(",");
    var results = [];
    for (var i in rangeList) {
        var range = rangeList[i].split("-");
        var coRange = { "start": parseInt(range[0], 10), "end": parseInt(range[1], 10) };
        if (isNaN(coRange.start)) {
            coRange.start = filesize - coRange.end;
            coRange.end = filesize - 1;
        } else if (isNaN(coRange.end)) {
            coRange.end = filesize - 1;
        }
        if (!isNaN(coRange.start) && !isNaN(coRange.end) && coRange.start <= coRange.end)
            results.push(coRange);
    }
    return results;
}

3.2 其它HTTP头

附带地,我们还学会在浏览器中摁F12调出调试界面查看浏览器发送的头。为确定我们搭建的简易服务器和普通的服务器在Header中到底差了啥, 我们打开某网页,摁下F12,

HTTP/1.1 200 OK 
Server: nginx/1.15.12 
Date: Sat, 22 Feb 2020 11:12:02 GMT 
Content-Type: text/html 
Content-Length: 6204 
Last-Modified: Sun, 16 Feb 2020 16:08:00 GMT 
Connection: keep-alive 
ETag: "5e4968e0-183c" 
Expires: Sat, 22 Feb 2020 23:12:02 GMT 
Cache-Control: max-age=43200 
Accept-Ranges: bytes

在其中找到Etag Last-Motified Cache-Control Content-Length Accept-Ranges Content-Type 等信息,查阅资料可以知道各自的含义。例如Etag和Last-Motified记录文件更新信息,如果文件未更新,可以发送304状态码以减少不必要的文件传输。Cache-Control可以控制是否缓存,何种方式缓存,最长缓存期限等. Content-Type 记录文件属性,一般可根据后缀名获取。加入这些信息,最终,我们的简单文件服务器如下:

var MimeSet = {
    "css": "text/css", "gif": "image/gif", "html": "text/html",
    "php": "text/html", "ico": "image/x-icon", "jpeg": "image/jpeg",
    "jpg": "image/jpeg", "js": "text/javascript", "json": "application/json",
    "pdf": "application/pdf", "png": "image/png", "svg": "image/svg+xml",
    "swf": "application/x-shockwave-flash", "tiff": "image/tiff", "txt": "text/plain",
    "wav": "audio/x-wav", "wma": "audio/x-ms-wma", "wmv": "video/x-ms-wmv",
    "xml": "text/xml", "mp3": "audio/mpeg", "mp4": "video/mp4"
};
 
function TypeChoose(realPath) {
    var suffix = realPath.match(/(.[^.]+|)$/)[0];
    suffix = suffix.slice(1);
    if (typeof MimeSet[suffix] == "undefined") return "application/octet-stream";
    else return MimeSet[suffix];
}
 
function parseRange(rangeStr, filesize) {...}
 
function sendFile(realPath, Request, Response, callback) {
    try {
        var stats = fs.statSync(realPath);
        if (stats.isDirectory()) {
            callback("error: isDirectory");
            return;
        }
        var LastModified = stats.mtime.toUTCString();
        var Etag = 'W/"' + stats.size.toString(16) + '-' + stats.mtime.getTime().toString(16) + '"';
 
        if (Request.headers["if-none-match"] == Etag) {
            Response.statusCode = 304;
            Response.end();
            return;
        }
 
        Response.setHeader("Accpet-Ranges", "bytes");
        Response.setHeader("Cache-Control", "public, max-age=0");
        Response.setHeader("Content-type", TypeChoose(realPath));
        Response.setHeader("Last-Modified", LastModified);
        Response.setHeader("Etag", Etag);
 
        if (Request.headers["range"]) {
            var range = parseRange(Request.headers["range"], stats.size);
            if (range.length) { // has ranges
                range = range[0]; // only trans the first
                Response.statusCode = 206;
                Response.setHeader("Content-Length", (range.end - range.start + 1));
                Response.setHeader("Content-Range",
                    "bytes " + range.start + "-" + range.end + "/" + stats.size);
                fs.createReadStream(realPath, {
                    "start": range.start, "end": range.end
                }).pipe(Response);
            } else {
                Response.statusCode = 416;
                Response.end();
            }
        } else {
            Response.statusCode = 200;
            Response.setHeader("Content-Length", stats.size);
            fs.createReadStream(realPath).pipe(Response);
        }
 
    } catch (err) {
        callback(err);
    }
}
 
var http = require("http");
var fs = require("fs");
 
http.createServer((Request, Response) => {
    sendFile("/mnt/d/test/01._Take_Me_Away.mp3", Request, Response, console.log);
}).listen(7777);

现在,我们的音乐终于可以想跳哪就跳哪了。文件读取的部分也全部完成。此时,我们的响应头是这样的:

HTTP/1.1 206 Partial Content
Accpet-Ranges: bytes
Cache-Control: public, max-age=0
Content-type: audio/mpeg
Last-Modified: Sat, 02 Nov 2019 13:27:24 GMT
Etag: W/"6ccdec-16e2c4c8e26"
Content-Range: bytes 4554752-7130603/7130604
Date: Sat, 22 Feb 2020 12:55:07 GMT
Connection: keep-alive
Content-Length: 7130604

4 404页面和重定向

404页面非常简单,写就完事儿了。重定向主要靠发送location头和修改状态码(为301或302)来实现。代码如下:

function send404(Response) {
	Response.statusCode = 404;
       	Response.setHeader('Content-Type', 'text/html; charset=utf-8');
       	Response.write('<div style="text-align:center;font-weight:bold;font-size:12vw;');
       	Response.write('top:50%;left:50%;transform:translate(-50%,-50%);position:fixed;">');
       	Response.end('404 Not Found</div>');
}
 
function reDirect(statusCode, url, Request, Response) {
       	Response.statusCode = statusCode;
       	if (url == "back")
          	Response.setHeader('Location', Request.headers["referer"] || "/");
       	else
            	Response.setHeader('Location', url);
        Response.end();
}

未完待续...

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值