nodejs --- 融会贯通 (二)

539 阅读5分钟

网络

获取本地 IP

function get_local_ip() {
    const interfaces = require('os').networkInterfaces();
    let IPAdress = '';
    for (const devName in interfaces) {
        const iface = interfaces[devName];
        for (let i = 0; i < iface.length; i++) {
            const alias = iface[i];
            if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
                IPAdress = alias.address;
            }
        }
    }
    return IPAdress;
}

TCP 客户端

NodeJS 使用 net 模块创建 TCP 连接和服务。

启动与测试 TCP

const assert = require('assert');
const net = require('net');
let clients = 0;
let expectedAssertions = 2;

const server = net.createServer(function (client) {
    clients++;
    const clientId = clients;
    console.log('Client connected:', clientId);

    client.on('end', function () {
        console.log('Client disconnected:', clientId);
    });

    client.write('Welcome client: ' + clientId);
    client.pipe(client);
});

server.listen(8000, function () {
    console.log('Server started on port 8000');

    runTest(1, function () {
        runTest(2, function () {
            console.log('Tests finished');
            assert.equal(0, expectedAssertions);
            server.close();
        });
    });
});

function runTest(expectedId, done) {
    const client = net.connect(8000);

    client.on('data', function (data) {
        const expected = 'Welcome client: ' + expectedId;
        assert.equal(data.toString(), expected);
        expectedAssertions--;
        client.end();
    });

    client.on('end', done);
}

UDP 客户端

利用 dgram 模块创建数据报 socket,然后利用socket.send发送数据。

文件发送服务

const dgram = require('dgram');
const fs = require('fs');
const port = 41230;
const defaultSize = 16;

function Client(remoteIP) {
    const inStream = fs.createReadStream(__filename); // 从当前文件创建可读流
    const socket = dgram.createSocket('udp4'); // 创建新的数据流 socket 作为客户端

    inStream.on('readable', function () {
        sendData(); // 当可读流准备好,开始发送数据到服务器
    });

    function sendData() {
        const message = inStream.read(defaultSize); // 读取数据块

        if (!message) {
            return socket.unref(); // 客户端完成任务后,使用 unref 安全关闭它
        }

        // 发送数据到服务器
        socket.send(message, 0, message.length, port, remoteIP, function () {
            sendData();
        });
    }
}

function Server() {
    const socket = dgram.createSocket('udp4'); // 创建一个 socket 提供服务

    socket.on('message', function (msg) {
        process.stdout.write(msg.toString());
    });

    socket.on('listening', function () {
        console.log('Server ready:', socket.address());
    });

    socket.bind(port);
}

if (process.argv[2] === 'client') { // 根据命令行选项确定运行客户端还是服务端
    new Client(process.argv[3]);
} else {
    new Server();
}

HTTP 客户端

使用 http.createServerhttp.createClient 运行 HTTP 服务。

启动与测试 HTTP

const assert = require('assert');
const http = require('http');

const server = http.createServer(function (req, res) {
    res.writeHead(200, {
        'Content-Type': 'text/plain'
    }); // 写入基于文本的响应头
    res.write('Hello, world.'); // 发送消息回客户端
    res.end();
});

server.listen(8000, function () {
    console.log('Listening on port 8000');
});

const req = http.request({
    port: 8000
}, function (res) { // 创建请求
    console.log('HTTP headers:', res.headers);
    res.on('data', function (data) { // 给 data 事件创建监听,确保和期望值一致
        console.log('Body:', data.toString());
        assert.equal('Hello, world.', data.toString());
        assert.equal(200, res.statusCode);
        server.unref();
        console.log('测试完成');
    });
});

req.end();

重定向

