返回顶部
首页 > 资讯 > 精选 >如何用Node手写WebSocket协议
  • 453
分享到

如何用Node手写WebSocket协议

2023-07-05 04:07:46 453人浏览 安东尼
摘要

今天小编给大家分享一下如何用node手写websocket协议的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。我们知道,Htt

今天小编给大家分享一下如何用node手写websocket协议的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

我们知道,Http 是一问一答的模式,客户端向服务器发送 http 请求,服务器返回 http 响应。

这种模式对资源、数据的加载足够用,但是需要数据推送的场景就不合适了。

有同学说,http2 不是有 server push 么?

那只是推资源用的:

如何用Node手写WebSocket协议

比如浏览器请求了 html,服务端可以连带把 CSS 一起推送给浏览器。浏览器可以决定接不接收。

对于即时通讯等实时性要求高的场景,就需要用 WEBSocket 了。

websocket 严格来说和 http 没什么关系,是另外一种协议格式。但是需要一次从 http 到 websocekt 的切换过程。

如何用Node手写WebSocket协议

切换过程详细来说是这样的:

请求的时候带上这几个 header:

Connection: UpgradeUpgrade: websocketSec-WebSocket-Key: Ia3DQjfWrAug/6qm7mTZOg==

前两个很容易理解,就是升级到 websocket 协议的意思。

第三个 header 是保证安全用的一个 key。

服务端返回这样的 header:

HTTP/1.1 101 Switching ProtocolsConnection: UpgradeUpgrade: websocketSec-WebSocket-Accept: JkE58n3uIigYDmvc+KsBbGZsp1A=

和请求 header 类似,Sec-WebSocket-Accept 是对请求带过来的 Sec-WebSocket-Key 处理之后的结果。

加入这个 header 的校验是为了确定对方一定是有 WebSocket 能力的,不然万一建立了连接对方却一直没消息,那不就白等了么。

那 Sec-WebSocket-Key 经过什么处理能得到 Sec-WebSocket-Accept 呢?

我用 node 实现了一下,是这样的:

