三天node入门(day2)

前言

目录:

  1. 文件操作
  2. 网络操作
  3. 进程管理

day1在这里
有疑问或是文章中有错误的地方,请在评论区指出来,如果私信的话别人就看不到了,加油一起进步~~(゜▽゜*)♪

1.文件操作

a.遍历目录

在开发中我们会经常需要找到并处理指定目录下的某些文件时,这时就需要遍历目录。

同步遍历

举个例子,我们想得到某个路径下所有子文件路径,需要遍历路径。

var fs = require('fs')
const path = require('path')

function getDirFilePath(dir) {
    fs.readdirSync(dir).forEach((file) => {
        const pathname = path.join(dir, file)
        if (!fs.statSync(pathname).isDirectory()) { // 判断该路径是否为一个文件夹
            console.log(pathname)
        }
    })
}

getDirFilePath("C:/Users/mytac/Desktop/test")

然而当该路径上有子文件夹时,以上方法就不可取了,这时,需要递归遍历子文件夹的路径。

function getDirFilePath(dir) {
    fs.readdirSync(dir).forEach((file) => {
        const pathname = path.join(dir, file)
        if (fs.statSync(pathname).isDirectory()) { // 判断该路径是否为一个文件夹
            getDirFilePath(pathname) // 递归遍历子文件夹
        } else {
            console.log(pathname)
        }
    })
}

异步遍历

如果我们在遍历时使用的是异步的api,这个函数会变的复杂,但实现原理是一样的。具体实现步骤将会在后文介绍。

b.文本编码

BOM的移除

关于BOM是用来标记一个文本使用的Unicode编码,位于文本文件的头部,在不同的编码下,BOM字符对应的二进制字节如下:

    Bytes      Encoding
----------------------------
    FE FF       UTF16BE
    FF FE       UTF16LE
    EF BB BF    UTF8

我们可以根据文件前几个字节来判断文件是否包含BOM,以及使用哪种Unicode编码。但是BOM并不是文件内容的一部分,如果读取文本文件时没有去掉BOM,在某些使用场景中就会出现问题。比如说在我们在合并某些js文件时,每个js文件头部都有BOM,会导致解析错误,所以必须要删除BOM,以下为识别和去除BOM的代码

function removeBOM(pathname){
    const bin = fs.readFileSync(pathname)

    if(bin[0]===0xEF&&bin[1]===0xBB&&bin[2]===0xBF){
        bin.slice(3)
    }

    return bin.toString('utf-8')
}

GBK转UTF8

GBK编码不在node本身支持的范围内,所以没有直接可用的方法。通常我们使用icon-lite这个库来进行转码。

const iconv=require('iconv-lite')

function readGBKText(pathname){
    const bin=fs.readFileSync(pathname)
    return iconv.decode(bin,'gbk')
}

单字节编码

通常我们不看文件内容的情况下是无法确定该文件是哪种编码的,以下介绍的方法虽然局限,但非常简单。

大家都知道,一个文件中只有英文字符无论是gbk还是utf-8都能读取。如果文件中有中文字符,但中文字符并不是会产生解析错误的代码,如注释或是字符串。这时我们都可以使用单字节编码来读取文件,因为无论中文为何种编码类型,他对应的字节始终不变

1. GBK编码源文件内容:
    var foo = '中文';
2. 对应字节:
    76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B
3. 使用单字节编码读取后得到的内容:
    var foo = '{乱码}{乱码}{乱码}{乱码}';
4. 替换内容:
    var bar = '{乱码}{乱码}{乱码}{乱码}';
5. 使用单字节编码保存后对应字节:
    76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B
6. 使用GBK编码读取后得到内容:
    var bar = '中文';

node中提供了binary方法来实现以上过程。

function replace(pathname){
    let str=fs.readFileSync(pathname,'binary')
    str=str.replace('foo','bar')
    fs.writeFileSync(pathname,str,'binary')
    return str.toString('utf-8')
}

2. 网络操作

a.HTTP

使用createServer创建一个HTTP服务器,调用.listen方法监听端口。之后,每当一个客户端请求,创建的服务器传入的回调就被调用一次。

const http=require('http')

http.createServer((req,res)=>{ 
    res.writeHead(200,{'Content-Type':'text-plain'})
    res.end('hello world')
}).listen(8888) // 简历了一个http服务器,监听8888端口

打开浏览器访问localhost:8888就能看到hello world了。

HTTP请求本质上是一个数据流,由headers和body组成。以下为一个完整的http请求数据:

POST / HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
Host: localhost:8888
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Content-Length: 11
Content-Type: text-plain

Hello World

以上,hello world为请求体,hello world以上的内容为请求头。HTTP请求在发送给服务器时,是从头到尾的顺序,一个字节一个字节以数据流的方式发送的。当http服务器接收到完整的请求头后,就会调用回调函数。除了可以使用request对象访问请求头数据外,还可以吧request对象当作一个只读数据流来访问请求体数据。