HTTP 标准定义了标识重定向发生时的状态码,它也指出了客户端应该检查无限循环。

  • 300:多重选择
  • 301:永久移动到新位置
  • 302:找到重定向跳转
  • 303:参见其他信息
  • 304:没有改动
  • 305:使用代理
  • 307:临时重定向

    const http = require('http'); const https = require('https'); const url = require('url'); // 有很多接续 URLs 的方法

    // 构造函数被用来创建一个对象来构成请求对象的声明周期 function Request() { this.maxRedirects = 10; this.redirects = 0; }

    Request.prototype.get = function (href, callback) { const uri = url.parse(href); // 解析 URLs 成为 Node http 模块使用的格式,确定是否使用 HTTPS const options = { host: uri.host, path: uri.path }; const httpGet = uri.protocol === 'http:' ? http.get : https.get;

    console.log('GET:', href);
    
    function processResponse(response) {
        if (response.statusCode >= 300 && response.statusCode < 400) { // 检查状态码是否在 HTTP 重定向范围
            if (this.redirects >= this.maxRedirects) {
                this.error = new Error('Too many redirects for: ' + href);
            } else {
                this.redirects++; // 重定向计数自增
                href = url.resolve(options.host, response.headers.location); // 使用 url.resolve 确保相对路径的 URLs 转换为绝对路径 URLs
                return this.get(href, callback);
            }
        }
    
        response.url = href;
        response.redirects = this.redirects;
    
        console.log('Redirected:', href);
    
        function end() {
            console.log('Connection ended');
            callback(this.error, response);
        }
    
        response.on('data', function (data) {
            console.log('Got data, length:', data.length);
        });
    
        response.on('end', end.bind(this)); // 绑定回调到 Request 实例,确保能拿到实例属性
    }
    
    httpGet(options, processResponse.bind(this))
        .on('error', function (err) {
            callback(err);
        });
    

    };

    const request = new Request(); request.get('google.com/', function (err, res) { if (err) { console.error(err); } else { console.log( Fetched URL: ${res.url} with ${res.redirects} redirects ); process.exit(); } });

HTTP 代理

  • ISP 使用透明代理使网络更加高效
  • 使用缓存代理服务器减少宽带
  • Web 应用程序的 DevOps 利用他们提升应用程序性能

    const http = require('http'); const url = require('url');

    http.createServer(function (req, res) { console.log('start request:', req.url); const options = url.parse(req.url); console.log(options); options.headers = req.headers; const proxyRequest = http.request(options, function (proxyResponse) { // 创建请求来复制原始的请求 proxyResponse.on('data', function (chunk) { // 监听数据,返回给浏览器 console.log('proxyResponse length:', chunk.length); res.write(chunk, 'binary'); });

        proxyResponse.on('end', function () { // 追踪代理请求完成
            console.log('proxied request ended');
            res.end();
        });
    
        res.writeHead(proxyResponse.statusCode, proxyResponse.headers); // 发送头部信息给服务器
    });
    
    req.on('data', function (chunk) { // 捕获从浏览器发送到服务器的数据
        console.log('in request length:', chunk.length);
        proxyRequest.write(chunk, 'binary');
    });
    
    req.on('end', function () { // 追踪原始的请求什么时候结束
        console.log('original request ended');
        proxyRequest.end();
    });
    

    }).listen(8888); // 监听来自本地浏览器的连接

封装 request-promise

const https = require('https');
const promisify = require('util').promisify;

https.get[promisify.custom] = function getAsync(options) {
    return new Promise((resolve, reject) => {
        https.get(options, (response) => {
            response.end = new Promise((resolve) => response.on('end', resolve));
            resolve(response);
        }).on('error', reject);
    });
};
const rp = promisify(https.get);

(async () => {
    const res = await rp('https://jsonmock.hackerrank.com/api/movies/search/?Title=Spiderman&page=1');
    let body = '';
    res.on('data', (chunk) => body += chunk);
    await res.end;

    console.log(body);
})();

DNS 请求

使用 dns 模块创建 DNS 请求。

  • A:`dns.resolve`,A 记录存储 IP 地址
  • TXT:`dns.resulveTxt`,文本值可以用于在 - DNS 上构建其他服务
  • SRV:`dns.resolveSrv`,服务记录定义服务的定位数据,通常包含主机名和端口号
  • NS:`dns.resolveNs`,指定域名服务器
  • CNAME:`dns.resolveCname`,相关的域名记录,设置为域名而不是 IP 地址

    const dns = require('dns');

    dns.resolve('www.chenng.cn', function (err, addresses) { if (err) { console.error(err); }

    console.log('Addresses:', addresses);
    

    });

crypto 库加密解密

const crypto = require('crypto')

