6. 网络&http-server

383 阅读11分钟

OSI 七层模型(是理想化网络模型)

  1. 应用层
  2. 表示层
  3. 会话层 实际中 5.6.7 归成应用层
  4. 传输层 TCP UDP 协议
  5. 网络层 路由器
  6. 数据链路层 交换机 网卡
  7. 物理层 物理设备 网线 光纤

实际五层

  • 实际应用中会将会话层、表示层、应用层合并在一起为应用层
  1. 用户要传递的数据 装包 HTTP DNS
  2. 传输层 用户要传递很大的数据包 会对大的数据包进行拆分并标明序号 端口号用于传输到哪 TCP、UDP
  3. 网络层 对数据再次进行包装 ip 地址 如果传输层的数据还是非常大 网络层会再次进行分包 IP 协议
  4. 数据链路层 再次包装成
  5. 物理层 这两层涉及到的是硬件

五层模型(统称为 TCP/IP 协议 协议簇)

  • 能称为协议的都在数据链路层之上,所以在网络层、传输层、应用层才有协议
  • 协议的功能就是把数据按照某个规范去封装,再把数据进行传输(协议就是对数据的封装+传输)
  • 网络层
    • ip 协议 寻址
    • ARP 协议 从 ip 地址获取 mac 地址(局域网下)
  • 传输层:TCP、UDP
  • 应用层: http 超文本传输协议、 DNS 域名解析成 ip 地址、 FTP 文件传输协议

DNS 协议

  • DNS 服务器进行域名和对应 ip 地址转换的服务器

IP 协议

  • 寻址 通过 ip 地址定位到最终设备

ARP协议

  • 怎么通过ip地址找到mac地址的
    • ARP协议:目的就是通过ip地址找到mac地址 (在局域网下)
    • 会广播给每一个,对应到要找的IP地址会做出响应,将mac地址返回
    • ARP会有缓存记录 有记录就不用广播 缓存的是 ip:mac
    • 交换机缓存的是 mac:端口

TCP 协议

  • 传输控制协议 特点是可靠、传输效率低
  • TCP 提供全双工服务,即数据可在同一时间双向传播
  • 数据是无序的在网络间传递,接收方需要有一种算法在接收到数据后恢复原有的顺序
  • 三次握手
    • 我能主动给你打电话吗
    • 可以啊 那我也能给你打电话吗
    • 可以 建立连接成功
  • 四次挥手
    • 我们分手吧
    • 回复收到分手的信息
    • 好吧 分手
    • 行 那就到这
  • 小结
    • TCP 是双工的,所以握手需要三次,保证双方达成一致(建立连接浪费性能)
    • 当断开连接时,发送(FIN)时另一方需要马上回复(ACK),但此时可能不能立即关闭(有未发送完的数据,还有一些准备断开的操作),所以等待确认可以关闭时再发送(FIN)
  • 滑动窗口
    • 窗口大小是以字节为单位
    • 建立TCP连接时,接收方会告知自己的窗口可以接收多少字节的数据
    • 滑动窗口的目的是实现丢包重传,并且将数据有效的发送给接收方,而且可以知道每次发送多少,从而做到流量控制
    • 所以滑动窗口的核心是控制流量
    • TCP会做什么:传输的时候会做一个滑动窗口,不停地在协商滑动窗口的大小,如果值满了就停止传输,不停地发探测包,如果有了可用窗口大小,接着去传输数据。按照顺序,成功接收到一个数据之后滑动窗口会向后移动,并根据协商的结果修改窗口大小
  • 慢启动、拥塞避免、快重传、快恢复
    • 也是用来做流量控制的
    • 慢开始 超时
    • 快重传 不等超时 三次确认就立马重发

