大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

背景

因为 Serverless 的“无服务器架构”应用相比于传统应用有很多优点,比如:无需关心服务器、免运维、弹性伸缩、按需付费、开发可以更加关注业务逻辑等等,所以现在 Serverless 应用已经逐渐广泛起来。

但是目前原生的 Serverless 开发框架还比较少,也没有那么成熟,另外主流的 Web 框架还不支持直接 Serverless 部署,但好在是现在国内各大云厂商比如阿里云、腾讯云已经提供能力能够将我们的传统框架以简单、快速、科学的方式部署到 Serverless 上,下面让我们一起研究看看它们是怎么做的吧。

我们以 Node.js 的 Express 应用为例,看看如何通过阿里云函数计算,实现不用按照传统部署方式购买云主机去部署,不用自己运维,快速部署到 Serverless 平台上。

传统应用与函数计算的入口差异

传统应用的入口文件

首先看下传统 Express 应用的入口文件:

const express = require('express')
const app = express()
const port = 3000// 监听 / 路由,处理请求
app.get('/', (req, res) => {res.send('Hello World!')
})// 监听 3000 端口,启动 HTTP 服务
app.listen(port, () => {console.log(`Example app listening on port ${port}`)
})

可以看到传统 Express 应用是:

1.通过 app.listen() 启动了 HTTP 服务,其本质上是调用的 Node.js http 模块的 createServer() 方法创建了一个 HTTP Server

2.监听了 / 路由,由回调函数 function(request, response) 处理请求

函数计算的入口函数

Serverless 应用中, FaaS 是基于事件触发的,触发器是触发函数执行的方式, 其中 API 网关触发器与 HTTP 触发器与均可应用于 Web应用的创建。函数计算会从指定的入口函数开始执行,其中 API 网关触发器对应的入口函数叫事件函数,HTTP 触发器对应的入口函数叫 HTTP 函数,它们的入口函数形式不同。

API 网关触发器的入口函数形式

API 网关触发器的入口函数形式如下,函数入参包括 event、context、callback,以 Node.js 为例,如下:

