一、通信基本原理
通信必要条件
- 主机之间需要有传输介质
- 主机上必须有网卡设备
- 主机之间需要协商网络速率
二、网络通讯方式
常见的通讯方式
- 交换机通讯
- 路由器通讯
如何建立多台主机互连?
如何定位局域网中的其他主机?
通过Mac地址来唯一标识一台主机
- 交换机接口数量有上限
- 局域网存在大量主机会造成广播风暴
明确目标主机IP地址
三、网络层次模型
七层模型
- 应用层:用户与网络的接口
- 表示层:数据加密、转换、压缩
- 会话层:控制网络连接建立与终止
- 传输层:控制数据传输可靠性
- 网络层:确定目标网络
- 数据链路层:确定目标主机
- 物理层:各种物理设备和标准
数据从A至B,先封装再解封
四、TCP三次握手和四次挥手
TCP协议
- TCP属于传输层协议
- TCP是面向连接的协议
- TCP用于处理实时通信
常见控制字段
- SYN=1表示请求建立连接
- FIN=1表示请求断开连接
- ACK=1表示数据信息确认
三次握手
看着4次握手,但是中间两次是同时进行的,进行合并,这样最终就有了3次握手
四次挥手
1-客户端发送断开连接请求给服务端
2-服务端发送消息确认给客户端
3-服务端发送断开连接请求给客户端
4-客户端发送消息确认给服务端
为什么是三次握手四次挥手呢
三次握手的原因是:防止过期的连接请求到达服务器端,如果只有两次握手,则服务器端会建立一个不需要的连接,因此会造成服务器资源的浪费。
四次挥手的原因是:四次挥手的2-3不能合并,因为一个服务端要服务于多个客户端,不能保证某一个客户端发送断开请求之后服务端立即将结果数据全部传输回给这个客户端
详解参考:https://blog.csdn.net/qzwzsl/article/details/126252110
https://mp.weixin.qq.com/s?__biz=MzI4NjE4NTUwNQ==&mid=2247494421&idx=1&sn=8a1e925f02a3c491c3a0b34f201c344c&chksm=ebe26a5bdc95e34d590609df45c207948c0fd97ec9baee54c70a8993e2c7937c8d0da2086ab2&scene=27
TCP协议
- TCP处于传输层,基于端口,面向连接
- 主机之间要想通信需要先建立双向数据通道
- TCP的握手和挥手本质上都是四次
五、通讯过程-net模块(模拟三次握手与四次挥手)
Net
模块实现了底层通信接口
通信过程
- 创建服务端:接收和会写客户端数据
- 创建客户端:发送和接收服务端数据
- 数据传输:内置服务事件和方法读写数据
通信事件
- listening 事件:调用 server.listen 方法之后触发
- connection 事件:新的连接建立时触发
- close 事件:当 server 关闭时触发
- error 事件:当错误出现时触发
通信事件&方法
- data 事件:当接收到数据的时候触发该事件
- write 方法:在 socket 上发送数据,默认是 UTF8 编码
- end 操作:当 socket 的一端发送 FIN 包时触发,结束可读端
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文章来源:https://www.toymoban.com/news/detail-563959.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>
文章来源地址https://www.toymoban.com/news/detail-563959.html
到了这里,关于nodejs 高级编程-通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!