http

  • 发展历程
    • http/0.9 传输过程中没有请求头和请求体 内容采用ASCii字符流传输html
    • http/1.0 加了请求头和响应头 实现多类型数据传输
    • http/1.1
      • 持久连接 一个TCP连接上可以传输多个http请求
      • 管线化方式 每个域名最多维护6个TCP持久连接 有队头阻塞问题
      • 引入客户端cookie机制,安全机制等
    • http/2.0
      • 采用多路复用机制 一个域名使用一个TCP长链接
      • 头部压缩
      • 服务端推送
    • http/3.0 基于UDP
  • 当访问网页,发生了哪些事情
    • 客户端发送一个请求 发送的是一个域名 域名会发送给dns服务器
    • dns解析出一个ip给到客户端
    • http会把数据进行传输,http协议生成针对目标web服务器的http请求报文
    • 交给TCP进行传输:为了方便通信,将http请求报文按序号分割成报文段,把每个报文段可靠的传给对方(丢包或者超时就重发)
    • (TCP自身没有传输能力,如何传靠的是网络层)ip协议:寻址和路由 找到对方地址,通过mac地址一个个找到下一站,不停中转
    • 从对方接收到TCP报文段, 对报文段按照原来的序号进行重组报文
    • http解析传递过来的数据,对内容进行处理
    • 处理完成之后再增加一些响应信息,同样利用TCP/IP通信协议向用户进行回传

HTTP是不保存状态的协议, 使用cookie来管理状态

为了防止每次请求都进行tcp链接的建立和断开, 采用保持链接的方式keep-alive

以前发送请求后需要等待并收到响应,才能发下一个,现在都是管线化方式

http应用

  • http也是封装了一些信息和传输
  • curl -v www.baidu.com
  • http分为两部分 (发送请求 client)req -> (接收请求 server)res
  • 请求分为三部分(三部分都可以传输数据)
    • 请求行 通过方法 路径(传输的数据有限制 url大小限制)
      • 如 GET / 版本号
    • 请求头 放一些自定义信息 约定的信息 请求头不要过大
    • 请求体 传输的数据
  • 响应也分为三部分
    • 响应行
      • 如 版本号 200 ok
    • 响应头
    • 响应体
  • restFul风格
    • GET/POST/PUT/SELETE/OPTIONS
    • OPTIONS请求代表的是跨域访问时可能会出现 预检请求 试探请求 如果对方确认后 可以发真实的请求
    • OPTIONS请求只在复杂请求的状态下才能发送(get、post都是简单请求,如果增加了自定义header那么就是复杂请求)
    • OPTIONS可以定义发送的时间间隔
  • 常见的状态码
    • 可以自己设定 但是浏览器和服务器之间是有一些约定的
    • 1xx 服务器收到了信息 等到浏览器后续要做的事 websocket
    • 2xx 成功
    • 3xx 重定向 缓存
    • 4xx 客户端出错 (浏览器参数、或者服务器无法解析客户端参数)
    • 5xx 服务器错误
    • 200 请求成功
    • 204 请求成功 但是无响应内容
    • 206部分内容 分段请求
    • 301 302 永久重定向和临时重定向
    • 304缓存
    • 400 客户端请求错误
    • 401 权限问题 当前用户没登录 无权限观看
    • 403 登陆了 但是还是没有权限
    • 404 找不到
    • 405 服务器只支持get、post 但是发送了put请求 服务器就会响应找不到此方法
    • 500 请求服务解析出错了 无法完成响应
    • 502 服务期收到的内容无效
    • 503 负载均衡挂了
const http = require("http");

// 下面两种写法是等价的
const server = http.createServer(function(req,res) {
    console.log("request")
});
server.on('request', function(req,res) {
    console.log("request")
});

let port = 3000;

server.listen(3000, function()  {
    console.log('server listen on 3000')
});

// 监听错误
server.on("error", function(err){
    if(err.code === 'EADDRINUSE') { // 说明端口号被占用
        server.listen(++port);// 不用再写回调 监听成功之后会走到上面对应的回调,打印server listen on 3000
    }
})
  • http模块对req、res的封装
const server = http.createServer(function(req, res) {
    // req是一个可读流

    // 请求行
    console.log(req.method); //大写的
    console.log(req.url); 
    console.log(req.httpVersion);

    // 请求头
    console.log(req.headers); // 统一node处理后全部都是小写

    // 请求体
    const arr = [];
    req.on("data", function(data) {
        console.log(data);
        arr.push(data);
    });
    req.on("end", function(){
        const data = Buffer.concat(arr).toString();
        console.log("end", data);
    })
})

const server = http.createServer(function(req, res) {
    // res 是一个可写流

    res.statusCode = 202;
    res.statusMessage = "my 202";
    res.setHeader("token", "ok");
    res.write("1");
    res.end("2");
});

