FatTiger's Blog - 胖虎
type: channel
link: https://www.wangshenjie.com/index.php/author/1/
description: 胖虎
items: 以太坊黑暗森林-抢跑（front running）
items: C#：IDisposable 和 析构函数
items: ThreadLocal引发的灾难
items: 我在币安智能链的日子-区块链基础
items: All about ExecutionContext and SynchronizationContext
items: 业务规则引擎(Business Rule Engine)
items: Create React App(CRA)本地开发配置代理
items: 无题
items: 使用VS Code进行Java开发
items: real mode protected mode and segmentation

以太坊黑暗森林-抢跑（front running）
type: item
date: 2023-05-05T11:01:00+00:00
description: 前言鸽了很久之后的今天突然心血来潮，准备写一个系列：以太坊黑暗森林，它介绍以太坊生态上的各种奇思妙想和逆天的攻击方式，会从简单的、常见的攻击方式开始介绍。取这个名字是因为我接触以太坊不久后看的一篇文章 [Ethereum is a Dark Forest](https://www.paradigm.xyz/2020/08/ethereum-is-a-dark-forest)，让我想起了《三体》小说中刘慈欣描述的黑暗森林，以太坊是一个弱肉强食的、没有规则的世界，猎人们总是躲在背后监听所有的交易，一旦发现猎物，它们会把它的血给吸干。开盘抢币相信进入以太坊生态的韭菜们，一定有过在 uniswap 上买刚开盘新币的经历，新开盘的币，一般会上涨几倍甚至十几倍，越早买入则越能低价买入。你守着时间，等着项目方添加流动性后第一时间买入代币，但是你发现，无论你的手速多块，总是看到一开盘，价格已经飚了几倍，你骂骂咧咧，开始不断拉高 gas 费用，尝试继续买入，但是你眼睁睁的看着代币涨到十倍，自己的交易却一直失败，你开始怀疑项目方自己抢跑，怀疑项目方捣鬼：肯定是项目方吃相难看，用老鼠仓提前买了。另一些聪明人，研究了以太坊的基本技术，他们在 ethscan 了解了关于“Block”、“Transaction”、“Event”的基本知识，他们聪明的想到：只要我写个程序，不断监听新的区块，只要发现有uniswap添加流动性的事件，我立即买入代币，岂不是比我手动，买入更快？于是他们开发了程序，监听了区块，也成功监听到事件，但是他们遗憾的发现，他们买入时的价格已经上涨十倍了。mempool 内存池韭菜们百思不得其解，正当他们一筹莫展之际，突然发现一个新的名词：mempool（内存池）。什么是 mempool？以太坊的出块时间，是 12 秒一个块，在这 12 秒的间隔中，每笔交易都被存放在一个叫做 mempool 的地方， 它只是这些交易暂存的空间，在矿工打包区块时，从 mempool 拿到所有交易，并对这些交易进行排序：按照 gas 价格进行排序，或者按照某种其它的特殊循序（正常来说都是按照 gas 价格进行排序，但是 flashbot 及其它 MEV 相关技术出现导致某些矿工不会按照这个顺序打包），mempool 存放的都是未决交易，它们还未打包但是即将在下个区块中打包，并且 mempool 是公开的，可以被任何人查询。至此，韭菜们发现为什么他们买入时价格已经是涨了几倍之后：如果在 mempool 中监听流动性事件，并且设置 gas 费用总是低于流动性事件的 gas 费用，然后立即发送交易，这样买入交易就可以与流动性事件的交易在同一个区块中一起被打包，打包上链后，即所谓的开盘，韭菜们才一拥而入，然而抢先的买入交易已经将价格拉高数倍。三明治另一些聪明的韭菜相信，刚开盘剧烈波动的代币不是买入的好时机，他们要等到价格平稳后再视情况买入。他们选择在开盘几个小时后，币价相对平稳，不像开盘时剧烈波动的时机买入。只见他熟练的打开 uniswap，选个 10、20% 滑点（滑点是为了防止频繁的交易失败的参数，因为你的交易会在下个区块打包，在这期间，可能会有别人的交易也会在同一个区块打包，如果别人也买了相同的代币，那么币价会上涨，在这之后的买入交易代币价格会上涨，滑点的设置表示你最多可以接受多大的价格涨幅，超出这个涨幅则交易失败），然后点击 SWAP，几十秒之后，他买到了自己的 “价值币”，心满意足的离开了，他不知道的是，他原本可以得到更多代币。他如果去 ethscan 点开自己的交易，会发现在自己交易的一前一后，刚好有同一个代币的“买入/卖出”交易，这两笔像三明治一样，将他的交易夹在中间：在他交易前先一步买入代币，在他交易后卖出代币，如果他再仔细调查，会发现提前买入代币的交易，刚好将代币价格拉升到他滑点的极限，等他买入后，再卖出获利。这与上文所描述的实际上是使用相同的技术，监听 mempool 的交易，发现目标后，发送一前一后两个交易，这两个交易的 gas 价格都非常讲究，在矿工按照 gas 价格打包时刚好将目标夹在中间然后获利。尾言当然，猎人也面临着各种各样的问题，实际情况没这么简单，原理是这么个原理，但是细节上还有很多问题要解决，例如，如何检测 mempool 的交易内容？mempool 的交易都是未执行的交易，因此没有 Event，也没有执行结果。还有如何保证在与别的竞争对手的竞争中胜出？要知道黑暗森林中盯着猎物的可不止一个猎人。以及如何避免各式各样的钓鱼，蜜獾？有些智能合约专门针对机器人制造了一些陷阱。这些内容以后再开贴细说，现在，我已经没耐心接着说了。

C#：IDisposable 和 析构函数
type: item
date: 2022-08-24T06:01:00+00:00
description: C# 中有两种释放资源的方式：实现 IDisposable 或使用析构函数。通常，必须在特定时间释放资源的场景中，我们实现 IDisposable，像这样：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } public void Dispose() { if (disposed) { return; } disposed = true; // 调用其它托管资源的Dispose _stream.Dispose(); // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); } 通常来说，如果你在使用完后正确调用了 Dispose() 方法或者使用了 using，就可以正确释放资源。其他情况，则需要依赖 GC 在回收托管对象前先释放引用的非托管资源，一个实现了 IDisposable 接口和具有析构函数的类可能如下所示：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleDispose(Stream stream, IntPtr handle) { this._stream = stream; this._handle = handle; } ~ExampleDispose() { // 此代码有异常情况，不要在生产环境使用 Dispose(); } public void Dispose() { if (disposed) { return; } disposed = true; // 调用其它托管资源的Dispose _stream.Dispose(); // 释放非托管资源 CloseHandle(_handle); _handle = IntPtr.Zero; } [System.Runtime.InteropServices.DllImport("Kernel32")] private extern static Boolean CloseHandle(IntPtr handle); }相比上部分代码，只是多增加了一个析构函数 ~ExampleDispose()，仔细观察它非常有意思，构造函数用于对象的创建，析构函数有点像构造函数，只是没有访问修饰符，且前面多了个 “~”，我们知道这个符号通常用于取反操作，构造函数取反，那就是“销毁函数”，也正如它所隐含的含义那样：GC 会在回收对象前调用析构函数来释放资源。现在，无论用户是否主动调用过 Dispose()，~ExampleDispose() 都会在 GC 回收对象时被调用，如果用户主动释放过资源，那么 disposed 为 true，~ExampleDispose() 实际上调用 Dispose() 后直接返回，没有多于的操作，但是另一种情况，用户从未主动调用过 Dispose()，这时候在 GC 在调用 ~ExampleDispose() 时，会调用到 _stream.Dispose();。注意，这里可能会产问题，因为 GC 在回收对象时，对象内部其它对象可能已经被回收，这里 _stream 是有可能已经被回收的，因此，我们需要稍微更改一下，以实现在 GC 时不引用任何托管代码，为此，我们实现 dispose pattern：public class ExampleDispose : IDisposable { // 非托管资源 private IntPtr _handle; // 使用的其它托管资源 private readonly Stream _stream; private bool disposed = false; public ExampleD…

ThreadLocal引发的灾难
type: item
date: 2022-08-01T10:09:00+00:00
description: 在 Java 里有个称之为线程本地变量的类型叫做 ThreadLocal，它与 ThreadLocal 之于 C# 中是一样的作用，可以在线程范围内设置变量，这个变量只会在当前线程可被访问，但是它们有一点不同的是，在 Java 中，当你设置好变量后，在线程使用完毕回到线程池之前，需要手动调用 ThreadLocal.remove() 方法去清除线程本地变量，否则变量随着线程回到线程池，并且在下次使用此线程时此变量继续存在，而在 C# 中，线程回到线程池时会自动清除本地变量，因此无需手动去清除。我们的业务有这样一个场景：某个业务 UserService 类中，具有多个方法会频繁（甚至循环）调用一个获取用户标签的接口，具体原因是因为某些方法会进行递归，数据结构有个树状结构，因此，为了优化接口响应时间以及看起来不那么蠢，我使用 ThreadLocal 将用户标签接口的返回数据存储到当前线程，因为在单个请求中，多次调用此接口获取数据是不必要的，它看起来像这样： /** * 此静态变量ThreadLocal会为每个线程创建本地副本, 因此USER_TAGS_THREAD_LOCAL的更改只会影响当前线程 注意, * 即使线程被回收到线程池, 此变量副本也不会被清除, 因此, 需要在使用完毕后手动清除, USER_TAGS_THREAD_LOCAL.remove() * 否则可能会导致从线程池拿到的线程带有之前的变量 无论如何, 这不会产生内存泄漏问题 * 此变量在controller返回或controller异常时进行清除操作 */ public final static ThreadLocal<List<Tag>> USER_TAGS_THREAD_LOCAL = ThreadLocal.withInitial(() -> new ArrayList<>()); /** * 获取用户标签 */ private List<Tag> getUserTags(String userId) { if (StringUtil.isNullOrEmpty(userId)) { return new ArrayList<>(); } if (!CollectionUtils.isEmpty(USER_TAGS_THREAD_LOCAL.get())) { // 返回线程本地存储的用户标签 return USER_TAGS_THREAD_LOCAL.get(); } // 调用接口获取用户标签 List<Tag> tags = getUserTags(userId); USER_TAGS_THREAD_LOCAL.get().addAll(tags); return USER_TAGS_THREAD_LOCAL.get(); } // 在多个方法中会用到获取用户标签，这里假设会递归调用 public User queryUserInfo(String userId) { User user = getUser(userId); List<Tag> userTags = getUserTags(userId); user.Parent = queryUserInfo(user.ParentId); ... } 这里有个问题，那就是清除 ThreadLocal 的时机，假设我们在 queryUserInfo 方法中，增加一个 try/catch/finally 块，在每次调用完毕后进行清除，这肯定不合适，这样的递归方法，每次都清除，会失去 ThreadLocal 的意义，那么在 queryUserInfo 外层再增加一个方法，例如： public User queryUserInfoWapper(String userId) { try { return queryUserInfo(userId); } catch { ... } finally { USER_TAGS_THREAD_LOCAL.remove(); } }这看起来确实解决了问题。但是我们在这个 UserService 类中有多个类似 queryUserInfo 的方法，为每个类似 queryUserInfo 的方法都增加 try/catch/finally 块，你得找到所有的点去清除 ThreadLocal，很可能会出现某些点漏掉，或者一次请求线程中多次清除了 ThreadLocal。于是我开始寻找，某种能在线程池回收线程时的钩子，在线程回收时清除线程上的变量，当然最后由于实现难度或开发进度等种种原因，我选择了在控制器入口进行清除，我将 USER_TAGS_THREAD_LOCAL 属性暴露给外层，在控制器入口调用 USER_TAGS_THREAD_LOCAL.remove()： @Po…

我在币安智能链的日子-区块链基础
type: item
date: 2022-01-11T03:37:00+00:00
description: 区块和链无论是比特币还是以太坊，都是具有一个个区块（称之为Block）的链式结构，学过<数据结构>的肯定明白链表，区块链就像一个链表，每个区块都存储上一个区块哈希。链（称之为Chain），有非常多的链，他们的协议不同，技术也不尽相同，比特币网络是一个链，以太坊网络是另一个链，每个链都有自己的目标（甚至目标只是为了圈钱），每个链也都有自己的代币，比特币网络的代币是比特币，每次交易都需要比特币作为手续费，以太坊网络代币是以太币，每次在以太坊网络的交易都需要以太币作为手续费。所以，链实际上作为基础设施，非常多的团队喜欢创建新的链，但是一个链光有网络光有代币不行，没有生态，很难成功。币安智能链（Binance Smart Chain：BSC）我的主要操作都是在BSC上，没有其它原因，只因为一个穷字。在BTC网络交易，需要BTC用作手续费，这个我可用不起，在以太坊（Ethereum）网络交易，需要以太币（ETH）作为用作手续费，按照以太币目前（2022-01-01）的价格，单次交易大概要40刀左右（取决于具体操作），因此称为富人链，而BSC网络的代币币安币（BNB）价格要便宜很多，目前单次交易手续费在3刀（取决于具体操作）左右。因此，我实际上没有频繁在BTC与以太坊网络交易，实际经验来自于BSC。前面说过，每个Chain的技术不尽相同，BSC是从以太坊拓展而来，它兼容以太坊虚拟机(EVM)，因此完全兼容以太坊，甚至同一个钱包地址在以太坊与BSC完全一致，你拥有的BSC钱包即是你的以太坊钱包，以太坊钱包地址也可以是你的BSC钱包地址。BSC采用共识机制来降低交易费用和提升交易速度，这个咱不用管，重点是完全兼容以太坊，这有个很大的好处是可以吃一部分以太坊的生态，以前那些在以太坊圈钱的项目现在可以轻松跨链到BSC继续圈一波钱，本来圈不到以太坊大佬钱的项目现在可以上BSC圈一波韭菜的钱，BSC手续费低准入门槛就低，我这种韭菜就喜欢这种链，另外一个好处就是支持智能合约，以太坊的（EVM）能够执行图灵完备的智能合约代码，BSC当然也能。智能合约（Smart Contract）智能合约是以太坊的一项重大创新，它允许开发人员自己编写合约并部署到网络上，由以太坊虚拟机EVM来执行，合约代码是图灵完备的并且它一旦部署就不允许修改，且只能被动的被调用不可以主动，它也像个人账户一样可以存储各种代币的余额。同质化代币与非同质化代币（NFT）ERC20协议 同质化代币简单来说如果你想在以太坊或BSC上发行新的代币（称为Token），必须通过部署合约来操作，部署一个合约，合约指定了发行代币的数量，同时也存储每个地址所持有代币的数量，代币的转移，从一个账户到另一个账户的转移（交易），都需要的调用代币对应的合约来执行，合约内部将某个地址持有的代币数量转移到另一个地址。这套标准的东西既是ERC20，实现ERC20标准的合约必须具有某些特定的方法（例如必须实现代币转移的方法transferFrom等）。因为每个代币都是完全相等可等同置换的，因此是同质化代币。ERC721协议 非同质化代币（NFT）与ERC720一样，它也是一套标准，实现ERC721协议的合约必须实现某些方法，唯一的区别在于，它的每个代币都有独一无二的ID，例如你要发行一个鞋子NFT，每双鞋都是不一样的，不可以等同置换，鞋子也是代币，但是每双鞋子都是一个独一无二的NFT，因此它是非同质化代币。ERC721的代币转移需要指定代币的具体ID进行转移。我发行的新代币有价值吗？因为有ERC20协议的存在，它制定了一套标准，因此可以根据这个标准制定出一套合约模板，你只需要填写代币名称，填写发行数量，就可以发行任意数量的新代币，任何人都可以部署智能合约，因此任何人都可以发行新的代币，但是新的代币是没有价值的，价值是人的共识，股票为什么有价值？因为股票背后是公司，有实体，如果代币发行的团队或个人没有价值，那么这个代币不会有价值。

All about ExecutionContext and SynchronizationContext
type: item
date: 2021-09-06T09:06:00+00:00
description: 前言这篇文章深入探讨了ExecutionContext和SynchronizationContext, 这是大部分开发人员都不需要了解的 .NET 高级领域.SynchronizationContext我最早关于 SynchronizationContext 了解可能是在 WindowsForm 程序, 当我错误的在其它线程去更新 UI 控件时, 总会出现一个异常 Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on 翻译过来即 跨线程操作无效：控件从创建它的线程以外的线程访问, 例如这个代码片段:private void button_Click(object sender, EventArgs e) { Task.Run(() => { //异常: 跨线程操作无效：控件从创建它的线程以外的线程访问 textBox.Text = "异步更新"; }); }我相信如果你写过 WindowsForm 程序, 肯定遇到过这个错误, 这是因为 WindowsForm 程序希望所有的 UI 操作都通过 UI 线程去更新, 当你在单线程中, 无论怎样操作, 都是可以的, 因为不管是按钮事件还是文本框事件, 都是通过 UI 线程触发的, 那么事件内代码也是通过 UI 线程执行的, 因此, 如果你没有其它线程去操作控件的话, 是不会出现此异常的. 我当时通过简单的禁止此检查来避免异常, 实际上正确的方式应当通过我们的主角之一 SynchronizationContext 去更新UI, 像这样:private void button_Click(object sender, EventArgs e) { var sc = SynchronizationContext.Current; Task.Run(() => { sc.Post(delegate { textBox.Text = "异步更新"; }, null); }); }这里发生了什么? 抓住我的手, 我们一步步进行探索. SynchronizationContext 是一个具有抽象功能的类, 主要的两个方法 Send 以及 Post, 这两个方法的动作一致, 仅仅是同步与非同步的区别, Post 接受一个委托, 用来将委托排队到线程上下文, 这里的上下文可能是多个线程的上下文, 也可能是单个线程的上下文, 这取决于具体的 SynchronizationContext 实现. WindowsFormsSynchronizationContext 在 WindowsForm 程序启动时被设置到 UI 线程, 因此在这里, SynchronizationContext.Current, 实际上是 WindowsFormsSynchronizationContext, 它的上下文是单个 UI 线程, WindowsFormsSynchronizationContext 重写了 Post 及 Send, 它将委托传递给基础 Win32 消息循环, 而后 UI 线程接受消息并执行委托方法. 因此这段代码解释为: 在异步线程中将委托方法排队到 UI 线程, 随后 UI 线程更新 textBox.Text.前面说到, SynchronizationContext 并不表示真正的线程同步上下文, 实际情况要看它具体的实现, 例如在 WPF 和 Silverlight 程序中, 它是 DispatcherSynchronizationContext, 它将委托以 Normal 优先级排列到 UI 线程的 Dispatcher. 当线程通过调用 Dispatcher.Run 开始其 Dispatcher 循环时, 此 SynchronizationContext 被设置为当前上下文. DispatcherSynchronizationContext 的上下文是单个 UI 线程. 默认的 SynchronizationContext 是 SynchronizationContext 实例, 如果线程的 SynchronizationContext.Current 为 null, 则认为具有默认的SynchronizationContext. 默认的 SynchronizationContext 将 Post 委托排队到线程池上, 而 Send 委托则使用调用 Send 方法的线程同步执行. 因此从这个角度来说, SynchronizationContext 上下文是整个 ThreadPool.SynchronizationContext 的 Post …

业务规则引擎(Business Rule Engine)
type: item
date: 2021-06-28T09:46:00+00:00
description: 业务规则引擎(Business Rule Engine)什么是规则引擎在实际的业务场景中, 会有多个业务规则, 这些政策可能是公司策略, 也可能是法律法规要求. 例如在注册新用户时, 如果年龄小于18岁则需要同意额外的用户协议, 或者在促销时, 满足一定的金额要求则进行部分费用减免. 这些都是业务规则, 而基于这些规则, 给定一个或多个事实, 事实满足规则后可执行一些动作, 这便是规则引擎. 简单来说，你可以看成一堆 if-else 语句.基本上, 规则引擎围绕以下几个点进行操作:规则(Rule), 即是条件.事实(Fact), 存在某种东西是一种事实, 不存在某种东西也是一种事实, 规则引擎将现有的事实带入到规则中, 如果满足则执行动作.动作(Action), 满足某种规则后需要执行的动作.为什么需要规则引擎主要有以下几个原因:业务规则是多变的. 例如今天可能针对22岁以下的用户给予学生优惠, 明天则可能只针对18岁以下的用户给予学生优惠. 你肯定不想天天改这部分代码.业务规则具有种类多的特点. 例如满100元减免50元. 满200元减免110元. 满300元打1折 等等等等很多种规则, 如果将这部分逻辑硬写到代码里, 会有大量的条件判断, 且难以维护. 并且业务反复更改时, 都需要反复变更这部分代码.需要满足时效性要求. 规则硬编码到程序内部意味着规则的更改需要编码和部署, 整个上线流程是需要花费时间的, 如果能够在业务规则发生变化时, 动态重新加载规则, 甚至无需重新部署则新的规则便可应用, 那就极大的减少了变更规则带来的时间成本.让运营人员自己可动态配置规则. 这是很多人使用规则引擎所期望的一件事情, 让运营自己管理业务规则, 不占用开发资源. 但是与往常一样，这听起来似乎有道理，但在实践中却很少行得通. 要满足这个要求, 大部分情况下需要对规则引擎进行二次开发, 否则最终结果很可能便是配置了一堆没有人知道是干什么的规则.业务场景优惠券满减折扣指定商品扣减立减活动全场七折某个日期范围内立减某个品牌七折女用户七折如果现在有如上促销活动逻辑, 你应该怎么做? 将所有的业务逻辑硬编码到代码里? 哼哧哼哧开干, 刚完工, 这时候业务运营大佬来了: 不行啊, 全场七折要亏死了, 得改成全场9折, 另外这个某品牌七折活动他们突然不合作了, 得拿掉这个规则. 那么今天你和运营大佬可能只有一个人能站着走出去.但是, 如果你用了规则引擎, 则可以泰然自若, 喝一口浓茶并花一分钟修改和禁用规则.DroolsDrools是KIE(Knowlegde is Everything)社区的产物, 专门用来做业务规则引擎, 具有基于前向链和后向链推理的规则引擎，可以快速可靠地评估业务规则和复杂事件处理, 还可以与jBMP进行业务流程处理上的配合.运行原理Drools引擎使用以下基本组件运行规则(Rules): 业务规则, 所有规则必须至少包含触发规则的条件和规则规定的操作.事实(Facts): 在Drools引擎中输入或更改的数据, 与规则条件匹配以执行适用的规则.生产内存(Production memory)：规则(Rules)存储在Drools引擎中的位置.工作内存(Working memory)：事实(Facts)存储在Drools引擎中的位置.议程(Agenda): 满足条件的规则准备执行的位置.当业务用户或自动化系统往Drools引擎中添加事实(Facts)时, 这些事实被存储在Drools引擎的工作内存(Working memory)中, Drools引擎将这些事实与存储在生产内存(Production memory)中的规则(Rules)条件进行匹配，以确定符合条件的规则执行(这种将事实与规则匹配的过程通常称为模式匹配(pattern matching)). 当满足规则条件时，Drools引擎激活并在议程中(Agenda)注册规则，然后Drools引擎在议程中排序优先或冲突的规则以准备执行。下图说明了Drools引擎的这些基本组件Drools引擎中的执行逻辑议程评估(Agenda evaluation): 在这个阶段, Drools引擎选择所有可以执行的规则. 如果不存在可执行规则, 则执行周期结束. 如果找到可执行规则，Drools引擎会在议程(Agenda)中注册激活, 然后进入工作记忆操作阶段以执行规则满足后的动作.工作内存动作(Working memory actions): 在此阶段，Drools引擎为之前在议程(Agenda)中注册的所有已激活规则执行Action操作。在所有操作完成或主Java应用程序进程再次调用fireAllRules()后,Drools引擎返回议程评估(Agenda evaluation)阶段重新评估…

Create React App(CRA)本地开发配置代理
type: item
date: 2021-03-11T09:48:00+00:00
description: 前言现代的前端开发，通常使用前后端分离的形式，前端专注页面，数据来源于后端接口。这种开发方式使得职责更加明确且项目文件也更加的清晰起来，但是在本地开发过程中则有一点小问题：我如何调用后端接口？如果直接调用服务器上的接口，或者本地启动后端项目，由于端口不同，都会出现跨域问题，而服务端配置允许跨域也不太合适，那么，能否通过设置代理来绕过浏览器限制呢？声明Create React App以下简称CRA。根据CRA文档，本地配置代理有两种方式：在package.json中新增属性proxy来设置目标地址。通过http-proxy-middleware模块手动配置灵活的代理。两种方式各有好处，通过在package.json文件增加proxy属性的方式非常简单，一行代码即可解决代理问题，但是这个proxy地址没法根据配置进行动态切换，且package.json文件是需要提交到代码仓库的，如果每个人都有不同的目标地址，则package.json一定被大家改来改去且发生合并冲突。另外一种方式则是通过手动配置http-proxy-middleware的方式，根据CRA文档的描述，分为三个步骤：安装http-proxy-middleware模块：$ npm install http-proxy-middleware --save $ # 或者 $ yarn add http-proxy-middleware在src目录下创建setupProxy.js文件：$ mkdir src\setupProxy.js配置setupProxy.js：const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( "/api", createProxyMiddleware({ target: "http://localhost:5000", changeOrigin: true }) ); }然后启动项目，CRA会自动将src/setupProxy.js注册，这样请求则被代理到http://localhost:5000。可问题还是没有被解决，setupProxy.js依然会被提交到代码仓库，所以target依然不能写死配置。但是setupProxy.js中已经可以使用node环境变量了，因此，我们做如下配置：在项目根目录新建.env.development.local文件：$ mkdir .env.development.local在.env.development.local文件新增一行：REACT_APP_PROXY_ENDPOINT=http://localhost:5000配置setupProxy.js中的target值为process.env.REACT_APP_PROXY_ENDPOINT：... createProxyMiddleware({ target: process.env.REACT_APP_PROXY_ENDPOINT, changeOrigin: true }) ...至此，你已经可以根据环境变量来动态的切换代理的地址，且此文件无需提交到代码仓库。ReferenceCRA Proxying API Requests in DevelopmentCRA Adding Custom Environment Variables

无题
type: item
date: 2021-01-22T02:45:00+00:00
description: 老渔夫在海边哭泣。一个人民富豪走了过了。“先生，你为什么哭泣呀？和我说说吧。我这个人是最爱做慈善的了。”老渔夫擦了擦眼泪，眼中流露出喜悦“老爷，您是个有善心的人。这可真的是太好了，我的苦难对您来说，一定不是什么问题。我现在就和您说一说我的困难吧。请不要嫌弃老头子的啰嗦。”人民富豪点点头“老爷子，您请讲，我这个人还是很有礼貌的。”老渔夫开始讲了起来：“我家祖上一直都靠捕鱼为生。我们靠自己的双手和技术吃饭。但到了我这代，日子越来越难了起来。先是有人说海边的沙滩被人承包了，不让我们住。这也倒不是不能接受，我们习惯了住在船上的。这已经是我年轻时的事了，我就这样在船上活了大半辈子。可最近有人又说这片海被人承包了，不让我们在这儿捕鱼。这可真的是要了我的老命了！”老渔夫叹了口气，忍不住又哭起来。“要是就这样，还能苟活，我们是惯于冒险的，近处的海域不让捕捞，到那远点的海域捕鱼就是了。可近来又有人把那做渔船的材料都给买光了，我们是万万买不起的。没有了土地，海洋，还失去了捕鱼的网和船，这可让我们怎么活？”富豪点了点头，笑着说：“这可真的是太简单了。我这个人是惯于做慈善的。我最近组织了一个渔业公司。不但买了这些沙滩和海域，还买了一大批渔船。老先生，你这样有技术的老渔夫，我是很欢迎的。欢迎你来我的公司工作呀。如果你有能耐，你还能管理其它渔夫，那你可是发达，成为上流人士喽。”老渔夫看了几眼富豪说：“先生，我老头子不是白活了这么久的。“授人以鱼不如授人以渔”的理我还是懂的。如果你不把这海洋和土地承包了，不把渔船垄断了，我们便可以自由的捕鱼。哪里需要您的施舍呢？您要我去，不只是帮你捕鱼那么简单的。没去你那里的时候，我是一个自由人，我的时候，我的生命都是属于我自己的。我可以三天打鱼，两天晒网。签了您那出卖生命的契约，我可就得听您的喽。您让我干几个小时，我就得干几个小时。”人民富豪笑着说：“可你也不得不签不是嘛，不签？不签这份《奋斗者协议》你怎么活得下去？现在干什么不需要钱？”老渔夫叹了口气说：“以前有个老先生说“终身役役而不见其成功，苶然疲役而不知其所归”。我是听不懂的，今天我算是懂了一点了。正因为懂得，我是更不愿咬您那名利的鱼饵。人们嘲笑鱼的贪婪，不经意咬中鱼饵。却不知道人间的饵要危险的多。我是个老渔夫了，我以天为衣，以地为居，找个遮风挡雨的地方就能存世，几块咸鱼就能生存，何必把自己自由的生命交给您呢？”人民富豪面有怒色：“人的需求是无限的，你没有需求只是没被开发出来罢了。”老渔夫转过头不愿理会人民富豪：“先生，您是个有钱人。和我这样的穷人买不起东西，是不同的。既然您的需求是无限。那牛奶何以被倒在河里，钢铁，汽车，房子何以堆积如山。”富豪视线游移不定说：“这···这个···”老渔夫继续说：“你一天只有二十四个小时，你的时间是有限的，你的需求怎么可能是无限的？你能一个人同时住在你所有的房子里面？还是你能喝干一切的牛奶。人都需求水，可即使人喝干了水杯，夸父喝干了黄河，又有谁能喝干这地球，这宇宙的水？您不要太自大了，您的需求根本就不是无限的，您的占有倒是无限的。您占有了无限了大地，您占有了江河胡海，您占有茫茫黎庶，他们都为你打工。可你还不满足，您还能到哪去满足自己呢？”人民富豪挤出个笑容说:“先生，你这可是把我想的太坏了呀。我这个人是要把商业做成慈善的。我会捐出去很多税，养着你们，给你们高福利。”老渔夫瞥了眼说：“先生，请问是我们养着你，还是你养着我们？如果是你养活了我们，我们不求你的饲养。如果是我们是像牛羊一样养活了你，那就请你不要一边说什么高福利，一边咒骂着民众懒惰。我知道您是更愿意养一批为你工作的奴隶的，又施舍一些从指尖露下的面包屑以示你的慈悲。”人民富豪嗤笑道:“你这个愚昧不化的老渔夫。我所创造的福利社会岂不是比你强的多了。只有跟着我才能过好日子，只有努力工作才能有福报。”老渔夫笑道:“我不知道你的日子好不好。但我知道你的日子是过不下去的。你从还能勉强工作的那些人手里把钱夺走，送给那些连工作都没有的人。让人活得下去。而你自己是害怕自己的钱少一分的。你想方设法逃税。哪怕你捐出自己的钱，可这资本还是越来越集中。这就是你说的福利社会吧？”人民富豪说:“所以说你这个老头子坏的狠啊。我可是遵纪守法的纳税大户。你不要污蔑人。”老渔夫笑了笑:“就算你捐了你大部分钱，这个社会也维持不下去。我闲来无事也曾观看天道，那天道损有余以补不足的，而人道则是损不足以奉有余。因为你们有钱。他们没钱。他们要为你们打工。他们的劳动时间是有限的，不能增长，而你们的资本是无限的，可以越来越壮大，让更多人为你们挣钱。”人民富豪纳闷道:“所以为什么维持不下去？”老渔夫说:“你是比我懂得挣钱的，你来说说怎么赚钱。”人民富豪说:“让他们多工作，压低他们的工资，我就能在竞争中取胜了。这样可以挣到钱。”…

使用VS Code进行Java开发
type: item
date: 2020-12-28T16:01:00+00:00
description: 前言目前有很多小伙伴在用IntelliJ IDEA，这东西啥都好，就是贵，再一个就是占用内存很高，打开它也觉得非常的重。如果跟我一样有上面几个问题困扰，可以尝试使用VS Code。它插件众多，能DIY的地方也很多，特别是对于喜欢折腾的人来说，更是福音。当然如果不喜欢折腾想上手即用也可，全家桶插件非常方便。支持的Java版本范围为什么要单开一段说这件事，原因在于目前不少人认为VS Code只能支持Java 11及以上版本，这对于很多使用Java 1.8开发的同学来说简直劝退。但这其实是个误解，目前VS Code是能支持Java 1.5 - Java 15(截止本文发布时的最新版本)，基本能满足目前90%以上的Java使用者了（太老的Java版本这边移步Eclipse），至于说只能支持Java 11这件事，是指插件的运行环境需要安装最低Java 11的SDK，也就是说你如果想用VS Code开发Java程序必须要在本地安装Java 11 SDK才能够使用此插件。具体情况移步：Language Support for Java(TM) by Red Hatvscode-java JDK-Requirementsvscode-java CHANGELOG插件Java Extension Pack这是一个微软整合的Java开发环境所需要的插件集合，相当于一个插件捆绑包，安装之后会自动安装以下几个包：Language Support for Java(TM) by Red Hat提供Java语言支持Debugger for Java提供Java语言的调试支持Java Test Runner提供运行和调试单元测试的支持Maven for Java提供对Maven的支持Project Manager for Java提供对Java项目支持，例如创建Java项目和导出JarVisual Studio IntelliCode提供代码提示、自动补全等功能安装成功后按快捷键F1打开命令面板，可以看到具有一些Java项目的命令，我们先设置运行环境，选择Configure Java Runtime打开之后我们可以看到设置Java运行环境的界面，这里有几个地方需要说明一下，上面半部分的选项卡中三个表示：Project JDKs你项目所需要的JDK版本，例如你可能同时开发Java 1.8丶Java 11和Java 15，下面会介绍如何设置工作使用的JDK版本Java Tooling RuntimeJava工具所运行的环境，也就是此插件运行所需要的Java环境，这个需要最低安装Java 11且要设置到环境变量JAVA_HOME，如果你正确设置了JAVA_HOME这里就会展示你的JDK路径Installed JDKs当前机器上所安装的所有JDK路径在下半部分我们能看到有Install A JDK栏目，这里提供JDK的下载，选择你需要的JDK版本然后点下载即可，这里我们以最新的Java 15版本为例，下载后设置到环境变量JAVA_HOME，然后重新打开VS Code，使用F1快捷键打开命令面板，选择Configure Java Runtime，打开Java Tooling Runtime选项卡后，应当能看到正确设置的JDK路径：如此，第一步便已完成。Spring Boot Extension Pack这个插件也是一个全家桶插件，里面对Spring Boot项目开发所需的插件进行了打包：Spring Boot Tools提供了对Spring Boot项目的支持，例如对application*.properties及application*.yml的支持Spring Initializr Java Support提供了快速生成Spring Boot项目的支持，能够在VS Code中快速创建Spring Boot项目Spring Boot Dashboard提供了对Spring Boot项目快速启动和调试的支持，它能够检测到工作区所有的Spring Boot项目并提供统一管理的入口Cloudfoundry Manifest YML Support基本的YML格式支持Concourse CI Pipeline Editor提供了对**/*pipeline*.yml | **/pipeline/*.yml 和**/tasks/*.yml | **/*task.yml等文件的验证支持安装后我们即可进行Spring Boot的项目创建，像上面一样，先用快捷键F1打开命令面板，然后选择创建 Java 项目(Create Java Project)`：接着选择Spring Boot项目：再选择Maven或Gradle就看个人选择了，我们使用Maven做示例：再会让你选择Spring…

real mode protected mode and segmentation
type: item
date: 2020-11-23T15:29:00+00:00
description: 前言在操作系统启动时，会先进入实模式(Real Mode)，然后再转换为保护模式(Protected Mode)，为什么需要这么做？两种模式的寻址方式有何不同？本文通过Intel 8086和Intel 80286分段寻址的不同来解释这两种模式的区别。Intel 80868086被设计为完全的16位处理器，所有的内部寄存器、内部及外部数据总线都是16位宽，不过它具有20位宽的外部地址总线，这意味着它能够寻址2^20=1,048,576个地址，也就是最大能够支持1MB的寻址空间。这里可能会有疑问，刚才不是说8086是16位处理器吗？怎么能够进行20位地址总线寻址呢？这是由于8086处理器支持分段机制(Segmentation)，它能够通过16 * Segment + Offset的方式进行寻址，例如Segment为0xf000，Offset为0xfff0，则进行如下转换：16 * 0xf000 + 0xfff0 = 0xf0000 + 0xfff0 = 0xffff0 Segment为16位的基地址被存储在段寄存器(Segment Registers)，Offset也同样为16位的偏移地址，通过左移位的方式组合得到的结果0xffff0称之为线性地址(Linear Address)，可以理解为Segment为一个内存区域，Offset为相对于这个内存区域起始地址的偏移量。这时有的人可能会问了，如果Segment为0xffff且Offset为0xffff，通过上述公式计算:16 * 0xffff + 0xffff = 0xffff0 + 0xffff = 0x10ffef最终结果为0x10ffef，这已经超过20位地址总线的寻址范围(至少得24位地址总线才能够寻址此地址)，这时CPU会丢弃最高位，实际访问的地址为0xffef(这也是在使用更大的内存地址空间前启用A20总线的原因，既启用高位地址)。这种直接能够访问内存地址的方式称之为实模式(Real Mode)。Intel 80286当80286问世时，外部地址总线被拓宽到24位，使得它的寻址能力进一步提升，但是它所带来的不仅仅是寻址能力的提升，还有一个新东西：保护模式(Protected Mode)。保护模式下的分段机制与实模式下的分段机制有些许不同，它通过以下数据结构进行段翻译(Segment Translation)：描述符(Descriptor) 用来存储分段信息描述符表(Descriptor Table) 有描述符所组成的数组结构段选择器(Selector) 从描述符表中找到具体的某个描述符虽然80286的段寄存器(Segment Registers)依然是16位，但是它保存的不再是段基地址，而是称之为Selector的选择器，这与实模式下的分段机制有所不同。保护模式下每个分段信息是存储在对应的Descriptor中，那么我们要进行地址转换则需要先在Descriptor Table中找到这个Descriptor，这就需要Selector，通过Selector定位到Descriptor，然后翻译Segment成为线性地址(Linear Address)：Descriptor先看下Descriptor，它是由8-byte组成的数据结构：段基地址(Base Address)由三个片段组成占用32位，用来表示地址的开始位置(后续配合Offset来得到一个具体的地址)Segment Limit由两个片段组成占用20位，这两用来表示地址范围G=Granularity如果清除，Segment Limit单位为bytes最大能够表示 2^20 bytes如果设置，表示以4KB(页大小)为单位，最大可表示 2^32 bytesD=Default operand size如果清除，则为16位代码段如果设置，则为32位B=Big如果设置，则数据段的最大偏移量增加到32位0xffffffff否则为最大16位0x0000ffff，与 D 基本相同L=Long如果设置，则为64位段（D 必须为零），并且该段中的代码使用64位指令编码不能将 L 与 D 或 B 同时设置AVL=Available仅供软件使用，硬件不做处理P=Present如果清除，则对此段的任何引用都会生成“段不存在”异常DPL=Descriptor privilege level访问此描述符所需的特权级别Type如果设置，则为代码段描述符。如果清除，则为数据/堆栈段描述符C=Conforming可以从特权级别较低的级别调用此段中的代码E=Expand-Down如果清除，则该段将从基地址扩展到基数+限制如果设置，它将从最大偏移向下扩展到限制，这是通常用于堆栈的行为R=Readable如果清除，则该段可以执行但不能读取W=Writable如…