/*
* handler: 函数名 handler 需要与创建函数时的 handler 字段相对应。例如创建函数时指定的 handler 为 index.handler,那么函数计算会去加载 index.js 文件中定义的 handler 函数
* event: 您调用函数时传入的数据,其类型是 Buffer,是函数的输入参数。您在函数中可以根据实际情况对 event 进行转换。如果输入数据是一个 JSON 字符串 ,您可以把它转换成一个 Object。
* context: 包含一些函数的运行信息,例如 request Id、 临时 AK 等。您在代码中可以使用这些信息
* callback: 由系统定义的函数,作为入口函数的入参用于返回调用函数的结果,标识函数执行结束。与 Node.js 中使用的 callback 一样,它的第一个参数是 error,第二个参数 data。
*/
module.exports.handler = (event, context, callback) => {// 处理业务逻辑callback(null, data);};

HTTP 触发器的入口函数形式

一个简单的 Node.js HTTP 函数示例如下所示:

module.exports.handler = function(request, response, context)  {response.send("hello world");
}

差异对比

对比可以看出,在传统应用中,是启动一个服务监听端口号去处理 HTTP 请求,服务处理的是 HTTP 的请求和响应参数;而在 Serverless 应用中, Faas 是基于事件触发的,触发器类型不同,参数映射和处理不同:

  • 若是 API 网关触发器

    • 当有请求到达后端服务设置为函数计算的 API 网关时,API 网关会触发函数的执行,触发器会将事件信息生成 event 参数,然后 FaaS 以 event 为参数执行入口函数,最后将执行结果返回给 API 网关。所以传统应用和 Serverless 应用在请求响应方式和参数的数据结构上都有很大差异,要想办法让函数计算的入口方法适配 express。

  • 若是 HTTP 触发器

    • 相对 API 网关触发器参数处理会简单些。因为 HTTP 触发器通过发送 HTTP 请求触发函数执行,会把真实的 HTTP 请求直接传递给 FaaS 平台,不需要编码或解码成 JSON 格式,不用增加转换逻辑,性能也更优。

适配层

下面我们通过解读阿里云 FC 提供的将函数计算的请求转发给 express 应用的 npm 包 @webserverless/fc-express 源码,看看函数计算的入口方法是如何适配 express 的,如何适配 API 网关 和 HTTP 触发器这两种类型。

根据上述分析,Web 应用若想 Serverless 化需要开发一个适配层,将函数计算接收到的请求转发给 express 应用处理,最后再返回给函数计算。

API 网关触发的适配层

实现原理

API 网关触发的情况下,通过适配层将 FaaS 函数接收到的 API 网关事件参数 event 先转化为标准的 HTTP 请求,再去让传统 Web 服务去处理请求和响应,最后再将 HTTP 响应转换为函数返回值。整体工作原理如下图所示:

适配层核心就是:把 event 映射到 express 的 request 对象上, 再把 express 的 response 对象映射到 callback 的数据参数上。

API 网关调用函数计算的事件函数时,会将 API 的相关数据转换为 Map 形式传给函数计算服务。函数计算服务处理后,按照下图中 Output Format 的格式返回 statusCode、headers、body 等相关数据。API 网关再将函数计算返回的内容映射到 statusCode、header、body等位置返回给客户端。

(此图来源于阿里云)

核心过程

通过分析 @webserverless/fc-express 源码,我们可以抽取核心过程实现一个简易版的适配层。

1.创建一个自定义 HTTP Server,通过监听 Unix Domain Socket,启动服务

(友情链接:不清楚 Unix Domain Socket 的小伙伴可以先看下这篇文章: Unix domain socket 简介 (https://www.cnblogs.com/sparkdev/p/8359028.html))

第一步我们若想把函数计算接收的 event 参数映射到 Express.js 的 request 对象上,就需要创建并启动一个自定义的 HTTP 服务来代替 Express.js 的 app.listen,然后接下来就可以将函数的事件参数 event 转换为 Express.js 的 request 请求参数。

首先创建一个 server.js 文件如下:

// server.js
const http = require('http');
const ApiGatewayProxy = require('./api-gateway-proxy');// api-gateway-proxy.js 文件下一步会说明其内容/*
* requestListener:被代理的 express 应用
* serverListenCallback:http 代理服务开始监听的回调函数
* binaryTypes: 当 express 应用的响应头 content-type 符合 binaryTypes 中定义的任意规则,则返回给 API 网关的 isBase64Encoded 属性为 true
*/
function Server(requestListener,serverListenCallback,binaryTypes) { this.apiGatewayProxy = new ApiGatewayProxy(this);   // ApiGatewayProxy 核心过程 2 会介绍this.server = http.createServer(requestListener);// 1.1 创建一个自定义 HTTP Serverthis.socketPathSuffix = getRandomString(); // 随机生成一个字符串,作为 Unix Domain Socket 使用this.binaryTypes = binaryTypes ? binaryTypes.slice() : [];// 当 express 应用响应的 content-type 符合 Server 构造函数参数 binaryTypes 中定义的任意规则时,则函数的返回值的 isBase64Encoded 为 true,从而告诉 API 网关如何解析函数返回值的 body 参数this.server.on("listening", () => {this.isListening = true;if (serverListenCallback) serverListenCallback();});this.server.on("close", () => {this.isListening = false;}).on("error", (error) => {// 异常处理});}// 暴露给函数计算入口函数 handler 调用的方法
Server.prototype.proxy = function (event, context, callback) {const e = JSON.parse(event);this.apiGatewayProxy.handle({event: e,context,callback});
}// 1.2 启动服务
Server.prototype.startServer = function () {return this.server.listen(this.getSocketPath()); //  采用监听 Unix Domain Socket 方式启动服务,减少函数执行时间,节约成本
}Server.prototype.getSocketPath = function () {/* istanbul ignore if *//* only running tests on Linux; Window support is for local dev only */if (/^win/.test(process.platform)) {const path = require('path');return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);} else {return `/tmp/server-${this.socketPathSuffix}.sock`;}
}function getRandomString() {return Math.random().toString(36).substring(2, 15);
}module.exports = Server;

在 server.js 中,我们定义了一个构造函数 Server 并导出。在 Server 中,我们创建了一个自定义的 HTTP 服务,然后随机生成了一个 Unix Domain Socket,采用监听该 Socket 方式启动服务来代替 Express.js 的 app.listen

2.将函数计算参数 event 转换为 Express.js 的 HTTP request

下面开始第 2 步,创建一个 api-gateway-proxy.js 文件,将函数计算参数 event 转换为 Express.js 的 HTTP request。

//api-gateway-proxy.js
const http = require('http');
const isType = require('type-is');function ApiGatewayProxy(server) {this.server = server;
}ApiGatewayProxy.prototype.handle = function ({event,context,callback
}) {this.server.startServer().on('listening', () => {this.forwardRequestToNodeServer({event,context,callback});});
}ApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({event,context,callback
}) {const resolver = data => callback(null, data);try {// 2.1将 API 网关事件转换为 HTTP requestconst requestOptions = this.mapContextToHttpRequest({event,context,callback});// 2.2 通过 http.request() 将 HTTP request 转发给 Node.js Server 处理,发起 HTTP 请求const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));req.on('error', error => {//...});req.end();} catch (error) {// ...}
}ApiGatewayProxy.prototype.mapContextToHttpRequest = function ({event,context,callback
}) {const headers = Object.assign({}, event.headers);return {method: event.httpMethod,path: event.path,headers,socketPath: this.server.getSocketPath()// protocol: `${headers['X-Forwarded-Proto']}:`,// host: headers.Host,// hostname: headers.Host, // Alias for host// port: headers['X-Forwarded-Port']};
}// 核心过程 3 会介绍
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');const successResponse = {statusCode,body,headers,isBase64Encoded};resolver(successResponse);});
}module.exports = ApiGatewayProxy;

在 api-gateway-proxy.js 中,我们定义了一个构造函数 ApiGatewayProxy 并导出。在这里我们会将 event 转换为 HTTP request,然后向 Node.js Server 发起请求,由 Node.js Server 再进行处理做出响应。

3.将 HTTP response 转换为 API 网关标准数据结构,作为 callback 的参数返回给 API 网关

接着继续对 api-gateway-proxy.js 文件中的http.request(requestOptions, response => this.forwardResponse(response, resolver))分析发出 HTTP 请求后的响应处理部分。

//api-gateway-proxy.jsApiGatewayProxy.prototype.forwardRequestToNodeServer = function ({event,context,callback
}) {const resolver = data => callback(null, data); // 封装 callback 为 resolver//...// 请求、响应const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));//...
}//3.Node.js Server 对 HTTP 响应进行处理,将 HTTP response 转换为 API 网关标准数据结构,作为函数计算返回值
ApiGatewayProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');// 函数返回值const successResponse = {statusCode,body,headers,isBase64Encoded //当函数的 event.isBase64Encoded 是 true 时,会按照 base64 编码来解析 event.body,并透传给 express 应用,否则就按照默认的编码方式来解析,默认是 utf8};// 将 API 网关标准数据结构作为回调 callback 参数,执行 callback,返回给 API 网关resolver(successResponse);});
}

接着第 2 步,Node.js Server 对 http.request() 发出的 HTTP 请求做出响应处理,将 HTTP response 转换为 API 网关标准数据结构,把它作为回调 callback 的参数,调用 callback 返回给 API 网关。

4.在入口函数中引入适配层代码并调用

以上 3 步就将适配层核心代码完成了,整个过程就是:将 API 网关事件转换成 HTTP 请求,通过本地 socket 和函数起 Node.js Server 进行通信。

最后我们在入口函数所在文件 index.js 中引入 server.js,先用 Server 构建一个 HTTP 代理服务,然后在入口函数 handler 中调用 server.proxy(event, context, callback); 即可将函数计算的请求转发给 express 应用处理。

// index.js
const express = require('express');const Server = require('./server.js'); const app = express();
app.all('*', (req, res) => {res.send('express-app hello world!');
});const server = new Server(app); // 创建一个自定义 HTTP Servermodule.exports.handler = function(event, context, callback) {server.proxy(event, context, callback); // server.proxy 将函数计算的请求转发到 express 应用
};