实现静态服务

const http = require("http");

// url组成: 协议 ://(用户名:密码)域名:端口号/资源路径?查询参数#hash
const url = require("url");
// 参数加上true 将query转成对象形式
// const {pathname,query} = url.parse("http://username:password@www.zz.com:3000/xxx?a=1#hash", true);
// pathname => /xxx

const mime = require("mime");

const server = http.createServer((req,res) => {
  const {pathname} = url.parse(req.url, true);
  // 根据路径来读取文件 /public/index.html
  const filePath = path.join(__dirname, pathname); // 获取绝对路径
  fs.readFile(filePath, function(err, data) {
    if(err) {
      res.statusCode = 404;
      return res.end('not found');
    }
    // 直接返回 浏览器可以正常展示 是因为在html中设置了 <meta charset="UTF-8">
    // 如果浏览器不给编码 浏览器显示就会乱码 
    // 所以浏览器有可能不加编码 服务器在返回数据的时候就需要添加编码格式
    res.setHeader("Content-Type", mime.getType(filePath) + ";charset=utf8");
    res.end(data);
  })
});

server.listen(3000);

  • 写一个静态服务器
    • 官方:npm i http-server -g 可以在本地启动一个服务
    • 使用:命令行 hs 或者 http-server 默认会找public文件夹
// package.json
{
    "name": "zhuhaha-server-listen",
    "version":"1.0.0",
    "description": "",
    "main":"1.http.js",
    "bin":{
        "zsl": "./bin/www"
    }
}
/*
这个包默认是无法使用的,要把它放到全局下进行测试
使用 npm link
会生成软链 一个叫做 zhuhaha-server-listen 一个叫 zsl
*/
www文件中:
#!/usr/bin/env node

重新 npm link --fore
  • 服务器要支持: 使用commander模块
    • --port 改端口号
    • --directory 制定以哪个目录为基准
    • --help 命令
    • --version
    • --useage
  • 模版引擎的实现
    • with + new Function
    • ejs.renderFile(path.resolve(__dirname, 'tmpl.html'), {arr:[1,2,3,4,5]}, {async: true}).then(data =>. console.log(data))
  • 命令行配置的实现 -- commander模块
// www  对命令行做一些配置
const {program} = require("commander");
const version = require("../package.json").version;
const Server = require("../src/main.js");
const config  =  {
    'port': {
        option: '-p,--port <n>',
        description: 'set server port',
        default: 8080,
        usage: 'zsl --port <n>'
    },
    'directory': {
        option: '-d,--directory <n>',
        description: 'set server directory',
        default: process.cwd(),
        usage: 'zsl -d D:'
    }
}

program.version(version)  // 版本
.name("zsl")
.usage("[options]")

const usages =  [];
Object.entries(config).forEach(([key, value]) => {
    usages.push(value.usage);
    program.option(value.option,  value.description,  value.default)  // 配置选项
})

program.on("--help", function() { // 使用示例
    console.log("\nExamples:")
    usages.forEach(usage => console.log("  " + usage))
})

program.parse(process.argv); // 解析命令行参数

let ops = program.opts(); // 得到参数对象
console.log(ops);

let server = new Server(ops);
server.start(); // 开启服务


  • 模版引擎的实现
// 自实现模版引擎
async function renderFile(filePath, data) {
    let tmplStr = await fs.readFile(filePath, 'utf8');
    let myTemplate = `let str = ''\r\n`;
    myTemplate += 'with(obj){'
    myTemplate +=  'str+=`'
    tmplStr = tmplStr.replace(/<%=(.*?)%>/g, function() {
        return '${' + arguments[1] + '}';
    })
    myTemplate += tmplStr.replace(/<%(.*)%>/g, function() {
        return '`\r\n' + arguments[1] + '\r\nstr+=`'
    })
    myTemplate += '`\r\n return str \r\n}';
    let fn = new Function('obj', myTemplate);

    return fn(data);
}

async function render(tmplStr, data) {
    let myTemplate = `let str = ''\r\n`;
    myTemplate += 'with(obj){'
    myTemplate +=  'str+=`'
    tmplStr = tmplStr.replace(/<%=(.*?)%>/g, function() {
        return '${' + arguments[1] + '}';
    })
    myTemplate += tmplStr.replace(/<%(.*)%>/g, function() {
        return '`\r\n' + arguments[1] + '\r\nstr+=`'
    })
    myTemplate += '`\r\n return str \r\n}';
    let fn = new Function('obj', myTemplate);

    return fn(data);
}
  • 模版
