如何用 Node,微信web开发教程

本文提供了一份Web前端学习资料,包括WebSocket技术详解、实现步骤、数据帧处理、ES6特性介绍以及微信小程序开发要点,旨在帮助程序员系统学习和避免浅尝辄止。
摘要由CSDN通过智能技术生成

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

  • WebSocket详解(一):初步认识WebSocket技术:是一系列的文章,从浅入深,配有丰富的图文

  • WebSocket:5分钟从入门到精通:全文以 Q&A 的方式组织而成,协议的要点都解读到了,除此之外还很全面, 涉及了WebSocket如何建立连接、交换数据的细节、数据帧的格式以及网络安全等。

  • MDN - Writing WebSocket servers:MDN 官方教程,读一遍没啥坏处。

然后开始写代码,在实现过程中的大部分代码可以从下面 3 篇文章中找到并借鉴(copy):

  • nodejs 实现:简化版本的从这儿借鉴过来的

  • 学习WebSocket协议—从顶层到底层的实现原理(修订版)

  • WebSocket协议解析:虽然是 C++ 写的,但不影响代码逻辑的理解

阅读完上面的文章,你会有发现一个共同点,就是在实现 WebSockets 过程中,最最核心的部分就是 解析 或者 生成 Frame(帧),就是下面这结构:

帧结构标准

截图来自规范Base Framing Protocol

想要理解 frame 各个字段的含义,可参考 WebSocket详解(三):深入WebSocket通信协议细节,文中作者绘制了一副图来解释这个 frame 结构;

而在代码层面,frame 的解析或生成可以在  RocketEngine - parser 或者 _processBuffer 中找到。

在完成上面几个方面的知识储备之后,而且大多有现成的代码,所以自己边抄边写一个 Websocket 服务器并不算太难。

对于 Websocket 初学者,请务必阅读以上参考文章,对 Websocket 协议有大概的了解之后再继续本文剩下部分的阅读,否则很有可能会觉得我写得云里雾里,不知所云。

===

实战

======


实现代码放在自己的  demos 仓库的 micro-ws 的目录 了,git clone 后本地运行,执行

node index.js

将会在 http://127.0.0.1:3000 创建服务。运行服务之后,打开控制台就能看到效果:

实战效果图

动图中浏览器 console 所执行的 js 代码步骤如下:

先建立连接:

var ws = new WebSocket(“ws://127.0.0.1:3000”);ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data);};

然后发送消息:(注意一定要在建立连接之后再执行该语句,否则发不出消息的)

ws.send(‘hello world’);

从效果可见,我们已经实现 Websocket 最基本的通讯功能了。

接下来我们详细看一下具体实现的细节。

 调用所写的 Websocket 类

站在使用者的角度,假设我们已经完成 Websocket 类了,那么应该怎么使用?

客户端通过 HTTP Upgrade 请求,即 101 Switching Protocol 到 HTTP 服务器,然后由服务器进行协议转换。

在 Node.js 中我们通过 http.createServer 获取 http.server 实例,然后监听 upgrade 事件,在处理这个事件:

// HTTP服务器部分var server = http.createServer(function(req, res) { res.end(‘websocket test\r\n’);});

// Upgrade请求处理server.on(‘upgrade’, function(req, socket, upgradeHead){ // 初始化 ws var ws = new WebSocket(req, socket, upgradeHead);

// … ws 监听 data、error 的逻辑等

});

这里监听 upgrade 事件的回调函数中第二个参数 socket 是 net.Socket 实例,这个类是 TCP 或 UNIX Socket 的抽象,同时一个 net.Socket 也是一个 duplex stream,所以它能被读或写,并且它也是一个 EventEmitter。

我们就利用这个 socket 对象上进行 Websocket 类实例的初始化工作;

 构造函数

class WebSocket extends EventEmitter {

constructor(req, socket, upgradeHead){

super(); // 调用 EventEmitter 构造函数

// 1. 构造响应头 resHeaders 部分

// 2. 监听 socket 的 data 事件,以及 error 事件

// 3. 初始化成员属性

}

}

注意,我们需要继承内置的 EventEmitter ,这样生成的实例才能监听、绑定事件;

Node.js 采用事件驱动、异步编程,天生就是为了网络服务而设计的,继承 EventEmitter 就能享受到非阻塞模式的 IO 处理;

讲一下其中 响应头的构造 和 事件监听 部分。

返回响应头(Response Header)

根据协议规范,我们能写出响应头的内容:

  1. 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。

  2. 通过 SHA1 计算出摘要,并转成 base64 字符串。

具体代码如下:

var resKey = hashWebSocketKey(req.headers[‘sec-websocket-key’]);

// 构造响应头 var resHeaders = [ ‘HTTP/1.1 101 Switching Protocols’, ‘Upgrade: websocket’, ‘Connection: Upgrade’, ‘Sec-WebSocket-Accept: ’ + resKey ] .concat(’‘, ‘’) .join(’\r\n’); socket.write(resHeaders);

