Istio1.5 Envoy 数据面 WASM 实践

Istio 1.5 回归单体架构,并抛却原有的 out-of-process 的数据面扩展方式,转而拥抱基于 WASM 的 in-proxy 扩展,以期获得更好的性能。本文基于网易杭州研究院轻舟云原生团队的调研与探索,介绍 WASM 的社区发展与实践。

超简单版解释:

--> Envoy 内置 Google V8 引擎,支持WASM字节码运行,并开放相关接口用于和 WASM 虚拟机交互数据;
--> 使用各种语言开发相关扩展并编译为 .WASM 文件;
--> 将扩展文件挂载或者打包进入 Envoy 容器镜像,通过xDS动态下发文件路径及相关配置由虚拟机执行。

WebAssembly 简述

Istio 最新发布的 1.5 版本,架构发生了巨大调整,从原有的分布式结构回归为单体,同时抛却了原有的 out-of-process 的 Envoy 扩展方式,转而拥抱基于 WASM 的 in-proxy 扩展,以期获得更好的性能,同时减小部署和使用的复杂性。所有的 WASM 插件都在 Envoy 的沙箱中运行,相比于原生 C++ Envoy 插件,WASM 插件具有以下的优点:

  • 接近原生插件性能(存疑,待验证,社区未给出可信测试结果,但是 WASM 字节码和机器码比较接近,它的性能极限确实值得期待);

  • 沙箱运行,更安全,单个 filter 故障不会影响到 Envoy 主体执行,且 filter 通过特定接口和 Envoy 交互数据,Envoy 可以对暴露的数据进行限制(沙箱安全性对于 Envoy 整体稳定性保障具有很重要的意义);

  • 可动态分发和载入运行(单个插件可以编译为 .WASM 文件进行分发共享,动态挂载,动态载入,且没有平台限制);

  • 无开发语言限制,开发效率更高(WASM 本身支持语言众多,但是限定到 Envoy 插件开发,必然依赖一些封装好的 SDK 用于和 Envoy 进行交互,目前只有 C++ 语言本身、Rust 以及 AssemblysScript 有一定的支持)。

WASM 的诞生源自前端,是一种为了解决日益复杂的前端 web 应用以及有限的 JavaScript 性能而诞生的技术。它本身并不是一种语言,而是一种字节码标准,一个“编译目标”。WASM 字节码和机器码非常接近,因此可以非常快速的装载运行。任何一种语言,都可以被编译成 WASM 字节码,然后在 WASM 虚拟机中执行(本身是为 web 设计,必然天然跨平台,同时为了沙箱运行保障安全,所以直接编译成机器码并不是最佳选择)。理论上,所有语言,包括 JavaScript、C、C++、Rust、Go、Java 等都可以编译成 WASM 字节码并在 WASM 虚拟机中执行。

社区发展及现状

Envoy & WASM

Envoy 提供了一个特殊的 Http 七层 filter,名为 wasm,用于载入和执行 WASM 字节码。该七层 filter 同样也负责 WASM 虚拟机的创建和管理,使用的是 Google 内部的 v8 引擎(支持 JS 和 WASM)。当前 filter 未进入 Envoy 主干,而是在单独的一个工程中。该工程会周期性从主干合并代码。从机制看,WASM 扩展和 Lua 扩展机制非常相似,只是 Lua 载入的是原始脚本,而 WASM 载入的是编译后的 WASM 字节码。Envoy 暴露相关的接口如获取请求头、请求体,修改请求头,请求体,改变插件链执行流程等等,用于 WASM 插件和 Envoy 主体进行数据交互。

对于每一个 WASM 扩展插件都可以被编译为一个 *.WASM 文件,而 Envoy 七层提供的 wasm Filter 可以通过动态下发相关配置(指定文件路径)使其载入对应的文件并执行:前提是对应的文件已经在镜像中或者挂载进入了对应的路径。当然,WASM Filter 也支持从远程获取对应的 *.WASM 文件(和目前网易轻舟 API 网关对 Lua 脚本扩展的支持非常相似)。

Istio & WASM