// tmpl.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <%dirs.forEach(item =>%>
        <a href="<%=item.url%>"><%=item.dir%></a>
    <%)%>
</body>
</html>
  • 搭建静态http服务
// src/main.js
const http = require("http");
const fs = require("fs").promises;
const path = require("path");
const url = require("url");
const mime = require("mime");
const os = require("os");
const chalk = require("chalk");
const zlib = require("zlib");
const crypto. = require('crypto');
const {createReadStream, readFileSync} = require("fs");

let address = Object.values(os.networkInterfaces()).flat().find(item => item.family == 'IPv4').address;

const template = readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8');

module.exports = class MyServer{
    constructor(opts={}) {
        this.port = opts.port;
        this.directory = opts.directory;
        this.address = address;
        this.template = template;
    }

    handleRequest = async (req, res) => {
        console.log(this);
        // 请求到来的时候 需要监控路径 看一下路径是否是文件 如果是文件 直接将文件返回 如果不是文件则读取文件中的目录
        let {pathname} = url.parse(req.url);
        pathname = decodeURIComponent(pathname); // 对路径中的中文进行转译 保证正常显示
        let filePath = path.join(this.directory, pathname); // 文件绝对路径  在当前执行目录下进行查找
        try{
            let statObj =  await fs.stat(filePath);
            if(statObj.idDirectory()) {
                const dirs = await fs.readdir(filePath);
                // 通过模版引擎的方式来实现
                let content = await render(template, {
                    dirs: dirs.map(dir => ({
                        url: path.join(pathname, dir),
                        dir
                    }))
                });
                res.setHeader("Content-Type", "text/html;charset=utf-8");
                res.end(content);
            }else{
                // 文件
                this.sendFile(req,res,statObj,filePath);
            }
        }catch(e) {
            // 文件不存在则发生错误
            this.sendError(res, e);
        }
    }
    gzip(req,res){
        // 在发送前要进行压缩处理
        // 浏览器在请求头重会告知:Accept-Encoding:gzip,deflate,br
        // node默认只支持gaip和deflate
        // 服务器会在响应头告知:content-encoding:gzip 通过gzip压缩
        let encoding = req.headers['accept-encoding'];
        let zip;
        if(encoding){
            // 说明支持压缩
            let ways = encoding.split(",");
            for(let i = 0; i < ways.length; i++){
                if(ways[i] === 'gzip'){
                    res.setHeader('content-encoding', 'gzip'); 
                    zip = zlib.createGzip();
                    break;
                }else if(ways[i] === 'deflate'){
                    res.setHeader('content-encoding', 'deflate');
                    zip = zlib.createDeflate();
                    break;
                }
            }
        }
        return zip;
    }
    cache(req, res, statObj, filePath){
        // 在发送文件之前可以要求此文件以后多少时间内不要再来访问了
        // 只针对引用的资源 首次访问的资源不会被设置
        // 即强制缓存 直接访问的页面是不能被缓存的
        // res.setHeader('Cache-Control', 'max-age=10'); // s单位,10s内引用的其他资源不要再访问了,Cache-Control会比Expires优先级更高一些
        // res.setHeader('Expires', new Date(Date.now()+10*1000).toGMTString()); // 设置一个到期时间戳,这个设置项是为低版本浏览器设置的
        
        res.setHeader('cache-control', 'no-cache'); // 表示每次都来服务器询问 依旧会将文件放入缓存当中;no-store是每次都来询问服务器,还不缓存
        // 协商缓存
        // 有的文件可能10s后还是没有变 希望对比一下 如果文件没变 继续去缓存中找
        // last-modified 服务器告诉浏览器 次文件最后修改时间是多少
        // if-modified-since 浏览器下次访问的时候带过来的
        console.log(req.headers['if-modified-since']); // 服务器设置了之后,才会打印出值
        const ifModifiedSince = req.headers['if-modified-since'];
        const ctime = statObj.ctime.toGMTString();
        res.setHeader('last-modified', )
        // if(ifModifiedSince !== ctime){
            // return false;
        // } 
        // 根据最后修改时间 可能会出现变化后但是内容没变 或者如果1s内多次变化也监控不到 因为缓存时间的单位是秒
        // tag 根据内容来生成唯一的标识 
        // md5摘要 不可逆的 无法判断原来的值 特点是相同内容摘要出的结果是相同的,所以可以去撞库来反推原来的结果是什么
        // 服务器提供 etag   浏览器提供 if-none-match
        const ifNoneMatch = req.headers['if-none-match'];
        // 真正开发的时候不会去读文件,一般会采用文件的某一部分或者计算文件的大小 总长度
        const etag = crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('base64');
        res.setHeader('ETag', etag);
        if(ifNoneMatch != etag){
            return false;
        }
        return true;
    }
    
    sendFile(req,res,statObj,filePath) {
        // 缓存 
        if(this.cache(req,res,statObj,filePath)) {
            res.statusCode = 304;
            return res.end();
        }
        res.setHeader("Content-Type", (mime.getType(filePath) || "text/plain") + ";charset=utf-8")
        // 之前文件没有经过压缩 读取之后直接返回给浏览器
        // createReadStream(filePath).pipe(res); // 异步方法
        let zip = this.gzip(req,res);
        if(zip){ // 支持压缩就压缩
            createReadStream(filePath).pipe(zip).pipe(res);
        }else{
            createReadStream(filePath).pipe(res);
        }
    }
    sendError(res, e) { 
        console.log('err', e);
        res.statusCode = 404;
        res.end("not found");
    }
    start() {
        console.log('start  ' + this.port  + " " + this.directory);
        const server = http.createServer(this.handleRequest);
        server.listen(this.port, () => {
            console.log(`${chalk.yellow('starting up http-server, seerving:')}` + this.directory);
            consolee.log(`  http://${address}:${chalk.green(this.port)}`);
            consolee.log(`  http://127.0.0.1:${chalk.green(this.port)}`)
        })
    }
}