const crypto = require('crypto');function hashKey(key) {  const sha1 = crypto.createHash('sha1');  sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');  return sha1.digest('base64');}

也就是用客户端传过来的 key,加上一个固定的字符串,经过 sha1 加密之后,转成 base64 的结果。

这个字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是固定的,不信你搜搜看:

随便找个有 websocket 的网站,比如知乎就有:

如何用Node手写WebSocket协议

过滤出 ws 类型的请求,看看这几个 header,是不是就是前面说的那些。

这个 Sec-WebSocket-Key 是 wk60yiym2FEwCAMVZE3FgQ==

如何用Node手写WebSocket协议

而响应的 Sec-WebSocket-Accept 是 XRfPnS+8xl11QWZherej/dkHPHM=

如何用Node手写WebSocket协议

我们算算看:

如何用Node手写WebSocket协议

是不是一毛一样!

这就是 websocket 升级协议时候的 Sec-WebSocket-Key 对应的 Sec-WebSocket-Accept 的计算过程。

这一步之后就换到 websocket 的协议了,那是一个全新的协议:

勾选 message 这一栏可以看到传输的消息,可以是文本、可以是二进制:

如何用Node手写WebSocket协议

全新的协议?那具体是什么样的协议呢?

这样的:

如何用Node手写WebSocket协议

大家习惯的 http 协议是 key:value 的 header 带个 body 的:

如何用Node手写WebSocket协议

它是文本协议,每个 header 都是容易理解的字符。

这样好懂是好懂,但是传输占的空间太大了。

而 websocket 是二进制协议,一个字节可以用来存储很多信息:

如何用Node手写WebSocket协议

比如协议的第一个字节,就存储了 FIN(结束标志)、opcode(内容类型是 binary 还是 text) 等信息。

第二个字节存储了 mask(是否有加密),payload(数据长度)。

仅仅两个字节,存储了多少信息呀!

这就是二进制协议比文本协议好的地方。

我们看到的 weboscket 的 message 的收发,其实底层都是拼成这样的格式。

如何用Node手写WebSocket协议

只是浏览器帮我们解析了这种格式的协议数据。

这就是 weboscket 的全部流程了。

其实还是挺清晰的,一个切换协议的过程,然后是二进制的 weboscket 协议的收发。

那我们就用 node.js 自己实现一个 websocket 服务器吧!

定义个 MyWebsocket 的 class:

const { EventEmitter } = require('events');const http = require('http');class MyWebsocket extends EventEmitter {  constructor(options) {    super(options);    const server = http.createServer();    server.listen(options.port || 8080);    server.on('upgrade', (req, socket) => {          });  }}

继承 EventEmitter 是为了可以用 emit 发送一些事件,外界可以通过 on 监听这个事件来处理。

我们在构造函数里创建了一个 http 服务,当 ungrade 事件发生,也就是收到了 Connection: upgrade 的 header 的时候,返回切换协议的 header。

返回的 header 前面已经见过了,就是要对 sec-websocket-key 做下处理。

server.on('upgrade', (req, socket) => {  this.socket = socket;  socket.seTKEepAlive(true);  const resHeaders = [    'HTTP/1.1 101 Switching Protocols',    'Upgrade: websocket',    'Connection: Upgrade',    'Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),    '',    ''  ].join('\r\n');  socket.write(resHeaders);  socket.on('data', (data) => {    console.log(data)  });  socket.on('close', (error) => {      this.emit('close');  });});

我们拿到 socket,返回上面的 header,其中 key 做的处理就是前面聊过的算法

function hashKey(key) {  const sha1 = crypto.createHash('sha1');  sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');  return sha1.digest('base64');}

就这么简单,就已经完成协议切换了。

不信我们试试看。

引入我们实现的 ws 服务器,跑起来:

const MyWebSocket = require('./ws');const ws = new MyWebSocket({ port: 8080 });ws.on('data', (data) => {  console.log('receive data:' + data);});ws.on('close', (code, reason) => {  console.log('close:', code, reason);});

如何用Node手写WebSocket协议

然后新建这样一个 html:

<!DOCTYPE HTML><html><body>    <script>        const ws = new WebSocket("ws://localhost:8080");        ws.onopen = function () {            ws.send("发送数据");            setTimeout(() => {                ws.send("发送数据2");            }, 3000)        };        ws.onmessage = function (evt) {            console.log(evt)        };        ws.onclose = function () {        };    </script></body></html>

用浏览器的 WebSocket api 建立连接,发送消息。

用 npx http-server . 起个静态服务。

然后浏览器访问这个 html:

这时打开 devtools 你就会发现协议切换成功了:

如何用Node手写WebSocket协议

这 3 个 header 还有 101 状态码都是我们返回的。

message 里也可以看到发送的消息:

如何用Node手写WebSocket协议

再去服务端看看,也收到了这个消息:

如何用Node手写WebSocket协议

只不过是 Buffer 的,也就是二进制的。

接下来只要按照协议格式解析这个 Buffer,并且生成响应格式的协议数据 Buffer 返回就可以收发 websocket 数据了。

这一部分还是比较麻烦的,我们一点点来看。

如何用Node手写WebSocket协议

我们需要第一个字节的后四位,也就是 opcode。

这样写:

const byte1 = bufferData.readUInt8(0);let opcode = byte1 & 0x0f;

读取 8 位无符号整数的内容,也就是一个字节的内容。参数是偏移的字节,这里是 0。

通过位运算取出后四位,这就是 opcode 了。

然后再处理第二个字节:

如何用Node手写WebSocket协议

第一位是 mask 标志位,后 7 位是 payload 长度。

可以这样取:

const byte2 = bufferData.readUInt8(1);const str2 = byte2.toString(2);const MASK = str2[0];let payloadLength = parseInt(str2.substring(1), 2);

还是用 buffer.readUInt8 读取一个字节的内容。

先转成二进制字符串,这时第一位就是 mask,然后再截取后 7 位的子串,parseInt 成数字,这就是 payload 长度了。

这样前两个字节的协议内容就解析完了。

有同学可能问了,后面咋还有俩 payload 长度呢?

如何用Node手写WebSocket协议

这是因为数据不一定有多长,可能需要 16 位存长度,可能需要 32 位。

于是 websocket 协议就规定了如果那个 7 位的内容不超过 125,那它就是 payload 长度。

如果 7 位的内容是 126,那就不用它了,用后面的 16 位的内容作为 payload 长度。

如果 7 位的内容是 127,也不用它了,用后面那个 64 位的内容作为 payload 长度。

其实还是容易理解的,就是 3 个 if else。

用代码写出来就是这样的:

let payloadLength = parseInt(str2.substring(1), 2);let curByteIndex = 2;if (payloadLength === 126) {  payloadLength = bufferData.readUInt16BE(2);  curByteIndex += 2;} else if (payloadLength === 127) {  payloadLength = bufferData.readBigUInt64BE(2);  curByteIndex += 8;}

这里的 curByteIndex 是存储当前处理到第几个字节的。

如果是 126,那就从第 3 个字节开始,读取 2 个字节也就是 16 位的长度,用 buffer.readUInt16BE 方法。

如果是 127,那就从第 3 个字节开始,读取 8 个字节也就是 64 位的长度,用 buffer.readBigUInt64BE 方法。

如何用Node手写WebSocket协议

这样就拿到了 payload 的长度,然后再用这个长度去截取内容就好了。

但在读取数据之前,还有个 mask 要处理,这个是用来给内容解密的:

如何用Node手写WebSocket协议

读 4 个字节,就是 mask key。

再后面的就可以根据 payload 长度读出来。

let realData = null;if (MASK) {  const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4);    curByteIndex += 4;  const payloadData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);  realData = handleMask(maskKey, payloadData);} else {  realData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);;}

然后用 mask key 来解密数据。

这个算法也是固定的,用每个字节的 mask key 和数据的每一位做按位异或就好了:

function handleMask(maskBytes, data) {  const payload = Buffer.alloc(data.length);  for (let i = 0; i < data.length; i++) {    payload[i] = maskBytes[i % 4] ^ data[i];  }  return payload;}

这样,我们就拿到了最终的数据!

但是传给处理程序之前,还要根据类型来处理下,因为内容分几种类型,也就是 opcode 有几种值:

const OPCODES = {  CONTINUE: 0,  TEXT: 1, // 文本  BINARY: 2, // 二进制  CLOSE: 8,  PING: 9,  PONG: 10,};

我们只处理文本和二进制就好了:

handleRealData(opcode, realDataBuffer) {    switch (opcode) {      case OPCODES.TEXT:        this.emit('data', realDataBuffer.toString('utf8'));        break;      case OPCODES.BINARY:        this.emit('data', realDataBuffer);        break;      default:        this.emit('close');        break;    }}

文本就转成 utf-8 的字符串,二进制数据就直接用 buffer 的数据。

这样,处理程序里就能拿到解析后的数据。

我们来试一下:

之前我们已经能拿到 weboscket 协议内容的 buffer 了:

如何用Node手写WebSocket协议

而现在我们能正确解析出其中的数据:

如何用Node手写WebSocket协议

至此,我们 websocket 协议的解析成功了!

这样的协议格式的数据叫做 frame,也就是帧:

如何用Node手写WebSocket协议

解析可以了,接下来我们再实现数据的发送。

发送也是构造一样的 frame 格式。

定义这样一个 send 方法:

send(data) {    let opcode;    let buffer;    if (Buffer.isBuffer(data)) {      opcode = OPCODES.BINARY;      buffer = data;    } else if (typeof data === 'string') {      opcode = OPCODES.TEXT;      buffer = Buffer.from(data, 'utf8');    } else {      console.error('暂不支持发送的数据类型')    }    this.doSend(opcode, buffer);}doSend(opcode, bufferDatafer) {   this.socket.write(encodeMessage(opcode, bufferDatafer));}

根据发送的是文本还是二进制数据来对内容作处理。

然后构造 websocket 的 frame:

function encodeMessage(opcode, payload) {  //payload.length < 126  let bufferData = Buffer.alloc(payload.length + 2 + 0);;    let byte1 = parseInt('10000000', 2) | opcode; // 设置 FIN 为 1  let byte2 = payload.length;  bufferData.writeUInt8(byte1, 0);  bufferData.writeUInt8(byte2, 1);  payload.copy(bufferData, 2);    return bufferData;}

我们只处理数据长度小于 125 的情况。

第一个字节是 opcode,我们把第一位置 1 ,通过按位或的方式。

如何用Node手写WebSocket协议

服务端给客户端回消息不需要 mask,所以第二个字节就是 payload 长度。

分别把这前两个字节的数据写到 buffer 里,指定不同的 offset:

bufferData.writeUInt8(byte1, 0);bufferData.writeUInt8(byte2, 1);

之后把 payload 数据放在后面:

 payload.copy(bufferData, 2);

这样一个 websocket 的 frame 就构造完了。

我们试一下:

如何用Node手写WebSocket协议

收到客户端消息后,每两秒回一个消息。

如何用Node手写WebSocket协议

收发消息都成功了!

就这样,我们自己实现了一个 websocket 服务器,实现了 websocket 协议的解析和生成!

完整代码如下:

MyWebSocket:

//ws.jsconst { EventEmitter } = require('events');const http = require('http');const crypto = require('crypto');function hashKey(key) {  const sha1 = crypto.createHash('sha1');  sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');  return sha1.digest('base64');}function handleMask(maskBytes, data) {  const payload = Buffer.alloc(data.length);  for (let i = 0; i < data.length; i++) {    payload[i] = maskBytes[i % 4] ^ data[i];  }  return payload;}const OPCODES = {  CONTINUE: 0,  TEXT: 1,  BINARY: 2,  CLOSE: 8,  PING: 9,  PONG: 10,};function encodeMessage(opcode, payload) {  //payload.length < 126  let bufferData = Buffer.alloc(payload.length + 2 + 0);;    let byte1 = parseInt('10000000', 2) | opcode; // 设置 FIN 为 1  let byte2 = payload.length;  bufferData.writeUInt8(byte1, 0);  bufferData.writeUInt8(byte2, 1);  payload.copy(bufferData, 2);    return bufferData;}class MyWebsocket extends EventEmitter {  constructor(options) {    super(options);    const server = http.createServer();    server.listen(options.port || 8080);    server.on('upgrade', (req, socket) => {      this.socket = socket;      socket.setKeepAlive(true);      const resHeaders = [        'HTTP/1.1 101 Switching Protocols',        'Upgrade: websocket',        'Connection: Upgrade',        'Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),        '',        ''      ].join('\r\n');      socket.write(resHeaders);      socket.on('data', (data) => {        this.processData(data);        // console.log(data);      });      socket.on('close', (error) => {          this.emit('close');      });    });  }  handleRealData(opcode, realDataBuffer) {    switch (opcode) {      case OPCODES.TEXT:        this.emit('data', realDataBuffer.toString('utf8'));        break;      case OPCODES.BINARY:        this.emit('data', realDataBuffer);        break;      default:        this.emit('close');        break;    }  }  processData(bufferData) {    const byte1 = bufferData.readUInt8(0);    let opcode = byte1 & 0x0f;         const byte2 = bufferData.readUInt8(1);    const str2 = byte2.toString(2);    const MASK = str2[0];    let curByteIndex = 2;        let payloadLength = parseInt(str2.substring(1), 2);    if (payloadLength === 126) {      payloadLength = bufferData.readUInt16BE(2);      curByteIndex += 2;    } else if (payloadLength === 127) {      payloadLength = bufferData.readBigUInt64BE(2);      curByteIndex += 8;    }    let realData = null;        if (MASK) {      const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4);        curByteIndex += 4;      const payloadData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);      realData = handleMask(maskKey, payloadData);    }         this.handleRealData(opcode, realData);  }  send(data) {    let opcode;    let buffer;    if (Buffer.isBuffer(data)) {      opcode = OPCODES.BINARY;      buffer = data;    } else if (typeof data === 'string') {      opcode = OPCODES.TEXT;      buffer = Buffer.from(data, 'utf8');    } else {      console.error('暂不支持发送的数据类型')    }    this.doSend(opcode, buffer);  }  doSend(opcode, bufferDatafer) {    this.socket.write(encodeMessage(opcode, bufferDatafer));  }}module.exports = MyWebsocket;

Index:

const MyWebSocket = require('./ws');const ws = new MyWebSocket({ port: 8080 });ws.on('data', (data) => {  console.log('receive data:' + data);  setInterval(() => {    ws.send(data + ' ' + Date.now());  }, 2000)});ws.on('close', (code, reason) => {  console.log('close:', code, reason);});

html:

<!DOCTYPE HTML><html><body>    <script>        const ws = new WebSocket("ws://localhost:8080");        ws.onopen = function () {            ws.send("发送数据");            setTimeout(() => {                ws.send("发送数据2");            }, 3000)        };        ws.onmessage = function (evt) {            console.log(evt)        };        ws.onclose = function () {        };    </script></body></html>

以上就是“如何用Node手写WebSocket协议”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注编程网精选频道。

--结束END--

本文标题: 如何用Node手写WebSocket协议

本文链接: https://lsjlt.com/news/349294.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 如何用Node手写WebSocket协议
    今天小编给大家分享一下如何用Node手写WebSocket协议的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。我们知道,htt...
    99+
    2023-07-05
  • 手把手带你用Node手写WebSocket协议
    我们知道,http 是一问一答的模式,客户端向服务器发送 http 请求,服务器返回 http 响应。这种模式对资源、数据的加载足够用,但是需要数据推送的场景就不合适了。有同学说,http2 不是有 server push 么?那只是推资源...
    99+
    2023-05-14
    前端 JavaScript Node.js
  • 基于node实现websocket协议
    一、协议 WebSocket是一种基于TCP之上的客户端与服务器全双工通讯的协议,它在HTML5中被定义,也是新一代webapp的基础规范之一。 它突破了早先的AJAX的限制,关键在于实时性,服务器可以主动...
    99+
    2022-06-04
    协议 node websocket
  • Java应用层协议WebSocket如何实现消息推送
    这篇“Java应用层协议WebSocket如何实现消息推送”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“Java应用层协议W...
    99+
    2023-07-05
  • 怎么在HTML5中使用WebSocket协议
    怎么在HTML5中使用WebSocket协议?很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。WebSocket介绍传统的http也是一种协议,WebSocket是...
    99+
    2023-06-09
  • 如何使用WebSocket网络通信协议开发聊天室
    本篇内容主要讲解“如何使用WebSocket网络通信协议开发聊天室”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何使用WebSocket网络通信协议开发聊天室...
    99+
    2024-04-02
  • 如何使用Redis协议
    如何使用Redis协议?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。redis协议解析数据的过程主要依赖于redis的协议了。我们写个简单例子...
    99+
    2024-04-02
  • Java应用层协议WebSocket实现消息推送
    目录前言浏览器端服务器端前言   大部分的web开发者,开发的业务都是基于Http协议的:前端请求后端接口,携带参数,后端执行业务代码,再返回结果给前端。作者参与...
    99+
    2023-02-22
    Java WebSocket Java WebSocket消息推送
  • 如何用C写一个web服务器之CGI协议
    目录前言CGICGI请求CGI响应Nginx和PHP的CGI实现SAPIPHP-FPM纠偏代码实现http_parsercJSON前言 这次更新主要实现一下 CGI 协议。 先放上G...
    99+
    2024-04-02
  • Websocket通信协议在数字孪生中的应用
    目录写在前面数字孪生中的通讯协议Websocket 是什么Websocket 配置基于 Node.js 的 Websocket 服务器搭建基于Vue 的 Websocket 客户端搭...
    99+
    2024-04-02
  • 如何使用SSH和SFTP协议
    这篇文章主要讲解了“如何使用SSH和SFTP协议”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“如何使用SSH和SFTP协议”吧!有一个不必要但很重要的步骤,就是保证你的这个可以访问的系统是安...
    99+
    2023-06-03
  • ssl协议如何关闭
    SSL协议可以通过以下方式关闭:1. 在服务器上禁用SSL:可以通过修改服务器的配置文件,将SSL相关的配置项设置为禁用或者注释掉,...
    99+
    2023-09-05
    ssl
  • 如何打开ssl协议
    要打开SSL协议,您需要在使用SSL协议的应用程序或网络服务器上进行设置。下面是一些常见的步骤:1. 确保您已安装了SSL证书。SS...
    99+
    2023-09-04
    ssl
  • 如何修复ssl协议
    要修复SSL协议,您可以采取以下步骤:1. 更新SSL库:确保您使用的SSL库是最新版本,以获取最新的安全修复程序和功能改进。2. ...
    99+
    2023-08-25
    ssl
  • 如何查看ssl协议
    要查看SSL协议的版本和详细信息,可以使用一些工具和方法:1. 浏览器开发者工具大多数现代浏览器都提供了开发者工具,可以用于检查网络...
    99+
    2023-08-25
    ssl
  • winXP系统该如何手动安装TCP/IP协议
    Transmission Control Protocol/Internet Protocol,即TCP/IP协议,网络通讯协议,在一些局域网络游戏中,要求用户使用安装TCP/IP协议才能进行游戏,在windowsXP系...
    99+
    2023-06-04
    WinXP 协议 IP winXP 系统 TCP
  • websocket:客户端未使用 websocket 协议:“连接”标头中未找到“升级”令牌
    在进行 WebSocket 连接时,有时候会出现“客户端未使用 WebSocket 协议:“连接”标头中未找到“升级”令牌”的错误。这个错误通常是由于客户端没有正确地使用 WebSoc...
    99+
    2024-02-09
  • Win10如何启用netbios网络协议
    在Windows 10中,启用NetBIOS网络协议的步骤如下:1. 打开控制面板。可以通过在开始菜单中搜索“控制面板”来快速打开。...
    99+
    2023-08-22
    win10
  • Redis RESP协议如何实现
    本文小编为大家详细介绍“Redis RESP协议如何实现”,内容详细,步骤清晰,细节处理妥当,希望这篇“Redis RESP协议如何实现”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深...
    99+
    2024-04-02
  • ubuntu如何安装ssh协议
    ubuntu安装ssh协议的方法:登录ubuntu,打开终端。输入命令:“sudo apt-get update”更新设置到最新系统。再输入:“sudo apt-get install openssh-server”安装SSH协议。当出现询...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作