了解Nodejs API,写一个web静态服务器脚手架
通信必要条件
- 主机之间需要有传输介质(网线,光纤等,建立物理连接)
- 主机上必须有网卡设备(信号的调制与解调制,数字信号和电信号的转换)
- 主机之间需要协商网络速率。
网络通讯方式
- 交换机通讯,局域网中的主机通过交换机来进行通信。局域网存在大量主机会造成广播风暴。
- 路由器通讯,不同局域网之间的主机进行通讯,需要通过ip地址查找到对应的局域网段。
OSI 七层模型
-
应用层:用户与网络的接口
-
表示层:数据加密、转换、压缩
-
会话层:控制网络连接建立与终止
-
传输层:控制数据传输可靠性
-
网络层:确定目标网络
-
数据链路层:确定目标主机
-
物理层:各种物理设备和标准
TCP通信
粘包
通过延时发送或者封包解包来解决粘包问题。
// 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('客户端断开连接')
})
封包和解包
数据传输过程
- 进行数据编码,获取二进制数据包
- 按照规则拆解数据,获取指定长度的数据。
读取数据
- 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服务器
对于我们开发来说,有很多现成的插件,例如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>
最后发布到npm上面。hmserve
npm adduser
npm publish
转载自:https://juejin.cn/post/7265565809824301113