现有的 Istio 提供了名为 Mixer 插件模型用于扩展 Envoy 数据面功能,具体来说,在 Envoy 内部,Istio 开发了一个原生 C++ 插件用于收集和获取运行时请求信息并通过 gRPC 将信息上报给 Mixer,外部 Mixer 则调用各个 Mixer Adapter 用于监控、授权控制、限流等等操作,相关处理结果如有必要再返回给 Envoy 中 C++ 插件用于做相关控制。
Mixer 模型虽然提高了极高的灵活性,且对 Envoy 侵入性极低,但是引入了大量的额外的外部调用和数据交互,带来了巨大的性能开销(相关的测试结果很多,按照 istio 社区的数据:移除 Mixer 可以使整体 CPU 消耗减少 50%)。而且 Istio 插件扩展模型和 Envoy 插件模型整体是割裂的,Istio 插件在 out-of-process 中执行,通过 gRPC 进行插件与 Envoy 主体的数据交互,而 Envoy 原生插件则是 in-proxy 模式,在同一个进程中通过虚函数接口进行调用和执行。

因此在 Istio 1.5 中,Istio 提供了全新的插件扩展模型:WASM in proxy。使用 Envoy 支持的WASM机制来扩展插件:兼顾性能、多语言支持、动态下发动态载入、以及安全性。唯一的缺点就是现有的支持还不够完善。

为了提升性能,Istio 社区在 1.5 发布中,已经将几个扩展使用 in-proxy 模型(基于 WASM API 而非原生 Envoy C++ HTTP 插件 API)进行实现。但是目前考虑到 WASM 还不够稳定,所以相关扩展默认不会执行在 WSAM 沙箱之中(在所谓 NullVM 中执行)。虽然 istio 也支持将相关扩展编译为 WASM 模块,并在沙箱中执行,但是不是默认选项。

所谓 Mixer V2 其最终目标就是将现有的 out-of-process 的插件模型最终用基于 WASM 的 in-proxy 扩展模型来替代。但是目前举例目标仍旧有较长一段路要走,毕竟即使 Istio 社区本身的插件,也未能完全在 WASM 沙箱中落地。但从 Istio 1.5 开始,Istio 社区应该会快速推动 WASM 的发展。

solo.io & WASM

solo.io 推出了 WebAssembly Hub,用于构建、发布以及共享 Envoy WASM 扩展。WebAssembly Hub 包括一套用于简化扩展开发的 SDK(目前 solo.io 提供了AssemblysScript SDK,而 Istio/Envoy 社区提供了 Rust/C++ SDK),相关的构建、发布命令,一个用于共享和复用的扩展仓库。具体的内容可以参考 solo.io 提供的教程。

WASM 实践

下面简单实现一个 WASM 扩展作为演示 DEMO,可以帮助大家对 WASM 有进一步了解。此处直接使用了 solo.io 提供的构建工具,避免环境搭建等各个方面的一些冗余工作。该扩展名为 path_rewrite,可以根据路由原始的 path 值匹配,来将请求 path 重写为不同值

执行以下命令安装 wasme:

curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH

wasme 是 solo.io 提供的一个命令行工具,一个简单的类比就是:docker cli 之于容器镜像,wasme 之于 WASM 扩展。

ping@ping-OptiPlex-3040:~/Desktop/wasm_example$ wasme init ./path_rewrite
Use the arrow keys to navigate: ↓ ↑ → ←
? What language do you wish to use for the filter:▸ cppassemblyscript

执行 wasme 初始化命令,会让用户选择使用何种语言开发 WASM 扩展,目前 wasme 工具仅支持 C++ 和 AssemblyScript,当前仍旧选择 cpp 进行开发(AssemblyScript 没有开发经验,后续有机会可以学习一下)。执行命令之后,会自动创建一个 bazel 工程,目录结构如下:其中关键的几个文件已经添加了注释。从目录结构看,solo.io 没有在 wasme 中添加任何黑科技,生成的模板非常的干净,完整而简洁。

.
├── bazel
│   └── external
│       ├── BUILD
│       ├── emscripten-toolchain.BUILD
│       └── envoy-wasm-api.BUILD      # 说明如何编译envoy api依赖
├── BUILD                             # 说明如何编译插件本身代码
├── filter.cc                         # 插件具体代码
├── filter.proto                      # 扩展数据面接口
├── README.md
├── runtime-config.json
├── toolchain
│   ├── BUILD
│   ├── cc_toolchain_config.bzl
│   ├── common.sh
│   ├── emar.sh
│   └── emcc.sh
└── WORKSPACE                         # 工程描述文件包含对envoy api依赖