function aesEncrypt(data, key = 'key') {
    const cipher = crypto.createCipher('aes192', key)
    let crypted = cipher.update(data, 'utf8', 'hex')
    crypted += cipher.final('hex')
    return crypted
}

function aesDecrypt(encrypted, key = 'key') {
    const decipher = crypto.createDecipher('aes192', key)
    let decrypted = decipher.update(encrypted, 'hex', 'utf8')
    decrypted += decipher.final('utf8')
    return decrypted
}

发起 HTTP 请求的方法

  • HTTP 标准库

  • 无需安装外部依赖
  • 需要以块为单位接受数据,自己监听 end 事件
  • HTTP 和 HTTPS 是两个模块,需要区分使用
  • Request 库

  • 使用方便
  • 有 promise 版本 request-promise
  • Axios

  • 既可以用在浏览器又可以用在 NodeJS
  • 可以使用 axios.all 并发多个请求
  • SuperAgent

  • 可以链式使用
  • node-fetch

  • 浏览器的 fetch 移植过来的

子进程

执行外部应用

基本概念

  • 4个异步方法:exec、execFile、fork、spawn

  • spawn:处理一些会有很多子进程 I/O 时、进程会有大量输出时使用
  • execFile:只需执行一个外部程序的时候使用,执行速度快,处理用户输入相对安全
  • exec:想直接访问线程的 shell 命令时使用,一定要注意用户输入
  • fork:想将一个 Node 进程作为一个独立的进程来运行的时候使用,是的计算处理和文件描述器脱离 Node 主进程
  • Node
  • 非 Node
  • 3个同步方法:execSync、execFileSync、spawnSync

  • 通过 API 创建出来的子进程和父进程没有任何必然联系

execFile

会把输出结果缓存好,通过回调返回最后结果或者异常信息

const cp = require('child_process');

cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => {
    if (err) {
        console.error(err);
    }
    console.log('stdout: ', stdout);
    console.log('stderr: ', stderr);
});

pawn

  • 通过流可以使用有大量数据输出的外部应用,节约内存
  • 使用流提高数据响应效率
  • spawn 方法返回一个 I/O 的流接口

单一任务

const cp = require('child_process');

const child = cp.spawn('echo', ['hello', 'world']);
child.on('error', console.error);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

多任务串联

const cp = require('child_process');
const path = require('path');

const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]);
const sort = cp.spawn('sort');
const uniq = cp.spawn('uniq');

cat.stdout.pipe(sort.stdin);
sort.stdout.pipe(uniq.stdin);
uniq.stdout.pipe(process.stdout);

exec

  • 只有一个字符串命令
  • 和 shell 一模一样

    const cp = require('child_process');

    cp.exec(cat ${__dirname}/messy.txt | sort | uniq, (err, stdout, stderr) => { console.log(stdout); });

fork

  • fork 方法会开发一个 IPC 通道,不同的 Node 进程进行消息传送
  • 一个子进程消耗 30ms 启动时间和 10MB 内存
  • 子进程:`process.on('message')`、`process.send()`
  • 父进程:`child.on('message')`、`child.send()`

父子通信

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

const child = cp.fork('./child', {
    silent: true
});
child.send('monkeys');
child.on('message', function (message) {
    console.log('got message from child', message, typeof message);
})
child.stdout.pipe(process.stdout);

setTimeout(function () {
    child.disconnect();
}, 3000);

// child.js
process.on('message', function (message) {
    console.log('got one', message);
    process.send('no pizza');
    process.send(1);
    process.send({
        my: 'object'
    });
    process.send(false);
    process.send(null);
});

console.log(process);

常用技巧

退出时杀死所有子进程

保留对由 spawn 返回的 ChildProcess 对象的引用,并在退出主进程时将其杀死

const spawn = require('child_process').spawn;
const children = [];

process.on('exit', function () {
    console.log('killing', children.length, 'child processes');
    children.forEach(function (child) {
        child.kill();
    });
});

children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));
children.push(spawn('/bin/sleep', ['10']));

setTimeout(function () {
    process.exit(0);
}, 3000);