/**
如果是文件 就通过流的方式将文件内容读取之后返回给浏览器
如果读取的是文件夹 就通过模版引擎将文件夹下面的文件或者子目录渲染出来,返回浏览器,当点击相应的子目录,显示相应的内容
*/
// 至此 静态服务就基本实现了

压缩

  • gzip压缩 主要的压缩方式是替换 重复率越高压缩就越有效果
const zlib = require("zlib");
zlib.createGzip // 流的方式 读一点操作一点
zlib.gzip()  // 非流的方式


zlib.gzip(fs.readFileSync(path.resolve(__dirname, '1.txt')), function(err,data){
    fs.writeFileSync('1.txt.gz', data)
})
  • 流的四种方式
    • 可读流 on('data') on('end')
    • 可写流 write end
    • 双工流 能读能写
    • 转化流 将数据进行转化如压缩 加密等
// 标准输入  读取用户的输入
process.stdin.on('data', function(chunk){ //能读
    console.log(chunk); // 会监听到用户在命令行输入的内容,
})

process.stdin.on('data', function(chunk){
    process.stdout.write(chunk); // 能写 相当于 console.log
})

// 这两串的简化写法就是:
process.stdin.pipe(process.stdout)


// 如果想实现输入小写输出转大写,就会用到转化流
const {Transform} = require('stream');
class MyTransform extends Transform{
    _transfrom(chunk, encoding, clearBuffer){ // 参数和可写流一样
        this.push(chunk.toString().toUpperCase());
        clearBuffer();
    }
}
let transform = new MyTransform();
process.stdin.pipe(transform).pipe(process.stdout);
  • 在静态服务中 发送文件给浏览器之前先进行压缩

缓存

  • 强制缓存
    • Cache-Control: 'max-age=10'
    • Expires 设置到期时间戳
  • 协商缓存
    • last-modified 服务器返给客户端的
    • if-modified-since 客户端发请求时会带上的
  • md5的特点
    • 不可逆
    • 相同的内容出来的结果相同
    • 不同的内容 结果完全不同
    • 摘要后的长度都是一致的
    • const r = crypto.createHash('md5').update('内容').digest('base64'); 创建md5摘要
    • 服务器提供 etag
    • 浏览器提供 if-none-match