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

通信必要条件

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

网络通讯方式

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

OSI 七层模型

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

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

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

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

  • 网络层:确定目标网络

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

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

具体内容可以看这里

TCP通信

粘包

通过延时发送或者封包解包来解决粘包问题。

js 复制代码
// 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)
  }
})
js 复制代码
// 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('客户端断开连接')
})

封包和解包

数据传输过程

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

读取数据

  • writeInt16BE 将value从指定位置写入
  • redInt16BE 从指定位置开始读取数据
js 复制代码
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去运行对应的命令了。

json 复制代码
{
  "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去解析输入的命令行参数。

js 复制代码
#! /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()

工具文件

js 复制代码
// 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模板

html 复制代码
// 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>

最后发布到npm上面。hmserve

npm adduser
npm publish
相关推荐
持久的棒棒君11 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979122 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
VXbishe3 小时前
(附源码)基于springboot的“我来找房”微信小程序的设计与实现-计算机毕设 23157
java·python·微信小程序·node.js·c#·php·课程设计
IT小白33 小时前
node启动websocket保持后台一直运行
websocket·node.js
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
J老熊4 小时前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构