Cluster 的理解

  • 解决 NodeJS 单进程无法充分利用多核 CPU 问题
  • 通过 master-cluster 模式可以使得应用更加健壮
  • Cluster 底层是 child_process 模块,除了可以发送普通消息,还可以发送底层对象 TCP、UDP 等
  • TCP 主进程发送到子进程,子进程能根据消息重建出 TCP 连接,Cluster 可以决定 fork 出合适的硬件资源的子进程数

Node 多线程

单线程问题

  • 对 cpu 利用不足
  • 某个未捕获的异常可能会导致整个程序的退出

Node 线程

  • Node 进程占用了 7 个线程

  • Node 中最核心的是 v8 引擎,在 Node 启动后,会创建 v8 的实例,这个实例是多线程的

  • 主线程:编译、执行代码
  • 编译/优化线程:在主线程执行的时候,可以优化代码
  • 分析器线程:记录分析代码运行时间,为 Crankshaft 优化代码执行提供依据
  • 垃圾回收的几个线程
  • JavaScript 的执行是单线程的,但 Javascript 的宿主环境,无论是 Node 还是浏览器都是多线程的

异步 IO

  • Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集计算(Zlib,Crypto)会启用 Node 的线程池
  • 线程池默认大小为 4,可以手动更改线程池默认大小

    process.env.UV_THREADPOOL_SIZE = 64

cluster 多进程

const cluster = require('cluster');const http = require('http');const numCPUs = require('os').cpus().length;if (cluster.isMaster) {  console.log(`主进程 ${process.pid} 正在运行`);  for (let i = 0; i < numCPUs; i++) {    cluster.fork();  }  cluster.on('exit', (worker, code, signal) => {    console.log(`工作进程 ${worker.process.pid} 已退出`);  });} else {  // 工作进程可以共享任何 TCP 连接。  // 在本例子中,共享的是 HTTP 服务器。  http.createServer((req, res) => {    res.writeHead(200);    res.end('Hello World');  }).listen(8000);  console.log(`工作进程 ${process.pid} 已启动`);}
  • 一共有 9 个进程,其中一个主进程,cpu 个数 x cpu 核数 = 2 x 4 = 8 个 子进程
  • 无论 child_process 还是 cluster,都不是多线程模型,而是多进程模型
  • 应对单线程问题,通常使用多进程的方式来模拟多线程

真 Node 多线程

Node 10.5.0 的发布,给出了一个实验性质的模块 worker_threads 给 Node 提供真正的多线程能力

worker_thread 模块中有 4 个对象和 2 个类

  • isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
  • MessagePort: 用于线程之间的通信,继承自 EventEmitter。
  • MessageChannel: 用于创建异步、双向通信的通道实例。
  • threadId: 线程 ID。
  • Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
  • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
  • workerData: 用于在主进程中向子进程传递数据(data 副本)

    isMainThread, parentPort, workerData, threadId, MessageChannel, MessagePort, Worker } = require('worker_threads');

    function mainThread() { for (let i = 0; i < 5; i++) { const worker = new Worker(__filename, { workerData: i }); worker.on('exit', code => { console.log(main: worker stopped with exit code ${code}); }); worker.on('message', msg => { console.log(main: receive ${msg}); worker.postMessage(msg + 1); }); } }

    function workerThread() { console.log(worker: workerDate ${workerData}); parentPort.on('message', msg => { console.log(worker: receive ${msg}); }), parentPort.postMessage(workerData); } if (isMainThread) { mainThread(); } else { workerThread(); }

线程通信

const assert = require('assert');
const {
    Worker,
    MessageChannel,
    MessagePort,
    isMainThread,
    parentPort
} = require('worker_threads');
if (isMainThread) {
    const worker = new Worker(__filename);
    const subChannel = new MessageChannel();
    worker.postMessage({
        hereIsYourPort: subChannel.port1
    }, [subChannel.port1]);
    subChannel.port2.on('message', (value) => {
        console.log('received:', value);
    });
} else {
    parentPort.once('message', (value) => {
        assert(value.hereIsYourPort instanceof MessagePort);
        value.hereIsYourPort.postMessage('the worker is sending this');
        value.hereIsYourPort.close();
    });
}

多进程 vs 多线程

进程是资源分配的最小单位,线程是CPU调度的最小单位

原文:mp.weixin.qq.com/s/yY3fBE5zw…