当执行 socket.write(resHeaders); 到后就和客户端建立起 WebSocket 连接了,剩下去就是数据的处理。

监听事件

socket 就是 TCP 协议的抽象,直接在上面监听已有的 data 事件和 close 事件这两个事件。

还有其他事件,比如 error、end 等,详细参考 net.Socket 文档

socket.on(‘data’, data => { this.buffer = Buffer.concat([this.buffer, data]); while (this._processBuffer()) {} // 循环处理返回的 data 数据 });

socket.on(‘close’, had_error => { if (!this.closed) { this.emit(‘close’, 1006); this.closed = true; } });

close 的事件逻辑比较简单,比较重要的是 data 的事件监听部分。核心就是 this._processBuffer() 这个方法,用于处理客户端传送过来的数据(即 Frame 数据)

注意该方法是放在 while 循环语句里,处理好边界情况,防止死循环。

 Frame 帧数据的处理

WebSocket 客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。

这 this._processBuffer() 部分代码逻辑就是用来解析帧数据的,所以它是实现 Websocket 代码的关键;(该方法里面用到了大量的位操作符以及 Buffer 类的操作)

帧数据结构详细定义可参考 RFC6455 5.2节,上面罗列的参考文章都有详细的解读,我在这儿也不啰嗦讲细节了,直接看代码比听我用文字讲要好。

这里就其中两个细节需要铺垫一下,方便更好地理解代码。

操作码(Opcode)

Opcode 即 操作代码,Opcode 的值决定了应该如何解析后续的数据载荷(data payload)

根据 Opcode 我们可以大致将数据帧分成两大类:数据帧 和 控制帧。

数据帧:目前只有 3 种,对应的 opcode 是:

  • 0x0:数据延续帧

  • 0x1:utf-8文本

  • 0x2:二进制数据;

  • 0x3 - 0x7:目前保留,用于后续定义的非控制帧。

**控制帧:**除了上述 3 种数据帧之外,剩下的都是控制帧

  • 0x8:表示连接断开

  • 0x9:表示 ping 操作

  • 0xA:表示 pong 操作

  • 0xB - 0xF:目前保留,用于后续定义的控制帧

在代码里,我们会先从帧数据中提取操作码:

var opcode = byte1 & 0x0f; //截取第一个字节的后 4 位,即 opcode 码

然后根据协议获取到真正的数据载荷(data payload),然后将这两部分传给 _handleFrame 方法:

this._handleFrame(opcode, payload); // 处理操作码

该方法会根据不同的 opcode 做出不同的操作:

_handleFrame(opcode, buffer) {

var payload;

switch (opcode) {

case OPCODES.TEXT:

payload = buffer.toString(‘utf8’); //如果是文本需要转化为utf8的编码

this.emit(‘data’, opcode, payload); //Buffer.toString()默认utf8 这里是故意指示的

break;

case OPCODES.BINARY: //二进制文件直接交付

payload = buffer;

this.emit(‘data’, opcode, payload);

break;

case OPCODES.PING: // 发送 pong 做响应

this._doSend(OPCODES.PONG, buffer);

break;

case OPCODES.PONG: //不做处理

console.log(‘server receive pong’);

break;

case OPCODES.CLOSE: // close有很多关闭码

let code, reason; // 用于获取关闭码和关闭原因

if (buffer.length >= 2) {

code = buffer.readUInt16BE(0);

reason = buffer.toString(‘utf8’, 2);

}

this.close(code, reason);

this.emit(‘close’, code, reason);

break;

default:

this.close(1002, ‘unhandle opcode:’ + opcode);

}

}

分片(Fragment)

规范文档:5.4 -  Fragmentation

一旦 WebSocket 客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。理论上来说,每个帧(Frame)的大小是没有限制的。

对于大块的数据,Websocket 协议建议对数据进行分片(Fragment)操作。

分片的意义主要是两方面:

  • 主要目的是允许当消息开始但不必缓冲该消息时发送一个未知大小的消息。如果消息不能被分片,那么端点将不得不缓冲整个消息以便在首字节发生之前统计出它的长度。对于分片,服务器或中间件可以选择一个合适大小的缓冲,当缓冲满时,再写一个片段到网络。

  • 另一方面分片传输也能更高效地利用多路复用提高带宽利用率,一个逻辑通道上的一个大消息独占输出通道是不可取的,因此多路复用需要可以分割消息为更小的分段来更好的共享输出通道。参考文档 I/O多路复用技术(multiplexing)是什么?

WebSocket 协议提供的分片方法,是将原本一个大的帧拆分成数个小的帧。下面是把一个大的Frame分片的图示:

分片图示

根据 FIN 的值来判断,是否已经收到消息的最后一个数据帧。由图可知,第一个分片的 FIN 为 0,Opcode 为非0值(0x1 或 0x2),最后一个分片的FIN为1,Opcode为 0。中间分片的 FIN 和 opcode 二者均为 0。

    • `FIN=1` 表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。
  • `FIN=0`,则接收方还需要继续监听接收其余的数据帧。

  • opcode在数据交换的场景下,表示的是数据的类型。

    • `0x01` 表示文本,永远是 `utf8` 编码的
  • `0x02` 表示二进制

  • 而 `0x00` 比较特殊,表示 延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

代码里,我们需要检测 FIN 的值,如果为 0 说明有分片,需要记录第一个 FIN 为 0 时的 opcode 值,缓存到 this.frameOpcode 属性中,将载荷缓存到 this.frames 属性中:

var FIN = byte1 & 0x80; // 如果为0x80,则标志传输结束,获取高位 bit // 如果是 0 的话,说明是延续帧,需要保存好 opCode if (!FIN) { this.frameOpcode = opcode || this.frameOpcode; // 确保不为 0; }

//… // 有可能是分帧,需要拼接数据 this.frames = Buffer.concat([this.frames, payload]); // 保存到 frames 中

当接收到最后一个 FIN 帧的时候,就可以组装后给 _handleFrame 方法:

if (FIN) { payload = this.frames.slice(0); // 获取所有拼接完整的数据 opcode = opcode || this.frameOpcode; // 如果是 0 ,则保持获取之前保存的 code this.frames = Buffer.alloc(0); // 清空 frames this.frameOpcode = 0; // 清空 opcode this._handleFrame(opcode, payload); // 处理操作码 }

发送数据帧

上面讲的都是接收并解析来自客户端的数据帧,当我们想给客户端发送数据帧的时候,也得按协议来。

这部分操作相当于是上述 _processBuffer 方法的逆向操作,在代码里我们使用 encodeMessage 方法(为了简单起见,我们发送给客户端的数据没有经过掩码处理)将发送的数据分装成数据帧的格式,然后调用 socket.write 方法发送给客户端;

_doSend(opcode, payload) { // 1. 考虑数据分片 this.socket.write( encodeMessage(count > 0 ? OPCODES.CONTINUE : opcode, payload) ); //编码后直接通过socket发送

为了考虑分片场景,特意设置 MAX_FRAME_SIZE 来对每次发送的数据长度做截断做分片:

// … var len = Buffer.byteLength(payload); // 分片的距离逻辑 var count = 0; // 这里可以针对 payload 的长度做分片 while (len > MAX_FRAME_SIZE) { var framePayload = payload.slice(0, MAX_FRAME_SIZE); payload = payload.slice(MAX_FRAME_SIZE); this.socket.write( encodeMessage( count > 0 ? OPCODES.CONTINUE : opcode, framePayload, false ) ); //编码后直接通过socket发送 count++; len = Buffer.byteLength(payload); } // …

至此已经实现 Websocket 协议的关键部分,所组装起来的代码就能和客户端建立 Websocket 连接并进行数据交互了。

 Q&A

字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 怎么来的?

这个标志性字符串是专门标示 Websocket 协议的 UUID;UUID 是长度为 16-byte(128-bit)的ID,一般以形如f81d4fae-7dec-11d0-a765-00a0c91e6bf6的字符串作为 URN(Uniform Resource Name,统一资源名称)

ES6

  • 列举常用的ES6特性:

  • 箭头函数需要注意哪些地方?

  • let、const、var

  • 拓展:var方式定义的变量有什么样的bug?

  • Set数据结构

  • 拓展:数组去重的方法

  • 箭头函数this的指向。

  • 手写ES6 class继承。

微信小程序

  • 简单描述一下微信小程序的相关文件类型?

  • 你是怎么封装微信小程序的数据请求?

  • 有哪些参数传值的方法?

  • 你使用过哪些方法,来提高微信小程序的应用速度?

  • 小程序和原生App哪个好?

  • 简述微信小程序原理?

  • 分析微信小程序的优劣势

  • 怎么解决小程序的异步请求问题?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ES6

  • 列举常用的ES6特性:

  • 箭头函数需要注意哪些地方?

  • let、const、var

  • 拓展:var方式定义的变量有什么样的bug?

  • Set数据结构

  • 拓展:数组去重的方法

  • 箭头函数this的指向。

  • 手写ES6 class继承。

微信小程序

  • 简单描述一下微信小程序的相关文件类型?

  • 你是怎么封装微信小程序的数据请求?

  • 有哪些参数传值的方法?

  • 你使用过哪些方法,来提高微信小程序的应用速度?

  • 小程序和原生App哪个好?

  • 简述微信小程序原理?

  • 分析微信小程序的优劣势

  • 怎么解决小程序的异步请求问题?

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-dqnuPczm-1713552242733)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 14
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值