http.createServer((req,res)=>{
    let body=[]

    console.log(req.method)
    console.log(req.headers)

    req.on('data',chunk=>{
        body.push(chunk)
    })

    req.on('end',()=>{
        body=Buffer.concat(body)
        console.log(body.toString())
    })

    res.end('hello world')
}).listen(8888)

在回调函数中,除了可以使用request对象来写入响应头数据,还能把response对象当作一个只写数据流来写入响应体数据。下面的例子,服务端将客户端的请求体数据原样返回给客户端。

http.createServer((req,res)=>{
    res.writeHead(200,{'Content-Type':'text/plain'})

    req.on('data',chunk=>{
        res.write(chunk)
    })

    req.on('end',()=>{
        console.log('end')
        res.end()
    })
}).listen(8888)

创建一个客户端,指定请求目标和请求头数据。之后,把request当成一个只写数据流来写入请求体数据和结束请求。

const options={
    hostname: 'www.mytac.cn',
        port: 8888,
        path: '/upload',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
}
const request=http.request(options,response=>{

})

request.write('hello world')
request.end()

另外,由于HTTP请求中GET是最常见的一种,并且不需要请求体,因此http模块提供了简单的API。

http.get('http://www.example.com/', function (response) {});

当客户端发送请求并接收到完整的服务端响应头时,就会调用回调函数。在回调函数中,除了可以使用response对象访问响应头数据外,还能把response对象当作一个只读数据流来访问响应体数据。请看下例:

http.get('http://localhost:8888',res=>{
    let body=[]

    res.on('data',chunk=>{
        body.push(chunk)
    })

    res.on('end',()=>{
        body=Buffer.concat(body)
        console.log(body.toString())
        console.log('req end')
    })
})

http.createServer((req,res)=>{
    res.writeHead(200,{'Content-Type':'text/plain'})

    req.on('data',chunk=>{
        res.write(chunk)
    })

    req.on('end',()=>{
        console.log('end')
        res.end()
    })
}).listen(8888)

HTTPS

HTTPS模块与HTTP模块非常相似,区别在于HTTPS需要额外处理SSL证书。SSL(sercure socket layer)指安全套接层

创建一个HTTPS服务器:

const options = { 
        key: fs.readFileSync('./ssl/default.key'), // 私钥
        cert: fs.readFileSync('./ssl/default.cer') // 公钥
    };

// 与http.createServer相比,多出一个option对象
const server = https.createServer(options, function (request, response) {
        // ...
    });

node支持sni技术,可以根据HTTPS客户端请求使用的域名动态使用不同的证书,因此同一个HTTPS服务器可以与使用多个域名提供服务。接着上面的例子,为https服务器添加多组证书。

const server=https.createServer((req,res)=>{
    res.writeHead(200,{'Content-Type':'text/plain'})

    req.on('data',chunk=>{
        res.write(chunk)
    })

    req.on('end',()=>{
        console.log('end')
        res.end()
    })
}).listen(8888)

server.addContext('mytac.cn',{
    key:fs.readFileSync('./ssl/default.key'),
    cert:fs.readFileSync('./ssl/default.cer')
})

在客户端模式下与HTTP几乎没有差别

var options = {
        hostname: 'www.example.com',
        port: 443,
        path: '/',
        method: 'GET'
    };

var request = https.request(options, function (response) {});

request.end();

URL

该模块对URL进行生成、解析、拼接等。

url的组成

                           href
 -----------------------------------------------------------------
                            host              path
                      --------------- ----------------------------
 http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash
 -----    ---------   --------   ---- -------- ------------- -----
protocol     auth     hostname   port pathname     search     hash
                                                ------------
                                                   query

url字符串与url对象的转换

url字符串转换为对象

使用.parse将字符串转换为对象

console.log(url.parse('http://nodejs.cn/api/url.html#url_url_parse_urlstring_parsequerystring_slashesdenotehost'))
/*
Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'nodejs.cn',
  port: null,
  hostname: 'nodejs.cn',
  hash: '#url_url_parse_urlstring_parsequerystring_slashesdenotehost',
  search: null,
  query: null,
  pathname: '/api/url.html',
  path: '/api/url.html',
  href: 'http://nodejs.cn/api/url.html#url_url_parse_urlstring_parsequerystring_
slashesdenotehost' }
*/

传给.parse方法的不一定是一个完整的url,比如在HTTP服务器回调函数中,request.url不包含协议头和域名,但同样可以用.parse解析。当parse传入的第二个参数对象为true时,返回的query字段由字符串变成字段;当第三个参数为true时,该方法可以解析没有协议头的URL,这两个参数默认为false

URL对象转换为字符串

使用format方法将对象转换为字符串。