filter.cc 中已经填充了样板代码,包括所有的插件需要实现的接口。开发者只需要按需修改某个接口的具体实现即可(此处列出了整个插件的全部代码,以供参考。虽然该代码没有实现什么特许功能,但是已经包含了一个 WASM 扩展(C++ 语言版)应当具备的所有结构,无论多么复杂的插件,都只是在该结构的基础上填充相关的逻辑代码而已

// NOLINT(namespace-envoy)
#include <string>
#include <unordered_map>#include "google/protobuf/util/json_util.h"
#include "proxy_wasm_intrinsics.h"
#include "filter.pb.h"class AddHeaderRootContext : public RootContext {
public:explicit AddHeaderRootContext(uint32_t id, StringView root_id) : RootContext(id, root_id) {}bool onConfigure(size_t /* configuration_size */) override;bool onStart(size_t) override;std::string header_name_;std::string header_value_;
};class AddHeaderContext : public Context {
public:explicit AddHeaderContext(uint32_t id, RootContext* root) : Context(id, root), root_(static_cast<AddHeaderRootContext*>(static_cast<void*>(root))) {}void onCreate() override;FilterHeadersStatus onRequestHeaders(uint32_t headers) override;FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override;FilterHeadersStatus onResponseHeaders(uint32_t headers) override;void onDone() override;void onLog() override;void onDelete() override;
private:AddHeaderRootContext* root_;
};
static RegisterContextFactory register_AddHeaderContext(CONTEXT_FACTORY(AddHeaderContext),ROOT_FACTORY(AddHeaderRootContext),"add_header_root_id");bool AddHeaderRootContext::onConfigure(size_t) { auto conf = getConfiguration();Config config;google::protobuf::util::JsonParseOptions options;options.case_insensitive_enum_parsing = true;options.ignore_unknown_fields = false;google::protobuf::util::JsonStringToMessage(conf->toString(), &config, options);LOG_DEBUG("onConfigure name " + config.name());LOG_DEBUG("onConfigure " + config.value());header_name_ = config.name();header_value_ = config.value();return true; 
}bool AddHeaderRootContext::onStart(size_t) { LOG_DEBUG("onStart"); return true;}void AddHeaderContext::onCreate() { LOG_DEBUG(std::string("onCreate " + std::to_string(id()))); }FilterHeadersStatus AddHeaderContext::onRequestHeaders(uint32_t) {LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id()));return FilterHeadersStatus::Continue;
}FilterHeadersStatus AddHeaderContext::onResponseHeaders(uint32_t) {LOG_DEBUG(std::string("onResponseHeaders ") + std::to_string(id()));addResponseHeader(root_->header_name_, root_->header_value_);replaceResponseHeader("location", "envoy-wasm");return FilterHeadersStatus::Continue;
}FilterDataStatus AddHeaderContext::onRequestBody(size_t body_buffer_length, bool end_of_stream) {return FilterDataStatus::Continue;
}void AddHeaderContext::onDone() { LOG_DEBUG(std::string("onDone " + std::to_string(id()))); }void AddHeaderContext::onLog() { LOG_DEBUG(std::string("onLog " + std::to_string(id()))); }void AddHeaderContext::onDelete() { LOG_DEBUG(std::string("onDelete " + std::to_string(id()))); }

注意到生成的样板代码类型名称仍旧以 AddHeader 为前缀,而没有根据提供的路径名称生成,此处是 wasme 可以优化的一个地方。此外,自动生成的样板代码中已经包含了 AddHeader 的一些代码,逻辑简单,但是配置解析、API 访问,请求头修改等过程都具备,麻雀虽小,五脏俱全,正好可以帮助初次的开发者可以依葫芦画瓢熟悉 WASM 插件的开发过程。对于入门是非常友好的。

针对 path_rewrite 具体的开发步骤如下:

STEP ONE 首先修改模板代码中 filter.proto 文件,因为 path rewrite 肯定不能简单的只能替换固定值,修改后 proto 文件如下所示:

syntax = "proto3";message PathRewriteConfig {message Rewrite {string regex_match = 1;      # path正则匹配时替换string custom_path = 2;      # 待替换值}repeated Rewrite rewrites = 1;
}

STEP TWO 修改配置解析接口,具体方法名为 onConfigure。修改后解析接口如下:

bool AddHeaderRootContext::onConfigure(size_t) {auto conf = getConfiguration();PathRewriteConfig config; // message type in filter.protoif (!conf.get()) {return true;}google::protobuf::util::JsonParseOptions options;options.case_insensitive_enum_parsing = true;options.ignore_unknown_fields = false;// 解析字符串配置并转换为PathRewriteConfig类型:配置反序列化google::protobuf::util::JsonStringToMessage(conf->toString(), &config,options);// 配置阶段编译regex避免请求时重复编译,提高性能for (auto &rewrite : config.rewrites()) {rewrites_.push_back({std::regex(rewrite.regex_match()), rewrite.custom_path()});}return true;
}

STEP THREE 修改请求头接口,具体方法名为 onRequestHeaders,修改后接口代码如下:

FilterHeadersStatus AddHeaderContext::onRequestHeaders(uint32_t) {LOG_DEBUG(std::string("onRequestHeaders ") + std::to_string(id()));// Envoy中path同样存储在header中,key为:pathauto path = getRequestHeader(":path");if (!path.get()) {return FilterHeadersStatus::Continue;}std::string path_string = path->toString();for (auto &rewrite : root_->rewrites_) {if (std::regex_match(path_string, rewrite.first) &&!rewrite.second.empty()) {replaceRequestHeader(":path", rewrite.second);replaceRequestHeader("location", "envoy-wasm");return FilterHeadersStatus::Continue;}}return FilterHeadersStatus::Continue;
}

从上述过程不难看出,整个扩展的开发体验相当简单,按需实现对应接口即可,扩展本身内容非常轻,内部具体的功能逻辑才是决定扩展开发复杂性的关键。而且借助 wasme 工具,自动生成代码后,效率可以更高(和目前在内部使用的 filter_creator.py 有部分相似,样板代码自动生成)。

至此,插件已经开发完成,可以打包编译了。wasm 同样提供了打包编译的功能,甚至可以类似于容器镜像将编译后结构推送到远端仓库之中,用于分享或者存储。不过有一个提示,在开发之前,先直接执行 bazel 命令编译,编译过程中,一些基础依赖会被自动拉取并缓存到本地,借助 IDE 可以获得更好的代码提示和开发体验。

bazel build :filter.wasm

接下来是 wasme 命令编译:

wasme build cpp -t webassemblyhub.io/wbpcode/path_rewrite:v0.1 .

该命令会使用固定镜像作为编译环境,但是本质和直接使用 bazel 编译并无不同。具体的编译日志可以看出,实际上,该命令也是使用的bazel build :filter.wasm

Status: Downloaded newer image for quay.io/solo-io/ee-builder:0.0.19
Building with bazel...running bazel build :filter.wasm
Extracting Bazel installation...
Starting local Bazel server and connecting to it...

注意,上述命令中 wbpcode 为用户名,具体实践时提议替换为自身用户名,如果注册了 webassemblyhub.io 账号,甚至可以进行 push 和 pull 操作。此次就不做相关操作了,直接本地启动带 WASM 的 envoy。命令如下:

# --config参数用于指定wasm扩展配置
wasme deploy envoy webassemblyhub.io/wbpcode/path_rewrite:v0.1 --config "{\"rewrites\": [ {\"regex_match\":\"...\", \"custom_path\": \"/anything\"} ]}" --envoy-run-args "-l trace"

从 envoy 执行日志可以看到:最终 envoy 会执行七层 Filter:envoy.filters.http.wasm,相关配置为:wasm 文件位置(docker 执行时挂载进入容器内部)、 wasm 文件对应插件配置、runtime 等等。通过在 http_filters 中重复添加多个envoy.filters.http.wasm,即可实现多个 WASM 扩展的执行。从下面的日志也可以看出,即使不使用 solo.io 的工具,只需要为 Envoy 指定编译好的 wasm 文件,其执行结果是完全相同的。

[2020-03-31 08:41:24.831][1][debug][config] [external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:388]       name: envoy.filters.http.wasm
[2020-03-31 08:41:24.831][1][debug][config] [external/envoy/source/extensions/filters/network/http_connection_manager/config.cc:390]     config: {"config": {"rootId": "add_header_root_id","vmConfig": {"code": {"local": {"filename": "/home/ping/.wasme/store/e58ddd90347b671ad314f1c969771cea/filter.wasm"}},"runtime": "envoy.wasm.runtime.v8"},"configuration": "{\"rewrites\": [ {\"regex_match\":\"...\", \"custom_path\": \"/anything\"} ]}","name": "add_header_root_id"}
}

之后使用对应 path 调用接口:可发现 WASM 插件已经生效:

':authority', 'localhost:8080'
':path', '/ab' # 原始请求path匹配"..."
':method', 'GET'
'user-agent', 'curl/7.58.0'
'accept', '*/*'
':authority', 'localhost:8080'
':path', '/anything'
':method', 'GET'
':scheme', 'https'
'user-agent', 'curl/7.58.0'
'accept', '*/*'
'x-forwarded-proto', 'http'
'x-request-id', '1009236e-ab57-4ded-a8ff-3d1b17c6787b'
'location', 'envoy-wasm'
'x-envoy-expected-rq-timeout-ms', '15000'

WASM 总结

WASM 扩展仍在快速发展当中,但是 Isito 使用 WASM API 实现了相关的插件,说明已经做好了迁移的准备。前景美好,值得期待,但有待进一步确定 WASM 沙箱本身稳定性和性能。

从开发体验来说:

  • 借助 solo.io 工具,简单插件的开发几乎没有任何的难度,只是目前支持的语言只有 C++/AssemblyScript(Envoy 社区开发了 Rust 语言 SDK,但是正在开发当中而且使用 Rust 开发 WASM 扩展的价值存疑:Rust 相比于 C++ 最大的优势是通过严格的编译检查来保证内存安全,但是也使得上手难度又提升了一个台阶,在有 WASM 沙箱为内存安全兜底的情况下,使用 Rust 而不使用 JS、Go 等上手更简易的语言来开发扩展,实无必要)。

  • 对于相对复杂的插件,如果使用 WASM 的话,测试相比于原生插件会更困难一些,WASM 扩展配置的输入只能依赖手写 JSON 字符串,希望未来能够改善。

  • 缺少路由粒度的配置,所有配置都是全局生效,依赖插件内部判断,但是这一部分如果确实有需要,支持起来应该很快,不存在技术上的阻碍,倒是不用担心。

作者简介

王佰平,网易杭州研究院轻舟云原生团队工程师,负责轻舟 Envoy 网关与轻舟 Service Mesh 数据面开发、功能增强、性能优化等工作,对 Envoy 数据面开发、增强、落地具有较为丰富的经验。

点击 阅读原文 查看更多

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310291.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elasticSearch -- (文档,类型,索引)

问题:大规模数据如何检索 当系统数据量达到10亿&#xff0c;100亿级别的时候&#xff0c;我们系统该如何去解决这种问题。 数据库选择—mysql&#xff0c; sybase&#xff0c;oracle&#xff0c;mongodb&#xff0c;hbase…单点故障如何解决—lvs&#xff0c; F5&#xff0c;…

asp后台调用产品数据_后台产品经理,需掌握这些数据交互知识

人们每天都在接收信息和发送信息&#xff0c;在传递信息的过程中&#xff0c;明白对方要表达的意思。数据也是如此&#xff0c;在系统交换数据的过程中&#xff0c;就伴随着数据交互。本篇文章将为大家具体分析前端和后台的数据交互与协议。本文所说的”数据交换” 是指在计算机…

使用c# .net core开发国标gb28181 sip +流媒体服务完成视频监控实例教程 亲身完美体验过程...

目前使用C# .net core 来实现国标gb28181标准的摄像头播放、ptz云台控制、视频回放等视频监控功能&#xff0c;项目可运行于linux/docker/.net core环境&#xff0c;也是当前非常罕有的能做到毫秒级国标gb28181公网视频传送案例&#xff0c;也是少有的能同时具有播放、ptz云台控…

[Java基础]字节流读数据

代码如下: package InstreamPack;import java.io.FileInputStream; import java.io.IOException;public class FileInputStreamDemo01 {public static void main(String[] args) throws IOException {FileInputStream fis new FileInputStream("D:\\JavaDemo\\java.txt&…

丁可以组什么词_有哪些量词可以用来描述生意经?

分别有&#xff1a;本&#xff0c; 一(本)生意经。笔&#xff0c; 一(笔)生意经。次&#xff0c;一(次)生意量词用名量词&#xff1a;表示事物的计量单位。基本定义&#xff1a;通常用来表示人、事物或动作的数量单位的词&#xff0c;叫做量词。量词 lingc&#xff0c;与代表可…

硬核技能k8s初体验

&#xff0c;Kubernetes 是一个软件系统&#xff0c;使你在数以万计的电脑节点上运行软件时就像所有节点是以单个大节点一样&#xff0c; 它将底层基础设施抽象&#xff0c;这样做同时简化了应用开发、部署&#xff0c;以及对开发和运维团队的管理。Kubernetes集群架构Kubernet…

线上问题排查流程

问题排查 针对各种常见的线上问题&#xff0c;梳理下排查思路。 业务问题 线上问题大多数时候都是业务问题引发的问题&#xff0c;当线上环境绝大多数请求都是正常&#xff0c;当有部分或者某一个用户有问题&#xff0c;此时怎么针对性的排查在当前微服务体系下&#xff0c;…

springboot超详细教程_CG原画插画教程:超详细线稿教程

艺学绘小编收集整理了超详细线稿教程今天我们从起稿开始1. 用圆表示出人物的头部、胸腔和骨盆&#xff0c;分别画出头部的十字线和身体的中线&#xff0c;表示头和身体的朝向。用“火柴棍”表示四肢的动作形态。▼2. 根据圆和十字线画出脸的轮廓&#xff0c;再在身体骨架上画出…

Asp.Net.Core WebApi 版本控制

前言在后端Api的开发过程中,无法避免的会遇到接口迭代的过程,如何保证新老接口的共存和接口的向前的兼容呢,这时候就需要对Api进行版本的控制,那如何优雅的控制Api的版本呢&#xff1f;开始Microsoft.AspNetCore.Mvc.Versioning 是一个微软官方推出的一个用于管理Api版本的包,配…

[Java基础]字节缓冲流

字节缓冲输出流: package InstreamPack;import java.io.BufferedOutputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets;public class BufferStreamDemo01 {public s…

用asp.net core结合fastdfs打造分布式文件存储系统

最近被安排开发文件存储微服务&#xff0c;要求是能够通过配置来无缝切换我们公司内部研发的文件存储系统&#xff0c;FastDFS&#xff0c;MongDb GridFS&#xff0c;阿里云OSS&#xff0c;腾讯云OSS等。根据任务紧急度暂时先完成了通过配置来无缝切换内部文件系统以及FastDFS。…

仅仅有人物没背景的图片怎么弄_五分钟写作课 人物篇 人物的出场是个关键时刻...

我看过不少故事&#xff0c;人物出场了&#xff0c;说了几句话&#xff0c;然后离开&#xff0c;读者知道他是某人的爸爸&#xff0c;学校的老师&#xff0c;某个企业的老板&#xff0c;除了这些定为之外&#xff0c;他自己是怎样的性格&#xff1f;怎样的形象呢&#xff1f;没…

数据结构与算法一篇帮助你吃下KMP算法

模式匹配 什么是模式匹配&#xff0c;我们用一个案例来说明&#xff1a; 当S “s1,s2,s3,s4 …sn” T“t1,t2,t3,t4 … tn” 在字符串S中寻找T字符串的过程就是模式匹配的过程&#xff0c;T就说模式串&#xff0c;S是主串 实现方案&#xff1a; 暴力破解&#xff0c;逐字符判…

在远程 CSM 课程中体验线上工作坊

4.11 日周六&#xff0c;我参与了由 Bob 老师组织讲授的一期 Certified Scrum Master&#xff08;即 CSM&#xff09;课程&#xff0c;从中收获颇丰&#xff0c;特记于此&#xff0c;与君分享。CSM 通常是现场授课&#xff0c;但本次由于疫情的限制导致人们不得不尽可能减少外出…

数据结构与算法--力扣108题将有序数组转换为二叉搜索树

力扣108提将有序数组转换为二叉搜索树 近一年都比较关注算法相关的知识&#xff0c;也刷了不少题&#xff0c;之前的文章中大多也是算法相关的文章&#xff0c;但是感觉每次遇到树相关的题型都不能应对自如&#xff0c;因此还是有必要在相关知识上下功夫&#xff0c;因此有此次…

C#验证IP是否为局域网地址的三种方法

C#验证IP是否为局域网地址的三种方法前一阵子有【广州.NET群】的客户问起这个问题&#xff0c;说他们需要验证客户输入的网站是否为局域网。其实局域网的 IP并没有确定的定义&#xff0c;只要是局域网中&#xff0c;即可设置为任何一个 IP。但确实存在一个 内网保留地址的定义&…