likes
comments
collection
share

了解Nodejs API,写一个web静态服务器脚手架

作者站长头像
站长
· 阅读数 45

通信必要条件

  • 主机之间需要有传输介质(网线,光纤等,建立物理连接)
  • 主机上必须有网卡设备(信号的调制与解调制,数字信号和电信号的转换)
  • 主机之间需要协商网络速率。

网络通讯方式

  • 交换机通讯,局域网中的主机通过交换机来进行通信。局域网存在大量主机会造成广播风暴。
  • 路由器通讯,不同局域网之间的主机进行通讯,需要通过ip地址查找到对应的局域网段。

了解Nodejs API,写一个web静态服务器脚手架

OSI 七层模型

  • 应用层:用户与网络的接口

  • 表示层:数据加密、转换、压缩

  • 会话层:控制网络连接建立与终止

  • 传输层:控制数据传输可靠性

  • 网络层:确定目标网络

  • 数据链路层:确定目标主机

  • 物理层:各种物理设备和标准

了解Nodejs API,写一个web静态服务器脚手架

具体内容可以看这里

TCP通信

粘包

了解Nodejs API,写一个web静态服务器脚手架 通过延时发送或者封包解包来解决粘包问题。

// server.js
const net = require('net')

// 创建服务端实例
const server = net.createServer()

const PORT = 1234
const HOST = 'localhost'

server.listen(PORT, HOST)

server.on('listening', () => {
  console.log(`服务端已经开启在 ${HOST}: ${PORT}`)
})

// 接收消息 回写消息
server.on('connection', (socket) => {
  socket.on('data', (chunk) => {
    const msg = chunk.toString()
    console.log(msg)

    // 回数据
    socket.write(Buffer.from('您好' + msg))
  })
})

server.on('close', () => {
  console.log('服务端关闭了')
})

server.on('error', (err) => {
  if (err.code == 'EADDRINUSE') {
    console.log('地址正在被使用')
  }else{
    console.log(err)
  }
})
// client.js
// 基于流的tcp通信

const net = require('net')

// 连接服务端
const client = net.createConnection({
  port: 1234, 
  host: '127.0.0.1'
})

// sleep
async function sleep() {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, 1000);
  })
}

client.on('connect', async () => {
  client.write('昊淼您好')
  // 出现连包
  // client.write('昊淼您好2')
  // client.write('昊淼您好3')
  // client.write('昊淼您好4')
  /**
   * 解决方案
   * 1. 延时发送
   */
  await sleep()
  client.write('昊淼您好2')
  await sleep()
  client.write('昊淼您好3')
  await sleep()
  client.write('昊淼您好4')
})

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

client.on('error', (err) => {
  console.log(err)
})

client.on('close', () => {
  console.log('客户端断开连接')
})

封包和解包

了解Nodejs API,写一个web静态服务器脚手架

数据传输过程

  • 进行数据编码,获取二进制数据包
  • 按照规则拆解数据,获取指定长度的数据。

读取数据

  • writeInt16BE 将value从指定位置写入
  • redInt16BE 从指定位置开始读取数据
class MyTransformCode{
  constructor() {
    // header总长度
    this.packageHeaderLen = 4
    // 包编号
    this.serialNum = 0
    // 消息体的长度
    this.serialLen = 2
  }

  // 编码
  encode(data, serialNum) {
    // 将数据转为二进制
    const body = Buffer.from(data)

    // 01 先按照指定的长度来申请一片内存空间做为 header 来使用
    const headerBuf = Buffer.alloc(this.packageHeaderLen)

    // 02 将数据写入buffer
    headerBuf.writeInt16BE(serialNum || this.serialNum)
    // 写入消息的长度
    headerBuf.writeInt16BE(body.length, this.serialLen)

    if (serialNum == undefined) {
      this.serialNum++
    }

    return Buffer.concat([headerBuf, body])
  }

  // 解码
  decode(buffer) {
    // 取出消息头(消息长度和消息编号)
    const headerBuf = buffer.slice(0, this.packageHeaderLen)
    // 取出消息体
    const bodyBuf = buffer.slice(this.packageHeaderLen)

    return {
      // 消息编号
      serialNum: headerBuf.readInt16BE(),
      // 消息体长度
      bodyLength: headerBuf.readInt16BE(this.serialLen), // 可以指定从哪个位置开始读取
      body: bodyBuf.toString()
    }
  }

  // 获取包长度的方法
  getPackageLen(buffer) {
    if (buffer.length < this.packageHeaderLen) {
      return 0
    } else {
      return this.packageHeaderLen + buffer.readInt16BE(this.serialLen)
    }
  }
}

module.exports = MyTransformCode

node充当静态web服务器

node充当静态web服务器

对于我们开发来说,有很多现成的插件,例如live server。

创建一个npm工程,并指定bin属性,配置脚手架入口文件及脚手架下载后的使用名称。例如这里配置的是hmserve,后面下载完脚手架后,就可以直接使用hmserve去运行对应的命令了。