url.format({
    protocol: 'http:',
    host: 'www.example.com',
    pathname: '/p/a/t/h',
    search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/

使用resolve方法将字符串拼接

url.resolve('http://www.example.com/foo/bar', '../baz');
/* =>
http://www.example.com/baz
*/

url参数对象和字符串的相互转换

这里需要queryString模块。

const queryString=require('querystring')
// 将字符串转换为对象
const str='foo=bar&baz=qux&baz=quux&corge'
console.log(queryString.parse(str))
// ->  { foo: 'bar', baz: [ 'qux', 'quux' ], corge: '' }

// 将对象转换为字符串

const obj={ foo: 'bar', baz: [ 'qux', 'quux' ], corge: '' }
// -> foo=bar&baz=qux&baz=quux&corge=

数据压缩和解压

这里需要引入zlib 模块。当我们处理HTTP请求和响应时会用到这个模块。

它可以用来压缩HTTP响应体数据。在下面这个例子中,他判断客户端是否支持gzip,并在支持的情况下使用zlib模块返回gzip之后的响应体数据。

const options={
        hostname: 'localhost',
        port: 8888,
        path: '/',
        method: 'GET',
        headers: {
            'Accept-Encoding': 'gzip, deflate'
        }
    }

http.request(options,res=>{
    let body=[]

    res.on('data',chunk=>{
        body.push(chunk)
    })

    res.on('end',()=>{
        body=Buffer.concat(body)

        if(res.headers['content-encoding']=='gzip'){
            zlib.gunzip(body,(err,data)=>{
                console.log(data)
            })
        }else{
            console.log('not gzip')
        }
    })
}).end()

创建Socket服务器或Socket客户端

这里要引入net模块。接下来我们来演示一下如何从Socket层面来实现HTTP请求和响应。

创建一个HTTP服务器,不论收到什么请求,都返回固定相同的响应。

const net=require('net')

net.createServer(conn=>{
    conn.on('data',data=>{
        conn.write([
            'HTTP/1.1 200 OK',
            'Content-Type: text/plain',
            'Content-Length: 11',
            '',
            'Hello World'
            ].join('\n'))
    })
}).listen(8888)

下面这个例子是使用socket发起http客户端请求。建立连接后发送了一个http的get请求,通过data事件监听函数获取服务器响应。

const net=require('net')

const options={
    port:'8010',
    host:'127.0.0.1'
}

var client=net.connect(options,()=>{
     client.write([
            'GET / HTTP/1.1',
            'User-Agent: curl/7.26.0',
            'Host: www.baidu.com',
            'Accept: */*',
            '',
            ''
        ].join('\n'));
})

client.on('data',data=>{
    console.log(data.toString())
    client.end()
})

3.进程管理

node可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得node可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。

举个栗子,使用node调用终端命令简化目录拷贝,在windows下拷贝目录时使用copy path,linux下时使用cp -r source/* target

const child_process=require('child_process')
const util=require('util')
const path=require('path')

const dir="C:/testNode"
function copy(source){
    child_process.exec(`copy ${path.normalize(source)}`, function(err,stdout,stderr) {
        if(err){
            throw err
        }
        console.log('stdout',stdout)
        console.log('stderr',stderr)
    })
}

copy(dir)
// -------输出
/* 乱码不知道时啥 应该是复制的文件描述
stdout C:\testNode\1.txt
�Ѹ���         1 ���ļ���

stderr*/

Process对象

process是一个全局变量,不需要require引入,提供相关信息,控制当前进程。任何一个进程都有启动进程时使用的命令行参数,有标准输入输出,有运行权限,有运行环境和状态。在node中,通过process对象感知和控制node自身进程的方方面面。

方法介绍

  1. process.platform返回当前的系统平台
  2. cwd返回运行当前脚本的工作目录路径
  3. abort立即结束进程
  4. nextTick指定下次事件循环首先运行的任务

child_process模块

使用child_process模块可以创建和控制子进程。这个模块中最核心的方法是child_process.spawn(),他会异步的衍生子进程,而且不会阻塞node事件循环,他的同步方法为child_process.spawnSync(),但会阻塞事件循环,直到衍生的子进程退出或终止。其余的api都是针对使用场景对他的进一步封装,算是他的语法糖。

cluster模块

cluster模块允许简单容易的创建共享服务器端口的子进程。它是对child_process模块的进一步封装,专门用它来解决web服务器无法充分利用多核cpu的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行一个工作进程,并统一通过主进程监听端口和分发请求。

结合应用场景来介绍相关api

a.获取命令行参数

在上文很多个示例中都出现了process.argv。命令行参数是从```prcess.argv[2]``开始的,因为第0个为node程序的执行路径,第1个为该文件的路径。

b.退出程序

程序只有在正常退出时的状态码才为0。当一个程序捕获到异常,但并不想让程序再执行下去,需要程序立即退出,并把状态码设置为指定数字,比如1,如下:

try {
    // ...
} catch (err) {
    // ...
    process.exit(1);
}

c.控制输入输出

node中分别有process.stdout,process.stdin,process.stderr对应标准输出流、标准输入流和标准错误流,前一个为只读数据流,后两个为只写数据流,对他们的操作按照对数据流的操作方式即可。如果想要实现一个console.log

function log(){
    process.stdout.write(
        util.format.apply(util,arguments)
        )
}

log(process.argv.slice(2).join(' '))
==============================
// 输入
$ node node_test.js fuck job
// 输出
fuck job

d.降权

有时候我们需要root权限才能进行某些操作。但一旦执行结束后,继续让程序拥有root权限会存在安全隐患,因此,最好把权限降下来。

http.createServer(options, (req, res) => {
    const env = process.env
    const uid = parseInt(env['SUDO_UID'] || process.getuid(), 10) // 通过从权限的获取方式来获得对应的gid、uid
    const gid = parseInt(env['SUDO_UID'] || process.getgid(), 10)

    process.setgid(gid) // 这两个方法只接受number类型的参数
    process.setuid(uid) // 降权时必须先将gid再降uid
}).listen(8888)

e.创建子进程

创建子进程的例子:

const child_process = require('child_process')
const child=child_process.spawn('node',['child.js'])

child.stdout.on('data',data=>{
    console.log('stdout:'+data)
})

child.stderr.on('data',data=>{
    console.log('stderr:'+data)
})

child.on('close',code=>{
    console.log('child process exited width code '+code)
})

新增一个子进程文件child.js

// child.js
console.log('child works')

最后将在命令行输出

$ node node_test.js
-----------------
stdout:child works

child process exited width code 0

我们将child.js改写为一串乱码,这时会抛出错误,并输出:

stderr:C:\Users\mytac\Desktop\test\child.js:1
(function (exports, require, module, __filename, __dirname) { sgaduysag
                                                              ^

ReferenceError: sgaduysag is not defined
    at Object.<anonymous> (C:\Users\mytac\Desktop\test\child.js:1:63)
    at Module._compile (module.js:569:30)
    at Object.Module._extensions..js (module.js:580:10)
    at Module.load (module.js:503:32)
    at tryModuleLoad (module.js:466:12)
    at Function.Module._load (module.js:458:3)
    at Function.Module.runMain (module.js:605:10)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:575:3

child process exited width code 1

.spawn(exec, args, options)这个方法支持三个参数。第一个参数代表需要执行的命令,第二个参数代表字符串参数列表,第三个参数代表用于配置子进程的执行环境与命令。上例中虽然通过子进程对象的.stdout.stderr访问子进程输出,但通过options.stdio字段的不同配置,可以将子进程的输入输出重定向到任何数据流上,或者让子进程共享父进程的标准输入输入流,或是直接忽略子进程的输入输出。

进程间如何通讯

在linux系统下,进程间可以通过信号相互通信。

// parent.js
const child_process = require('child_process')
const child=child_process.spawn('node',['child.js'])

child.kill('SIGTERM')

// child.js
process.on('SIGTERM',()=>{
    console.log('child run')
    cleanUp()
    process.exit(0)
})

上例中,父进程通过.kill方法向子进程发送SIGTERM信号,子进程监听process对象的SIGTERM事件响应信号。

当父子进程都是node进程,就可以通过IPC(进程间通讯)双向传递数据。

// parent.js
const child_process = require('child_process')

var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ]
    });

child.on('message', function (msg) {
    console.log(msg);
});

child.send({ hello: 'hello' });

// child.js
process.on('message', function (msg) {
    msg.hello = msg.hello.toUpperCase();
    process.send(msg);
});

// 输出
---------------
{ hello: 'HELLO' }

父进程在创建子进程时,在options.stdio字段中通过ipc通道,之后就可以监听子进程对象的message事件接受来自子进程的消息,并通过.send方法发送给子进程,子进程监听message事件接受来自父进程的消息。再通过send方法向父进程发送消息。

守护子进程

守护进程用于监控工作进程的运行状态,在工作进程不正常退出时重启工作进程,保障工作进程不间断运行。在进程非正常退出时,守护进程立即重启工作进程。

function spawn(mainModule){
    const worker=child_process.spawn('node', [mainModule])

    worker.on('exit',code=>{
        if(code!==0){
            spawn(mainModule)
        }
    })
}

spawn('child.js')
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,607评论 2 41
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,297评论 0 6
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    Myselfyan阅读 4,061评论 2 58
  • 1、 石破天没有羊,只有一条叫做“阿黄”的狗。 有天他的妈妈梅芳姑不见了,阿黄也不见了,他于是开始了自己生命的旅程...
    顾秋水阅读 439评论 1 5