一、通信基本原理
通信必要条件
- 主机之间需要有传输介质
- 主机上必须有网卡设备
- 主机之间需要协商网络速率
二、网络通讯方式
常见的通讯方式
- 交换机通讯
- 路由器通讯
如何建立多台主机互连?
如何定位局域网中的其他主机?
通过Mac地址来唯一标识一台主机
- 交换机接口数量有上限
- 局域网存在大量主机会造成广播风暴
明确目标主机IP地址
三、网络层次模型
七层模型
- 应用层:用户与网络的接口
- 表示层:数据加密、转换、压缩
- 会话层:控制网络连接建立与终止
- 传输层:控制数据传输可靠性
- 网络层:确定目标网络
- 数据链路层:确定目标主机
- 物理层:各种物理设备和标准
数据从A至B,先封装再解封
四、TCP三次握手和四次挥手
TCP协议
- TCP属于传输层协议
- TCP是面向连接的协议
- TCP用于处理实时通信
常见控制字段
- SYN=1表示请求建立连接
- FIN=1表示请求断开连接
- ACK=1表示数据信息确认
三次握手
看着4次握手,但是中间两次是同时进行的,进行合并,这样最终就有了3次握手
四次挥手
TCP协议
- TCP处于传输层,基于端口,面向连接
- 主机之间要想通信需要先建立双向数据通道
- TCP的握手和挥手本质上都是四次
五、通讯过程-net模块
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
const net = require('net')
const client = net.createConnection({
port: 1234,
host: '127.0.0.1'
})
client.on('connect', () => {
client.write('success')
})
client.on('data', (chunk) => {
console.log(chunk.toString())
})
client.on('error', (err) => {
console.log(err)
})
client.on('close', () => {
console.log('客户端断开连接')
})
六、TCP粘包及解决
通信包括数据的发送端和接收端
发送端累积数据统一发送
接受端缓冲数据之后再消费
TCP拥塞机制决定发送机制
可以看到数据粘在了一起
解决方式:可以加延迟
七、封包拆包实现
数据传输过程
- 进行数据编码,获取二进制数据包
- 按规则拆解数据,获取指定长度的数据
Buffer数据读写
- writeInt16BE:将value从指定位置写入。
- readInt16BE:从指定位置开始读取数据。
transform.js
class MyTransformCode{
constructor() {
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
headerBuf.writeInt16BE(serialNum || this.serialNum)
headerBuf.writeInt16BE(body.length, this.serialLen)
if (serialNum == undefined) {
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
test.js
const MyTransform = require('./myTransform.js')
let ts = new MyTransform()
let str1 = '11'
// console.log(Buffer.from(str1))
// console.log(ts.encode(str1, 1))
let encodeBuf = ts.encode(str1, 1)
/*let a = ts.decode(encodeBuf)
console.log(a) */
let len = ts.getPackageLen(encodeBuf)
console.log(len)
八、封包解决粘包
server.js
const net = require('net')
const MyTransform = require('./myTransform.js')
const server = net.createServer()
let overageBuffer = null
let ts = new MyTransform()
server.listen('1234', 'localhost')
server.on('listening', () => {
console.log('服务端运行在 localhost:1234')
})
server.on('connection', (socket) => {
socket.on('data', (chunk) => {
if (overageBuffer) {
chunk = Buffer.concat([overageBuffer, chunk])
}
let packageLen = 0
while(packageLen = ts.getPackageLen(chunk)) {
const packageCon = chunk.slice(0, packageLen)
chunk = chunk.slice(packageLen)
const ret = ts.decode(packageCon)
console.log(ret)
socket.write(ts.encode(ret.body, ret.serialNum))
}
overageBuffer = chunk
})
})
client.js
const net = require('net')
const MyTransform = require('./myTransform.js')
let overageBuffer = null
let ts = new MyTransform()
const client = net.createConnection({
host: 'localhost',
port: 1234
})
client.write(ts.encode('11'))
client.write(ts.encode('22'))
client.write(ts.encode('33'))
client.write(ts.encode('44'))
client.write(ts.encode('55'))
client.on('data', (chunk) => {
if (overageBuffer) {
chunk = Buffer.concat([overageBuffer, chunk])
}
let packageLen = 0
while(packageLen = ts.getPackageLen(chunk)) {
const packageCon = chunk.slice(0, packageLen)
chunk = chunk.slice(packageLen)
const ret = ts.decode(packageCon)
console.log(ret)
}
overageBuffer = chunk
})
mytransform.js
class MyTransformCode{
constructor() {
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
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
九、模拟一个http协议请求
server.js
const net = require('net')
// 创建服务端
let server = net.createServer()
server.listen(1234, () => {
console.log('服务端启动了....')
})
server.on('connection', (socket) => {
socket.on('data', (data) => {
console.log(data.toString())
})
socket.end('test http request')
})
client.js
const http = require('http')
// 创建服务端
let server = http.createServer((req, res) => {
// 针对于请求和响应完成各自的操作
console.log('1111')
})
server.listen(1234, () => {
console.log('server is running......')
})
十、获取http请求信息
request.js
const http = require('http')
const url = require('url')
const server = http.createServer((req, res) => {
// console.log('请求进来了')
// 请求路径
let {pathname, query} = url.parse(req.url, true)
console.log(pathname, '----', query)
// 请求方式
console.log(req.method)
// 版本号
// console.log(req.httpVersion)
// 请求头
// console.log(req.headers)
// 请求体数据获取
let arr = []
req.on('data', (data) => {
arr.push(data)
})
req.on('end', () => {
console.log(Buffer.concat(arr).toString())
})
})
server.listen(1234, () => {
console.log('server is start......')
})
十一、设置http响应
response.js
const http = require('http')
const server = http.createServer((req, res) => {
console.log('有请求进来了')
// res
// res.write('ok')
// res.end()
// res.end('test ok')
res.statusCode = 302
res.setHeader('Content-type', 'text/html;charset=utf-8')
res.end('success')
})
server.listen(1234, () => {
console.log('server is start.....')
})
十二、代理客户端
服务端-》服务端 是没有跨域的
client.js
const http = require('http')
let options = {
host: 'localhost',
port: 1234,
path: '/',
method: 'POST'
}
let server = http.createServer((request, response) => {
let req = http.request(options, (res) => {
let arr = []
res.on('data', (data) => {
arr.push(data)
})
res.on('end', () => {
// console.log(Buffer.concat(arr).toString())
let ret = Buffer.concat(arr).toString()
response.setHeader('Content-type', 'text/html;charset=utf-8')
response.end(ret)
})
})
req.end('success')
})
server.listen(2345, () => {
console.log('本地的服务端启动了')
})
server.js
const http = require('http')
const server = http.createServer((req, res) => {
// console.log('请求进来了')
let arr = []
req.on('data', (data) => {
arr.push(data)
})
req.on('end', () => {
console.log(Buffer.concat(arr).toString())
res.end('111111')
})
})
server.listen(1234, () => {
console.log('外部服务端启动了')
})
十三、代理客户端解决跨域
解决方案:因为服务端访问服务端是不需要跨域的,所以在本地搭建一个服务
server.js
const http = require('http')
const server = http.createServer((req, res) => {
// console.log('请求进来了')
let arr = []
req.on('data', (data) => {
arr.push(data)
})
req.on('end', () => {
console.log(Buffer.concat(arr).toString())
res.end('111111')
})
})
server.listen(1234, () => {
console.log('外部服务端启动了')
})
agent-client.js
const http = require('http')
let options = {
host: 'localhost',
port: 1234,
path: '/',
method: 'POST'
}
let server = http.createServer((request, response) => {
let req = http.request(options, (res) => {
let arr = []
res.on('data', (data) => {
arr.push(data)
})
res.on('end', () => {
// console.log(Buffer.concat(arr).toString())
let ret = Buffer.concat(arr).toString()
response.setHeader('Content-type', 'text/html;charset=utf-8')
response.end(ret)
})
})
req.end('拉勾教育')
})
server.listen(2345, () => {
console.log('本地的服务端启动了')
})
十四、http静态服务
server.js
const mime = require(‘mime’)获取文件类型
const http = require('http')
const url = require('url')
const path = require('path')
const fs = require('fs')
const mime = require('mime')
const server = http.createServer((req, res) => {
// console.log('请求进来了')
// 1 路径处理
let {pathname, query} = url.parse(req.url)
pathname = decodeURIComponent(pathname)
let absPath = path.join(__dirname, pathname)
// console.log(absPath)
// 2 目标资源状态处理
fs.stat(absPath, (err, statObj) => {
if(err) {
res.statusCode = 404
res.end('Not Found')
return
}
if (statObj.isFile()) {
// 此时说明路径对应的目标是一个文件,可以直接读取然后回写
fs.readFile(absPath, (err, data) => {
res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8')
res.end(data)
})
} else {
fs.readFile(path.join(absPath, 'index.html'), (err, data) => {
res.setHeader('Content-type', mime.getType(absPath) + ';charset=utf-8')
res.end(data)
})
}
})
})
server.listen(1234, () => {
console.log('server is start.....')
})
index.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>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<h2>测试内容1111</h2>
</body>
</html>
十五、开发命令行工具- 命令行配置
利用一些 node 内置的核心模块,配合着一些第三方工具包,实现自己的命令行工具,使可以调用相应的命令,在指定的目录下来启动一个 web 服务,接着就可以使用浏览器来访问当前目录下的所有静态资源。
这里参照一个已经存在的 server 工具的使用。
创建结构:
为了便于看语法,www 先加上.js
再运行 npm init -y
生成 package.json 文件,修改 bin 里边的指令路径为 bin/www.js
在 www.js 里写上说明行的东西 #! /usr/bin/env node
再随便输出个东西:console.log(‘执行了’)
然后再将这个包link
到全局,npm link
即可
直接运行 lgserve
OK, 全局的命令 lgserve 就生成了
利用第三方工具包: npm install commander
,注意:这里当前是
"commander": "^6.0.0"
版本
#! /usr/bin/env node
const {program} = require('commander');
// program.option('-p --port', 'set server port')
// 配置信息
let options = {
'-p --port <dir>': {
'description': 'init server port',
'example': 'myloserver -p 3306',
},
'-d --directory <dir>': {
'description': 'init server directory',
'example': 'myloserver -d c:',
},
}
function formatConfig(configs, callback){
Object.entries(configs).forEach(([key, val])=>{
callback(key, val)
})
}
formatConfig(options, (cmd, val)=>{
program.option(cmd, val.description)
})
program.parse(process.argv)
添加上示例和修改当前 Usage name:
program.on('--help', ()=>{
console.log('examples: ');
formatConfig(options, (cmd, val)=>{
console.log(val.example)
})
})
program.name('mylgserver')
添加上版本号:注意,版本号在 package.json 里边
const version = require('../package.json').version;
program.version(version)
let cmdConfig = program.parse(process.argv)
console.log(cmdConfig);
执行:lgserve -p 1234 -d d:
#! /usr/bin/env node
const {program} = require('commander')
// console.log('执行了')
// program.option('-p --port', 'set server port')
// 配置信息
let options = {
'-p --port <dir>': {
'description': 'init server port',
'example': 'lgserve -p 3306'
},
'-d --directory <dir>': {
'description': 'init server directory',
'example': 'lgserve -d c:'
}
}
function formatConfig (configs, cb) {
Object.entries(configs).forEach(([key, val]) => {
cb(key, val)
})
}
formatConfig(options, (cmd, val) => {
program.option(cmd, val.description)
})
program.on('--help', () => {
console.log('Examples: ')
formatConfig(options, (cmd, val) => {
console.log(val.example)
})
})
program.name('lgserve')
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()
十六、开发命令行工具- 启动web服务
这里的主要逻辑都放在 main.js 里在www.js 里直接引入就行,也可以直接放在 www.js 里,比较臃肿就不便于维护了
const Server = require('../main.js');
new Server(cmdConfig).start()
// main.js
const http = require('http');
class Server{
constructor(config){
this.config = config
}
start(){
console.log('服务端已经运行了');
}
}
module.exports = Server
const http = require('http');
function mergeConfig(config){
return {
port: 1234,
directory: process.cwd(),
...config
}
}
class Server{
constructor(config){
this.config = mergeConfig(config)
console.log(this.config);
}
start(){
console.log('服务端已经运行了');
}
}
module.exports = Server
加上参数:lgserve -p 3307 -d c:
启动一个服务:
start(){
let server = http.createServer(this.serveHandle.bind(this))
server.listen(this.config.port, ()=>{
console.log('服务端已经启动了......')
})
}
serveHandle(req, res){
console.log('有请求进来了');
}
十七、开发命令行工具- 处理文件资源
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');
function mergeConfig(config) {
return {
port: 1234,
directory: process.cwd(),
...config
}
}
class Server {
constructor(config) {
this.config = mergeConfig(config)
}
start() {
let server = http.createServer(this.serveHandle.bind(this))
server.listen(this.config.port, () => {
console.log('服务端已经启动了......')
})
}
async serveHandle(req, res) {
let { pathname, query } = url.parse(req.url)
pathname = decodeURIComponent(pathname)
let absPath = path.join(this.config.directory, pathname)
console.log(absPath);
try {
let statObj = await fs.stat(absPath)
if (statObj.isFile()) { // 是个文件
this.fileHandle(req, res, absPath)
} else { // 是个文件夹
this.fileHandle(req, res, absPath + '/www.js')
}
} catch (error) {
this.errorHandle(req, res, error)
}
}
fileHandle(req, res, absPath) {
res.statusCode = 200
res.setHeader('Content-Type', mime.getType(absPath) + '; charset=utf-8')
createReadStream(absPath).pipe(res)
}
errorHandle(req, res, error) {
// console.log(error);
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end('Not Found')
}
}
module.exports = Server
十八、开发命令行工具- 处理目录资源
已经存在的 serve 工具的操作是如果我们访问的是一个目录,它就会把它下边的所有资源名称展示出来,放在浏览器所对应的界面当中进行显示,这里也这样操作。
# 使用了 ejs 模板引擎
npm install ejs
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');
const ejs = require('ejs');
const { promisify } = require('util')
function mergeConfig(config) {
return {
port: 1234,
directory: process.cwd(),
...config
}
}
class Server {
constructor(config) {
this.config = mergeConfig(config)
}
start() {
let server = http.createServer(this.serveHandle.bind(this))
server.listen(this.config.port, () => {
console.log('服务端已经启动了......')
})
}
async serveHandle(req, res) {
console.log('请求进来了')
console.log(req.url)
let { pathname, query } = url.parse(req.url)
pathname = decodeURIComponent(pathname)
let absPath = path.join(this.config.directory, pathname)
console.log(absPath);
try {
let statObj = await fs.stat(absPath)
if (statObj.isFile()) { // 是个文件
this.fileHandle(req, res, absPath)
} else { // 是个文件夹
let dirs = await fs.readdir(absPath)
console.log(dirs);
// promisify
let renderFile = promisify(ejs.renderFile)
let ret = await renderFile(path.resolve(__dirname, 'template.html'), {arr: dirs})
res.end(ret)
}
} catch (error) {
this.errorHandle(req, res, error)
}
}
fileHandle(req, res, absPath) {
res.statusCode = 200
res.setHeader('Content-Type', mime.getType(absPath) + '; charset=utf-8')
createReadStream(absPath).pipe(res)
}
errorHandle(req, res, error) {
// console.log(error);
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end('Not Found')
}
}
module.exports = Server
<!-- template.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>
<ul>
<% for(let i=0; i< arr.length; i++) { %>
<!-- <li><%=arr[i]%></li> -->
<li><a href="#"><%=arr[i]%></a></li>
<% } %>
</ul>
</body>
</html>
十九、开发命令行工具- 模版数据渲染
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')
const ejs = require('ejs')
const {promisify} = require('util')
function mergeConfig (config) {
return{
port: 1234,
directory: process.cwd(),
...config
}
}
class Server{
constructor(config) {
this.config = mergeConfig(config)
// console.log(this.config)
}
start() {
let server = http.createServer(this.serveHandle.bind(this))
server.listen(this.config.port, () => {
console.log('服务端已经启动了.......')
})
}
async serveHandle(req, res) {
let {pathname} = url.parse(req.url)
pathname = decodeURIComponent(pathname)
let abspath = path.join(this.config.directory, pathname)
// console.log(abspath)
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)
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) {
console.log(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
index.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>