{
  "name": "hmserve",
  "version": "1.0.0",
  "description": "",
  "main": "main.js",
  "bin": {
    "hmserve": "bin/www.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "commander": "^6.0.0",
    "ejs": "^3.1.5",
    "mime": "^2.4.6"
  }
}

在脚手架入口文件的开头加上#! /usr/bin/env node表示当前直接运行时去环境变量中查找node,并执行当前文件。

我们使用commander去解析输入的命令行参数。

#! /usr/bin/env node

const {program} = require('commander')


// 配置信息,配置默认参数值
let options = {
  '-p --port <dir>': {
    'description': 'init server port',
    'example': 'hmserve -p 8888'
  },
  '-d --directory <dir>': {
    'description': 'init server directory',
    'example': 'hmserve -d c:'
  }
}

function formatConfig (configs, cb) {
  Object.entries(configs).forEach(([key, val]) => {
    cb(key, val)
  })
}

// 注册配置选项
formatConfig(options, (cmd, val) => {
  // 配置options
  program.option(cmd, val.description)
})

// 增加-h 时的详细信息。本身会输出使用方式和注册的options
/**
 * Usage: hmserve [options]
  Options:
    -p --port <dir>       init server port
    -d --directory <dir>  init server directory
    -V, --version         output the version number
    -h, --help            display help for command
 */
program.on('--help', () => {
  console.log('Examples: ')
  formatConfig(options, (cmd, val) => {
    console.log(val.example)
  })
})

// 定义脚手架名称,这里只是-h时输出的提示名称。这里都是和脚手架名称一致的。如果不指定读取的时脚手架入口文件名称
program.name('hmserve')
// 读取package文件并获取版本号,用于-v时显示
let version = require('../package.json').version
program.version(version)

// 解析参数, 返回参数配置对象
let cmdConfig = program.parse(process.argv)
// console.log(cmdConfig) // 这里包含解析的参数对象

let Server = require('../main.js')
new Server(cmdConfig).start()

工具文件

// main.js
const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs').promises
const {createReadStream} = require('fs')
// 用于解析响应文件类型
const mime = require('mime')
// 通过ejs语法处理模板信息,展示到页面。
const ejs = require('ejs')
// 将文件处理包过程promise形式
const {promisify} = require('util')

// 合并默认配置和解析参数获取的配置
function mergeConfig (config) {
  return{
    port: 8888, 
    directory: process.cwd(),
    ...config
  }
}

class Server{
  constructor(config) {
    this.config = mergeConfig(config)
    // console.log(this.config)
  }
  // 通过node.js启动一个服务
  start() {
    let server = http.createServer(this.serveHandle.bind(this))
    server.listen(this.config.port, () => {
      console.log('服务端已经启动了.......')
    })
  }
  // 处理请求回调
  async serveHandle(req, res) {
    // 解析url,获取path
    let {pathname} = url.parse(req.url)
    // 解码,防止url中path被%编码
    pathname = decodeURIComponent(pathname)
    // 拼接当前访问路径和指定的文件夹路径
    let abspath = path.join(this.config.directory, pathname)
    try {
      // 获取目录及文件信息
      let statObj = await fs.stat(abspath)
      if (statObj.isFile()) { // 文件
        this.fileHandle(req, res, abspath)
      } else { // 文件夹,需要将目录 / 文件渲染到页面上
        let dirs = await fs.readdir(abspath) // 返回当前路径下的目录和文件名
        dirs = dirs.map((item) => {
          return {
            path: path.join(pathname, item),
            dirs: item
          }
        })
        // console.log(dirs)
        // 将ejs操作改造成返回promise对象
        let renderFile = promisify(ejs.renderFile)

        // 获取路径所在的文件夹
        let parentpath = path.dirname(pathname)

        // 将文件列表渲染到模板中
        let ret = await renderFile(path.resolve(__dirname, 'template.html'), {
          arr: dirs,
          parent: pathname == '/' ? false : true,
          parentpath: parentpath,
          title: path.basename(abspath)
        })
        res.end(ret)
      }
    } catch (err) {
      this.errorHandle(req, res, err)
    }
  }
  
  // 错误统一处理
  errorHandle(req, res, err) {
    res.statusCode = 404
    res.setHeader('Content-type', 'text/html;charset=utf-8')
    res.end('Not Found')
  }
  // 处理文件
  fileHandle(req, res, abspath) {
    res.statusCode = 200
    res.setHeader('Content-type', mime.getType(abspath) + ';charset=utf-8')
    createReadStream(abspath).pipe(res)
  }
}

module.exports = Server

ejs模板

// template.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    *{
      list-style: none;
    }
  </style>
</head>
<body>
  <h3>IndexOf <%=title%></h3>
  <ul>
    <%if(parent) {%>
      <li><a href="<%=parentpath%>">上一层</a></li>
    <%}%>
    
    <%for(let i = 0; i < arr.length; i++) {%>
      <li><a href="<%=arr[i].path%>"><%=arr[i].dirs%></a></li>
    <%}%>
  </ul>
</body>
</html>

了解Nodejs API,写一个web静态服务器脚手架 最后发布到npm上面。hmserve

npm adduser
npm publish
转载自:https://juejin.cn/post/7265565809824301113
评论
请登录