我们将以上代码在 FC 上部署、调用,执行成功结果如下:

HTTP 触发的适配层

实现原理

HTTP 触发的情况下,不用对请求参数做转换,其它原理与 API 网关触发器一致:通过适配层将 FaaS 函数接收到的请求参数直接转发到自定义的 Web 服务内,最后再将 HTTP 响应包装返回即可,整体工作原理如下图所示:

核心过程

同样我们抽取核心过程简单实现一个适配层,与 API 网关触发器原理相同的部分将不再赘述 。

1.创建一个自定义 HTTP Server,通过监听 Unix Domain Socket,启动服务

server.js 代码如下:

// server.js
const http = require('http');
const HttpTriggerProxy = require('./http-trigger-proxy');function Server(requestListener,serverListenCallback) {this.httpTriggerProxy = new HttpTriggerProxy(this);this.server = http.createServer(requestListener); // 1.1 创建一个自定义 HTTP Serverthis.socketPathSuffix = getRandomString();this.server.on("listening", () => {this.isListening = true;if (serverListenCallback) serverListenCallback();});this.server.on("close", () => {this.isListening = false;}).on("error", (error) => {// 异常处理,例如判读 socket 是否已被监听});}// 暴露给函数计算入口函数 handler 调用的方法
Server.prototype.httpProxy = function (request, response, context) {this.httpTriggerProxy.handle({ request, response, context });
}// 1.2 启动服务
Server.prototype.startServer = function () {return this.server.listen(this.getSocketPath());
}Server.prototype.getSocketPath = function () {/* istanbul ignore if *//* only running tests on Linux; Window support is for local dev only */if (/^win/.test(process.platform)) {const path = require('path');return path.join('\\\\?\\pipe', process.cwd(), `server-${this.socketPathSuffix}`);} else {return `/tmp/server-${this.socketPathSuffix}.sock`;}
}function getRandomString() {return Math.random().toString(36).substring(2, 15);
}module.exports = Server;

2.将 HTTP request 直接转发给 Web Server,再将 HTTP response 包装返回

创建一个 api-trigger-proxy.js 文件如下:

// api-trigger-proxy.js
const http = require('http');
const isType = require('type-is');
const url = require('url');
const getRawBody = require('raw-body');function HttpTriggerProxy(server) {this.server = server;
}HttpTriggerProxy.prototype.handle = function ({request,response,context
}) {this.server.startServer().on('listening', () => {this.forwardRequestToNodeServer({request,response,context});});
}HttpTriggerProxy.prototype.forwardRequestToNodeServer = function ({request,response,context
}) {// 封装 resolverconst resolver = data => {response.setStatusCode(data.statusCode);for (const key in data.headers) {if (data.headers.hasOwnProperty(key)) {const value = data.headers[key];response.setHeader(key, value);}}response.send(data.body); // 返回 response body};try {// 透传 requestconst requestOptions = this.mapContextToHttpRequest({request,context});// 2.将 HTTP request 直接转发给 Web Server,再将 HTTP response 包装返回const req = http.request(requestOptions, response => this.forwardResponse(response, resolver));req.on('error', error => {// ...});// http 触发器类型支持自定义 body:可以获取自定义 bodyif (request.body) {req.write(request.body);req.end();} else {// 若没有自定义 body:http 触发器触发函数,会通过流的方式传输 body 信息,可以通过 npm 包 raw-body 来获取getRawBody(request, (err, body) => {req.write(body);req.end();});}} catch (error) {// ...}
}HttpTriggerProxy.prototype.mapContextToHttpRequest = function ({request,context
}) {const headers = Object.assign({}, request.headers); headers['x-fc-express-context'] = encodeURIComponent(JSON.stringify(context));return {method: request.method,path: url.format({ pathname: request.path, query: request.queries }),headers,socketPath: this.server.getSocketPath()// protocol: `${headers['X-Forwarded-Proto']}:`,// host: headers.Host,// hostname: headers.Host, // Alias for host// port: headers['X-Forwarded-Port']};
}HttpTriggerProxy.prototype.forwardResponse = function (response, resolver) {const buf = [];response.on('data', chunk => buf.push(chunk)).on('end', () => {const bodyBuffer = Buffer.concat(buf);const statusCode = response.statusCode;const headers = response.headers;const contentType = headers['content-type'] ? headers['content-type'].split(';')[0] : '';const isBase64Encoded = this.server.binaryTypes && this.server.binaryTypes.length > 0 && !!isType.is(contentType, this.server.binaryTypes);const body = bodyBuffer.toString(isBase64Encoded ? 'base64' : 'utf8');const successResponse = {statusCode,body,headers,isBase64Encoded};resolver(successResponse);});
}module.exports = HttpTriggerProxy;

3.入口函数引入适配层代码

// index.js
const express = require('express');
const Server = require('./server.js');const app = express();
app.all('*', (req, res) => {res.send('express-app-httpTrigger hello world!');
});const server = new Server(app);module.exports.handler  = function (req, res, context) { server.httpProxy(req, res, context);
};

同样地,我们将以上代码在 FC 上部署、调用,执行成功结果如下:

看到最后,大家会发现 API 网关触发器和 HTTP 触发器很多代码逻辑是可以复用的,大家可以自行阅读优秀的源码是如何实现的~

其他部署到 Serverless 平台的方案

将传统 Web 框架部署到 Serverless 除了通过适配层转换实现,还可以通过 Custom Runtime 或者 Custom Container Runtime (https://juejin.cn/post/6981921291980767269#heading-5) ,3 种方案总结如下:

  • 通过引入适配层,将函数计算接收的事件参数转换为 HTTP 请求交给自定义的 Web Server 处理

  • 通过 Custom Runtime

    • 本质上也是一个 HTTP Server接管了函数计算平台的所有请求,包括事件调用或者 HTTP 函数调用等

    • 开发者需要创建一个启动目标 Server 的可执行文件 bootstrap

  • 通过 Custom Container Runtime

    • 工作原理与 Custom Runtime 基本相同

    • 开发者需要把应用代码和运行环境打包为 Docker 镜像

小结

本文介绍了传统 Web 框架如何部署到 Serverless 平台的方案:可以通过适配层和自定义(容器)运行时。其中主要以 Express.js 和阿里云函数计算为例讲解了通过适配层实现的原理和核心过程,其它 Web 框架 Serverless 化的原理也基本一致,腾讯云也提供了原理一样的 tencent-serverless-http (https://github.com/serverless-plus/tencent-serverless-http) 方便大家直接使用(但腾讯云不支持 HTTP 触发器),大家可以将自己所使用的 Web 框架对照云厂商函数计算的使用方法亲自开发一个适配层实践一下~

Node 社群

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章

2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

参考资料

Webserverless - FC Express extension (https://github.com/awesome-fc/webserverless/tree/master/packages/fc-express)

如何将 Web 框架迁移到 Serverless (https://zhuanlan.zhihu.com/p/152391799)

Serverless 工程实践 | 传统 Web 框架迁移 (https://developer.aliyun.com/article/790302)

阿里云-触发器简介 (https://help.aliyun.com/document_detail/53102.html)

前端学 serverless 系列—— WebApplication 迁移实践 (https://zhuanlan.zhihu.com/p/72076708)

如何将传统 Node.js 项目部署到 Serverless相关推荐

  1. vue+mysql+node.js项目部署到腾讯云-云服务器/轻量应用服务器

    node 14.16.1 nginx 1.18.0 mysql 8.0.26 nginx 静态高性能的HTTP和反向代理web服务器 nginx主配置文件 /usr/local/lighthouse/ ...

  2. node.js PM2部署项目

    文章更新 2023-05-21 更新NSSM安装服务的方式 pm2 是什么 pm2 是一个守护进程管理工具,它能帮你守护和管理你的应用程序.通常一般会在服务上线的时候使用 pm2 进行管理.本文围绕以 ...

  3. 部署Node.js项目(CentOS)

    原文链接 部署Node.js项目(CentOS) 驻云科技  2017-05-11 17:46:43  浏览334  评论1  发表于: 阿里云服务 >> 最佳实践 云栖社区 linux  ...

  4. Node.js项目实践:构建可扩展的Web应用

    2019独角兽企业重金招聘Python工程师标准>>> Node.js项目实践:构建可扩展的Web应用 <Node.js项目实践:构建可扩展的Web应用>用专业的讲解方式 ...

  5. SAP UI5 应用开发教程之五十五 - 如何将本地 SAP UI5 应用通过 Node.js Express 部署到公网上试读版

    一套适合 SAP UI5 初学者循序渐进的学习教程 教程目录 SAP UI5 本地开发环境的搭建 SAP UI5 应用开发教程之一:Hello World SAP UI5 应用开发教程之二:SAP U ...

  6. WebStorm中Node.js项目配置教程(1)——创建项目

    Node.js绝对是一个web开发的热点话题,作为web神器的WebStorm也是开发Node.js的佼佼者. 接下来就Node.js项目在WebStorm的配置操作就行详细的讲解,首先是创建项目.两 ...

  7. 有没有办法为Node.js项目自动构建package.json文件

    本文翻译自:Is there a way to automatically build the package.json file for Node.js projects Is package.js ...

  8. 带你开发一个完整的 node.js 项目

    「他们根本不知道,现在的电商大促有多么依赖 Node.js」任职阿里的架构师朋友这么说. 说真的,我倒并不意外.作为一个定位明确的高性能 Web 服务器,Node.js 非常火热,几乎霸占了前端生态. ...

  9. Node.js项目中动态加载环境变量配置

    NODE_MODULES:项目中动态加载环境变量配置 开始 在平时的 Node.js 项目开发中,我们需要在项目中添加各种各样的配置:服务端口.服务地址.图片上传.数据库.Redis 等等. 通常情况 ...

最新文章

  1. Oracle学习之段区块初步概念
  2. sql server转mysql工具下载_SQL Server转换为MySQL工具推荐(Mss2sql)
  3. 生产订单结算时候的几个差异
  4. g++ linux 编译开栈_Linux下编写C++服务器(配置C++编译调试环境)
  5. 串口通信与编程:串口基础知识
  6. CV学习笔记-特征提取
  7. mysql 主主_MySQL双主(主主)架构
  8. location匹配
  9. VBA玩转系统剪贴板
  10. 【AI模型部署】maskrcnn在tfserver部署以及调用时遇到问题:Servable not found for request “xx”、‘incompatible_shape_error‘
  11. android呼叫转移代码,动态Android呼叫转移
  12. 十五年学不会英语的原因
  13. 硬件工程师成长之路(1)——元件基础
  14. 一战北邮计专考研经验分享
  15. Echarts 3d地球toolstips实现
  16. Hive中的left semi join和left anti join
  17. 马丁富勒微服务论文连接
  18. 企业微信社群该如何引流
  19. ECharts如何制作省份地图并在地图上显示自定义图标/散点图
  20. 上海市“专精特新”中小企业认定条件、流程、奖励政策

热门文章

  1. 大学生如何准备华为hcip认证及相关资料?
  2. C# 实现手写输入功能
  3. 2011年10月高等教育国际金融全国统一命题考试
  4. C#大华监控画面切换
  5. 初识Top4“不安全设计”以及Top5“安全配置错误”
  6. 《口吃者的自我治疗》(5. 矫正疗法的概要)
  7. 基于IndRNN的微博短文本情感分析设计与实现
  8. [苹果开发者账号]05 换收款的银行账号
  9. danmu.js pc端弹幕兼容ie9
  10. stm32中空的I/O管脚是高电平还是低电平