NodeJS超长总结 2

10/28/2018 Node

# 🌹🌹 🐱 🐶 🐭 🐘 🐳 ✈️ 🚄 🚗 ⚽️ 💆 🥚 🧒 🌹 🐯

上次总结到了流,快两千五百行。。。。另开一章,继续总结记录


# 🌹 TCP


# 🌹🌹 TCP基础知识

# 🌹🌹 创建TCP服务器

let net = require('net'); // 应用net模块
let server = net.createServer();
function connectionListener(socket){
    console.log('客户端已经链接')
}
server.on('connection',connectionListener); // 链接服务器
server.listen(8080,'hostname',function(){ // 监听端口 设置回调
    console.log('服务器开始监听')
});

// 端口占用 可以重启新的端口号
server.on('error', function (err) {
  if (err.code === 'EADDRINUSE') {
    server.listen(3001);
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

或者

let server = net.createServer();
function connectionListener(socket){
    console.log('客户端已经链接')
}
let server = net.createServer(options,connectionListener);
server.listen('3000','hostname',function(){
    console.log('服务器启动成功')
})
1
2
3
4
5
6
7
8
  • option.allowHalfOpen默认为false,如果改为true,当TCP服务器接收到客户端发送一个FIN包时不会回发FIN包
  • connectionListener参数用于指定当客户端与服务器建立连接时所要调用的回调函数,回调中有一个参数socket,指的是TCP服务器监听的socket端口对象

# 🌹🌹 服务器参数

server创建后可以,自带各种参数:

let net = require('net');
let server = net.createServer();
server.on('connection',(socket) => {
    // socket 是一个双工流
    // 每次链接都会产生一个新的socket
})
server.address();
server.getConnections((err,count) =>{
    // count
})
server.close(); //不允许新进来的链接 只有调用close时才会触发关闭
server.unref(); // 关闭 当饭店最后一个人离开了 就会关闭
server.getConnections((err,count)=>{
    console.log('已经链接'+count+'个用户')
});
server.on('close',callback);
server.listen(3000);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 🌹🌹🌹 server.address();

返回操作系统报告的 socket 的地址、地址族和端口。返回的对象有三个属性,例如: { port: 12346, family: 'IPv4', address: '127.0.0.1' }

  • port 端口号
  • address TCP服务器监听的地址
  • family 协议的版本

# 🌹🌹🌹 connections

异步获取服务器的当前并发连接数。当 socket 被传递给子进程时工作。 回调函数的两个参数是 err 和 count。

server.getConnections((err,count)=>{ // 
    console.log('已经链接'+count+'个用户')
});
server.maxConnections = 2; // 最大连接数
1
2
3
4

# 🌹🌹🌹 close

server.close();
server.on('close',callback);
1
2

# 🌹🌹🌹 unref&ref

unref方法指定发客户端连接被全部关闭时退出应用程序 server.unref();

# 🌹🌹 socket对象

net.Socket代表一个socket端口对象,他是一个双工流

let net = require('net');
let server = net.createServer(function(socket){
  console.log(socket.address()) // 获取客户端地址
  socket.setEncoding('utf8'); // 转码
  socket.on('data',function(data){ // 读文件
    console.log(data);
    socket.write('hello');  // 写,返回到客户端
  })
  socket.on('end',function(){
      console.log('客户都按关闭')
  })
});
server.listen(8080);
```
### 🌹🌹🌹 `socket.address()`

### 🌹🌹🌹 读取数据 
````javascript
let net = require('net')
let server = net.createServer(function(socket){
    socket.setEncoding('utf8');
    socket.on('data',function(data){
        console.log(data);
    })
    socket.on('end',function(){
        console.log('客户都按关闭')
    })
})
server.listen(8000)
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

# 🌹🌹🌹 pipe&unpipe

  • pipe方法可以将客户端发送的数据写到文件或其它目标中 socket.pipe(destinatin,[options]);
  • options.end 设置为false时当客户端结束写操作或关闭后并不会关闭目标对象,还可以继续写入数据
let net = require('net');
let path = require('path');
let ws = require('fs').createWriteStream(path.resolve(__dirname, 'msg.txt'));
let server = net.createServer(function (socket) {
  socket.on('data', function (data) {
      console.log(data);
  });
  socket.pipe(ws, { end: false }); // 还可以继续写入数据
  socket.on('end', function () {
      ws.end('over', function () {
          socket.unpipe(ws);
      });
  });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 🌹🌹🌹 write

像客户端写数据
socket.write(data,[encoding],[callback]);

# 🌹🌹🌹 bufferSize

write的返回值和bufferSize属性值

let net = require('net');
let fs = require('fs');
let path = require('path');
let server = net.createServer({ allowHalfOpen: true }, function (socket) {
    console.log("客户端已经连接");
    socket.setEncoding('utf8');
    let rs = fs.createReadStream(path.resolve(__dirname, '1.txt'), { highWaterMark: 2 });
    rs.on('data', function (data) {
        let flag = socket.write(data);
        console.log("flag:", flag);
        console.log('缓存字节:' + socket.bufferSize); // 
        console.log('已发送字节:' + socket.bytesWritten);
    })
    socket.on('data', function (data) {
        console.log('data', data);
    });
    socket.on('drain', function (err) {
        console.log("缓存区已全部发送")
    });
});
server.listen('8080')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 🌹🌹🌹 setTimeout

let net = require('net');
let path = require('path');
let ws = require('fs').createWriteStream(path.resolve(__dirname, 'msg.txt'));
let server = net.createServer(function (socket) {
    socket.setTimeout(5 * 1000);
    socket.on('timeout', function () {
        socket.end(); // 超时关闭
    });
    //socket.setTimeout(0);取消超时时间的设置
});
server.listen(8080);
1
2
3
4
5
6
7
8
9
10
11

# 🌹🌹🌹 pause&&resume

pause可以暂停data事件触发,服务器会把客户端发送的数据暂存在缓存区里

const net = require('net');
const path = require('path');
let file = require('fs').createWriteStream(path.join(__dirname, 'msg.txt'));
let server = net.createServer(function (socket) {
    socket.pause(); // 暂停
    setTimeout(function () {
        socket.resume();
        socket.pipe(file);
    }, 10 * 1000);
});
server.listen(8080);
1
2
3
4
5
6
7
8
9
10
11

# 🌹🌹🌹 keepAlive

  • 当服务器和客户端建立连接后,当一方主机突然断电、重启、系统崩溃等意外情况时,将来不及向另一方发送FIN包,这样另一方将永远处于连接状态。 可以使用setKeepAlive方法来解决这一个问题
    socket.setKeepAlive([enaable],[initialDelay]);
  • enable 是否启用嗅探,为true时会不但向对方发送探测包,没有响应则认为对方已经关闭连接,自己则关闭连接
  • initialDelay 多久发送一次探测包,单位是毫秒

# 🌹🌹 TCP客户端

# 🌹🌹🌹 创建TCP客户端

const net = require('net');
let socket = new net.Socket(options);

socket.connect(port, host, callback);
----------或者-----------
socket.on('connect', callback);
1
2
3
4
5
6
  • write表示向服务器写入数据
  • end 用于结束连接
  • error 连接发生错误
  • destroy 销毁流
  • close 表示连接关闭成功,hasError=true代表有可能有错误

# 🌹🌹 聊天室

# 🌹🌹🌹 简单版本

通过telnet建立客户端,模拟多用户

let net = require('net');
// 聊天室 
let server = net.createServer();
let client = []; 
server.on('connection',
  function (socket) {
    client.push(socket);
    server.getConnections((err, count) => {
      socket.write(`当前聊天室可以容纳${server.maxConnections},你是当前第${count}人\r\n`);
    })
    socket.setEncoding('utf8');
    socket.on('data', function (data) {
      data = data.replace("\r\n", '');
      client.forEach(s => {
        if (s == socket) return;
        s.write(data);
      });
    });
    socket.on('end', () => {
      client = client.filter(s => s != socket);
    });
  });
// 最大连接数
server.on('close', function () {
  console.log('close');
})
server.maxConnections = 3;
server.listen(3000);

// 服务器关闭
      // server.close(); //不允许新进来的链接 只有调用close时才会触发关闭
      // server.unref(); // 关闭 当饭店最后一个人离开了 就会关闭
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

# 🌹🌹🌹 进阶版本 可以设置用户名,私聊功能

// 指令
// l: 显示所有用户
// b: 我爱你 广播
// s:zs:我爱你
// r:zs
// xxx: 命令不存在
let net = require('net');
let clients = {
  //'唯一的值':{name:'匿名',socket:soclet}
}
let server = net.createServer(function (socket) {
  let key = socket.remoteAddress + socket.remotePort;
  clients[key] =  {
      name: '匿名',
      socket
    }
  server.getConnections(()=>{
    socket.write('欢迎来到聊天室:\r\n');
  });
  socket.setEncoding('utf8');
  socket.on('data',function (data) {
    data = data.replace("\r\n",'');
    let char = data.split(":");
    switch (char[0]) {
      case 'l':
        list(socket); // 把列表显示给当前的用户
        break;
      case 'b': // b:我爱你
        broadCast(key,char[1]);
        break;
      case 's': // s:zs:你好
        private(char[1],char[2],key);
        break;
      case 'r': // r:zs
        rname(socket,char[1],key);
        break;
      default:
        break;
    }
  })
});
// let c = { 'xxx': { name: 'zfpx',socket }, 'qqq': { name: 'zs' }}
function private(username,content,key) {
  let user = Object.values(clients).find(item=>item.name==username);
  user.socket.write(`${clients[key].name}:${content}\r\n`);
}
function broadCast(key,content) {
  for(let k in clients){
    if(k !=key){
      clients[k].socket.write(`${clients[key].name}:${content}\r\n`);
    }
  }
}
function rname(socket,newName,key) {
    clients[key].name = newName;
    socket.write(`恭喜:新的名字${newName}\r\n`);
}
function list(socket) {
  let userList = Object.values(clients).map(item=>item.name).join('\r\n');
  socket.write(`当前用户列表\r\n${userList}\r\n`);
}
server.listen(3000);
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

# 🌹🌹 UDP

# 🌹🌹🌹 UDP服务器

let dgram = require('dgram');
let socket = dgram.createSocket('udp4');
socket.on('message',function(msg,rinfo){
    console.log(msg.toString());
    socket.send('你好',rinfo.port,rinfo.address)
});
socket.bind(41234,'localhost');
1
2
3
4
5
6
7

# 🌹🌹🌹 UDP客户端

let dgram = require('dgram');
let socket = dgram.createSocket('udp4');
socket.send('珠峰',41234,'localhost',function(err,bytes){
    console.log('send');
});
socket.on('message',function(data){
    console.log(data.toString());
})
1
2
3
4
5
6
7
8

# 🌹🌹 软件/工具

  • putty https://www.putty.org/
  • window用户装 科来网络分析系统 http://www.colasoft.com.cn/
  • mac用户装 wireshark https://www.wireshark.org/download.html

# 🌹🌹🌹 telnet

FF FB 1F FF FB 20 FF FB 18 FF FB 27 FF FD 01 FF FB 03 FF FD 03
FF FB 1F window size
FF FB 20 terminal speed
FF FB 18 terminal type
FF FB 27 Telnet Environment Option
FF FD 01 echo
FF FB 03 suppress go ahead
FF FD 03 suppress go ahead
如果不需要這些, 改用RAW模式就可以了
1
2
3
4
5
6
7
8
9

# 🌹🌹🌹 curl

# 🌹 http 和 tcp

# 🌹🌹 基础知识

  • 长链接
  • 管线化
  • URL和URI
  • http协议和tpc

# 🌹 HTT核心模块 todo

# 🌹🌹 HTTP服务器

# 🌹🌹🌹 创建http服务器

###🌹🌹🌹 启动http服务器

# 🌹🌹🌹 关闭HTTP服务器

# 🌹🌹🌹 监听服务器错误

# 🌹🌹🌹 connection

# 🌹🌹🌹 setTimeout

# 🌹🌹🌹 获取客户端请求信息

# 🌹🌹🌹 url模块

# 🌹🌹🌹 发送服务器响应流

# 🌹🌹🌹 模拟http服务

# 🌹🌹🌹 获取客户端请求信息

# 🌹 HTTP客户端 todo

向其他网站请求数据

# http常见的功能 action

# 1 多语言切换

# 2 图片防盗链

# 3 WEB服务器

# 4 代理服务器

正向代理和反向代理 (opens new window)

# 5 虚拟主机

# 6 User-Agent

# 🌹🌹 querystring核心模块

# http实现功能

# 🌹🌹 crypto 加密模块

# 🌹🌹 cache 缓存

  • 强制缓存
  • 对比缓存
  • 浏览器到缓存策略
  • http到缓存策略

# 简单实现

// 304 服务端设置 
// 缓存策略:强制缓存 200 from memory from disk
// 对比缓存 先比一下在走缓存

let http = require('http');
let path = require('path');
let fs = require('fs');
let { promisify} = require('util');
let stat = promisify(fs.stat);
// 静态服务器
// localhost:3000/index.html?a=1
let url = require('url'); // 专门用来处理url路径的
// http://username:password@hostname:port/pathname?query
let server = http.createServer(async function (req,res) {
  let { pathname,query} = url.parse(req.url,true); // 就是将query转化成对象
  let readPath = path.join(__dirname, 'public', pathname);
  try {
  let statObj = await stat(readPath);
  // 根客户端说 10m内别来烦我
  res.setHeader('Cache-Control','max-age=10');
  res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
  
    if (statObj.isDirectory()) {
      let p = path.join(readPath, 'index.html');
      await stat(p);
      // 如果当前目录下有html那么就返回这个文件
      fs.createReadStream(p).pipe(res);
    } else {
      // 是文件 读取对应的文件直接返回即可
      fs.createReadStream(readPath).pipe(res);
    }
  }catch(e){
    res.statusCode = 404;
    res.end(`Not found`);
  }
}).listen(3000);
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

# 进阶实现 http缓存

let http = require('http');
let path = require('path');
let fs = require('fs');
let { promisify} = require('util');
let stat = promisify(fs.stat);
let url = require('url'); 
// 先访问服务器 服务器告诉他10s别来烦我
// 过了10s会继续烦我,先看下一下是否被修改过,如果没改过返回304,客户端从缓存中读取结果
let server = http.createServer(async function (req,res) {
  let { pathname,query} = url.parse(req.url,true);
  let readPath = path.join(__dirname, 'public', pathname);
  try {
  let statObj = await stat(readPath);
  res.setHeader('Cache-Control','no-cache');
    if (statObj.isDirectory()) {
      let p = path.join(readPath, 'index.html');
      let statObj = await stat(p);
      res.setHeader('Last-Modified', statObj.ctime.toGMTString());
      if (req.headers['if-modified-since'] === statObj.ctime.toGMTString()){
        res.statusCode = 304;
        res.end();
        return; // 走缓存
      }
      fs.createReadStream(p).pipe(res);
    } else {
      res.setHeader('Last-Modified', statObj.ctime.toGMTString());
      if (req.headers['if-modified-since'] === statObj.ctime.toGMTString()) {
        res.statusCode = 304;
        res.end();
        return; // 走缓存
      }
      fs.createReadStream(readPath).pipe(res);
    }
  }catch(e){
    res.statusCode = 404;
    res.end(`Not found`);
  }
}).listen(3000);

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

# 终极实现

let http = require('http');
let path = require('path');
let fs = require('fs');
let { promisify} = require('util');
let stat = promisify(fs.stat);
let url = require('url'); 
let crypto = require('crypto');
let server = http.createServer(async function (req,res) {
  let { pathname,query} = url.parse(req.url,true);
  let readPath = path.join(__dirname, 'public', pathname);
  try {
  let statObj = await stat(readPath);
  res.setHeader('Cache-Control','no-cache');
    if (statObj.isDirectory()) {
      let p = path.join(readPath, 'index.html');
      let statObj = await stat(p);
      
      // 我要根据文件内容 生成一个md5的摘要 最耗性能 ,给实体加一个标签
      let rs = fs.createReadStream(p);
      let md5 = crypto.createHash('md5'); // 不能写完相应体在写头
      let arr = [];
      rs.on('data',function (data) {
        md5.update(data);
        arr.push(data);
      });
      rs.on('end',function () {
        let r = md5.digest('base64');
        res.setHeader('Etag', r);
        if (req.headers['if-none-match'] === r ){
          res.statusCode = 304;
          res.end();
          return;
        }
        res.end(Buffer.concat(arr));
      })
    } else {
      let rs = fs.createReadStream(readPath);
      let md5 = crypto.createHash('md5'); // 不能写完相应体在写头
      let arr = [];
      rs.on('data', function (data) {
        md5.update(data);
        arr.push(data);
      });
      rs.on('end', function () {
        let r = md5.digest('base64');
        res.setHeader('Etag', r);
        if (req.headers['if-none-match'] === r) {
          res.statusCode = 304;
          res.end();
          return;
        }
        res.end(Buffer.concat(arr));
      })
    }
  }catch(e){
    res.statusCode = 404;
    res.end(`Not found`);
  }
}).listen(3000);

// 强制缓存 Cache-Control Expires
// Last-Modified if-modified-since
// Etag if-none-match
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

# 🌹🌹 yargs

# 🌹🌹 proxy 代理

# 🌰 反向的例子

// 正向代理 科学上网 反向代理 nginx
// 正向代理 我们客户端配置的
// 反向代理 cdn 实现
// 反向代理 webpack proxyTable 典型的反向代理
// ngix 典型的反向代理 
// 虚拟主机 www.zf1.cn  localhost:3001 
// 虚拟主机 www.zf2.cn  localhost:3002 
// http-proxy http-proxy-middleware

let http = require('http');
let httpProxy = require('http-proxy');
let proxyServer = httpProxy.createProxy();
let map = {
  'www.zf1.cn':'http://localhost:3001',
  'www.zf2.cn':'http://localhost:3002'
}
http.createServer(function (req,res) {
  let target = req.headers['host'];
  proxyServer.web(req,res,{
    target: map[target]
  }); // 将代理的网站的结果 返回给我自己的服务
}).listen(80);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 🌰 正向到栗子 虚拟主机

// 我们先访问自己的服务器 80
// 我们的服务器其实是想代理到3001的服务器上
// 80 不能直接访问3001
// 代理服务器可能需要加一个凭证才能访问3001
// 80 才能访问3000

let http = require('http');
let proxy = require('http-proxy');
let httpProxy = proxy.createProxyServer();
http.createServer(function (req,res) {
  httpProxy.on('proxyReq', function (proxyReq, req, res, options) {
    proxyReq.setHeader('key', 'zfpx');
  });
  httpProxy.web(req,res,{
    target:'http://localhost:3001'
  });
}).listen(80);
// 这个是我们的服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 🌹🌹 gzip 压缩

# zlib包

// 服务端要启用压缩
//Content-Encoding: gzip 服务器返回的头 告诉客户端我用gzip压缩过了
//Accept-Encoding: gzip, deflate, br 浏览器和服务器说 我支持这几种压缩格式

let zlib = require('zlib'); // 专门做压缩的包
let fs = require('fs');
let path = require('path');
function gzip(filePath) {
  let transform = zlib.createGzip();
  fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(filePath+'.gz'));
}
 gzip('1.txt');

function gunzip(filePath) {
  let transform = zlib.createGunzip();
  fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(path.basename(filePath,'.gz')));
}
//gunzip('1.txt.gz');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 自己实现一个http压缩

let http = require('http');
let fs = require('fs');
let path = require('path');
let zlib = require('zlib');
http.createServer(function (req,res) {
  if(req.url === '/download'){
    res.setHeader('Content-Disposition', 'attachment' )
    return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
  }
  let rule = req.headers['accept-encoding'];
  if(rule){
    if(rule.match(/\bgzip\b/)){
      res.setHeader('Content-Encoding','gzip');
      fs.createReadStream(path.join(__dirname, '1.html'))
      .pipe(zlib.createGzip())
      .pipe(res);
    } else if (rule.match(/\bdeflate\b/)){
      res.setHeader('Content-Encoding', 'deflate');
      fs.createReadStream(path.join(__dirname, '1.html'))
        .pipe(zlib.createDeflate())
        .pipe(res);
    }else{
      fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
    }
  }else{
    fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
  }
}).listen(3000);
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

# 🌹🌹 safe 防盗链

let http =  require('http');
let fs = require('fs');
let url = require('url');
let path = require('path');
// 这是百度的服务器
let whiteList = ['www.zf1.cn:3000'];
let server = http.createServer(function (req,res) {
  let { pathname } = url.parse(req.url);
  let realPath = path.join(__dirname,pathname);
  fs.stat(realPath,function(err,statObj) {
    if(err){
      res.statusCode = 404;
      res.end();
    }else{
      let referer = req.headers['referer'] || req.headers['referred'];
      if(referer){
        let current = req.headers['host'] // 代表的是当前图片的地址
        referer = url.parse(referer).host// 引用图片的网址
        if (current === referer || whiteList.includes(referer)){
          fs.createReadStream(realPath).pipe(res);
        }else{
          fs.createReadStream(path.join(__dirname,'images/2.jpg')).pipe(res);
        }
      }else{
        fs.createReadStream(realPath).pipe(res);
      }
    }
  })
}).listen(3000);
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

# 🌹🌹 language 多语言

// Accept-Language: zh-CN,zh;q=0.9
// 特点是多个语言用 , 分割 权重用=分割 没有默认权重为1
let langs = {
  en:  'hello world',
  'zh-CN':'你好世界',
  zh:'你好',
  ja: 'こんにちは、世界'
}
// 
let defualtLanguage = 'en'
// 多语言方案  服务端来做 (浏览器会发一个头) 前端来做  通过url实现多语言
let http = require('http');
http.createServer(function (req,res) {
    let lan = req.headers['accept-language'];
    //[[zh,q=0.9],[zh-CN]] =>[{name:'zh-CN',q=1},{name:'zh',q:0.9}]
    if(lan){
      lan = lan.split(',');
      lan = lan.map(l=>{
        let [name,q] = l.split(';');
        q = q?Number(q.split('=')[1]):1 
        return {name,q}
      }).sort((a,b)=>b.q-a.q); // 排出 权重数组

      for(let i = 0 ;i <lan.length;i++){
        // 将每个人的名字 取出来
        let name= lan[i].name;
        if(langs[name]){ //去语言包查找 查找到就返回
          res.end(langs[name]);
          return;
        }
      }
      res.end(langs[defualtLanguage]); // 默认语言
    }else{
      res.end(langs[defualtLanguage]); // 默认语言
    }
}).listen(3000);
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

# 自己实现一个http-server包

// 要实现打开网页后可以将目录下的内容展示给用户,海可以进行对应的点击操作
// 缓存,gzip压缩 , 范围请求....
// 提示用户的命令
// 发包
let http = require('http');

let util = require('util');
let url = require('url');
let zlib = require('zlib');
let fs = require('fs');
let path = require('path');
//第三方
let ejs = require('ejs'); // 模板引擎
let template = fs.readFileSync(path.join(__dirname, 'template.html'), 'utf8');

let chalk = require('chalk'); // 粉笔
let mime = require('mime');   // 类型模块
// 需要 当你电脑上有 环境变量 DEBUG=hello时 才会输出对应的内容
let debug = require('debug')('hello'); // 调试模块
// 在写代码的时候 可能会打印出很多日志 发包后不希望日志被打印
let config = require('./config');

let stat = util.promisify(fs.stat);
let readdir = util.promisify(fs.readdir);
class Server {
  constructor(command) { // 用用户在命令行种输入的内容进行显示
    this.config = { ...config, ...command};
    this.template = template;
  }
  async handleRequest(req, res) {
    let { dir } = this.config; // 需要将请求的路径和dir拼接在一起
    // http://localhost:3000/index.html
    let { pathname } = url.parse(req.url);
    if (pathname === '/favicon.ico') return res.end();
    let p = path.join(dir, pathname);
    try {
      // 判断当前路径是文件 还是文件夹
      let statObj = await stat(p);
      if (statObj.isDirectory()) {
        // 读取当前访问的目录下的所有内容 readdir 数组 把数组渲染回页面
        res.setHeader('Content-Type', 'text/html;charset=utf8')
        let dirs = await readdir(p);
        dirs = dirs.map(item=>({
          name:item,
          // 因为点击第二层时 需要带上第一层的路径,所有拼接上就ok了
          href:path.join(pathname,item)
        }))
        let str = ejs.render(this.template, {
          name: `Index of ${pathname}`,
          arr: dirs
        });
        res.end(str);
      } else {
        this.sendFile(req, res, statObj, p);
      }
    } catch (e) {
      debug(e); // 发送错误
      this.sendError(req, res);
    }
  }
  /**
   * 此方法用来处理用户缓存
   */
  cache(req, res, statObj, p){
    res.setHeader('Cache-Control','no-cache');
    res.setHeader('Expires', new Date(Date.now() + 10 * 1000).getTime());
    // 比较etag ctime+size 比较last-modified ctime
    // 服务端想要设置的
    let etag = statObj.ctime.getTime()+'-'+statObj.size;
    let lastModified = statObj.ctime.getTime();
    // 把这两个参数设置给客户端
    res.setHeader('Etag', etag);
    res.setHeader('Last-Modified', lastModified);
    // 客户端把上次设置的带过来的
    let ifNoneMatch = req.headers['if-none-match'];
    let ifModifiedSince = req.headers['if-modified-since'];
    // 有其中任何一个不生效就不生效
    if (etag !== ifNoneMatch && lastModified !== ifModifiedSince){
      return false
    }
    return true;
  }
  gzip(req, res, statObj, p){
    let encoding = req.headers['accept-encoding'];
    if(encoding){
      if(encoding.match(/\bgzip\b/)){
        res.setHeader('Content-Encoding','gzip')
        return zlib.createGzip();
      }
      if (encoding.match(/\bdeflate\b/)) {
        res.setHeader('Content-Encoding', 'deflate')
        return zlib.createDeflate();
      }
      return false;
    }else{
      return false
    }
  }
  // 范围请求
  range(req, res, statObj, p){
    let range  = req.headers['range'];
    if(range){
    let [,start,end] = range.match(/bytes=(\d*)-(\d*)/);
    start = start? Number(start):0;
    end = end? Number(end):statObj.size - 1;
      res.statusCode = 206;
      res.setHeader('Content-Range', `bytes ${start}-${end}/${statObj.size - 1}`);
      fs.createReadStream(p,{start,end}).pipe(res);
    }else{
      return false;
    }
  }
  sendFile(req, res, statObj, p) {
    // 设置缓存,如果文件以及打开过了 要下一次多少秒内不要再次访问了
    // 下次再访问服务器的时候 要使用对比缓存
    if (this.cache(req, res, statObj, p)){
      res.statusCode = 304;
      return res.end();
    }
    if (this.range(req, res, statObj, p)) return
    res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8');
    let transform = this.gzip(req, res, statObj, p)
    if (transform){ // 返回一个压缩后的压缩流
      return fs.createReadStream(p).pipe(transform).pipe(res);
    }
    fs.createReadStream(p).pipe(res);
  }
  sendError(req, res) {
    res.statusCode = 404;
    res.end(`Not found`);
    this.start();
  }
  start() {
    let server = http.createServer(this.handleRequest.bind(this));
    server.listen(this.config.port, this.config.host, () => {
      console.log(`server start http://${this.config.host}:${chalk.green(this.config.port)}`);
    });
  }
}
module.exports = Server;
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

# cookie-session到实现

Last Updated: 1/7/2020, 7:42:12 AM
    asphyxia
    逆时针向