<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Levi&apos;s Blog</title><description>A coder-ready Astro blog theme with 59 of your favorite color schemes to choose from</description><link>https://monkeywie.cn</link><item><title>发布jar包到maven中央仓库</title><link>https://monkeywie.cn/posts/publish-jar-to-maven</link><guid isPermaLink="true">https://monkeywie.cn/posts/publish-jar-to-maven</guid><pubDate>Mon, 23 Jul 2018 14:00:24 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;我们知道在 maven 中引入第三方 jar 包是非常简单的，只需要使用 groupId+artifactId+version 就能从 maven 仓库中下载下来对应的 jar 包。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;例如：引入 fastjson 的 jar 包&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;com.alibaba&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;fastjson&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.2.46&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那如果想要发布自己的 jar 包到 maven 仓库应该如何操作呢。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;创建 issue&lt;/h2&gt;
&lt;p&gt;要发布 jar 包到 maven 仓库首先需要人工审核，在审核过了的话才可以进行后续发布 jar 包的操作。首先需要在&lt;a href=&quot;https://issues.sonatype.org/secure/Dashboard.jspa&quot;&gt;https://issues.sonatype.org/secure/Dashboard.jspa&lt;/a&gt;上注册一个账号，登录之后点击&lt;strong&gt;Create&lt;/strong&gt;,在弹出来的界面中填写响应的信息&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;publish-jar-to-maven/1532326564463.png&quot; alt=&quot;Create issue&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Project 选择 Community Support，Issue type 选择 New Project。&lt;/li&gt;
&lt;li&gt;注意 Group Id，如果有对应域名的话则使用域名对应的 Group Id(例如 netty 项目的域名是 netty.io，则 Group Id 为 io.netty)，没有的域名的话最好就填&lt;strong&gt;com.github.xxx&lt;/strong&gt;，因为在 issue 里会有人问你是否拥有 Group Id 对应的域名，没有的话是审核不过的，而托管在 github 上的话就可以直接使用 github 的域名来完成审核。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;issue 审核&lt;/h2&gt;
&lt;p&gt;创建成功后等 1-2 个小时左右就会有工作人员评论 issue，问你是否持有域名。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;publish-jar-to-maven/1532332576577.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果是用&lt;strong&gt;com.github.xxx&lt;/strong&gt;的 Group Id，就回复要使用&lt;strong&gt;com.github.xxx&lt;/strong&gt;作为你的域名，否则有域名就回复有就好,接着等待工作人员确认(我等了一天)，确认成功之后 issue 的状态就会变成&lt;code&gt;RESOLVED&lt;/code&gt;，这个时候就有资格上传 jar 包到 maven 仓库了。&lt;/p&gt;
&lt;h2&gt;gpg 管理密钥&lt;/h2&gt;
&lt;p&gt;在上传 jar 包之前，先要生成 gpg 工具生成 RSA 密钥对，并把公钥上传到公共密钥服务器，这样在发布 jar 包的时候能校验用户的身份。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载 gpg 工具，下载地址：&lt;a href=&quot;https://www.gnupg.org/download/index.html&quot;&gt;https://www.gnupg.org/download/index.html&lt;/a&gt;，下载对应操作系统的版本然后进行安装。&lt;/li&gt;
&lt;li&gt;验证安装和上传生成的公钥&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;验证 gpg 是否安装成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成 RAS 密钥对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --gen-key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着需要填写名字和邮箱等等基本信息，这些都不是重点，最主要的是有个&lt;code&gt;Passphase&lt;/code&gt;的选项在填完之后记下来，到时候发布 jar 包的时候要用到。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看生成的密钥，并上传至密钥服务器
需要上传到服务器的就是 pub 里的公钥串&lt;code&gt;FC27E7C61FC5D176DD7F67198C6EFA8E944CD6BA&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-keys
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;----------------------------------------------
pub   rsa2048 2018-07-19 [SC] [expires: 2020-07-18]
      FC27E7C61FC5D176DD7F67198C6EFA8E944CD6BA
uid           [ultimate] liwei &amp;lt;liwei2633@163.com&amp;gt;
sub   rsa2048 2018-07-19 [E] [expires: 2020-07-18]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传公钥至密钥服务器，国内我测试了的服务器基本就这个&lt;code&gt;hkp://keyserver.ubuntu.com:11371&lt;/code&gt;能用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --keyserver hkp://keyserver.ubuntu.com:11371 --send-keys FC27E7C61FC5D176DD7F67198C6EFA8E944CD6BA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上传完后验证是否成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg --keyserver hkp://keyserver.ubuntu.com:11371 --receive-keys FC27E7C61FC5D176DD7F67198C6EFA8E944CD6BA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpg: Total number processed: 1
gpg:              unchanged: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;maven 配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;修改项目中的&lt;code&gt;pom.xml&lt;/code&gt;文件，添加部署相关配置，这里引用贴下我的配置，只需要替换下相应的内容就好。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;name&amp;gt;pdown-core&amp;lt;/name&amp;gt;
&amp;lt;description&amp;gt;HTTP high speed downloader&amp;lt;/description&amp;gt;
&amp;lt;url&amp;gt;https://github.com/proxyee-down-org/pdown-core&amp;lt;/url&amp;gt;

&amp;lt;licenses&amp;gt;
  &amp;lt;license&amp;gt;
    &amp;lt;name&amp;gt;The MIT License (MIT)&amp;lt;/name&amp;gt;
    &amp;lt;url&amp;gt;http://opensource.org/licenses/mit-license.php&amp;lt;/url&amp;gt;
  &amp;lt;/license&amp;gt;
&amp;lt;/licenses&amp;gt;

&amp;lt;developers&amp;gt;
  &amp;lt;developer&amp;gt;
    &amp;lt;name&amp;gt;monkeyWie&amp;lt;/name&amp;gt;
    &amp;lt;email&amp;gt;liwei2633@163.com&amp;lt;/email&amp;gt;
  &amp;lt;/developer&amp;gt;
&amp;lt;/developers&amp;gt;

&amp;lt;scm&amp;gt;
  &amp;lt;connection&amp;gt;scm:git:https://github.com/proxyee-down-org/pdown-core.git&amp;lt;/connection&amp;gt;
  &amp;lt;developerConnection&amp;gt;scm:git:https://github.com/proxyee-down-org/pdown-core.git&amp;lt;/developerConnection&amp;gt;
  &amp;lt;url&amp;gt;https://github.com/proxyee-down-org/pdown-core&amp;lt;/url&amp;gt;
&amp;lt;/scm&amp;gt;

&amp;lt;profiles&amp;gt;
  &amp;lt;profile&amp;gt;
    &amp;lt;id&amp;gt;release&amp;lt;/id&amp;gt;
    &amp;lt;build&amp;gt;
      &amp;lt;plugins&amp;gt;
        &amp;lt;!--Compiler--&amp;gt;
        &amp;lt;plugin&amp;gt;
          &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
        &amp;lt;/plugin&amp;gt;
        &amp;lt;!-- Source --&amp;gt;
        &amp;lt;plugin&amp;gt;
          &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;maven-source-plugin&amp;lt;/artifactId&amp;gt;
          &amp;lt;executions&amp;gt;
            &amp;lt;execution&amp;gt;
              &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;
              &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;jar-no-fork&amp;lt;/goal&amp;gt;
              &amp;lt;/goals&amp;gt;
            &amp;lt;/execution&amp;gt;
          &amp;lt;/executions&amp;gt;
        &amp;lt;/plugin&amp;gt;
        &amp;lt;!-- Javadoc --&amp;gt;
        &amp;lt;plugin&amp;gt;
          &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;maven-javadoc-plugin&amp;lt;/artifactId&amp;gt;
          &amp;lt;executions&amp;gt;
            &amp;lt;execution&amp;gt;
              &amp;lt;phase&amp;gt;package&amp;lt;/phase&amp;gt;
              &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;jar&amp;lt;/goal&amp;gt;
              &amp;lt;/goals&amp;gt;
            &amp;lt;/execution&amp;gt;
          &amp;lt;/executions&amp;gt;
        &amp;lt;/plugin&amp;gt;
        &amp;lt;!-- GPG mvn clean deploy -P release -Dgpg.passphrase=YourPassphase--&amp;gt;
        &amp;lt;plugin&amp;gt;
          &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
          &amp;lt;artifactId&amp;gt;maven-gpg-plugin&amp;lt;/artifactId&amp;gt;
          &amp;lt;executions&amp;gt;
            &amp;lt;execution&amp;gt;
              &amp;lt;id&amp;gt;sign-artifacts&amp;lt;/id&amp;gt;
              &amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
              &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;sign&amp;lt;/goal&amp;gt;
              &amp;lt;/goals&amp;gt;
            &amp;lt;/execution&amp;gt;
          &amp;lt;/executions&amp;gt;
        &amp;lt;/plugin&amp;gt;
      &amp;lt;/plugins&amp;gt;
    &amp;lt;/build&amp;gt;
    &amp;lt;distributionManagement&amp;gt;
      &amp;lt;repository&amp;gt;
        &amp;lt;id&amp;gt;releases&amp;lt;/id&amp;gt;
        &amp;lt;url&amp;gt;https://oss.sonatype.org/service/local/staging/deploy/maven2/&amp;lt;/url&amp;gt;
      &amp;lt;/repository&amp;gt;
      &amp;lt;snapshotRepository&amp;gt;
        &amp;lt;id&amp;gt;snapshots&amp;lt;/id&amp;gt;
        &amp;lt;url&amp;gt;https://oss.sonatype.org/content/repositories/snapshots/&amp;lt;/url&amp;gt;
      &amp;lt;/snapshotRepository&amp;gt;
    &amp;lt;/distributionManagement&amp;gt;
  &amp;lt;/profile&amp;gt;
&amp;lt;/profiles&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;把之前创建 issue 时注册的账号配置到 maven 的配置文件里，找到 maven 安装目录下&lt;code&gt;conf/setting.xml&lt;/code&gt;文件，在&lt;code&gt;&amp;lt;servers&amp;gt;&lt;/code&gt;标签里添加。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;server&amp;gt;
  &amp;lt;id&amp;gt;releases&amp;lt;/id&amp;gt;
  &amp;lt;username&amp;gt;&amp;lt;/username&amp;gt;
  &amp;lt;password&amp;gt;&amp;lt;/password&amp;gt;
&amp;lt;/server&amp;gt;
&amp;lt;server&amp;gt;
  &amp;lt;id&amp;gt;snapshots&amp;lt;/id&amp;gt;
  &amp;lt;username&amp;gt;&amp;lt;/username&amp;gt;
  &amp;lt;password&amp;gt;&amp;lt;/password&amp;gt;
&amp;lt;/server&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把用户名和密码填好即可。&lt;/p&gt;
&lt;h2&gt;部署 jar 包&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;使用下面的命令行，会需要输入之前用 gpg 生成密钥时输入的 Passphase，也有可能会弹窗出来提示输入 Passphase。&lt;br /&gt;
(&lt;em&gt;我这加了-Dgpg.passphrase=YourPassphase 选项并没有生效，还是会弹窗出来提示输入 Passphase&lt;/em&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mvn clean deploy -P release -Dgpg.passphrase=YourPassphase
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果部署成功的话使用创建 issue 的帐号登录这个网址&lt;a href=&quot;https://oss.sonatype.org/&quot;&gt;https://oss.sonatype.org/&lt;/a&gt;，然后看图操作。
&lt;img src=&quot;publish-jar-to-maven/1532339455164.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;close 完了系统会验证 jar 包，点击刷新可以看到最新的进度，当全部验证通过的时候，状态会变成&lt;code&gt;closed&lt;/code&gt;,然后再选中文件&lt;code&gt;Release&lt;/code&gt;就发布完成了。然后等个几个小时就可以在中央仓库搜索到自己发布的 jar 包了。
&lt;img src=&quot;publish-jar-to-maven/1532339866124.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Levi</author></item><item><title>从JVM中dump出动态代理生成的class</title><link>https://monkeywie.cn/posts/jvm-dump-class</link><guid isPermaLink="true">https://monkeywie.cn/posts/jvm-dump-class</guid><pubDate>Wed, 25 Jul 2018 17:25:37 GMT</pubDate><content:encoded>&lt;p&gt;由于动态代理生成的 class 是直接以二进制的方式加载进内存中的，并没有对应的.class 文件生成，所以如果想通过反编译工具查看动态代理生成的代码需要通过特殊的手段来处理。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;方案一&lt;/h3&gt;
&lt;p&gt;设置运行环境变量,运行后会把 class 文件生成在 classpath 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//动态代理时生成class文件
System.getProperties().put(&quot;sun.misc.ProxyGenerator.saveGeneratedFiles&quot;,&quot;true&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;缺点是只适用于 JDK 动态代理&lt;/p&gt;
&lt;h3&gt;方案二&lt;/h3&gt;
&lt;p&gt;使用 ClassDump,可以 dump 出 JVM 中所有已加载的 class。ClassDump 位于$JAVA_HOME/lib/sa-jdi.jar 中(注：windows 版本 JDK 从 1.7 开始才有此工具),直接以命令行执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#查看PID
E:\work\Test\bin&amp;gt;jps

#缺省输出该PID下所有已加载的class文件至./目录
E:\work\Test\bin&amp;gt;java -classpath &quot;.;./bin;%JAVA_HOME%/lib/sa-jdi.jar&quot; sun.jvm.hotspot.tools.jcore.ClassDump &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;//导入sa-jdi.jar包，实现ClassFilter接口，只输出匹配的class文件
public class MyFilter implements ClassFilter{

	@Override
	public boolean canInclude(InstanceKlass arg0) {
		return arg0.getName().asString().startsWith(&quot;com/sun/proxy/$Proxy0&quot;);
	}

}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#查看PID
E:\work\Test\bin&amp;gt;jps

#使用ClassFilter输出匹配的class文件，并指定输出目录
E:\work\Test\bin&amp;gt;java -classpath &quot;.;./bin;%JAVA_HOME%/lib/sa-jdi.jar&quot; -Dsun.jvm.hotspot.tools.jcore.filter=proxy.MyFilter -Dsun.jvm.hotspot.tools.jcore.outputDir=e:/dump sun.jvm.hotspot.tools.jcore.ClassDump &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方案基于 JVM 层的 ClassDump 所以可以支持 javassist、cglib、asm 动态生成的 class。&lt;/p&gt;
&lt;h3&gt;最后贴下 JDK 动态代理反编译出来的代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;package com.sun.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Run;	//目标代理类接口

//继承了Proxy类，实现目标代理类接口
public final class $Proxy0
  extends Proxy
  implements Run
{
  private static Method m1;
  private static Method m3;
  private static Method m0;
  private static Method m2;

  public $Proxy0(InvocationHandler paramInvocationHandler)
  {
    super(paramInvocationHandler);
  }

  static
  {
    try
    {
      m1 = Class.forName(&quot;java.lang.Object&quot;).getMethod(&quot;equals&quot;, new Class[] { Class.forName(&quot;java.lang.Object&quot;) });
      //获取目标代理类的方法
      m3 = Class.forName(&quot;proxy.Run&quot;).getMethod(&quot;run&quot;, new Class[0]);
      m0 = Class.forName(&quot;java.lang.Object&quot;).getMethod(&quot;hashCode&quot;, new Class[0]);
      m2 = Class.forName(&quot;java.lang.Object&quot;).getMethod(&quot;toString&quot;, new Class[0]);
      return;
    }
    catch (NoSuchMethodException localNoSuchMethodException)
    {
      throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
    }
    catch (ClassNotFoundException localClassNotFoundException)
    {
      throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
    }
  }

  //方法重写
  public final String run()
  {
    try
    {
	  //this.h就是InvocationHandler的实现类了，调用invoke方法，在实现类里面做拦截处理
      return (String)this.h.invoke(this, m3, null);
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final boolean equals(Object paramObject)
  {
    try
    {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final String toString()
  {
    try
    {
      return (String)this.h.invoke(this, m2, null);
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }

  public final int hashCode()
  {
    try
    {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    }
    catch (Error|RuntimeException localError)
    {
      throw localError;
    }
    catch (Throwable localThrowable)
    {
      throw new UndeclaredThrowableException(localThrowable);
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;http://rednaxelafx.iteye.com/blog/727938&quot;&gt;http://rednaxelafx.iteye.com/blog/727938&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>如何优雅的使用VS Code+ESLint+Prettier写Vue程序</title><link>https://monkeywie.cn/posts/vscode-vue-eslint-prettier</link><guid isPermaLink="true">https://monkeywie.cn/posts/vscode-vue-eslint-prettier</guid><pubDate>Fri, 03 Aug 2018 09:17:15 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;本人是 JAVA 为主，开发工具用的 IDEA，之前写 Vue 前端的时候也是直接用的 IDEA+Vue 插件来开发的，一般也是写着玩，不是正式项目，所以也从来没用过&lt;code&gt;ESLint&lt;/code&gt;和&lt;code&gt;Prettier&lt;/code&gt;，然后最近要参与一个前端项目，用 IDEA 导入项目后打开，出于强迫症使用熟悉的&amp;lt;kbd&amp;gt;ctrl&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;alt&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;L&amp;lt;/kbd&amp;gt;格式了下代码，发现代码格式完全对不上啊，调研了下之后，义无反顾的转投&lt;code&gt;VS Code&lt;/code&gt;写前端了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装 vs code 和插件&lt;/h2&gt;
&lt;p&gt;首先在&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VS Code 官网&lt;/a&gt;下载安装包，安装好之后启动，然后按&amp;lt;kbd&amp;gt;ctrl&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;shift&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;X&amp;lt;/kbd&amp;gt;打开插件安装界面，搜索以下插件并安装好。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vetur&lt;/li&gt;
&lt;li&gt;ESLint&lt;/li&gt;
&lt;li&gt;Prettier - Code formatter&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;安装完之后最好是重启下&lt;code&gt;VS Code&lt;/code&gt;避免插件不生效的问题。&lt;/p&gt;
&lt;h2&gt;插件配置&lt;/h2&gt;
&lt;p&gt;按&amp;lt;kbd&amp;gt;ctrl&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;,&amp;lt;/kbd&amp;gt;打开设置界面，在窗口右边有两个 tab 页面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;USER SETTINGS
全局配置，也就是说在这里配置的话其他项目也会使用到这个配置。&lt;/li&gt;
&lt;li&gt;WORKSPACE SETTINGS
项目配置，会在当前项目的根路径里创建一个&lt;code&gt;.vscode/settings.json&lt;/code&gt;文件，然后配置只在当前项目生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我这里是把插件的配置写在了&lt;code&gt;WORKSPACE SETTINGS&lt;/code&gt;里，配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  //.vue文件template格式化支持，并使用js-beautify-html插件
  &quot;vetur.format.defaultFormatter.html&quot;: &quot;js-beautify-html&quot;,
  //js-beautify-html格式化配置，属性强制换行
  //文档：https://github.com/beautify-web/js-beautify#css--html
  &quot;vetur.format.defaultFormatterOptions&quot;: {
    &quot;js-beautify-html&quot;: {
      &quot;wrap_attributes&quot;: &quot;force&quot;
    }
  },
  //根据文件后缀名定义vue文件类型
  &quot;files.associations&quot;: {
    &quot;*.vue&quot;: &quot;vue&quot;
  },
  //保存时eslint自动修复错误
  &quot;eslint.validate&quot;: [
    &quot;javascript&quot;,
    &quot;javascriptreact&quot;,
    {
      &quot;language&quot;: &quot;vue&quot;,
      &quot;autoFix&quot;: true
    }
  ],
  &quot;eslint.autoFixOnSave&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ESLint 和 Prettier 的冲突修复&lt;/h2&gt;
&lt;p&gt;在用&lt;code&gt;Prettier&lt;/code&gt;格式化的时候，可以能会和&lt;code&gt;ESLint&lt;/code&gt;定义的校验规则冲突，比如&lt;code&gt;Prettier&lt;/code&gt;字符串默认是用双引号而&lt;code&gt;ESLint&lt;/code&gt;定义的是单引号的话这样格式化之后就不符合&lt;code&gt;ESLint&lt;/code&gt;规则了。所以要解决冲突就需要在&lt;code&gt;Prettier&lt;/code&gt;的规则配置里也配置上和&lt;code&gt;ESLint&lt;/code&gt;一样的规则，这里贴下&lt;code&gt;ESLint&lt;/code&gt;和&lt;code&gt;Prettier&lt;/code&gt;的配置文件。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.eslintrc.js
配置&lt;code&gt;ESLint&lt;/code&gt;&lt;a href=&quot;http://eslint.cn/docs/rules/&quot;&gt;选项&lt;/a&gt;，使用单引号、结尾不能有分号。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: [&quot;plugin:vue/essential&quot;, &quot;eslint:recommended&quot;],
  rules: {
    &quot;no-console&quot;: process.env.NODE_ENV === &quot;production&quot; ? &quot;error&quot; : &quot;off&quot;,
    &quot;no-debugger&quot;: process.env.NODE_ENV === &quot;production&quot; ? &quot;error&quot; : &quot;off&quot;,
    &quot;no-alert&quot;: process.env.NODE_ENV === &quot;production&quot; ? &quot;error&quot; : &quot;off&quot;,
    //强制使用单引号
    quotes: [&quot;error&quot;, &quot;single&quot;],
    //强制不使用分号结尾
    semi: [&quot;error&quot;, &quot;never&quot;]
  },
  parserOptions: {
    parser: &quot;babel-eslint&quot;
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;.prettierrc
配置&lt;code&gt;Prettier&lt;/code&gt;&lt;a href=&quot;https://prettier.io/docs/en/options.html&quot;&gt;选项&lt;/a&gt;，使用单引号、结尾不能有分号。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;eslintIntegration&quot;: true,
  //使用单引号
  &quot;singleQuote&quot;: true,
  //结尾不加分号
  &quot;semi&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样把&lt;code&gt;ESLint&lt;/code&gt;和&lt;code&gt;Prettier&lt;/code&gt;冲突的规则配置一致,格式化之后就不会冲突了。&lt;/p&gt;
&lt;h2&gt;效果预览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;vscode-vue-eslint-prettier/template-format.gif&quot; alt=&quot;格式化template&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;vscode-vue-eslint-prettier/js-format.gif&quot; alt=&quot;格式化js&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Elasticsearch6.x学习笔记 - 安装</title><link>https://monkeywie.cn/posts/hello-elasticsearch-install</link><guid isPermaLink="true">https://monkeywie.cn/posts/hello-elasticsearch-install</guid><pubDate>Mon, 06 Aug 2018 14:24:02 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近心血来潮，准备学习下开源届首选的搜索引擎&lt;code&gt;Elasticsearch&lt;/code&gt;，在了解相关概念之后就准备在虚拟机装上试一试了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;下载安装包&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://www.elastic.co/downloads/elasticsearch&quot;&gt;Elasticsearch 官网&lt;/a&gt;下载，下载完后解压至任意目录。&lt;/p&gt;
&lt;h2&gt;启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;./bin/elasticsearch
#后台运行
./bin/elasticsearch -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而并没有就这么容易启动就成功，下面一一排查启动故障。&lt;/p&gt;
&lt;h2&gt;排查启动故障&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;启动报 OOM 内存溢出
&lt;strong&gt;原因&lt;/strong&gt;：elasticsearch 默认的 jvm 堆内存大小为 1GB，而我虚拟机分配的内存也才 1GB，启动时系统分配不了这么大的堆内存所以直接内存溢出了。&lt;br /&gt;
&lt;strong&gt;解决办法&lt;/strong&gt;：把堆内存调小点，修改./config/jvm.options 文件，把堆内存设置为 512MB。&lt;pre&gt;&lt;code&gt;-Xms512m
-Xmx512m
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;不能以 root 用户启动
&lt;strong&gt;原因&lt;/strong&gt;：elasticsearch 为了安全性默认不允许 root 用户来启动。&lt;br /&gt;
&lt;strong&gt;解决办法&lt;/strong&gt;：
&lt;ol&gt;
&lt;li&gt;在启动参数中添加&lt;code&gt;-Des.insecure.allow.root=true&lt;/code&gt;，但是我测试了根本没效果，原来是 elasticsearch5.x 版本把这个属性给去掉了，也就是说绝对禁止 root 用户运行了，这个是相关&lt;a href=&quot;https://github.com/elastic/elasticsearch/pull/18694/files&quot;&gt;PR&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;添加一个用户来运行&lt;pre&gt;&lt;code&gt;#添加一个用户名为es
useradd es
#把elasticsearch目录的所属用户和组设置为es
chown -R es:es ./
#切换到es用户
su es
#运行
./bin/elasticsearch
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]
&lt;strong&gt;原因&lt;/strong&gt;：elasticsearch 启动时检测到用户最大文件描述符限制低于 65536 而抛出的异常。&lt;br /&gt;
&lt;strong&gt;解决办法&lt;/strong&gt;：修改 es 用户最大文件描述符限制&lt;pre&gt;&lt;code&gt;#切换到root用户
su root
#修改对应文件
vi /etc/security/limits.conf
&lt;/code&gt;&lt;/pre&gt;
在最后添加两行内容，设置 es 用户最大文件描述符限制为 65536&lt;pre&gt;&lt;code&gt;es soft nofile 65536
es hard nofile 65536
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;max number of threads [3802] for user [es] is too low, increase to at least [4096]
&lt;strong&gt;原因&lt;/strong&gt;：elasticsearch 启动时检测到用户最大的线程数限制低于 4096 而抛出的异常。
&lt;strong&gt;解决办法&lt;/strong&gt;：修改 es 用户最大的线程数限&lt;pre&gt;&lt;code&gt;#切换到root用户
su root
#修改对应文件
vi /etc/security/limits.d/20-nproc.conf
&lt;/code&gt;&lt;/pre&gt;
可以看到 root 用户是无限制的，而*代表的其他用户限制是 4096。(*很奇怪既然是 4096 为什么上面提示说最大线程数是 3802 呢？*)，先不管原因了搜到了解决方案在下面加一行。&lt;pre&gt;&lt;code&gt;*          hard    nproc     4096
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
&lt;strong&gt;原因&lt;/strong&gt;：elasticsearch 启动时检测到系统最大虚拟内存低于 262144 而抛出的异常。
&lt;strong&gt;解决办法&lt;/strong&gt;：修改系统最大虚拟内存&lt;pre&gt;&lt;code&gt;vi /etc/sysctl.conf
&lt;/code&gt;&lt;/pre&gt;
修改或添加 vm.max_map_count 参数&lt;pre&gt;&lt;code&gt;vm.max_map_count=262144
&lt;/code&gt;&lt;/pre&gt;
刷新配置&lt;pre&gt;&lt;code&gt;sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;启动成功&lt;/h2&gt;
&lt;p&gt;启动成功之后访问&lt;code&gt;http://127.0.0.1:9200&lt;/code&gt;，就可以看到服务器相关信息了。
&lt;img src=&quot;hello-elasticsearch-install/2018-08-06-15-32-56.png&quot; alt=&quot;&quot; /&gt;
默认情况下是服务器是监听&lt;code&gt;127.0.0.1&lt;/code&gt;，如果让别的网段访问到的话需要修改&lt;code&gt;./config/elasticsearch.yml&lt;/code&gt;文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#配置所有网段可以访问
network.host: 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>Proxyee Down 3.0 正式版发布</title><link>https://monkeywie.cn/posts/proxyee-down-3-0-guide</link><guid isPermaLink="true">https://monkeywie.cn/posts/proxyee-down-3-0-guide</guid><pubDate>Wed, 05 Sep 2018 11:33:46 GMT</pubDate><content:encoded>&lt;p&gt;在经历了近半年的时间之后，Proxyee Down 终于迎来了 3.0 版本,新版本改动非常的大同时进步也非常的大，来看看 3.0 版本的新特性吧。&lt;/p&gt;
&lt;h2&gt;新特性&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;UI 界面完全重构。&lt;/li&gt;
&lt;li&gt;使用全新的&lt;a href=&quot;https://github.com/proxyee-down-org/pdown-core&quot;&gt;下载核心&lt;/a&gt;，稳定性和下载速度全面提升，现在下载完的连接会去支持没下载完的连接，而不是和老版本一样下载完了就停止了。&lt;/li&gt;
&lt;li&gt;去除老版本自带的百度云下载插件，新增扩展模块，在扩展商城里可以下载各种各样的扩展而不是仅限于百度云下载插件，支持第三方开发扩展(&lt;a href=&quot;https://github.com/proxyee-down-org/proxyee-down-extension&quot;&gt;参与开发&lt;/a&gt;)。&lt;/li&gt;
&lt;li&gt;加入限速功能，包括单任务限速和全局限速。&lt;/li&gt;
&lt;li&gt;加入同时下载任务数设置功能。&lt;/li&gt;
&lt;li&gt;启动速度大幅度提高&lt;/li&gt;
&lt;li&gt;加入国际化支持(欢迎提交 PR)&lt;/li&gt;
&lt;li&gt;文件夹选择器使用操作系统原生选择器，支持局域网共享文件夹、移动硬盘、U 盘。&lt;/li&gt;
&lt;li&gt;移除老版本自带的百度云解压工具(之后会单独开发一款解压工具从下载器里独立出来)&lt;/li&gt;
&lt;li&gt;mac 系统打包成原生 app 应用，而不是像之前一样用批处理文件启动。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;软件下载&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://imhx-my.sharepoint.com/:f:/g/personal/pd_imhx_onmicrosoft_com/EnPrybHS3rVFuy_HdcP7RLoBwhb0k5ayJdIzwjU0hCM9-A?e=he0oIz&quot;&gt;OneDrive 下载&lt;/a&gt;(推荐)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://pdown.org/releases.html&quot;&gt;官网下载&lt;/a&gt;：
官网带宽比较低，建议用 OneDrive 下载&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用说明&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;windows&lt;/strong&gt;:&lt;br /&gt;
下载好 windows 版本的压缩包之后，解压至任意目录，会得到一个文件夹，执行文件夹里面的&lt;code&gt;Proxyee Down.exe&lt;/code&gt;文件即可。&lt;br /&gt;
(&lt;em&gt;注：1.如果启动闪退，把 APP 复制到别的目录就可以正常运行。2.mac 系统切换代理和安装证书需要管理员权限，所以在启动时会提示输入密码&lt;/em&gt;)&lt;br /&gt;
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-13-49-38.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mac&lt;/strong&gt;:&lt;br /&gt;
下载好 mac 版本的压缩包之后，解压至任意目录，会得到一个&lt;code&gt;Proxyee Down&lt;/code&gt;App，双击运行即可。&lt;br /&gt;
(&lt;em&gt;注：mac 系统切换代理和安装证书需要管理员权限，所以在启动时会提示输入密码&lt;/em&gt;)
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-13-51-38.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;linux&lt;/strong&gt;:&lt;br /&gt;
linux 系统目前没有打原生包，要自行下载 jar 包运行，需安装 JRE 或 JDK(&lt;em&gt;要求版本不低于 1.8&lt;/em&gt;)，下载完成后在命令行中运行：&lt;pre&gt;&lt;code&gt;java -jar proxyee-down-main.jar
&lt;/code&gt;&lt;/pre&gt;
(&lt;em&gt;注：如果使用 openjdk 的话需要安装 openjfx&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;任务模块&lt;/h2&gt;
&lt;p&gt;用于管理下载任务，可以在此页面创建、查看、删除、暂停、恢复下载任务。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;进阶&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/proxyee-down-org/proxyee-down/blob/v2.5/.guide/common/create/read.md&quot;&gt;自定义下载请求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/proxyee-down-org/proxyee-down/blob/v2.5/.guide/common/refresh/read.md&quot;&gt;刷新任务下载链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;扩展模块&lt;/h2&gt;
&lt;p&gt;在开启扩展模块时一定要手动安装一个由 Proxyee Down 随机生成的一个 CA 证书用于&lt;code&gt;HTTPS MITM&lt;/code&gt;的支持。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装证书&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;进入扩展页面，如果软件检测到没有安装 Proxyee Down CA 证书时，会有对应的安装提示，接受的话点击安装按照系统指引即可安装完毕。
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-08-36.png&quot; alt=&quot;安装证书&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;扩展商店&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;安装完证书后会进入扩展商店页面，目前扩展商店只有一款百度云下载扩展，以后会陆续开发更多的扩展(&lt;em&gt;例如：各大网站的视频下载扩展、其他网盘的下载扩展等等&lt;/em&gt;)。
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-12-21.png&quot; alt=&quot;扩展商城&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;扩展安装&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在操作栏找到安装按钮，点击安装即可安装扩展。
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-26-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;全局代理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;全局代理默认是不开启的，开启 Proxyee Down 会根据启用的扩展进行对应的系统代理设置，可能会与相同机制的软件发生冲突(&lt;em&gt;例如：SS、SSR&lt;/em&gt;)。
如果不使用全局代理，可以点击&lt;code&gt;复制PAC链接&lt;/code&gt;，配合&lt;a href=&quot;https://www.switchyomega.com/&quot;&gt;SwitchyOmega 插件&lt;/a&gt;来使用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;其他相关&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SwitchyOmega 设置教程&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;新建情景模式，选择 PAC 情景模式类型。
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-25-34.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;把复制的 PAC 链接粘贴进来并点击立即更新情景模式然后保存。
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-30-30.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;切换情景模式进行下载&lt;br /&gt;
&lt;img src=&quot;proxyee-down-3-0-guide/2018-09-05-14-32-00.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;参与扩展开发&lt;/strong&gt;&lt;br /&gt;
详见&lt;a href=&quot;https://github.com/proxyee-down-org/proxyee-down-extension&quot;&gt;proxyee-down-extension&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;扩展实现原理&lt;/strong&gt;&lt;br /&gt;
扩展功能是由 MITM(中间人攻击)技术实现的，使用&lt;a href=&quot;https://github.com/monkeyWie/proxyee&quot;&gt;proxyee&lt;/a&gt;框架拦截和修改&lt;code&gt;HTTP&lt;/code&gt;、&lt;code&gt;HTTPS&lt;/code&gt;的请求和响应报文，从而实现对应的扩展脚本注入。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;鸣谢&lt;/h2&gt;
&lt;p&gt;谢谢一直以来大家对本软件的支持和认可，相信在你们的反馈和支持下本软件会做的越来越好！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>git常用命令集</title><link>https://monkeywie.cn/posts/git-summary</link><guid isPermaLink="true">https://monkeywie.cn/posts/git-summary</guid><pubDate>Tue, 07 May 2019 09:32:44 GMT</pubDate><content:encoded>&lt;h3&gt;概念&lt;/h3&gt;
&lt;p&gt;git 在本地有三个工作区域：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工作区&lt;/li&gt;
&lt;li&gt;暂存区&lt;/li&gt;
&lt;li&gt;版本库&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;它们之间的转化关系如下图：
&lt;img src=&quot;git-summary/2019-05-17-23-04-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;还原工作区已修改的文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#还原指定文件
git checkout -- 文件名
#还原所有文件
git checkout .
#删除所有工作区没有add的文件
git clean -fd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;暂存区 -&amp;gt; 工作区&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#在工作区删除
git rm -f 文件名
#保留在工作区
git rm --cache 文件名
#撤销所有已经add的文件
git reset
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本库 -&amp;gt; 暂存区&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#通过git log查看提交记录，并记录下要回滚到的CommitId
git log
#回滚到指定的commit，工作区和暂存区保留修改
git reset CommitId --soft
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;版本库 -&amp;gt; 工作区&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#通过git log查看提交记录，并记录下要回滚到的CommitId
git log
#回滚到指定的commit，工作区保留修改
git reset CommitId --mixed
#默认模式就是mixed
git reset CommitId
#回滚到指定的commit，工作区不保留修改
git reset --hard
#回滚到指定的commit，并且将回滚的内容当做一次commit
git revert CommitId
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;远程仓库回滚&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#将上面回滚的记录强制推到远程分支上即可
git push -f
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;储藏&lt;/h3&gt;
&lt;p&gt;有时间会有一种情况，当我们在写需求的时候代码还没写完，突然来了个紧急的 BUG 要修复，这个时候我们就可以把我们现在写的代码储藏起来，并且从开发分支上拉一个新的分支去修复 bug，当修完 bug 时再切回我们刚刚写需求的分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#储藏
git stash
#还原
git stash pop
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;git log显示优化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#注册git logp命令
git config --global alias.logp &quot;log --color --graph --pretty=format:&apos;%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&amp;lt;%an&amp;gt;%Creset&apos; --abbrev-commit --&quot;
#查看log
git logp
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>HTTP调试工具之Fiddler使用教程</title><link>https://monkeywie.cn/posts/fiddler-tutorial</link><guid isPermaLink="true">https://monkeywie.cn/posts/fiddler-tutorial</guid><pubDate>Fri, 24 May 2019 18:20:57 GMT</pubDate><content:encoded>&lt;h2&gt;Fidder 简介&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fiddler&lt;/code&gt;是一个用于 HTTP 调试的代理服务器应用程序，最初由微软 Internet Explorer 开发团队的前程序经理 Eric Lawrence 编写。通过&lt;code&gt;Fiddler&lt;/code&gt;的代理服务器，可以捕获&lt;code&gt;HTTP&lt;/code&gt;和&lt;code&gt;HTTPS&lt;/code&gt;协议流量，并且可对&lt;code&gt;HTTP&lt;/code&gt;请求和响应做出修改，使用&lt;code&gt;Fiddler&lt;/code&gt;可以很方便的对&lt;code&gt;HTTP&lt;/code&gt;协议进行分析和调试。&lt;/p&gt;
&lt;h2&gt;工作原理&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;fiddler-tutorial/2019-05-25-18-17-30.png&quot; alt=&quot;&quot; /&gt;
客户端的请求经过 Fiddler 的代理服务器转发，再由&lt;code&gt;Fidder&lt;/code&gt;对目标服务器进行请求，得到响应之后再返回给客户端，在整个通讯过程中 Fidder 是一个中间人的角色，可以捕获到所有的请求和响应报文。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;下载&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fidder&lt;/code&gt; 是一款免费的应用，可以直接通过&lt;a href=&quot;https://www.telerik.com/download/fiddler&quot;&gt;官网&lt;/a&gt;下载，目前只支持 windows 操作系统(官网已经有 mac 和 linux 的 beta 版本)，不过本文只针对 windows 操作系统 下的 fiddler 使用。&lt;/p&gt;
&lt;h2&gt;基本使用&lt;/h2&gt;
&lt;p&gt;在下载并安装完成之后，运行&lt;code&gt;Fidder&lt;/code&gt;就会启动一个&lt;code&gt;8888&lt;/code&gt;端口的 HTTP 代理服务器，并且默认配置下在启动完之后会自动修改&lt;code&gt;IE&lt;/code&gt;浏览器中的 HTTP 代理，如果不需要可以在设置(&lt;code&gt;Tools-Options&lt;/code&gt;)里面关掉此功能(推荐使用&lt;code&gt;Chrome浏览器&lt;/code&gt;+&lt;code&gt;SwitchyOmega插件&lt;/code&gt;来进行浏览器代理的设置)。
&lt;img src=&quot;fiddler-tutorial/2019-05-25-17-52-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;运行完之后可以看到如下界面，软件的左侧&lt;code&gt;会话列表面板&lt;/code&gt;中会列出所有捕获的 HTTP 请求，当选中一个请求之后，右侧就会列出该请求的详细数据。
&lt;img src=&quot;fiddler-tutorial/2019-05-26-15-41-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;嗅探 HTTP 报文&lt;/h3&gt;
&lt;p&gt;嗅探报文时只需要为待调试的应用程序设置 HTTP 代理就可以在&lt;code&gt;Fidder&lt;/code&gt;中看到 HTTP 报文了，这里就以&lt;code&gt;Chrome&lt;/code&gt;为例来演示一下。&lt;/p&gt;
&lt;p&gt;首先给 Chrome 浏览器设置&lt;code&gt;127.0.0.1:8888&lt;/code&gt;的 HTTP 代理，这里使用&lt;code&gt;SwitchyOmega插件&lt;/code&gt;来进行代理设置：
&lt;img src=&quot;fiddler-tutorial/2019-05-25-18-00-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果不安装插件也可以直接通过&lt;code&gt;设置-高级-打开代理设置&lt;/code&gt;中进行代理设置：
&lt;img src=&quot;fiddler-tutorial/2019-05-26-15-52-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后通过浏览器访问&lt;a href=&quot;http://www.apache.org&quot;&gt;http://www.apache.org&lt;/a&gt;，就可以在&lt;code&gt;Fiddler&lt;/code&gt;中看到刚刚的 HTTP 报文：
&lt;img src=&quot;fiddler-tutorial/2019-05-26-15-20-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中选中的都是&lt;code&gt;Raw&lt;/code&gt;选项卡，即展示原始的 HTTP 报文内容，其它选项卡即根据报文做对应的视图展示，这里就不一一介绍了。&lt;/p&gt;
&lt;h3&gt;解码压缩报文&lt;/h3&gt;
&lt;p&gt;现在大多数 HTTP 服务器都开启了&lt;code&gt;gzip、deflate&lt;/code&gt;之类的压缩功能，在 fiddler 中默认捕获的响应是不会解码这些压缩报文的，需要手动解码或者设置 fiddler 自动解码，如下图：
&lt;img src=&quot;fiddler-tutorial/2019-05-30-18-30-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;过滤报文&lt;/h3&gt;
&lt;p&gt;在 fiddler 中默认是显示所有捕获到的 HTTP 请求，但有的时候不需要看到这么多不关心的请求，对此 fiddler 提供了&lt;code&gt;filter&lt;/code&gt;功能可以自定义过滤出需要关心的请求。&lt;/p&gt;
&lt;p&gt;通过右侧面板-filter 选项，即可进入&lt;code&gt;filter&lt;/code&gt;配置页面，可以支持各种各样的过滤规则，这里展示下按域名过滤，可以看到只显示了对应域名下的请求：
&lt;img src=&quot;fiddler-tutorial/2019-05-30-18-46-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;高级功能&lt;/h2&gt;
&lt;h3&gt;HTTPS 支持&lt;/h3&gt;
&lt;p&gt;默认情况情况下&lt;code&gt;Fidder&lt;/code&gt;是不支持嗅探&lt;code&gt;HTTPS&lt;/code&gt;报文的，需要在设置里手动开启，通过&lt;code&gt;Tools-Options&lt;/code&gt;打开设置面板，切换到&lt;code&gt;https&lt;/code&gt;标签页进行以下配置：
&lt;img src=&quot;fiddler-tutorial/2019-06-13-15-59-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在首次开启&lt;code&gt;HTTPS&lt;/code&gt;支持时，会提示安装一个 Fidder 生成的&lt;code&gt;CA根证书&lt;/code&gt;，安装完之后才能支持 HTTPS 的报文嗅探:
&lt;img src=&quot;fiddler-tutorial/2019-06-13-16-02-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再访问下&lt;code&gt;https://www.baidu.com&lt;/code&gt;，可以看到已经嗅探到了&lt;code&gt;HTTPS&lt;/code&gt;的明文：
&lt;img src=&quot;fiddler-tutorial/2019-06-13-16-03-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;AutoResponder&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;AutoResponder&lt;/code&gt;可以通过配置对应的规则自动替换响应内容，在右侧面板中的&lt;code&gt;AutoResponder&lt;/code&gt;选项卡中进行配置：&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>java8下HTTP代理身份验证设置</title><link>https://monkeywie.cn/posts/java8-http-proxy-auth</link><guid isPermaLink="true">https://monkeywie.cn/posts/java8-http-proxy-auth</guid><pubDate>Tue, 11 Jun 2019 17:13:12 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;由于公司内部应用要调用钉钉的 API，但是钉钉 API 有一个 &lt;code&gt;IP 白名单&lt;/code&gt;限制，而公司的外网 IP 经常变动，每次变动都需要在钉钉的后台配置一个 IP，在开发环境调试非常的麻烦，于是就让运维在一台外网服务器上搭建了一个&lt;code&gt;HTTP代理服务&lt;/code&gt;，通过代理服务器转发，只需要设置&lt;code&gt;代理服务器&lt;/code&gt;的外网 IP 就可以避免之前的问题了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;但是好景不长，用着用着发现&lt;code&gt;代理服务器&lt;/code&gt;越来越卡，接口请求经常超时，后来运维通过日志发现&lt;code&gt;代理服务器&lt;/code&gt;已经被别人扫描到并大量的在使用了(多半用于爬虫的 IP 池)，因为之前&lt;code&gt;代理服务器&lt;/code&gt;没有开启&lt;code&gt;身份验证&lt;/code&gt;，然后运维加上了&lt;code&gt;身份验证&lt;/code&gt;之后问题就解决了。&lt;/p&gt;
&lt;p&gt;这里主要是记录一下 JAVA 8 中使用 HTTP 代理并启用&lt;code&gt;身份验证&lt;/code&gt;的方法，在网上搜了好多资料才搞定(&lt;em&gt;很多都是过时的办法不适用 JAVA 8，差点就准备自己去看 JDK 源码了&lt;/em&gt;)。&lt;/p&gt;
&lt;h3&gt;全局设置&lt;/h3&gt;
&lt;p&gt;全局设置会影响所有由 JDK 中&lt;code&gt;HttpURLConnection&lt;/code&gt;发起的请求，很多 HTTP 客户端类库都是封装的此类，所以在类库没有暴露 HTTP 代理设置的就可以基于此方案来设置(&lt;em&gt;比如我用的钉钉 SDK&lt;/em&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 启用http代理
System.setProperty(&quot;http.proxySet&quot;, &quot;true&quot;);
// 发起http请求时使用的代理服务器配置
System.setProperty(&quot;http.proxyHost&quot;, &quot;ip&quot;);
System.setProperty(&quot;http.proxyPort&quot;, &quot;port&quot;);
// 发起https请求时使用的代理服务器配置
System.setProperty(&quot;https.proxyHost&quot;, &quot;ip&quot;);
System.setProperty(&quot;https.proxyPort&quot;, &quot;port&quot;);
// 不使用代理的域名，默认为&quot;localhost|127.*|[::1]&quot;
System.setProperty(&quot;http.nonProxyHosts&quot;, &quot;*.foo.com|localhost&quot;);
// 这行代码是身份验证的关键配置，不然身份验证不起作用
System.setProperty(&quot;jdk.http.auth.tunneling.disabledSchemes&quot;, &quot;&quot;);
// 身份验证
Authenticator.setDefault(
        new Authenticator() {
            @Override
            public PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(
                        &quot;user&quot;, &quot;password&quot;.toCharArray());
            }
        }
);

// 构造HTTP请求
URL url = new URL(&quot;https://www.baidu.com&quot;);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;局部设置&lt;/h3&gt;
&lt;p&gt;针对某次请求来进行代理设置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这行代码是身份验证的关键配置，不然身份验证不起作用
System.setProperty(&quot;jdk.http.auth.tunneling.disabledSchemes&quot;, &quot;&quot;);
// 身份验证
Authenticator.setDefault(
        new Authenticator() {
            @Override
            public PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(
                        &quot;user&quot;, &quot;password&quot;.toCharArray());
            }
        }
);
// 设置代理服务器
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(&quot;ip&quot;, port));

// 构造HTTP请求
URL url = new URL(&quot;https://www.baidu.com&quot;);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(proxy);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言中for range的&amp;#34;坑&amp;#34;</title><link>https://monkeywie.cn/posts/go-for-range-trap</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-for-range-trap</guid><pubDate>Mon, 01 Jul 2019 11:34:56 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;Go 中的&lt;code&gt;for range&lt;/code&gt;组合可以和方便的实现对一个数组或切片进行遍历，但是在某些情况下使用&lt;code&gt;for range&lt;/code&gt;时很可能就会被&lt;code&gt;&quot;坑&quot;&lt;/code&gt;，下面用一段代码来模拟下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	arr1 := []int{1, 2, 3}
	arr2 := make([]*int, len(arr1))

	for i, v := range arr1 {
		arr2[i] = &amp;amp;v
	}

	for _, v := range arr2 {
		fmt.Println(*v)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;代码解析：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个&lt;code&gt;int slice&lt;/code&gt;，变量名为&lt;code&gt;arr1&lt;/code&gt;并初始化 1,2,3 作为切片的值。&lt;/li&gt;
&lt;li&gt;创建一个&lt;code&gt;*int slice&lt;/code&gt;，变量名为&lt;code&gt;arr2&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;for range&lt;/code&gt;遍历&lt;code&gt;arr1&lt;/code&gt;，然后获取每一个元素的指针，赋值到对应&lt;code&gt;arr2&lt;/code&gt;中。&lt;/li&gt;
&lt;li&gt;逐行打印&lt;code&gt;arr2&lt;/code&gt;中每个元素的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;从代码上看，打印出来的结果应该是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1
2
3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而真正的结果是&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3
3
3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原因&lt;/h3&gt;
&lt;p&gt;因为&lt;code&gt;for range&lt;/code&gt;在遍历&lt;code&gt;值类型&lt;/code&gt;时，其中的&lt;code&gt;v&lt;/code&gt;变量是一个&lt;code&gt;值&lt;/code&gt;的拷贝，当使用&lt;code&gt;&amp;amp;&lt;/code&gt;获取指针时，实际上是获取到&lt;code&gt;v&lt;/code&gt;这个临时变量的指针，而&lt;code&gt;v&lt;/code&gt;变量在&lt;code&gt;for range&lt;/code&gt;中只会创建一次，之后循环中会被一直重复使用，所以在&lt;code&gt;arr2&lt;/code&gt;赋值的时候其实都是&lt;code&gt;v&lt;/code&gt;变量的指针，而&lt;code&gt;&amp;amp;v&lt;/code&gt;最终会指向&lt;code&gt;arr1&lt;/code&gt;最后一个元素的值拷贝。&lt;/p&gt;
&lt;p&gt;来看看下面这个代码，用&lt;code&gt;for i&lt;/code&gt;来模拟&lt;code&gt;for range&lt;/code&gt;，这样更易于理解:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	arr1 := []int{1, 2, 3}
	arr2 := make([]*int, len(arr1))

	var v int
	for i:=0;i&amp;lt;len(arr1);i++ {
		v = arr1[i]
		arr2[i] = &amp;amp;v
	}

	for _, v := range arr2 {
		fmt.Println(*v)
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;传递原始指针&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i := range arr1 {
        arr2[i] = &amp;amp;arr1[i]
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用临时变量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i, v := range arr1 {
        t := v
        arr2[i] = &amp;amp;t
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用闭包&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    arr1 := []int{1, 2, 3}
    arr2 := make([]*int, len(arr1))

    for i, v := range arr1 {
        func(v int){
             arr2[i] = &amp;amp;v
        }(v)
    }

    for _, v := range arr2 {
        fmt.Println(*v)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;官方提示&lt;/h3&gt;
&lt;p&gt;由于这一问题过于普遍，Golang甚至将其写入了文档的『常见错误』部分：&lt;a href=&quot;https://github.com/golang/go/wiki/CommonMistakes#using-reference-to-loop-iterator-variable&quot;&gt;文档&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>java反射使用getDeclaredMethods会获取到父类方法的解决办法</title><link>https://monkeywie.cn/posts/java-reflect-getdeclaredmethods-issue</link><guid isPermaLink="true">https://monkeywie.cn/posts/java-reflect-getdeclaredmethods-issue</guid><pubDate>Wed, 03 Jul 2019 10:18:25 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;最近在使用&lt;code&gt;getDeclaredMethods&lt;/code&gt;方法获取类中的方法时碰到一个奇怪的问题，先来看看&lt;code&gt;getDeclaredMethods&lt;/code&gt;方法的注释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Returns an array containing Method objects reflecting all the declared methods of the class or interface represented by this Class object, including public, protected, default (package) access, and private methods, &lt;strong&gt;but excluding inherited methods&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;谷歌翻译:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;返回一个包含 Method 对象的数组，这些对象反映此 Class 对象所表示的类或接口的所有声明方法，包括 public，protected，default（包）访问和私有方法，&lt;strong&gt;但不包括继承的方法&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意加粗的字体，可以看到 JDK 注释里明确的说明了&lt;code&gt;getDeclaredMethods&lt;/code&gt;方法不会返回继承的方法，我要的功能就是取当前类上的方法(不包含父类的)，但是事情并没有这么简单，下面一起来看看是为什么。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;h4&gt;正常案例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Test {

    class A {
        void add(Object obj) {
        }
    }

    class B extends A{
        @Override
        void add(Object obj) {
        }
    }

    public static void main(String[] args) {
        for (Method method : B.class.getDeclaredMethods()) {
            System.out.println(method.toString());
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码非常简单，就是一个子类(B)重写了父类(A)的&lt;code&gt;add(Object obj)&lt;/code&gt;方法，然后通过&lt;code&gt;B.class.getDeclaredMethods()&lt;/code&gt;来获取子类(B)上声明的所有方法，运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Test$B.add(java.lang.Object)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到打印出了子类(B)的&lt;code&gt;add(Object obj)&lt;/code&gt;方法，并没把父类(A)中的&lt;code&gt;add(Object obj)&lt;/code&gt;方法也打印出来，符合预期的结果。&lt;/p&gt;
&lt;h4&gt;非正常的案例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public class Test {

    class A&amp;lt;T&amp;gt; {
        void add(T t) {
        }
    }

    class B extends A&amp;lt;String&amp;gt;{
        @Override
        void add(String obj) {
        }
    }

    public static void main(String[] args) {
        for (Method method : B.class.getDeclaredMethods()) {
            System.out.println(method.toString());
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和之前稍有不同的是，在父类(A)上声明了一个&lt;code&gt;泛型&amp;lt;T&amp;gt;&lt;/code&gt;，然后子类(B)实现了泛型并重写父类的方法，运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void Test$B.add(java.lang.String)
void Test$B.add(java.lang.Object)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;震惊！不是不返回继承的方法吗？那碰到这种情况该怎么忽略掉来自父类上的方法呢？&lt;/p&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;p&gt;使用&lt;code&gt;method.isBridge()&lt;/code&gt;方法来判断是否为继承的方法，具体原因可以看&lt;a href=&quot;http://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html&quot;&gt;这里&lt;/a&gt;，改造后的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) {
    for (Method method : B.class.getDeclaredMethods()) {
        // 判断是非继承的方法
        if(!method.isBridge()){
            System.out.println(method.toString());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/questions/1961350/problem-in-the-getdeclaredmethods-java&quot;&gt;https://stackoverflow.com/questions/1961350/problem-in-the-getdeclaredmethods-java&lt;/a&gt;
&lt;a href=&quot;http://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html&quot;&gt;http://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>win10右键菜单在当前目录下打开CMD</title><link>https://monkeywie.cn/posts/win10-open-cmd</link><guid isPermaLink="true">https://monkeywie.cn/posts/win10-open-cmd</guid><pubDate>Mon, 08 Jul 2019 11:18:36 GMT</pubDate><content:encoded>&lt;p&gt;win10 右键菜单只能打开 Powershell，然而不知道是 Powershell 难用还是我不会用，各种莫名其妙的问题，这个时候想想还是&lt;code&gt;cmd&lt;/code&gt;真香，所以在网上找了个办法把 &lt;code&gt;cmd&lt;/code&gt; 加到右键菜单里已方便使用。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;p&gt;新建一个文档，赋值粘贴以下代码，并且将文档保存为.reg 格式的文件。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shell\cmd_here]

@=&quot;在此处打开CMD窗口&quot;
&quot;Icon&quot;=&quot;cmd.exe&quot;

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\background\shell\cmd_here\command]

@=&quot;\&quot;C:\\Windows\\System32\\cmd.exe\&quot;&quot;


[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\cmdPrompt]

@=&quot;在此处打开CMD窗口&quot;


[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Folder\shell\cmdPrompt\command]

@=&quot;\&quot;C:\\Windows\\System32\\cmd.exe\&quot; \&quot;cd %1\&quot;&quot;


[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell\cmd_here]

@=&quot;在此处打开CMD窗口&quot;
&quot;Icon&quot;=&quot;cmd.exe&quot;


[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell\cmd_here\command]

@=&quot;\&quot;C:\\Windows\\System32\\cmd.exe\&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双击运行即可&lt;/p&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;win10-open-cmd/2019-07-08-11-27-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Kubernetes之服务优雅升级</title><link>https://monkeywie.cn/posts/k8s-graceful-shutdown</link><guid isPermaLink="true">https://monkeywie.cn/posts/k8s-graceful-shutdown</guid><pubDate>Thu, 11 Jul 2019 10:22:46 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;k8s&lt;/code&gt;本身就支持服务滚动升级，但是如果程序没有正确的处理退出信号时，就会导致部分请求直接被中断从而影响用户体验。&lt;/p&gt;
&lt;h2&gt;滚动升级步骤&lt;/h2&gt;
&lt;p&gt;每个&lt;code&gt;pod&lt;/code&gt;代表一个集群中的节点，在 k8s 做&lt;code&gt;rolling-update&lt;/code&gt;的时候默认会向旧的&lt;code&gt;pod&lt;/code&gt;发送一个&lt;code&gt;SIGTERM&lt;/code&gt;信号，如果应用没有对&lt;code&gt;SIGTERM&lt;/code&gt;信号做处理的话，会立即强制退出程序，这样的话会导致有些请求还没处理完，前端应用请求错误。&lt;/p&gt;
&lt;p&gt;先来回顾下 k8s 的滚动升级步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动一个新的 pod&lt;/li&gt;
&lt;li&gt;等待新的 pod 进入 Ready 状态&lt;/li&gt;
&lt;li&gt;创建 Endpoint，将新的 pod 纳入负载均衡&lt;/li&gt;
&lt;li&gt;移除与老 pod 相关的 Endpoint，并且将老 pod 状态设置为 Terminating，此时将不会有新的请求到达老 pod&lt;/li&gt;
&lt;li&gt;给老 pod 发送 SIGTERM 信号，并且等待 terminationGracePeriodSeconds 这么长的时间。(默认为 30 秒)&lt;/li&gt;
&lt;li&gt;超过 terminationGracePeriodSeconds 等待时间直接强制 kill 进程并关闭旧的 pod&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里要注意，&lt;code&gt;SIGTERM信号如果进程没有处理的话也其实也就会导致进程被强杀&lt;/code&gt;，如果处理了但是超过&lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;配置的时间也一样会被强杀，所以这个时间可以根据具体的情况去设置。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;SpringBoot 处理 SIGTERM 信号&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;SpringBoot&lt;/code&gt;中处理 SIGTERM 信号非常简单，只需要一个&lt;code&gt;@PreDestroy&lt;/code&gt;注解就可以监听到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @PreDestroy
    public void shutdown() {
       System.out.println(&quot;shutdown&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;通过容器生命周期 hook 来优雅停止&lt;/h2&gt;
&lt;p&gt;在 pod 中容器将停止前，会执行&lt;code&gt;PreStop hook&lt;/code&gt;，hook 可以执行一个&lt;code&gt;HTTP GET&lt;/code&gt;请求或者&lt;code&gt;exec&lt;/code&gt;命令，并且它们执行是阻塞的，可以利用这个特性来做优雅停止。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;HTTP GET&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;lifecycle&quot;: {
    &quot;preStop&quot;: {
    &quot;httpGet&quot;: {
            &quot;path&quot;: &quot;/shutdown&quot;,
            &quot;port&quot;: 3000,
            &quot;scheme&quot;: &quot;HTTP&quot;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调用&lt;code&gt;exec&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;lifecycle&quot;: {
    &quot;preStop&quot;: {
        &quot;exec&quot;: {
            &quot;command&quot;: [&quot;/bin/sh&quot;, &quot;-c&quot;, &quot;sleep 30&quot;]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样的好处是可以在 k8s 层面来解决优雅停机的问题，而不需要应用程序对&lt;code&gt;SIGTERM&lt;/code&gt;信号做处理。&lt;/p&gt;
&lt;h2&gt;关于 PreStop 和 terminationGracePeriodSeconds&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;如果有&lt;code&gt;PreStop hook&lt;/code&gt;会执行&lt;code&gt;PreStop hook&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PreStop hook&lt;/code&gt;执行完成后会向 pod 发送&lt;code&gt;SIGTERM&lt;/code&gt;信号。&lt;/li&gt;
&lt;li&gt;如果在&lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;时间限制内，&lt;code&gt;PreStop hook&lt;/code&gt;没有执行完的话，一样会直接发送&lt;code&gt;SIGTERM&lt;/code&gt;信号，并且时间延长 2 秒。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;即在有&lt;code&gt;PreStop hook&lt;/code&gt;的情况下，也是在&lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;时间限制内，在超过这个时间点之后，还会给出 2 秒进程处理&lt;code&gt;SIGTERM&lt;/code&gt;信号的时间，最后直接强杀。&lt;/p&gt;
&lt;p&gt;以上情况已经过 k8s 上验证过，参考：&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods&quot;&gt;https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>通过web terminal来连接docker容器</title><link>https://monkeywie.cn/posts/docker-web-terminal</link><guid isPermaLink="true">https://monkeywie.cn/posts/docker-web-terminal</guid><pubDate>Fri, 19 Jul 2019 18:22:01 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在公司内部使用 Jenkins 做 CI/CD 时，经常会碰到项目构建失败的情况，一般情况下通过 Jenkins 的构建控制台输出都可以了解到大概发生的问题，但是有些特殊情况开发需要在 Jenkins 服务器上排查问题，这个时候就只能找运维去调试了，为了开发人员的体验就调研了下 web terminal，能够在构建失败时提供容器终端给开发进行问题的排查。&lt;/p&gt;
&lt;h2&gt;效果展示&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;docker-web-terminal/2019-07-22-09-24-49.png&quot; alt=&quot;&quot; /&gt;
支持颜色高亮，支持&amp;lt;kbd&amp;gt;tab&amp;lt;/kbd&amp;gt;键补全，支持复制粘贴，体验基本上与平常的 terminal 一致。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;基于 docker 的 web terminal 实现&lt;/h2&gt;
&lt;h3&gt;docker exec 调用&lt;/h3&gt;
&lt;p&gt;首先想到的就是通过&lt;code&gt;docker exec -it ubuntu /bin/bash&lt;/code&gt;命令来开启一个终端，然后将标准输入和输出通过 &lt;code&gt;websocket&lt;/code&gt; 与前端进行交互。&lt;/p&gt;
&lt;p&gt;然后发现 docker 有提供 API 和 &lt;a href=&quot;https://docs.docker.com/develop/sdk/&quot;&gt;SDK&lt;/a&gt; 进行开发的，通过 &lt;code&gt;Go SDK&lt;/code&gt;可以很方便的在 docker 里创建一个终端进程:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 sdk&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/docker/docker/client@8c8457b0f2f8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个项目新打的 tag 没有遵循 go mod server 语义，所以如果直接&lt;code&gt;go get -u github.com/docker/docker/client&lt;/code&gt;默认安装的是 2017 年的打的一个 tag 版本，这里我直接在 master 分支上找了一个 commit ID，具体原因参考&lt;a href=&quot;https://github.com/moby/moby/issues/39056&quot;&gt;issue&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用 exec&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;bufio&quot;
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;github.com/docker/docker/api/types&quot;
	&quot;github.com/docker/docker/client&quot;
)

func main() {
	// 初始化 go sdk
	ctx := context.Background()
	cli, err := client.NewClientWithOpts(client.FromEnv)
	if err != nil {
		panic(err)
	}

	cli.NegotiateAPIVersion(ctx)

	// 在指定容器中执行/bin/bash命令
	ir, err := cli.ContainerExecCreate(ctx, &quot;test&quot;, types.ExecConfig{
		AttachStdin:  true,
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{&quot;/bin/bash&quot;},
		Tty:          true,
	})
	if err != nil {
		panic(err)
	}

	// 附加到上面创建的/bin/bash进程中
	hr, err := cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
	if err != nil {
		panic(err)
	}
	// 关闭I/O
	defer hr.Close()
	// 输入
	hr.Conn.Write([]byte(&quot;ls\r&quot;))
	// 输出
	scanner := bufio.NewScanner(hr.Conn)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候 docker 的终端的输入输出已经可以拿到了，接下来要通过 websocket 来和前端进行交互。&lt;/p&gt;
&lt;h3&gt;前端页面&lt;/h3&gt;
&lt;p&gt;当我们在 linux terminal 上敲下&lt;code&gt;ls&lt;/code&gt;命令时，看到的是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@a09f2e7ded0d:/# ls
bin   dev  home  lib64  mnt  proc  run   srv  tmp  var
boot  etc  lib   media  opt  root  sbin  sys  usr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上从标准输出里返回的字符串却是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[0m[01;34mbin[0m   [01;34mdev[0m  [01;34mhome[0m  [01;34mlib64[0m  [01;34mmnt[0m  [01;34mproc[0m  [01;34mrun[0m   [01;34msrv[0m  [30;42mtmp[0m  [01;34mvar[0m
[01;34mboot[0m  [01;34metc[0m  [01;34mlib[0m   [01;34mmedia[0m  [01;34mopt[0m  [01;34mroot[0m  [01;34msbin[0m  [01;34msys[0m  [01;34musr[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这种情况，已经有了一个叫&lt;code&gt;xterm.js&lt;/code&gt;的库，专门用来模拟 Terminal 的，我们需要通过这个库来做终端的显示。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var term = new Terminal();
term.open(document.getElementById(&quot;terminal&quot;));
term.write(&quot;Hello from \x1B[1;3;31mxterm.js\x1B[0m $ &quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过官方的例子，可以看到它会将特殊字符做对应的显示：
&lt;img src=&quot;docker-web-terminal/2019-07-22-10-59-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样的话只需要在 websocket 连上服务器时，将获取到的终端输出使用&lt;code&gt;term.write()&lt;/code&gt;写出来，再把前端的输入作为终端的输入就可以实现我们需要的功能了。&lt;/p&gt;
&lt;p&gt;思路是没错的，但是没必要手写，&lt;code&gt;xterm.js&lt;/code&gt;已经提供了一个 websocket 插件就是来做这个事的，我们只需要把标准输入和输出的内容通过 websocket 传输就可以了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 xterm.js&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;npm install xterm
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;基于 vue 写的前端页面&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div ref=&quot;terminal&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script&amp;gt;
// 引入css
import &quot;xterm/dist/xterm.css&quot;;
import &quot;xterm/dist/addons/fullscreen/fullscreen.css&quot;;

import { Terminal } from &quot;xterm&quot;;
// 自适应插件
import * as fit from &quot;xterm/lib/addons/fit/fit&quot;;
// 全屏插件
import * as fullscreen from &quot;xterm/lib/addons/fullscreen/fullscreen&quot;;
// web链接插件
import * as webLinks from &quot;xterm/lib/addons/webLinks/webLinks&quot;;
// websocket插件
import * as attach from &quot;xterm/lib/addons/attach/attach&quot;;

export default {
  name: &quot;Index&quot;,
  created() {
    // 安装插件
    Terminal.applyAddon(attach);
    Terminal.applyAddon(fit);
    Terminal.applyAddon(fullscreen);
    Terminal.applyAddon(webLinks);

    // 初始化终端
    const terminal = new Terminal();
    // 打开websocket
    const ws = new WebSocket(&quot;ws://127.0.0.1:8000/terminal?container=test&quot;);
    // 绑定到dom上
    terminal.open(this.$refs.terminal);
    // 加载插件
    terminal.fit();
    terminal.toggleFullScreen();
    terminal.webLinksInit();
    terminal.attach(ws);
  }
};
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;后端 websocket 支持&lt;/h3&gt;
&lt;p&gt;在 go 的标准库中是没有提供 websocket 模块的，这里我们使用官方钦点的 websocket 库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/gorilla/websocket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// websocket握手配置，忽略Origin检测
var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func terminal(w http.ResponseWriter, r *http.Request) {
	// websocket握手
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Error(err)
		return
	}
	defer conn.Close()

	r.ParseForm()
	// 获取容器ID或name
	container := r.Form.Get(&quot;container&quot;)
	// 执行exec，获取到容器终端的连接
	hr, err := exec(container)
	if err != nil {
		log.Error(err)
		return
	}
	// 关闭I/O流
	defer hr.Close()
	// 退出进程
	defer func() {
		hr.Conn.Write([]byte(&quot;exit\r&quot;))
	}()

	// 转发输入/输出至websocket
	go func() {
		wsWriterCopy(hr.Conn, conn)
	}()
	wsReaderCopy(conn, hr.Conn)
}

func exec(container string) (hr types.HijackedResponse, err error) {
	// 执行/bin/bash命令
	ir, err := cli.ContainerExecCreate(ctx, container, types.ExecConfig{
		AttachStdin:  true,
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{&quot;/bin/bash&quot;},
		Tty:          true,
	})
	if err != nil {
		return
	}

	// 附加到上面创建的/bin/bash进程中
	hr, err = cli.ContainerExecAttach(ctx, ir.ID, types.ExecStartCheck{Detach: false, Tty: true})
	if err != nil {
		return
	}
	return
}

// 将终端的输出转发到前端
func wsWriterCopy(reader io.Reader, writer *websocket.Conn) {
	buf := make([]byte, 8192)
	for {
		nr, err := reader.Read(buf)
		if nr &amp;gt; 0 {
			err := writer.WriteMessage(websocket.BinaryMessage, buf[0:nr])
			if err != nil {
				return
			}
		}
		if err != nil {
			return
		}
	}
}

// 将前端的输入转发到终端
func wsReaderCopy(reader *websocket.Conn, writer io.Writer) {
	for {
		messageType, p, err := reader.ReadMessage()
		if err != nil {
			return
		}
		if messageType == websocket.TextMessage {
			writer.Write(p)
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;以上就完成了一个简单的 docker web terminal 功能，之后只需要通过前端传递&lt;code&gt;container ID&lt;/code&gt;或&lt;code&gt;container name&lt;/code&gt;就可以打开指定的容器进行交互了。&lt;/p&gt;
&lt;p&gt;完整代码：https://github.com/monkeyWie/docker-web-terminal&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>maven下载指定依赖jar包</title><link>https://monkeywie.cn/posts/maven-download-jar</link><guid isPermaLink="true">https://monkeywie.cn/posts/maven-download-jar</guid><pubDate>Thu, 25 Jul 2019 09:40:16 GMT</pubDate><content:encoded>&lt;h2&gt;命令格式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;mvn dependency:get -Dartifact=groupId:artifactId:version:jar:sources
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;下载jar包&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mvn dependency:get -Dartifact=junit:junit:4.12:jar
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;下载源码&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mvn dependency:get -Dartifact=junit:junit:4.12:jar:sources
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>服务器推送技术与websocket协议</title><link>https://monkeywie.cn/posts/server-push-and-websocket</link><guid isPermaLink="true">https://monkeywie.cn/posts/server-push-and-websocket</guid><pubDate>Thu, 08 Aug 2019 16:35:37 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;websocket 作为现代浏览器的长连接标准，可以很好的解决浏览器与服务器实时通讯的问题，那么在 websocket 出现之前是怎么解决这个问题的呢？首先来回顾一下在此之前浏览器和服务器的&quot;长连接&quot;之路。&lt;/p&gt;
&lt;h2&gt;回顾&lt;/h2&gt;
&lt;p&gt;在 websocket 协议出来之前，主要是有三种方向去实现类似 websocket 的功能的。&lt;/p&gt;
&lt;h3&gt;Flash&lt;/h3&gt;
&lt;p&gt;flash 支持 socket 通讯功能，基于 flash 可以很简单的实现与服务器建立通讯。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：开发简单、兼容性高&lt;/li&gt;
&lt;li&gt;缺点：需要浏览器启用 flash 功能，并且逐渐被浏览器淘汰&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;AJAX Polling&lt;/h3&gt;
&lt;p&gt;浏览器使用 ajax 去轮询服务器，服务器有内容就返回，轮询也分为短轮询和长轮询。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h4&gt;短轮询&lt;/h4&gt;
&lt;p&gt;短轮询即浏览器通过 ajax 按照一定时间的间隔去请求服务器，服务器会立即响应，不管有没有可用数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;流程图：
&lt;img src=&quot;./server-push-and-websocket/2019-08-08-10-08-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：短链接、服务器处理方便。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：实时性低、很多无效请求、性能开销大&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;长轮询&lt;/h4&gt;
&lt;p&gt;长短轮询则是浏览器通过 ajax 与服务器建立连接，服务器在没有数据返回时一直阻塞着，直到有数据之后才返回响应。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;流程图：
&lt;img src=&quot;./server-push-and-websocket/2019-08-08-10-22-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;优点：实时性高&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;缺点：每个连接只能返回一次数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;COMET&lt;/h3&gt;
&lt;p&gt;comet 也是常用的一种服务器推送技术，主要的原理是通过&lt;code&gt;HTTP Chunked&lt;/code&gt;响应，将消息源源不断的推送给浏览器，通常情况下服务器返回的响应内容都是定长的，会使用&lt;code&gt;Content-Length&lt;/code&gt;来指定响应报文的长度，而&lt;code&gt;Chunked&lt;/code&gt;编码的响应则是通过一种特殊的编码，只要浏览器没有遇到结束标识，就会边解析边执行对应的响应内容。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chunked 编码报文示例:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: text/html
Date: Thu, 08 Aug 2019 02:50:06 GMT
Transfer-Encoding: chunked

11
callback(&apos;data1&apos;)
11
callback(&apos;data2&apos;)
0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报文格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;length&amp;gt;\r\n
&amp;lt;chunk data&amp;gt;\r\n
&amp;lt;length&amp;gt;\r\n
&amp;lt;chunk data&amp;gt;\r\n
...
0\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由 16 进制的数字来标识一个&lt;code&gt;chunk data&lt;/code&gt;数据的长度，在读取到&lt;code&gt;0\r\n&lt;/code&gt;时结束，通过一直读取&lt;code&gt;chunk data&lt;/code&gt;来执行 js 代码，从而向客户端推送数据。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;go 语言实现：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func main() {
	http.HandleFunc(&quot;/push&quot;, func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set(&quot;Content-Type&quot;, &quot;text/html&quot;)
		flusher := w.(http.Flusher)
		w.Write([]byte(&quot;&amp;lt;script&amp;gt;console.log(&apos;data1&apos;)&amp;lt;/script&amp;gt;&quot;))
		flusher.Flush()
		// 延迟一秒，以便观察浏览器的边解析边执行
		time.Sleep(time.Second)
		w.Write([]byte(&quot;&amp;lt;script&amp;gt;console.log(&apos;data2&apos;)&amp;lt;/script&amp;gt;&quot;))
		flusher.Flush()
	})
	log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;通过 iframe 实现&lt;/h4&gt;
&lt;p&gt;iframe 实现是通过隐藏一个&lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;，通过&lt;code&gt;iframe&lt;/code&gt;连接到服务器，服务器响应带有&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;标签的内容，通过动态执行 js 代码从而实现服务器推送数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前端示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!doctype html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;iframe style=&quot;visibility: hidden;&quot; src=&quot;/push&quot;&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：兼容性高、实时性高、支持多次推送&lt;/li&gt;
&lt;li&gt;缺点：一些浏览器会一直处于加载状态&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;通过 ajax 实现&lt;/h4&gt;
&lt;p&gt;ajax 实现是通过&lt;code&gt;onreadystatechange&lt;/code&gt;回调，每次读取到一个&lt;code&gt;chunk data&lt;/code&gt;时，都会执行一次&lt;code&gt;readyState=3&lt;/code&gt;为的回调，通过这个机制也可以实现服务器推送数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前端示例：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;script&amp;gt;
    var xhr = new XMLHttpRequest();
    xhr.open(&quot;GET&quot;, &quot;/push&quot;, true);
    var received = 0;
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 3) {
        // 由于取responseText会把之前的响应一起拿到，所以要进行切分处理
        var response = xhr.responseText.substring(received);
        received += response.length;
        console.log(response);
      }
    };
    xhr.send();
  &amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;优点：实时性高、支持多次推送、浏览器不会处于加载状态&lt;/li&gt;
&lt;li&gt;缺点：兼容性低，一些浏览器不支持 readyState=3 回调&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Websocket&lt;/h2&gt;
&lt;p&gt;Websocket 是一种与 HTTP 不同的协议，两者都位与 OSI 模型的应用层， IETF 标准为&lt;a href=&quot;https://tools.ietf.org/html/rfc6455&quot;&gt;RFC 6455&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。浏览器和服务器只需要做一个 HTTP 握手的动作，然后浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。&lt;/p&gt;
&lt;p&gt;Websocket 和之前的方案最大的区别是，它是可以支持双向通讯的。而之前的技术实现上如果客户端需要发送一个新的请求，就需要创建一个新的连接。&lt;/p&gt;
&lt;h3&gt;示例：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;浏览器
浏览器上使用 Websocket 非常简单，下面看一段示例代码：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;var ws = new WebSocket(&quot;wss://echo.websocket.org&quot;);

// 连接成功
ws.onopen = function(evt) {
  console.log(&quot;Connection open ...&quot;);
  // 发送数据
  ws.send(&quot;Hello WebSockets!&quot;);
};

// 接收数据
ws.onmessage = function(evt) {
  console.log(&quot;Received Message: &quot; + evt.data);
  ws.close();
};

// 连接关闭
ws.onclose = function(evt) {
  console.log(&quot;Connection closed.&quot;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在基本上所有的服务器开发语言都有对 websocket 的支持，下面来看一段 go 编写的 websocket 服务器的示例代码，使用的是&lt;code&gt;github.com/gorilla/websocket&lt;/code&gt;库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

http.HandleFunc(&quot;/ws&quot;, func (w http.ResponseWriter, r *http.Request){
    // websocket握手处理
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Error(err)
		return
	}
	defer conn.Close()
    // 发送数据
    conn.WriteMessage(websocket.TextMessage,[]byte(&quot;hello&quot;))
    // 接收数据
	for {
		mt, b, err := conn.ReadMessage()
		if err != nil {
			return
		}
		if mt == websocket.TextMessage {
			log.Printf(&quot;Received Message: %s\n&quot;,string(b))
		}
	}
})
log.Fatal(http.ListenAndServe(&quot;:8080&quot;, nil))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现原理&lt;/h3&gt;
&lt;h4&gt;流程图：&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;./server-push-and-websocket/2019-08-08-13-44-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;握手&lt;/h4&gt;
&lt;p&gt;通过上面流程图可以看到，在建立 Websocket 连接时首先需要客户端和服务器需要完成一次握手，握手请求是使用 HTTP 协议，下面看一个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handshake request&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GET ws://localhost:8800/ HTTP/1.1
Host: localhost
Connection: Upgrade
Upgrade: websocket
Origin: http://www.websocket-test.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: GbHJKViBFhiUi4yT7CK3gA==
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求详解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;GET 请求的地址是以 ws://开头的地址。&lt;/li&gt;
&lt;li&gt;请求头 Connection: Upgrade 表示这个连接需要升级。&lt;/li&gt;
&lt;li&gt;请求头 Upgrade: websocket 表示升级到 websocket 协议。&lt;/li&gt;
&lt;li&gt;请求头 Sec-WebSocket-Key 是用于标识这个连接，在服务器响应时需要通过这个 key 来做对应匹配，以防止恶意的连接，或者无意的连接。&lt;/li&gt;
&lt;li&gt;请求头 Sec-WebSocket-Version 指定了客户端 WebSocket 的协议版本。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Handshake response&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: BcfEmZdVyUas6UtFTbjKohgqBs8=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应详解：&lt;/p&gt;
&lt;p&gt;根据规范，在服务器接收到 websocket 握手请求时，如果支持客户端对应 WebSocket 协议版本时，同样需要返回&lt;code&gt;Upgrade: websocket&lt;/code&gt;和&lt;code&gt;Connection: Upgrade&lt;/code&gt;响应头，并且返回对应的&lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt;响应头。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Sec-WebSocket-Accept&lt;/code&gt;的值通过握手请求中的&lt;code&gt;Sec-WebSocket-Key&lt;/code&gt;计算出来，计算公式为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。&lt;/li&gt;
&lt;li&gt;通过 SHA1 计算出摘要，并转成 base64 字符串。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base64(sha1(`${Sec - WebSocket - Key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;通讯格式&lt;/h4&gt;
&lt;p&gt;在握手完成之后即可开始双向通讯了，通讯报文格式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;FIN: 占 1 个 bit&lt;/p&gt;
&lt;p&gt;0：不是消息的最后一个分片&lt;/p&gt;
&lt;p&gt;1：是消息的最后一个分片&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RSV1, RSV2, RSV3：各占 1 个 bit&lt;/p&gt;
&lt;p&gt;一般情况下全为 0。当客户端、服务端协商采用 WebSocket 扩展时，这三个标志位可以非0，且值的含义由扩展进行定义。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Opcode: 4 个 bit&lt;/p&gt;
&lt;p&gt;%x0：表示一个延续帧。当 Opcode 为 0 时，表示本次数据传输采用了数据分片，当前收到的数据帧为其中一个数据分片；&lt;/p&gt;
&lt;p&gt;%x1：表示这是一个文本帧（frame）；&lt;/p&gt;
&lt;p&gt;%x2：表示这是一个二进制帧（frame）；&lt;/p&gt;
&lt;p&gt;%x3-7：保留的操作代码，用于后续定义的非控制帧；&lt;/p&gt;
&lt;p&gt;%x8：表示连接断开；&lt;/p&gt;
&lt;p&gt;%x9：表示这是一个 ping 操作；&lt;/p&gt;
&lt;p&gt;%xA：表示这是一个 pong 操作；&lt;/p&gt;
&lt;p&gt;%xB-F：保留的操作代码，用于后续定义的控制帧。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Mask: 1 个 bit&lt;/p&gt;
&lt;p&gt;表示是否要对数据载荷进行掩码异或操作。&lt;/p&gt;
&lt;p&gt;0：否&lt;/p&gt;
&lt;p&gt;1：是&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Payload length: 7bit or 7 + 16bit or 7 + 64bit&lt;/p&gt;
&lt;p&gt;表示传输数据的长度&lt;/p&gt;
&lt;p&gt;当 length == [0,126)：数据的长度为 length 字节；&lt;/p&gt;
&lt;p&gt;当 length == 126：后续 2 个字节代表一个 16 位的无符号整数，该无符号整数的值为数据的长度；&lt;/p&gt;
&lt;p&gt;当 length == 127：后续 8 个字节代表一个 64 位的无符号整数（最高位为 0），该无符号整数的值为数据的长度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这么定义的目的是在传输数据量小的时候可以节省网络传输开销。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Masking-key: 0 or 4bytes&lt;/p&gt;
&lt;p&gt;当 Mask 为 1，则携带了 4 字节的 Masking-key；&lt;/p&gt;
&lt;p&gt;当 Mask 为 0，则没有 Masking-key。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;掩码的作用并不是为了防止数据泄密，而是为了防止早期版本的协议中存在的代理缓存污染攻击（proxy cache poisoning attacks）等问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Payload Data: 传输的数据&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;掩码算法&lt;/h4&gt;
&lt;p&gt;在前面可以看到&lt;code&gt;mask&lt;/code&gt;和&lt;code&gt;mask key&lt;/code&gt;，当&lt;code&gt;mask&lt;/code&gt;为 1 时表示数据需要通过掩码处理，并且在报文中会带上一个 4 个字节长度的&lt;code&gt;mask key&lt;/code&gt;，掩码算法为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;originalData&lt;/code&gt; 作为原始的 Payload Data 数据，类型是[]byte&lt;/li&gt;
&lt;li&gt;&lt;code&gt;transformedData&lt;/code&gt; 作为掩码处理后的数据，类型是[]byte&lt;/li&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; 作为 mask key，类型是[]byte&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt;作为 originalData 读取 的下标&lt;/li&gt;
&lt;li&gt;&lt;code&gt;j&lt;/code&gt;作为 key 读取 的下标，公式为：i % 4&lt;/li&gt;
&lt;li&gt;transformedData[i] = originalData[i] ^ key[j]&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;完整示例，java 版：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void mask(byte[] key,byte[] data){
    for (int i = 0; i &amp;lt; data.length; i++) {
        int j = i%4;
        data[i] = (byte) (data[i]^key[j]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;p&gt;最后，厚颜无耻的推荐一下我的开源项目，一个用 netty 开发的 websocket 服务器：&lt;a href=&quot;https://github.com/monkeyWie/simple-websocket-server&quot;&gt;https://github.com/monkeyWie/simple-websocket-server&lt;/a&gt;，感兴趣的童鞋可以看看。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>使用免费的HTTPS证书</title><link>https://monkeywie.cn/posts/free-ssl-cert</link><guid isPermaLink="true">https://monkeywie.cn/posts/free-ssl-cert</guid><pubDate>Wed, 14 Aug 2019 15:20:56 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;众所周知 HTTPS 是保证 HTTP 通讯安全的协议，网站启用 HTTPS 可以避免很多安全性的问题， 而且 Chrome 浏览器 从 68 版本开始直接将 HTTP 网站标记为不安全了。&lt;/p&gt;
&lt;p&gt;所以把网站升级成 HTTPS 自然是大势所趋，不过启用 HTTPS 有个最重要的问题是 HTTPS 证书&lt;code&gt;要花钱&lt;/code&gt;！如果每年额外花钱去购买 HTTPS 证书，那也是一笔很大的开销。那么有没有免费的&lt;code&gt;HTTPS&lt;/code&gt;证书可以用呢，查了下资料有个叫&lt;a href=&quot;https://letsencrypt.org&quot;&gt;&lt;code&gt;Let’s Encrypt&lt;/code&gt;&lt;/a&gt;的项目就提供了免费签发 HTTPS 证书的服务，这里记录下如何使用&lt;code&gt;Let’s Encrypt&lt;/code&gt;来签发证书。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;certbot 介绍&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;certbot&lt;/code&gt;是用于从 Let&apos;s Encrypt 获取证书的命令行工具，代码开源在&lt;a href=&quot;https://github.com/certbot/certbot/&quot;&gt;github&lt;/a&gt;上。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;certbot&lt;/code&gt;命令行工具可以轻松的实现&lt;code&gt;HTTPS证书&lt;/code&gt;签发，在签发证书之前，需要证明签发的域名是属于你控制的，目前&lt;code&gt;certbot&lt;/code&gt;有两种验证方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;HTTP
HTTP 方式就是&lt;code&gt;certbot&lt;/code&gt;会生成一个特定的文件名和文件内容，要求放在你对应域名下对应路径(&lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;)下，然后&lt;code&gt;certbot&lt;/code&gt;再通过 HTTP 请求访问到此文件，并且文件内容与生成时候的一致。&amp;lt;/br&amp;gt;&amp;lt;/br&amp;gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;certbot&lt;/code&gt;生成文件名&lt;code&gt;check&lt;/code&gt;和内容&lt;code&gt;!@#$%^&lt;/code&gt;，你需要申请的域名为&lt;code&gt;baidu.com&lt;/code&gt;，则&lt;code&gt;certbot&lt;/code&gt;访问&lt;code&gt;http://baidu.com/.well-known/acme-challenge/check&lt;/code&gt;来校验是否与生成的内容一致。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DNS
DNS 则是&lt;code&gt;certbot&lt;/code&gt;生成一段特定的文本，要求在你对应域名中配置一条对应子域名(&lt;code&gt;_acme-challenge&lt;/code&gt;)的&lt;code&gt;TXT&lt;/code&gt;类型解析记录。&amp;lt;/br&amp;gt;&amp;lt;/br&amp;gt;&lt;/p&gt;
&lt;p&gt;例如：&lt;code&gt;certbot&lt;/code&gt;生成内容&lt;code&gt;!@#$%^&lt;/code&gt;，你需要申请的域名为&lt;code&gt;baidu.com&lt;/code&gt;，则需要添加一条&lt;code&gt;_acme-challenge.baidu.com&lt;/code&gt;的&lt;code&gt;TXT&lt;/code&gt;类型解析记录，值为之前生成的内容。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在域名验证通过之后，&lt;code&gt;certbot&lt;/code&gt;就可以签发&lt;code&gt;HTTPS&lt;/code&gt;证书了，注意在此验证步骤基础上，&lt;code&gt;certbot&lt;/code&gt;提供了很多开箱即用的自动验证方案，但是都不符合我的需求，原因是我需要支持&lt;code&gt;通配符&lt;/code&gt;域名的证书，但是这种证书只支持&lt;code&gt;DNS&lt;/code&gt;验证方式，而官方提供的&lt;code&gt;DNS&lt;/code&gt;插件中并没有支持我用的&lt;code&gt;阿里云DNS&lt;/code&gt;，所以只能自己去实现 阿里云的 DNS 自动校验。&lt;/p&gt;
&lt;h2&gt;使用 certbot 签发 HTTPS 证书&lt;/h2&gt;
&lt;p&gt;通过&lt;a href=&quot;https://certbot.eff.org&quot;&gt;官网教程&lt;/a&gt;可以选择对应操作系统，并获取安装步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;free-ssl-cert/2019-08-14-15-47-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我选择的&lt;code&gt;Debian 9&lt;/code&gt;，根据官网的提示进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install certbot -t stretch-backports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;注：如果install失败可以先执行下 apt-get update&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;开始签发证书&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot certonly --cert-name pdown.org -d *.pdown.org,*.proxyee-down.com --manual --register-unsafely-without-email  --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里签发了一个支持&lt;code&gt;*.pdown.org&lt;/code&gt;和&lt;code&gt;*.proxyee-down.com&lt;/code&gt;通配符域名的证书，注意如果是通配符域名证书需要指定&lt;code&gt;--server https://acme-v02.api.letsencrypt.org/directory&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Registering without email!

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for pdown.org
dns-01 challenge for proxyee-down.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you&apos;re running certbot in manual mode on a machine that is not
your server, please ensure you&apos;re okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.pdown.org with the following value:

Axdqtserd184wvJc86Dxen386UXqbK2wrgb-*******

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里会生成一串随机字符并阻塞住，需要去设置一条对应的 TXT 类型的 DNS 解析记录再继续，在设置好之后可以用&lt;code&gt;nslookup&lt;/code&gt;进行本地验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nslookup -type=txt _acme-challenge.pdown.org
服务器:  UnKnown
Address:  192.168.200.200

非权威应答:
_acme-challenge.pdown.org       text =

        &quot;Tit0SAHaO3MVZ4S-d6CjKLv6Z-********&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地验证通过之后按回车键继续，接着 Let&apos;s Encrypt 就会校验这个 DNS 解析记录是否正确，校验通过后就会进行下一个域名的验证直到全部验证通过。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/pdown.org/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/pdown.org/privkey.pem
   Your cert will expire on 2019-12-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   &quot;certbot renew&quot;
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let&apos;s Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当验证通过的时候会输出证书生成的目录，里面会包含证书和对应的私钥，这里目录是&lt;code&gt;/etc/letsencrypt/live/pdown.org/&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;证书截图：
&lt;img src=&quot;free-ssl-cert/2019-09-03-14-49-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;free-ssl-cert/2019-09-03-14-49-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样证书就生成好了，之后只需要把证书和私钥配置到&lt;code&gt;nginx&lt;/code&gt;中就可以用&lt;code&gt;https&lt;/code&gt;访问了。&lt;/p&gt;
&lt;h2&gt;使用 certbot hook 自动续签&lt;/h2&gt;
&lt;p&gt;上面证书虽然是生成好了，但是证书的有效期只有三个月，意味着每过三个月就得重新签发一个新的证书，一不注意证书就过期了，而且每次手动签发都非常的繁琐需要去手动设置 DNS 解析，所以&lt;code&gt;certbot&lt;/code&gt;提供了一种自动续签的方案：hook&lt;/p&gt;
&lt;p&gt;在创建证书的时候&lt;code&gt;certbot&lt;/code&gt;提供了两个&lt;code&gt;hook&lt;/code&gt;参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;manual-auth-hook
指定用于验证域名的脚本文件&lt;/li&gt;
&lt;li&gt;manual-cleanup-hook
指定用于清理的脚本文件，即验证完成之后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过自定义这两个脚本就可以做到自动续签了，文档参考&lt;a href=&quot;https://certbot.eff.org/docs/using.html#pre-and-post-validation-hooks&quot;&gt;pre-and-post-validation-hooks&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在此基础上，官方已经提供了很多云厂商的自动续签方案，但是我用的阿里云官方并没有提供，于是参照官网文档，写了一个基于阿里云的自动续签脚本，在验证域名的脚本中通过阿里提供的 DNS API 添加一条域名解析记录，在验证完成之后再把刚刚那条域名解析记录删除，命令行调用如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;certbot certonly --cert-name pdown.org -d *.pdown.org,*.proxyee-down.com --manual --register-unsafely-without-email --manual-auth-hook /path/to/dns/authenticator.sh --manual-cleanup-hook /path/to/dns/cleanup.sh --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了方便使用，提供了一个&lt;code&gt;docker镜像&lt;/code&gt;，通过环境变量将阿里云 API 调用的 AK 传递就可以生成和续签证书了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker run \
--name cert \
-itd \
-v /etc/letsencrypt:/etc/letsencrypt \
-e ACCESS_KEY_ID=XXX \
-e ACCESS_KEY_SECRET=XXX \
liwei2633/certbot-aliyun
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;首次创建证书&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker exec -it cert ./create.sh *.pdown.org
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建过程中会等待一段时间，来确保 dns 记录生效，完成之后在&lt;code&gt;/etc/letsencrypt/live&lt;/code&gt;目录下可以找到对应的证书文件&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;续签证书&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker exec cert ./renew.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码开源在&lt;a href=&quot;https://github.com/monkeyWie/certbot-dns-aliyun&quot;&gt;github&lt;/a&gt;，欢迎 start。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言flag接收slice和map类型参数</title><link>https://monkeywie.cn/posts/go-flag-slice-map</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-flag-slice-map</guid><pubDate>Tue, 03 Sep 2019 15:22:27 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;go 自带的&lt;code&gt;flag&lt;/code&gt;包可以很容易的实现一个命令行程序的参数解析，但是&lt;code&gt;flag&lt;/code&gt;包默认只支持几个基本类型的参数解析，如果需要传递&lt;code&gt;slice&lt;/code&gt;或者&lt;code&gt;map&lt;/code&gt;类型时就要自定义了，这里记录一下。&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;通过&lt;code&gt;flag.Var()&lt;/code&gt;方法传递一个&lt;code&gt;Value&lt;/code&gt;接口，即可自定义命令行参数的解析，&lt;code&gt;flag.Value&lt;/code&gt;接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Value interface {
	String() string
	Set(string) error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;接下来通过自定义类型来实现这个接口即可满足需求。&lt;/p&gt;
&lt;h2&gt;slice 传递&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type sliceFlag []string

func (f *sliceFlag) String() string {
	return fmt.Sprintf(&quot;%v&quot;, []string(*f))
}

func (f *sliceFlag) Set(value string) error {
	*f = append(*f, value)
	return nil
}

func main() {
    var hostsFlag sliceFlag
    flag.Var(&amp;amp;hostsFlag, &quot;host&quot;, &quot;Application hosts,for example: -host=a.com -host=b.com&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意的是数组的扩容需要使用&lt;code&gt;*f = append(*f, value)&lt;/code&gt;，来修改原本的数组&lt;/p&gt;
&lt;h2&gt;map 传递&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;
func (f mapFlag) String() string {
	return fmt.Sprintf(&quot;%v&quot;, map[string]string(f))
}

func (f mapFlag) Set(value string) error {
	split := strings.SplitN(value, &quot;=&quot;, 2)
	f[split[0]] = split[1]
	return nil
}

func main() {
    var hostsFlag sliceFlag
    flag.Var(&amp;amp;hostsFlag, &quot;env&quot;, &quot;env list,for example: -env key1=value1 -env key2=value2&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>k8s正确的更新姿势</title><link>https://monkeywie.cn/posts/k8s-update-operate</link><guid isPermaLink="true">https://monkeywie.cn/posts/k8s-update-operate</guid><pubDate>Mon, 16 Sep 2019 18:00:46 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;公司的 CI/CD 平台研发要告一个段落了，在此记录一下如何使用 k8s 的客户端工具 kubectl 来进行更新操作的。&lt;/p&gt;
&lt;h2&gt;更新 Deployment&lt;/h2&gt;
&lt;p&gt;要知道 kubectl 是不支持 update 操作的，假设有如下&lt;code&gt;Deployment.yaml&lt;/code&gt;需要进行部署：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  namespace: test
  name: hello
spec:
  selector:
    matchLabels:
      app: hello
  replicas: 1
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: hello
          image: nginx
          imagePullPolicy: Always
          ports:
            - containerPort: 80
          env:
            - name: LOGGING_LEVEL
              value: &quot;INFO&quot;

---
apiVersion: v1
kind: Service
metadata:
  namespace: test
  name: hello
spec:
  selector:
    app: hello
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只需要执行&lt;code&gt;kubectl apply -f Deployment.yaml&lt;/code&gt;即可，可以理解为 createOrUpdate 操作。&lt;/p&gt;
&lt;h2&gt;更新 Secret&lt;/h2&gt;
&lt;p&gt;因为测试环境和预备环境的 HTTPS 证书是使用的&lt;code&gt;letsencrypt&lt;/code&gt;的免费证书
，每过一段时间都需要续签一次生成新的证书相关文件，所以每次生成完都需要更新 k8s 中对应的 secret 信息。&lt;/p&gt;
&lt;p&gt;执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create secret tls hello \
  --namespace test \
  --key ./privkey.pem \
  --cert ./fullchain.pem \
  --dry-run \
  -o yaml \
  | \
  kubectl apply -f -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要是&lt;code&gt;--dry-run&lt;/code&gt;配合&lt;code&gt;-o yaml&lt;/code&gt;生成对应的 yaml 文件，然后再使用&lt;code&gt;kubectl apply -f -&lt;/code&gt;进行更新。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;--dry-run
表示不会发生实际的操作，也就是不会对 k8s 产生影响&lt;/li&gt;
&lt;li&gt;kubectl apply -f -
表示拿到上一个管道的输入进行执行&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言之交叉编译</title><link>https://monkeywie.cn/posts/go-cross-compile</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-cross-compile</guid><pubDate>Thu, 10 Oct 2019 15:22:27 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;当初学习 go 语言的原因之一就是看中了 go 可以直接编译成机器码运行，并且支持跨操作系统的交叉编译，这对开发跨操作系统软件提供了极大的便利，这篇文章目的就是记录下 go 是如何交叉编译的。&lt;/p&gt;
&lt;h2&gt;交叉编译&lt;/h2&gt;
&lt;p&gt;go 语言里交叉编译支持非常多的操作系统，可以通过&lt;code&gt;go tool dist list&lt;/code&gt;命令来查看支持的操作系统列表。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more--&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/386
darwin/amd64
darwin/arm
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
illumos/amd64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/s390x
nacl/386
nacl/amd64p32
nacl/arm
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译的时候只需要指定环境变量&lt;code&gt;GOOS&lt;/code&gt;(系统内核)和&lt;code&gt;GOARCH&lt;/code&gt;(CPU 架构)即可进行交叉编译。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Windows 上编译 Mac 和 Linux 上 64 位可执行程序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build main.go

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Linux 上编译 Mac 和 Windows 上 64 位可执行程序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Mac 上编译 Linux 和 Windows 上 64 位可执行程序&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;cgo 程序交叉编译&lt;/h3&gt;
&lt;p&gt;上面的示例都是基于程序里没有使用&lt;code&gt;cgo&lt;/code&gt;的情况下进行的，可以看到&lt;code&gt;CGO_ENABLED=0&lt;/code&gt;这个选项就是关闭&lt;code&gt;cgo&lt;/code&gt;，因为 go 的交叉编译是不支持&lt;code&gt;cgo&lt;/code&gt;的，如果程序里使用到了&lt;code&gt;cgo&lt;/code&gt;时要进行交叉编译就没这么简单了，需要安装一个跨平台的 C/C++ 编译器才可能实现交叉编译。&lt;/p&gt;
&lt;p&gt;好在已经有大佬把常用的编译环境都做成到 docker 镜像了，并且提供命令行工具让我们很方便的进行交叉编译，这个工具就是&lt;a href=&quot;https://github.com/karalabe/xgo&quot;&gt;https://github.com/karalabe/xgo&lt;/a&gt;，但是此仓库作者好像不怎么更新了，并且不支持&lt;code&gt;go mod&lt;/code&gt;，于是我找到了另一位大佬的 fork: &lt;a href=&quot;https://github.com/techknowlogick/xgo&quot;&gt;https://github.com/techknowlogick/xgo&lt;/a&gt;，支持&lt;code&gt;go mod&lt;/code&gt;并且支持最新的&lt;code&gt;go 1.13&lt;/code&gt;版本。&lt;/p&gt;
&lt;h3&gt;xgo 示例&lt;/h3&gt;
&lt;p&gt;首先要保证机器上有安装 &lt;code&gt;golang&lt;/code&gt; 和 &lt;code&gt;docker&lt;/code&gt;，接着按照教程来进行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拉取镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;镜像比较大，1 个多 G，拉取要一点时间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull techknowlogick/xgo:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装 xgo&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;go get src.techknowlogick.com/xgo
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;准备代码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里引用了&lt;code&gt;go-sqlite3&lt;/code&gt;这个库，里面用到了&lt;code&gt;cgo&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;database/sql&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;os&quot;

	_ &quot;github.com/mattn/go-sqlite3&quot;
)

func main() {
	os.Remove(&quot;./foo.db&quot;)

	db, err := sql.Open(&quot;sqlite3&quot;, &quot;./foo.db&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	sqlStmt := `
	create table foo (id integer not null primary key, name text);
	delete from foo;
	`
	_, err = db.Exec(sqlStmt)
	if err != nil {
		log.Printf(&quot;%q: %s\n&quot;, err, sqlStmt)
		return
	}

	tx, err := db.Begin()
	if err != nil {
		log.Fatal(err)
	}
	stmt, err := tx.Prepare(&quot;insert into foo(id, name) values(?, ?)&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	for i := 0; i &amp;lt; 100; i++ {
		_, err = stmt.Exec(i, fmt.Sprintf(&quot;こんにちわ世界%03d&quot;, i))
		if err != nil {
			log.Fatal(err)
		}
	}
	tx.Commit()

	rows, err := db.Query(&quot;select id, name from foo&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&amp;amp;id, &amp;amp;name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}

	stmt, err = db.Prepare(&quot;select name from foo where id = ?&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer stmt.Close()
	var name string
	err = stmt.QueryRow(&quot;3&quot;).Scan(&amp;amp;name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(name)

	_, err = db.Exec(&quot;delete from foo&quot;)
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec(&quot;insert into foo(id, name) values(1, &apos;foo&apos;), (2, &apos;bar&apos;), (3, &apos;baz&apos;)&quot;)
	if err != nil {
		log.Fatal(err)
	}

	rows, err = db.Query(&quot;select id, name from foo&quot;)
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var id int
		var name string
		err = rows.Scan(&amp;amp;id, &amp;amp;name)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(id, name)
	}
	err = rows.Err()
	if err != nil {
		log.Fatal(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;编译&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在项目目录下运行，编译 Mac,Windows,Linux 下的 64 位可执行程序,&lt;code&gt;-ldflags=&quot;-w -s&quot;&lt;/code&gt;选项可以减小编译后的程序体积。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xgo -targets=darwin/amd64,windows/amd64,linux/amd64 -ldflags=&quot;-w -s&quot; .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样使用&lt;code&gt;xgo&lt;/code&gt;轻松就完成了多操作系统的交叉编译，并且&lt;code&gt;xgo&lt;/code&gt;还有很多的特性，可以自行去 github 上看看。&lt;/p&gt;
&lt;h2&gt;使用 xgo 碰到的问题&lt;/h2&gt;
&lt;p&gt;上面提到使用&lt;code&gt;techknowlogick/xgo&lt;/code&gt;可以解决 go mod 交叉编译的问题，但是默认只会编译项目根目录下的 &lt;code&gt;main.go&lt;/code&gt; 文件，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── main.go
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我需要编译指定目录下的&lt;code&gt;main.go&lt;/code&gt;文件，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── cmd
│   └── main.go
├── main.go
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要编译 cmd/main.go，通过查看 xgo 源码可以看到有个&lt;code&gt;-pkg&lt;/code&gt;的参数就是用于指定编译路径的，遂尝试使用&lt;code&gt;-pkg&lt;/code&gt;进行交叉编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xgo -targets=darwin/amd64,windows/amd64,linux/amd64 -ldflags=&quot;-w -s&quot; -pkg=cmd/main.go .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而并没有编译成功，然后去看了一下源码，最后在&lt;a href=&quot;https://github.com/techknowlogick/xgo/blob/master/docker/base/build.sh#L154&quot;&gt;build.sh:154&lt;/a&gt;这行代码发现，如果启用了 go mod 的话，pkg 参数就会失效，为什么要这样做我也没太明白，不过既然知道原因了那就 &lt;a href=&quot;https://github.com/monkeyWie/xgo&quot;&gt;fork&lt;/a&gt; 一份修复吧。&lt;/p&gt;
&lt;p&gt;最终代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Go module-based builds error with &apos;cannot find main module&apos;
# when $PACK is defined
if [[ &quot;$USEMODULES&quot; = true ]]; then
  NAME=`sed -n &apos;s/module\ \(.*\)/\1/p&apos; /source/go.mod`
fi

# Support go module package
PACK_RELPATH=&quot;./$PACK&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺便把所有的 Dockerfile 更新了一遍，把过时的&lt;code&gt;MAINTAINER&lt;/code&gt;替换成&lt;code&gt;LABEL MAINTAINER=&quot;&quot;&lt;/code&gt;，接着就开始构建 docker 镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build -t liwei2633/xgo:base docker/base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是由于国内的网络问题，经常有请求超时导致构建失败，所以一次都没构建成功过，然后想起来前几天申请的&lt;code&gt;github actions&lt;/code&gt;内测资格通过了，可以试试&lt;code&gt;github actions&lt;/code&gt;,因为&lt;code&gt;github actions&lt;/code&gt;用的国外网络环境，应该构建不成问题,于是按照官方文档写了一个构建配置&lt;code&gt;.github/workflows/main.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI

# 当master分支有push时，并且docker/base目录下文件发生变动时触发构建
on:
  push:
    branches:
      - master
    paths:
      - &quot;docker/base/*&quot;

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
	  # 拉取源码
      - name: Checkout source
        uses: actions/checkout@v1
	  # 登录docker hub
      - name: Docker login
        run: docker login -u liwei2633 -p ${{ secrets.DOCKER_HUB_PWD }}
      - name: Docker build base
        run: |
          docker build -t liwei2633/xgo:base ./docker/base
          docker push liwei2633/xgo:base
      - name: Docker build other
        run: |
          docker build -t liwei2633/xgo:go-1.12.10 ./docker/go-1.12.10
          docker push liwei2633/xgo:go-1.12.10
          docker build -t liwei2633/xgo:go-1.12.x ./docker/go-1.12.x
          docker push liwei2633/xgo:go-1.12.x
          docker build -t liwei2633/xgo:go-1.13.1 ./docker/go-1.13.1
          docker push liwei2633/xgo:go-1.13.1
          docker build -t liwei2633/xgo:go-1.13.x ./docker/go-1.13.x
          docker push liwei2633/xgo:go-1.13.x
          docker build -t liwei2633/xgo:go-latest ./docker/go-latest
          docker push liwei2633/xgo:go-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后推送代码触发构建，这次构建成功了，但是非常的耗时大概要 30 多分钟，得想办法优化优化，由于&lt;code&gt;github actions&lt;/code&gt;是不支持缓存的，即每次构建都是一个全新的虚拟机，没法入手，只能通过 docker 的缓存机制来进行优化了，通过 google 发现有一个&lt;code&gt;--cache-from&lt;/code&gt;的构建参数，可以指定构建缓存镜像来源，于是构建流程改造成先&lt;code&gt;pull&lt;/code&gt;历史镜像，再指定历史镜像作为构建缓存镜像来进行构建，具体脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull liwei2633/xgo:base
docker build --cache-from=liwei2633/xgo:base -t liwei2633/xgo:base ./docker/base
docker push liwei2633/xgo:base

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的话因为下载镜像的耗时远比 Dockerfile 里的各种&lt;code&gt;apt-get install&lt;/code&gt;耗时要小，所以如果有了第一次构建的镜像之后，就可以通过 docker 的缓存机制来跳过许多耗时的步骤，就比如 xgo 中 Dockerfile 的一个&lt;code&gt;RUN&lt;/code&gt;语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN \
  apt-get update &amp;amp;&amp;amp; \
  apt-get install -y automake autogen build-essential ca-certificates                    \
    gcc-5-arm-linux-gnueabi g++-5-arm-linux-gnueabi libc6-dev-armel-cross                \
    gcc-5-arm-linux-gnueabihf g++-5-arm-linux-gnueabihf libc6-dev-armhf-cross            \
    gcc-5-aarch64-linux-gnu g++-5-aarch64-linux-gnu libc6-dev-arm64-cross                \
    gcc-5-mips-linux-gnu g++-5-mips-linux-gnu libc6-dev-mips-cross                       \
    gcc-5-mipsel-linux-gnu g++-5-mipsel-linux-gnu libc6-dev-mipsel-cross                 \
    gcc-5-mips64-linux-gnuabi64 g++-5-mips64-linux-gnuabi64 libc6-dev-mips64-cross       \
    gcc-5-mips64el-linux-gnuabi64 g++-5-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \
    gcc-5-multilib g++-5-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev             \
    gcc-6-arm-linux-gnueabi g++-6-arm-linux-gnueabi libc6-dev-armel-cross                \
    gcc-6-arm-linux-gnueabihf g++-6-arm-linux-gnueabihf libc6-dev-armhf-cross            \
    gcc-6-aarch64-linux-gnu g++-6-aarch64-linux-gnu libc6-dev-arm64-cross                \
    gcc-6-mips-linux-gnu g++-6-mips-linux-gnu libc6-dev-mips-cross                       \
    gcc-6-mipsel-linux-gnu g++-6-mipsel-linux-gnu libc6-dev-mipsel-cross                 \
    gcc-6-mips64-linux-gnuabi64 g++-6-mips64-linux-gnuabi64 libc6-dev-mips64-cross       \
    gcc-6-mips64el-linux-gnuabi64 g++-6-mips64el-linux-gnuabi64 libc6-dev-mips64el-cross \
    gcc-6-multilib gcc-7-multilib g++-6-multilib gcc-mingw-w64 g++-mingw-w64 clang llvm-dev \
    libtool libxml2-dev uuid-dev libssl-dev swig openjdk-8-jdk pkg-config patch \
    make xz-utils cpio wget zip unzip p7zip git mercurial bzr texinfo help2man cmake     \
    --no-install-recommends
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种情况下使用缓存无疑可以节省非常多的时间，修改之后提交，再看看构建记录：
&lt;img src=&quot;go-cross-compile/2019-10-14-14-31-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到已经走了缓存，最终耗时在 10 分钟左右节省了 2/3 的时间，&lt;code&gt;github actions&lt;/code&gt;真香！！！&lt;/p&gt;
&lt;p&gt;最后把&lt;a href=&quot;https://github.com/monkeyWie/xgo/blob/master/xgo.go&quot;&gt;xgo.go&lt;/a&gt;代码修改一下，顺带修复了个 BUG(docker image 检测问题)，推到 github 上。&lt;/p&gt;
&lt;p&gt;测试使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get github.com/monkeyWie/xgo
xgo -targets=windows/amd64,linux/amd64,darwin/amd64 -ldflags=&quot;-w -s&quot; -pkg=cmd/main.go .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功编译，完结撒花！&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;感谢 &lt;a href=&quot;https://github.com/karalabe/xgo&quot;&gt;https://github.com/karalabe/xgo&lt;/a&gt;和&lt;a href=&quot;https://github.com/techknowlogick/xgo&quot;&gt;https://github.com/techknowlogick/xgo&lt;/a&gt;为 go 交叉编译做出的贡献，然后就是&lt;code&gt;github actions&lt;/code&gt;真香，希望可以早日推出正式版。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Github Actions尝鲜</title><link>https://monkeywie.cn/posts/hello-github-actions</link><guid isPermaLink="true">https://monkeywie.cn/posts/hello-github-actions</guid><pubDate>Tue, 29 Oct 2019 13:41:56 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/features/actions&quot;&gt;Github Actions&lt;/a&gt;是 github 官方推出的一款 CI(持续集成)工具，目前还处于&lt;code&gt;Beta&lt;/code&gt;版本，需要&lt;a href=&quot;https://github.com/features/actions/signup/&quot;&gt;申请&lt;/a&gt;内测资格才能使用，申请成功之后在自己的代码仓库就可以看到&lt;code&gt;Actions&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;hello-github-actions/2019-10-30-11-30-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;使用说明&lt;/h2&gt;
&lt;p&gt;这里简单介绍下 &lt;code&gt;Github Actions&lt;/code&gt;中的概念，具体可以参考&lt;a href=&quot;https://help.github.com/en/github/automating-your-workflow-with-github-actions&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;术语&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;workflow&lt;/strong&gt;
表示一次持续集成的过程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;job&lt;/strong&gt;
表示构建任务，每个 workflow 可以由一个或者多个 job 组成，可支持并发执行 job，所有 job 执行完也就代表着 workflow 结束&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;step&lt;/strong&gt;
每个 job 由一个或多个 step 组成，按顺序依次执行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;action&lt;/strong&gt;
每个 step 由一个或多个 action 组成，按顺序依次执行，这里 action 需要特别说明一下，action 是可以是自定义脚本或引用第三方的脚本，依赖着 github 开源社区，许多 action 都可以直接复用，无需自己编写，github 已经提供了一个&lt;a href=&quot;https://github.com/marketplace?type=actions&quot;&gt;action 市场&lt;/a&gt;，可以搜索到各种第三方 actions，并且&lt;a href=&quot;https://github.com/actions&quot;&gt;官方&lt;/a&gt;也提供了许多 actions。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;构建环境&lt;/h3&gt;
&lt;p&gt;每个 job 都可以指定对应的操作系统，支持&lt;code&gt;Windows、Linux、macOS&lt;/code&gt;，github 会提供一个虚拟机来执行对应的 job。&lt;/p&gt;
&lt;p&gt;硬件规格：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;双核 CPU&lt;/li&gt;
&lt;li&gt;7GB 内存&lt;/li&gt;
&lt;li&gt;14GB 固态硬盘&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用限制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个仓库只能同时支持 20 个 workflow 并行&lt;/li&gt;
&lt;li&gt;每小时可以调用 1000 次 github API&lt;/li&gt;
&lt;li&gt;每个 job 最多可以执行 6 个小时&lt;/li&gt;
&lt;li&gt;免费版的用户最大支持 20 个 job 并发执行，macOS 系统的话最大只支持 5 个&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到这个配置下，普通的项目持续集成肯定没什么问题的。&lt;/p&gt;
&lt;h3&gt;构建记录&lt;/h3&gt;
&lt;p&gt;通过仓库中的&lt;code&gt;Actions&lt;/code&gt;选项卡，可以看到项目中的 workflow 构建记录：
&lt;img src=&quot;hello-github-actions/2019-10-31-16-07-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击一条记录可以进入详情页面，可以&lt;code&gt;实时&lt;/code&gt;查看每一个&lt;code&gt;action&lt;/code&gt;的控制台输出，方便调试：
&lt;img src=&quot;hello-github-actions/2019-10-31-16-10-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实例&lt;/h2&gt;
&lt;p&gt;前面大概介绍了一下基本的概念，下面就直接通过几个实例看看 &lt;code&gt;Github Actions&lt;/code&gt;是如何使用的。&lt;/p&gt;
&lt;h3&gt;自动部署 Hexo 博客到 Github Page&lt;/h3&gt;
&lt;p&gt;首先第一个想到能用到&lt;code&gt;Github Actions&lt;/code&gt;的就是我的博客了，项目托管在&lt;a href=&quot;https://github.com/monkeyWie/monkeywie.github.io&quot;&gt;https://github.com/monkeyWie/monkeywie.github.io&lt;/a&gt;，目前项目有两个分支，&lt;code&gt;master&lt;/code&gt;分支用于存放 hexo 编译之后的静态文件，另一个&lt;code&gt;hexo&lt;/code&gt;分支用于存放 hexo 项目环境和 markdown 文章，&lt;code&gt;master&lt;/code&gt;分支通过&lt;code&gt;Github Page&lt;/code&gt;配置之后可以通过&lt;code&gt;monkeywie.github.io&lt;/code&gt;域名访问。
&lt;img src=&quot;hello-github-actions/2019-10-30-14-18-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之前写完博客之后都是需要手动执行一遍命令进行部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hexo clean&amp;amp;&amp;amp;hexo d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再把&lt;code&gt;hexo&lt;/code&gt;分支代码推送到 github 上&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在使用&lt;code&gt;Github Actions&lt;/code&gt;之后，只需要把&lt;code&gt;hexo&lt;/code&gt;分支代码推送到 github 上，剩下的全部交给&lt;code&gt;Github Actions&lt;/code&gt;即可，在此之前我们需要生成一对&lt;code&gt;公私钥&lt;/code&gt;用于 hexo 的部署操作，因为 hexo 自带的部署命令&lt;code&gt;hexo d&lt;/code&gt;需要有 git 远程仓库读写权限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory &apos;/root/.ssh&apos;.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:XG1vkchp5b27tteZASx6ZrPRtTayGYmacRdjjRxR1Y0 root@8fe85d51123b
The key&apos;s randomart image is:
+---[RSA 2048]----+
|             .+o=|
|           o *Eoo|
|          . X B .|
|       . . + X +.|
|        S . = O..|
|         o O B =.|
|          O = *.*|
|         o . o ++|
|              .oo|
+----[SHA256]-----+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先把&lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt;中的公钥添加到 Github 对应仓库的&lt;code&gt;Deploye keys&lt;/code&gt;中：
&lt;img src=&quot;hello-github-actions/2019-10-30-16-19-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再将&lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;中的私钥添加到 Github 对应仓库的&lt;code&gt;Secrets&lt;/code&gt;中，Name 定义为&lt;code&gt;ACTION_DEPLOY_KEY&lt;/code&gt;，目的是在构建的时候可以读取该私钥并配添加到虚拟机中，以获取 git 仓库访问权限：
&lt;img src=&quot;hello-github-actions/2019-10-30-17-04-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;准备工作完成后，接着就按照&lt;a href=&quot;https://help.github.com/en/github/automating-your-workflow-with-github-actions/configuring-a-workflow#creating-a-workflow-file&quot;&gt;教程&lt;/a&gt;，在&lt;code&gt;hexo&lt;/code&gt;分支创建&lt;code&gt;.github/workflows/main.yaml&lt;/code&gt;文件用于配置 hexo 部署。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI

on:
  push:
    branches:
      - hexo
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@v1
        with:
          ref: hexo
      - name: Use Node.js ${{ matrix.node_version }}
        uses: actions/setup-node@v1
        with:
          version: ${{ matrix.node_version }}
      - name: Setup hexo
        env:
          ACTION_DEPLOY_KEY: ${{ secrets.ACTION_DEPLOY_KEY }}
        run: |
          mkdir -p ~/.ssh/
          echo &quot;$ACTION_DEPLOY_KEY&quot; &amp;gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan github.com &amp;gt;&amp;gt; ~/.ssh/known_hosts
          git config --global user.email &quot;liwei2633@163.com&quot;
          git config --global user.name &quot;monkeyWie&quot;
          npm install hexo-cli -g
          npm install
      - name: Hexo deploy
        run: |
          hexo clean
          hexo d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体的配置语法这里就不详细说明了，可以自行在官方文档中查阅。&lt;/p&gt;
&lt;p&gt;构建流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;监听&lt;code&gt;hexo&lt;/code&gt;分支的 push 操作&lt;/li&gt;
&lt;li&gt;运行一个 job，在&lt;code&gt;ubuntu&lt;/code&gt;虚拟机环境下&lt;/li&gt;
&lt;li&gt;使用官方提供的&lt;a href=&quot;https://github.com/actions/checkout&quot;&gt;actions/checkout@v1&lt;/a&gt;来拉取源码&lt;/li&gt;
&lt;li&gt;使用官方提供的&lt;a href=&quot;https://github.com/actions/setup-node&quot;&gt;actions/setup-node@v1&lt;/a&gt;来安装 node 环境&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;${{ secrets.ACTION_DEPLOY_KEY }}&lt;/code&gt;读取刚刚生成的私钥，并设置成环境变量，&lt;code&gt;${{ exp }}&lt;/code&gt;写法为 actions 内置的表达式语法，详细文档参考：&lt;a href=&quot;https://help.github.com/en/github/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions&quot;&gt;contexts-and-expression-syntax-for-github-actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;将私钥写入到&lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;文件中，并把&lt;code&gt;github.com&lt;/code&gt;域名加入到&lt;code&gt;~/.ssh/known_hosts&lt;/code&gt;文件中，以免第一次 ssh 访问时弹出交互式命令。&lt;/li&gt;
&lt;li&gt;配置 git 用户信息&lt;/li&gt;
&lt;li&gt;安装 hexo 命令行工具和项目的依赖&lt;/li&gt;
&lt;li&gt;调用 hexo 命令进行部署&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把&lt;code&gt;hexo&lt;/code&gt;分支代码推到 github 上触发 workflow ，通过&lt;code&gt;Actions&lt;/code&gt;选项卡进入就可以看到项目的构建情况了。
&lt;img src=&quot;hello-github-actions/2019-10-30-17-15-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此改造完成，以后只需要写完文章直接提交代码就可以自动部署了，甚至都可以不装 node 环境进行写作简直不要太方便。&lt;/p&gt;
&lt;h3&gt;自动创建项目 Release&lt;/h3&gt;
&lt;p&gt;有些项目在发布新版本时，一般都会创建一个&lt;code&gt;Github Release&lt;/code&gt;，并且把对应编译好之后的文件上传到&lt;code&gt;Release&lt;/code&gt;的资源列表中，例如：
&lt;img src=&quot;hello-github-actions/2019-10-30-17-44-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果这个使用手动操作的话，不仅步骤重复又繁琐(每次都要编译出各个操作系统对应的发行包再进行上传)，而且最蛋疼的是对于国内的网络环境来说，上传文件速度简直不能忍，好不容易上传了一大半搞不好就因为网络原因又要重新上传，相信用过的人都深有体会。&lt;/p&gt;
&lt;p&gt;我就在想如果能用&lt;code&gt;Github Actions&lt;/code&gt;来创建&lt;code&gt;Release&lt;/code&gt;,并且做对应的编译和上传，那上面的问题都可以迎刃而解了，于是在官方市场搜索了一下&lt;code&gt;Release&lt;/code&gt;关键字，果然已经有提供对应的&lt;code&gt;actions&lt;/code&gt;了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/actions/create-release&quot;&gt;create-release&lt;/a&gt;: 用于创建 release&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/actions/upload-release-asset&quot;&gt;upload-release-asset&lt;/a&gt;: 用于上传资源到对应的 release 中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接着创建一个&lt;code&gt;Github仓库&lt;/code&gt;，我测试的仓库地址是&lt;a href=&quot;https://github.com/monkeyWie/github-actions-demo&quot;&gt;https://github.com/monkeyWie/github-actions-demo&lt;/a&gt;，项目用 go 语言写的，代码非常简单就是两个 hello world 级别的代码，里面包含了普通的 go 程序和 cgo 程序。&lt;/p&gt;
&lt;p&gt;项目的构建流程是在项目&lt;code&gt;git push --tags&lt;/code&gt;的时候，触发 workflow，通过&lt;code&gt;Github Actions&lt;/code&gt;编译出来&lt;code&gt;Windows、Linux、macOS&lt;/code&gt;三个操作系统对应的 64 位可执行文件，再根据&lt;code&gt;tag name&lt;/code&gt;和&lt;code&gt;tag message&lt;/code&gt;来创建对应的&lt;code&gt;Github Release&lt;/code&gt;，并将编译好的文件上传。&lt;/p&gt;
&lt;p&gt;同样的创建一个&lt;code&gt;.github/workflows/main.yml&lt;/code&gt;文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI

on:
  push:
    # Sequence of patterns matched against refs/tags
    tags:
      - &quot;v*&quot; # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@v1
      - name: Use Golang
        uses: actions/setup-go@v1
        with:
          go-version: &quot;1.13.x&quot;
      - name: Build normal
        run: |
          CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o normal-windows-x64.exe cmd/normal/main.go
          CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o normal-linux-x64 cmd/normal/main.go
          CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o normal-darwin-x64 cmd/normal/main.go
          zip normal-windows-x64.zip normal-windows-x64.exe
          zip normal-linux-x64.zip normal-linux-x64
          zip normal-darwin-x64.zip normal-darwin-x64
      - name: Build cgo
        run: |
          go get github.com/monkeyWie/xgo
          ~/go/bin/xgo -targets=windows/amd64,linux/amd64,darwin/amd64 -ldflags=&quot;-w -s&quot; -pkg=cmd/cgo/main.go -out=cgo .
          mv cgo-windows-* cgo-windows-x64.exe
          mv cgo-linux-* cgo-linux-x64
          mv cgo-darwin-* cgo-darwin-x64
          zip cgo-windows-x64.zip cgo-windows-x64.exe
          zip cgo-linux-x64.zip cgo-linux-x64
          zip cgo-darwin-x64.zip cgo-darwin-x64
      - name: Create Release
        id: create_release
        uses: monkeyWie/create-release@master
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: Release ${{ github.ref }}
          draft: false
          prerelease: false

      - name: Upload Release normal windows
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./normal-windows-x64.zip
          asset_name: normal-${{ steps.create_release.outputs.tag }}-windows-x64.zip
          asset_content_type: application/zip
      - name: Upload Release normal linux
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./normal-linux-x64.zip
          asset_name: normal-${{ steps.create_release.outputs.tag }}-linux-x64.zip
          asset_content_type: application/zip
      - name: Upload Release normal darwin
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./normal-darwin-x64.zip
          asset_name: normal-${{ steps.create_release.outputs.tag }}-darwin-x64.zip
          asset_content_type: application/zip

      - name: Upload Release cgo windows
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./cgo-windows-x64.zip
          asset_name: cgo-${{ steps.create_release.outputs.tag }}-windows-x64.zip
          asset_content_type: application/zip
      - name: Upload Release cgo linux
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./cgo-linux-x64.zip
          asset_name: cgo-${{ steps.create_release.outputs.tag }}-linux-x64.zip
          asset_content_type: application/zip
      - name: Upload Release cgo darwin
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it&apos;s ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
          asset_path: ./cgo-darwin-x64.zip
          asset_name: cgo-${{ steps.create_release.outputs.tag }}-darwin-x64.zip
          asset_content_type: application/zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;监听 tag name 为&lt;code&gt;v&lt;/code&gt;开头的 push&lt;/li&gt;
&lt;li&gt;运行一个 job，在&lt;code&gt;ubuntu&lt;/code&gt;虚拟机环境下&lt;/li&gt;
&lt;li&gt;拉取源码，安装&lt;code&gt;golang 1.13.x&lt;/code&gt;环境&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;go build&lt;/code&gt;交叉编译出不同操作系统下 64 位可执行文件，并使用 zip 压缩&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;xgo&lt;/code&gt;交叉编译出不同操作系统下 64 位可执行文件，并使用 zip 压缩&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;monkeyWie/create-release@master&lt;/code&gt;创建 Release，其中会用到&lt;code&gt;${{ secrets.GITHUB_TOKEN }}&lt;/code&gt;，这是&lt;code&gt;Github Actions&lt;/code&gt;内置的一个&lt;a href=&quot;https://help.github.com/en/github/automating-your-workflow-with-github-actions/virtual-environments-for-github-actions#github_token-secret&quot;&gt;秘钥&lt;/a&gt;，用于授权访问你自己的 github 存储库，原理就是使用这个&lt;code&gt;TOKEN&lt;/code&gt;调用&lt;code&gt;Github API&lt;/code&gt;来进行创建 release，还有一个&lt;code&gt;${{ github.ref }}&lt;/code&gt;也是&lt;code&gt;Github Actions&lt;/code&gt;内置的一个&lt;a href=&quot;https://help.github.com/en/github/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context&quot;&gt;变量&lt;/a&gt;，然后通过 action 的&lt;code&gt;with&lt;/code&gt;进行参数传递。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;actions/upload-release-asset@v1.0.1&lt;/code&gt;上传文件，这里使用了两个表达式&lt;code&gt;${{ steps.create_release.outputs.upload_url }}&lt;/code&gt;和&lt;code&gt;${{ steps.create_release.outputs.tag }}&lt;/code&gt;，可以获取到指定&lt;code&gt;action&lt;/code&gt;的输出，第一个是获取创建好的 release 对应的上传地址，第二个是获取对应的 tag(例如：v1.0.0)，这样就可以在把上传的文件带上版本号。因为这个&lt;code&gt;action&lt;/code&gt;不支持多个文件上传，所以就写了多个 action 进行上传。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来在项目打个&lt;code&gt;tag&lt;/code&gt;，然后&lt;code&gt;push&lt;/code&gt;上去看看效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建tag名为v1.0.8，并添加描述
git tag -a &quot;v1.0.8&quot; -m &apos;发布v1.0.8版本
修复了以下bug:
1. xxxxx
2. xxxxx&apos;
# 把tag推到github上
git push --tags
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以看到已经有一个新的&lt;code&gt;workflow&lt;/code&gt;正在运行了：
&lt;img src=&quot;hello-github-actions/2019-10-31-13-36-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;运行完成后在&lt;code&gt;Releases&lt;/code&gt;页面查看结果：
&lt;img src=&quot;hello-github-actions/2019-10-31-13-40-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;完美！和预想的结果一致。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：由于官方的&lt;a href=&quot;https://github.com/actions/create-release&quot;&gt;create-release&lt;/a&gt;有点不能满足需求，于是我自己&lt;code&gt;fork&lt;/code&gt;了一份&lt;a href=&quot;https://github.com/monkeyWie/create-release&quot;&gt;create-release&lt;/a&gt;代码，就是把&lt;code&gt;tag name&lt;/code&gt;给输出来了，这里是相关的&lt;a href=&quot;https://github.com/actions/create-release/pull/10&quot;&gt;PR&lt;/a&gt;，还没被合并，所以上面的创建 Release 的 action 是用的我自己的仓库&lt;code&gt;monkeyWie/create-release@master&lt;/code&gt;，还有关于 go 交叉编译的知识，有兴趣可以看看我的这篇博客：&lt;a href=&quot;https://monkeywie.github.io/2019/10/10/go-cross-compile&quot;&gt;go-cross-compile&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;自动构建和部署 docker 镜像&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;Github Actions&lt;/code&gt;提供的虚拟机中，已经内置了&lt;code&gt;docker&lt;/code&gt;，而刚好我有一个项目因为国内的网络原因构建&lt;code&gt;docker镜像&lt;/code&gt;非常的慢，这是我&lt;code&gt;fork&lt;/code&gt;的一个用于 go 项目交叉编译的项目，仓库地址&lt;a href=&quot;https://github.com/monkeyWie/xgo&quot;&gt;https://github.com/monkeyWie/xgo&lt;/a&gt;，这个项目的主要工作原理就是通过 docker 里内置好各种&lt;code&gt;交叉编译&lt;/code&gt;的工具链，然后对外提供 go 项目交叉编译功能，下面节选一点&lt;code&gt;Dockerfile&lt;/code&gt;内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;hello-github-actions/2019-10-31-15-54-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看这大量的&lt;code&gt;apt-get install&lt;/code&gt;，就知道在本地构建有多慢了，下面就改用&lt;code&gt;Github Actions&lt;/code&gt;来帮忙构建和部署镜像。&lt;/p&gt;
&lt;p&gt;由于要将镜像推送到&lt;code&gt;docker hub&lt;/code&gt;官方镜像仓库上，需要验证账号信息，
这里我把自己的用户密码配置到了&lt;code&gt;Secrets&lt;/code&gt;中，以便在 workflow 配置文件中可以访问到：
&lt;img src=&quot;hello-github-actions/2019-10-31-16-00-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;编写构建文件&lt;code&gt;.github/workflows/main.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: CI

on:
  push:
    branches:
      - master
    paths:
      - &quot;docker/base/*&quot;

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@v1
      - name: Docker login
        run: docker login -u liwei2633 -p ${{ secrets.DOCKER_HUB_PWD }}
      - name: Docker build base
        run: |
          docker pull liwei2633/xgo:base
          docker build --cache-from=liwei2633/xgo:base -t liwei2633/xgo:base ./docker/base
          docker push liwei2633/xgo:base
      - name: Docker build other
        run: |
          docker build -t liwei2633/xgo:go-1.12.10 ./docker/go-1.12.10
          docker push liwei2633/xgo:go-1.12.10
          docker build -t liwei2633/xgo:go-1.12.x ./docker/go-1.12.x
          docker push liwei2633/xgo:go-1.12.x
          docker build -t liwei2633/xgo:go-1.13.1 ./docker/go-1.13.1
          docker push liwei2633/xgo:go-1.13.1
          docker build -t liwei2633/xgo:go-1.13.x ./docker/go-1.13.x
          docker push liwei2633/xgo:go-1.13.x
          docker build -t liwei2633/xgo:go-latest ./docker/go-latest
          docker push liwei2633/xgo:go-latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;监听 master 分支的 push 操作，并且&lt;code&gt;docker/base&lt;/code&gt;目录下文件有修改才进行构建，这样做的目的是在其它与 docker 构建无关的文件改动了不会去触发 workflow&lt;/li&gt;
&lt;li&gt;运行一个 job，在&lt;code&gt;ubuntu&lt;/code&gt;虚拟机环境下&lt;/li&gt;
&lt;li&gt;拉取源码&lt;/li&gt;
&lt;li&gt;登录 docker hub，通过之前配置的&lt;code&gt;${{ secrets.DOCKER_HUB_PWD }}&lt;/code&gt;，这里不用担心控制台输出会暴露密码，通过&lt;code&gt;secrets&lt;/code&gt;访问的变量在控制台输出时都会打上马赛克
&lt;img src=&quot;hello-github-actions/2019-10-31-16-47-31.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;构建镜像，这里使用了一个小技巧&lt;code&gt;--cache-from=liwei2633/xgo:base&lt;/code&gt;，预先下载好之前的镜像&lt;code&gt;liwei2633/xgo:base&lt;/code&gt;，然后可以使用&lt;code&gt;docker&lt;/code&gt;的缓存机制加快构建速度
&lt;img src=&quot;hello-github-actions/2019-10-31-16-50-06.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;推送镜像并编译和推送不同 go 版本的镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样通过&lt;code&gt;Github Actions&lt;/code&gt;就把构建镜像和部署时间的缩小了到了&lt;code&gt;13分钟&lt;/code&gt;：
&lt;img src=&quot;hello-github-actions/2019-10-31-16-54-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然还是挺慢的但是跟本地构建比起来快了不是一个量级，有次本地构建等了一个多小时，因为网络原因导致一个软件源安装失败直接没了又要重头开始构建，所以高下立判，&lt;code&gt;Github Actions&lt;/code&gt;真香！！&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;通过上面三个实例项目，可以看得出&lt;code&gt;Github Actions&lt;/code&gt;为我们节省大量的时间和重复的操作，且通过官方的 actions 市场很方便的就可以实现大部分编排功能，真是一个可以吹爆的良心产品，所以赶紧一起来尝鲜啊。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>docker远程连接</title><link>https://monkeywie.cn/posts/docker-connect-remote</link><guid isPermaLink="true">https://monkeywie.cn/posts/docker-connect-remote</guid><pubDate>Fri, 15 Nov 2019 16:19:32 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;docker 其实是一个 C/S 程序，执行&lt;code&gt;docker&lt;/code&gt;命令行其实就是在与&lt;code&gt;docker daemon&lt;/code&gt;服务进行通讯，这里主要是记录下&lt;code&gt;linux&lt;/code&gt;下的 &lt;code&gt;docker&lt;/code&gt; 如何配置可以被远程访问。&lt;/p&gt;
&lt;h2&gt;服务端配置&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;linux&lt;/code&gt;上 docker 默认是使用&lt;code&gt;unix socket&lt;/code&gt;进行通讯的，如果要远程访问是不支持的，对此需要开启 &lt;code&gt;tcp协议&lt;/code&gt;，以支持外部访问。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;开启 tcp 协议&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;修改&lt;code&gt;/lib/systemd/system/docker.service&lt;/code&gt;文件&lt;pre&gt;&lt;code&gt;vi /lib/systemd/system/docker.service
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;找到&lt;code&gt;ExecStart=/usr/bin/dockerd&lt;/code&gt;这一行，添加命令行参数&lt;code&gt;-H tcp://0.0.0.0:3272&lt;/code&gt;&lt;pre&gt;&lt;code&gt;[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:3272 --containerd=/run/containerd/containerd.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;重启 docker&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样服务端就已经配置好了。&lt;/p&gt;
&lt;h2&gt;客户端配置&lt;/h2&gt;
&lt;p&gt;首先需要下载一个 docker 客户端，这是一个非常小的可执行文件，不需要为了一个客户端安装整个 docker 应用。&lt;/p&gt;
&lt;h3&gt;docker 客户端下载&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;linux
&lt;ul&gt;
&lt;li&gt;通过&lt;a href=&quot;https://download.docker.com/linux/static/&quot;&gt;https://download.docker.com/linux/static/&lt;/a&gt;下载&lt;/li&gt;
&lt;li&gt;(Ubuntu/Debian): &lt;code&gt;apt-get install docker-ce-cli&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(Centos): &lt;code&gt;yum install docker-ce-cli&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;windows
&lt;ul&gt;
&lt;li&gt;通过&lt;a href=&quot;https://download.docker.com/win/static/&quot;&gt;https://download.docker.com/win/static/&lt;/a&gt;下载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mac
&lt;ul&gt;
&lt;li&gt;通过&lt;a href=&quot;https://download.docker.com/mac/static/&quot;&gt;https://download.docker.com/mac/static/&lt;/a&gt;下载&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下载完之后将&lt;code&gt;docker&lt;/code&gt;文件加入到&lt;code&gt;PATH&lt;/code&gt;中,即可在终端使用了。&lt;/p&gt;
&lt;h3&gt;远程访问&lt;/h3&gt;
&lt;p&gt;可以直接使用命令行参数来指定远程 docker 进行访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker -H 192.168.1.1:3272 ps
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不想每次都输入命令行参数，可以配置&lt;code&gt;DOCKER_HOST&lt;/code&gt;环境变量，这样每次运行&lt;code&gt;docker&lt;/code&gt;命令时，都会自动设定好对应的远程地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export DOCKER_HOST=&quot;tcp://192.168.1.1:3272&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://gist.github.com/kekru/4e6d49b4290a4eebc7b597c07eaf61f2&quot;&gt;Docker connect to remote server.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>OpenSSL创建带SAN扩展的证书并进行CA自签</title><link>https://monkeywie.cn/posts/create-ssl-cert-with-san</link><guid isPermaLink="true">https://monkeywie.cn/posts/create-ssl-cert-with-san</guid><pubDate>Fri, 15 Nov 2019 16:52:50 GMT</pubDate><content:encoded>&lt;h2&gt;什么是 SAN&lt;/h2&gt;
&lt;p&gt;SAN(Subject Alternative Name) 是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书，可以扩展此证书支持的域名，使得一个证书可以支持多个不同域名的解析。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;来看看百度的证书，百度证书的扩展域名有这么多，其中还有了*.hao123.com，那我们再看看 www.hao123.com 的证书
&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-54-49.png&quot; alt=&quot;&quot; /&gt;
发现的确是用的前面的百度证书&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-55-08.png&quot; alt=&quot;&quot; /&gt;
所以 SAN 带来的好处就可以看出来了，一个证书可以用在各种不同的域名下，不需要一个域名买一个证书了。&lt;/p&gt;
&lt;h2&gt;利用 OpenSSL 创建证书&lt;/h2&gt;
&lt;p&gt;因为是本地环境，直接用 OpenSSL 给自己颁发一个 CA 根证书用于后面给服务器做 CA 签署。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;生成 CA 密钥&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -des3 -out ca.key 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生成 CA 根证书&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl req -sha256 -new -x509 -days 365 -key ca.key -out ca.crt \
    -subj &quot;/C=CN/ST=GD/L=SZ/O=lee/OU=study/CN=testRoot&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生成服务器密钥&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl genrsa -des3 -out server.key 2048
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;生成服务器证书请求文件&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl req -new \
    -sha256 \
    -key server.key \
    -subj &quot;/C=CN/ST=GD/L=SZ/O=lee/OU=study/CN=bdstatic.com&quot; \
    -reqexts SAN \
    -config &amp;lt;(cat /etc/pki/tls/openssl.cnf \
        &amp;lt;(printf &quot;[SAN]\nsubjectAltName=DNS:*.bdstatic.com,DNS:*.baidu.com&quot;)) \
    -out server.csr
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;CA 签署服务器证书&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl ca -in server.csr \
    -md sha256 \
    -keyfile ca.key \
    -cert ca.crt \
    -extensions SAN \
    -config &amp;lt;(cat /etc/pki/tls/openssl.cnf \
        &amp;lt;(printf &quot;[SAN]\nsubjectAltName=DNS:*.bdstatic.com,DNS:*.baidu.com&quot;)) \
    -out server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后把生成好的服务器证书和服务器密钥在服务器(ngnix,tomcat)里配置好，并且把 ca.crt 证书导入到浏览器的受信任的根证书颁发机构里，在浏览器访问就不会有红叉叉了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-55-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-55-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;-subj &quot;/C=CN/ST=GD/L=SZ/O=lee/OU=study/CN=testRoot&quot;这行可以不要，会有命令交互填写相关信息。
&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-58-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;哈希算法不要使用 sha1,因为 Chrome 浏览器下会提示不安全，上面都是用的 sha256。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;/etc/pki/tls/openssl.cnf 文件是缺省的 OpenSSL 配置文件，可能环境不同路径也不同。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务器证书请求文件的国家，省，市要和 CA 证书一致,这个在 openssl.cnf 默认配置中指定了，可以修改。
&lt;img src=&quot;create-ssl-cert-with-san/2019-11-15-16-56-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关于私钥的加密格式，因为笔者是在 netty 里使用的 ssl 协议，而 netty 仅支持 PKCS8 格式的私钥见&lt;a href=&quot;http://netty.io/wiki/sslcontextbuilder-and-private-key.html&quot;&gt;http://netty.io/wiki/sslcontextbuilder-and-private-key.html&lt;/a&gt;，需要对密钥格式进行转换&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SslContext serverSslCtx = SslContextBuilder.forServer(new File(&quot;E:/server.crt&quot;),new File(&quot;E:/server.pem&quot;)).build();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;常见错误&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;遇到&lt;code&gt;unable to open &apos;/etc/pki/CA/index.txt&apos;&lt;/code&gt;解决办法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /etc/pki/CA/index.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遇到&lt;code&gt;error while loading serial number&lt;/code&gt;解决办法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /etc/pki/CA/serial
echo 00 &amp;gt; /etc/pki/CA/serial
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;遇到&lt;code&gt;failed to update database TXT_DB error number 2&lt;/code&gt;解决办法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -rf /etc/pki/CA/index.txt
touch /etc/pki/CA/index.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://liaoph.com/openssl-san/&quot;&gt;OpenSSL SAN 证书&lt;/a&gt;
&lt;a href=&quot;https://zhuanlan.zhihu.com/p/26646377&quot;&gt;使用 OpenSSL 生成多域名自签名证书进行 HTTPS 开发调试&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>minikube安装</title><link>https://monkeywie.cn/posts/minikube-install</link><guid isPermaLink="true">https://monkeywie.cn/posts/minikube-install</guid><pubDate>Fri, 15 Nov 2019 17:00:51 GMT</pubDate><content:encoded>&lt;h2&gt;minikube 介绍&lt;/h2&gt;
&lt;p&gt;minikube 是 k8s 官方维护的一个单机版的 k8s，通过 minikube 可以很方便的在本地机器上安装一套 k8s 环境用于日常的学习与开发。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;环境: 虚拟机中的&lt;code&gt;centos:7&lt;/code&gt;操作系统，其它情况可以参考&lt;a href=&quot;https://minikube.sigs.k8s.io/docs/start/&quot;&gt;官方文档&lt;/a&gt;，主要步骤都是类似的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;kubectl 安装&lt;/h3&gt;
&lt;p&gt;首先需要先安装 kubectl，用于后续访问 minikube 的 k8s 集群。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#配置源
cat &amp;lt;&amp;lt;EOF &amp;gt; /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
#baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
#使用阿里镜像
baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
#gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
#使用阿里镜像
gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
#安装kubectl
yum install -y kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;docker 配置国内镜像&lt;/h3&gt;
&lt;p&gt;修改&lt;code&gt;daemon.json&lt;/code&gt;文件，不存在可以创建一个&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/docker/daemon.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;registry-mirrors&quot;: [
    &quot;https://dockerhub.azk8s.cn&quot;,
    &quot;https://reg-mirror.qiniu.com&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启 docker&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;minikube 安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
  &amp;amp;&amp;amp; chmod +x minikube
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于墙的原因，可以去&lt;a href=&quot;https://github.com/kubernetes/minikube/releases&quot;&gt;&lt;code&gt;Github Releases&lt;/code&gt;&lt;/a&gt;页面下载：
&lt;img src=&quot;minikube-install/2019-11-15-17-32-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下载完之后直接通过命令行启动:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;minikube start --vm-driver=none \
 --image-mirror-country=cn \
 --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：--vm-driver=none，是因为本身系统就是在虚拟机中运行的，所以不需要指定虚拟驱动，其它两个选项可以避免国内网络原因导致下载镜像失败。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;成功执行之后就可以用&lt;code&gt;kubectl&lt;/code&gt;进行访问了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>用vscode + markdown制作简易幻灯片(PPT)</title><link>https://monkeywie.cn/posts/vscode-markdown-ppt</link><guid isPermaLink="true">https://monkeywie.cn/posts/vscode-markdown-ppt</guid><pubDate>Wed, 27 Nov 2019 12:07:03 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在公司里经常做技术分享，如果用 PPT 来做的话非常耗费时间，所以一般都是直接用 markdwon 写，但是 markdwon 有个缺点是不能像 PPT 那样翻页展示，于是就 google 了一番看看能不能把 markdown 做成像 PPT 那样的效果，果然已经有这样的技术了，下面就记录下具体步骤。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;vscode 安装插件&lt;/h2&gt;
&lt;p&gt;首先在 vscode 中安装&lt;code&gt;Markdown Preview Enhanced&lt;/code&gt;插件，这个插件可以在 vscode 中实时预览 markdown 生成的页面。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vscode-markdown-ppt/2019-11-27-12-19-35.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;并且还附带了其它非常多的功能，比如目前所需的&lt;code&gt;幻灯片&lt;/code&gt;功能，相关文档在这：&lt;a href=&quot;https://shd101wyy.github.io/markdown-preview-enhanced/#/zh-cn/presentation&quot;&gt;https://shd101wyy.github.io/markdown-preview-enhanced/#/zh-cn/presentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;语法介绍&lt;/h2&gt;
&lt;p&gt;在 markdown 中只需要使用&lt;code&gt;&amp;lt;!-- slide --&amp;gt;&lt;/code&gt;代码来标记内容为幻灯片，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- slide --&amp;gt;

## 第一页

内容一

1. 测试 1111111111111
2. 测试 2222222222222

&amp;lt;!-- slide --&amp;gt;

## 第二页

内容二

- 测试 1111111111111
- 测试 2222222222222
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果预览：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vscode-markdown-ppt/2019-11-27-13-43-10.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;样式调整&lt;/h2&gt;
&lt;p&gt;默认内容的 div 大小为：&lt;code&gt;width:960px;height:700px&lt;/code&gt;，在全屏预览的时候效果不好，可以通过&lt;code&gt;front-matter&lt;/code&gt;语法来设置对应的分辨率：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
presentation:
  width: 1920
  height: 1080
---

&amp;lt;!-- slide --&amp;gt;

## 第一页

内容一

1. 测试 1111111111111
2. 测试 2222222222222

&amp;lt;!-- slide --&amp;gt;

## 第二页

内容二

- 测试 1111111111111
- 测试 2222222222222
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果预览：
&lt;img src=&quot;vscode-markdown-ppt/2019-11-27-14-00-25.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;默认内容是居中的，通过自定义&lt;code&gt;css style&lt;/code&gt;可以把内容调整为左对齐和滚动条支持：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
presentation:
  width: 1920
  height: 1080
---

&amp;lt;style type=&quot;text/css&quot;&amp;gt;
  .reveal .slides {
      margin: auto 20px;
      text-align: initial;
  }
  section.slide{
    height: 100%;
    overflow-y: auto !important
  }
&amp;lt;/style&amp;gt;

&amp;lt;!-- slide --&amp;gt;

## 第一页

内容一

1. 测试 1111111111111
2. 测试 2222222222222

&amp;lt;!-- slide --&amp;gt;

## 第二页

内容二

- 测试 1111111111111
- 测试 2222222222222
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果预览：
&lt;img src=&quot;vscode-markdown-ppt/2019-11-27-14-02-38.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;演示&lt;/h2&gt;
&lt;p&gt;最后通过&lt;code&gt;Markdown Preview Enhanced&lt;/code&gt;插件的浏览器预览功能，通过浏览器打开，并进入全屏(F11)即可得到与 PPT 一致的体验了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tips:在浏览器中可以使用&amp;lt;kbd&amp;gt;←&amp;lt;/kbd&amp;gt;&amp;lt;kbd&amp;gt;→&amp;lt;/kbd&amp;gt;来控制 PPT 的翻页，按&amp;lt;kbd&amp;gt;ESC&amp;lt;/kbd&amp;gt;可以预览所有页面的缩略图。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;效果演示：
&lt;img src=&quot;vscode-markdown-ppt/2019-11-27-14-11-29.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>docker镜像加速器</title><link>https://monkeywie.cn/posts/docker-registry-mirror</link><guid isPermaLink="true">https://monkeywie.cn/posts/docker-registry-mirror</guid><pubDate>Fri, 06 Dec 2019 09:03:26 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;国内从 docker hub 拉取镜像时速度非常慢，这里记录下国内的一些免费加速镜像服务器。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;docker bub&lt;/h2&gt;
&lt;p&gt;docker 官方仓库加速镜像配置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;registry-mirrors&quot;: [
    &quot;https://dockerproxy.com&quot;,
    &quot;https://docker.m.daocloud.io&quot;,
    &quot;https://docker.nju.edu.cn&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改后重启 docker 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl daemon-reload
systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;注：也可以使用阿里云的 docker &lt;a href=&quot;https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors&quot;&gt;镜像服务&lt;/a&gt;，但是需要注册账号开启容器服务之后才有&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;s&gt;gcr.io(以下方法已失效)&lt;/s&gt;&lt;/h2&gt;
&lt;p&gt;google 仓库加速镜像，需要手动将前缀改一下，替换为&lt;code&gt;gcr.azk8s.cn/google_containers/&amp;lt;image-name&amp;gt;:&amp;lt;version&amp;gt;&lt;/code&gt; ,例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#docker pull k8s.gcr.io/k8s-dns-node-cache:1.15.7
# 通过镜像仓库拉取
docker pull gcr.azk8s.cn/google_containers/k8s-dns-node-cache:1.15.7
# 重新打tag
docker tag gcr.azk8s.cn/google_containers/k8s-dns-node-cache:1.15.7 k8s.gcr.io/k8s-dns-node-cache:1.15.7
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#docker pull gcr.io/kubernetes-e2e-test-images/echoserver:2.2
docker pull gcr.azk8s.cn/kubernetes-e2e-test-images/echoserver:2.2
docker tag gcr.azk8s.cn/kubernetes-e2e-test-images/echoserver:2.2 gcr.io/kubernetes-e2e-test-images/echoserver:2.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;s&gt;quay.io(以下方法已失效)&lt;/s&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#docker pull quay.io/deis/go-dev:v1.10.0
docker pull quay.azk8s.cn/deis/go-dev:v1.10.0
docker tag quay.azk8s.cn/deis/go-dev:v1.10.0 quay.io/deis/go-dev:v1.10.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Azure/container-service-for-azure-china/blob/master/aks/README.md&quot;&gt;https://github.com/Azure/container-service-for-azure-china/blob/master/aks/README.md&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>k8s域名解析超时问题记录</title><link>https://monkeywie.cn/posts/k8s-dns-lookup-timeout</link><guid isPermaLink="true">https://monkeywie.cn/posts/k8s-dns-lookup-timeout</guid><pubDate>Tue, 10 Dec 2019 11:42:26 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;近期线上 k8s 时不时就会出现一些内部服务间的调用超时问题，通过日志可以得知超时的原因都是出现在&lt;code&gt;域名解析&lt;/code&gt;上，并且都是 k8s 内部的域名解析超时，于是直接先将内部域名替换成 k8s service 的 IP，观察一段时间发现没有超时的情况发生了，但是由于使用 service IP 不是长久之计，所以还要去找解决办法。&lt;/p&gt;
&lt;h2&gt;复现&lt;/h2&gt;
&lt;p&gt;一开始运维同事在调用方 pod 中使用&lt;code&gt;ab&lt;/code&gt;工具对目标服务进行了多次压测，并没有发现有超时的请求，我介入之后分析&lt;code&gt;ab&lt;/code&gt;这类 http 压测工具应该都会有 dns 缓存，而我们主要是要测试 dns 服务的性能，于是直接动手撸了一个压测工具只做域名解析，代码如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;context&quot;
	&quot;flag&quot;
	&quot;fmt&quot;
	&quot;net&quot;
	&quot;sync/atomic&quot;
	&quot;time&quot;
)

var host string
var connections int
var duration int64
var limit int64
var timeoutCount int64

func main() {
	// os.Args = append(os.Args, &quot;-host&quot;, &quot;www.baidu.com&quot;, &quot;-c&quot;, &quot;200&quot;, &quot;-d&quot;, &quot;30&quot;, &quot;-l&quot;, &quot;5000&quot;)

	flag.StringVar(&amp;amp;host, &quot;host&quot;, &quot;&quot;, &quot;Resolve host&quot;)
	flag.IntVar(&amp;amp;connections, &quot;c&quot;, 100, &quot;Connections&quot;)
	flag.Int64Var(&amp;amp;duration, &quot;d&quot;, 0, &quot;Duration(s)&quot;)
	flag.Int64Var(&amp;amp;limit, &quot;l&quot;, 0, &quot;Limit(ms)&quot;)
	flag.Parse()

	var count int64 = 0
	var errCount int64 = 0
	pool := make(chan interface{}, connections)
	exit := make(chan bool)
	var (
		min int64 = 0
		max int64 = 0
		sum int64 = 0
	)

	go func() {
		time.Sleep(time.Second * time.Duration(duration))
		exit &amp;lt;- true
	}()
endD:
	for {
		select {
		case pool &amp;lt;- nil:
			go func() {
				defer func() {
					&amp;lt;-pool
				}()
				resolver := &amp;amp;net.Resolver{}
				now := time.Now()
				_, err := resolver.LookupIPAddr(context.Background(), host)
				use := time.Since(now).Nanoseconds() / int64(time.Millisecond)
				if min == 0 || use &amp;lt; min {
					min = use
				}
				if use &amp;gt; max {
					max = use
				}
				sum += use
				if limit &amp;gt; 0 &amp;amp;&amp;amp; use &amp;gt;= limit {
					timeoutCount++
				}
				atomic.AddInt64(&amp;amp;count, 1)
				if err != nil {
					fmt.Println(err.Error())
					atomic.AddInt64(&amp;amp;errCount, 1)
				}
			}()
		case &amp;lt;-exit:
			break endD
		}
	}

	fmt.Printf(&quot;request count：%d\nerror count：%d\n&quot;, count, errCount)
	fmt.Printf(&quot;request time：min(%dms) max(%dms) avg(%dms) timeout(%dn)\n&quot;, min, max, sum/count, timeoutCount)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译好二进制程序直接丢到对应的 pod 容器中进行压测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 200个并发,持续30秒
./dns -host {service}.{namespace} -c 200 -d 30
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次可以发现最大耗时有&lt;code&gt;5s&lt;/code&gt;多，多次测试结果都是类似：
&lt;img src=&quot;k8s-dns-lookup-timeout/2019-12-11-11-19-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而我们内部服务间 HTTP 调用的超时一般都是设置在&lt;code&gt;3s&lt;/code&gt;左右，以此推断出与线上的超时情况应该是同一种情况，在并发高的情况下会出现部分域名解析超时而导致 HTTP 请求失败。&lt;/p&gt;
&lt;h2&gt;原因&lt;/h2&gt;
&lt;p&gt;起初一直以为是&lt;code&gt;coredns&lt;/code&gt;的问题，于是找运维升级了下&lt;code&gt;coredns&lt;/code&gt;版本再进行压测，发现问题还是存在，说明不是版本的问题，难道是&lt;code&gt;coredns&lt;/code&gt;本身的性能就差导致的？想想也不太可能啊，才 200 的并发就顶不住了那性能也未免太弱了吧，结合之前的压测数据，平均响应都挺正常的(82ms)，但是就有个别请求会延迟，而且都是 5 秒左右，所以就又带着&lt;code&gt;k8s dns 5s&lt;/code&gt;的关键字去 google 搜了一下，这不搜不知道一搜吓一跳啊，原来是 k8s 里的一个大坑啊(其实和 k8s 没有太大的关系，只是 k8s 层面没有提供解决方案)。&lt;/p&gt;
&lt;h3&gt;5s 超时原因&lt;/h3&gt;
&lt;p&gt;linux 中&lt;code&gt;glibc&lt;/code&gt;的 resolver 的缺省超时时间是 5s，而导致超时的原因是内核&lt;code&gt;conntrack&lt;/code&gt;模块的 bug。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Weave works 的工程师 Martynas Pumputis 对这个问题做了很详细的分析：https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里再引用下&lt;a href=&quot;https://imroc.io/posts/kubernetes/troubleshooting-with-kubernetes-network/&quot;&gt;https://imroc.io/posts/kubernetes/troubleshooting-with-kubernetes-network/&lt;/a&gt;文章中的解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录，跟 DNS Server 通信自然会先 connect (建立 fd)，后面请求报文使用这个 fd 来发送，由于 UDP 是无状态协议， connect 时并不会发包，也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包，send 时各自发的包它们源 Port 相同(因为用的同一个 socket 发送)，当并发发包时，两个包都还没有被插入 conntrack 表项，所以 netfilter 会为它们分别创建 conntrack 表项，而集群内请求 kube-dns 或 coredns 都是访问的 CLUSTER-IP，报文最终会被 DNAT 成一个 endpoint 的 POD IP，当两个包恰好又被 DNAT 成同一个 POD IP 时，它们的五元组就相同了，在最终插入的时候后面那个包就会被丢掉，如果 dns 的 pod 副本只有一个实例的情况就很容易发生(始终被 DNAT 成同一个 POD IP)，现象就是 dns 请求超时，client 默认策略是等待 5s 自动重试，如果重试成功，我们看到的现象就是 dns 请求有 5s 的延时。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;h3&gt;方案（一）：使用 TCP 协议发送 DNS 请求&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;resolv.conf&lt;/code&gt;的&lt;code&gt;use-vc&lt;/code&gt;选项来开启 TCP 协议&lt;/p&gt;
&lt;h4&gt;测试&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;/etc/resolv.conf&lt;/code&gt;文件，在最后加入一行文本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options use-vc
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行压测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 200个并发,持续30秒,记录超过5s的请求个数
./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：
&lt;img src=&quot;k8s-dns-lookup-timeout/2019-12-11-11-26-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;确实没有出现&lt;code&gt;5s&lt;/code&gt;的超时问题了，但是部分请求耗时还是比较高，在&lt;code&gt;4s&lt;/code&gt;左右，而且平均耗时比 UPD 协议的还高，效果并不好。&lt;/p&gt;
&lt;h3&gt;方案（二）：避免相同五元组 DNS 请求的并发&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;resolv.conf&lt;/code&gt;的&lt;code&gt;single-request-reopen&lt;/code&gt;和&lt;code&gt;single-request&lt;/code&gt;选项来避免：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;single-request-reopen (glibc&amp;gt;=2.9)
发送 A 类型请求和 AAAA 类型请求使用不同的源端口。这样两个请求在 conntrack 表中不占用同一个表项，从而避免冲突。&lt;/li&gt;
&lt;li&gt;single-request (glibc&amp;gt;=2.10)
避免并发，改为串行发送 A 类型和 AAAA 类型请求，没有了并发，从而也避免了冲突。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;测试 single-request-reopen&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;/etc/resolv.conf&lt;/code&gt;文件，在最后加入一行文本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options single-request-reopen
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行压测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 200个并发,持续30秒,记录超过5s的请求个数
./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：
&lt;img src=&quot;k8s-dns-lookup-timeout/2019-12-11-11-33-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;测试 single-request&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;/etc/resolv.conf&lt;/code&gt;文件，在最后加入一行文本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options single-request
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行压测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 200个并发,持续30秒,记录超过5s的请求个数
./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：
&lt;img src=&quot;k8s-dns-lookup-timeout/2019-12-11-11-04-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;通过压测结果可以看到&lt;code&gt;single-request-reopen&lt;/code&gt;和&lt;code&gt;single-request&lt;/code&gt;选项确实可以显著的降低域名解析耗时。&lt;/p&gt;
&lt;h3&gt;关于方案（一）和方案（二）的实施步骤和缺点&lt;/h3&gt;
&lt;h4&gt;实施步骤&lt;/h4&gt;
&lt;p&gt;其实就是要给容器的&lt;code&gt;/etc/resolv.conf&lt;/code&gt;文件添加选项，目前有两个方案比较合适：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过修改 pod 的 postStart hook 来设置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;lifecycle:
  postStart:
    exec:
      command:
        - /bin/sh
        - -c
        - &quot;/bin/echo &apos;options single-request-reopen&apos; &amp;gt;&amp;gt; /etc/resolv.conf&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过修改 pod 的 template.spec.dnsConfig 来设置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;template:
  spec:
    dnsConfig:
      options:
        - name: single-request-reopen
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注&lt;/code&gt;: 需要 k8s 版本&amp;gt;=1.9&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;缺点&lt;/h4&gt;
&lt;p&gt;不支持&lt;code&gt;alpine&lt;/code&gt;基础镜像的容器，因为&lt;code&gt;apline&lt;/code&gt;底层使用的&lt;code&gt;musl libc&lt;/code&gt;库并不支持这些 resolv.conf 选项，所以如果使用&lt;code&gt;alpine&lt;/code&gt;基础镜像构建的应用，还是无法规避超时的问题。&lt;/p&gt;
&lt;h3&gt;方案（三）：本地 DNS 缓存&lt;/h3&gt;
&lt;p&gt;其实 k8s 官方也意识到了这个问题比较常见，给出了 coredns 以 cache 模式作为 daemonset 部署的解决方案: &lt;a href=&quot;https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns/nodelocaldns&quot;&gt;https://github.com/kubernetes/kubernetes/tree/master/cluster/addons/dns/nodelocaldns&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;大概原理就是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本地 DNS 缓存以 DaemonSet 方式在每个节点部署一个使用 hostNetwork 的 Pod，创建一个网卡绑上本地 DNS 的 IP，本机的 Pod 的 DNS 请求路由到本地 DNS，然后取缓存或者继续使用 TCP 请求上游集群 DNS 解析 (由于使用 TCP，同一个 socket 只会做一遍三次握手，不存在并发创建 conntrack 表项，也就不会有 conntrack 冲突)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;部署&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;获取当前&lt;code&gt;kube-dns service&lt;/code&gt;的 clusterIP&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# kubectl -n kube-system get svc kube-dns -o jsonpath=&quot;{.spec.clusterIP}&quot;
10.96.0.10
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;下载官方提供的 yaml 模板进行关键字替换&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;wget -O nodelocaldns.yaml &quot;https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml&quot; &amp;amp;&amp;amp; \
sed -i &apos;s/__PILLAR__DNS__SERVER__/10.96.0.10/g&apos; nodelocaldns.yaml &amp;amp;&amp;amp; \
sed -i &apos;s/__PILLAR__LOCAL__DNS__/169.254.20.10/g&apos; nodelocaldns.yaml &amp;amp;&amp;amp; \
sed -i &apos;s/__PILLAR__DNS__DOMAIN__/cluster.local/g&apos; nodelocaldns.yaml &amp;amp;&amp;amp; \
sed -i &apos;s/__PILLAR__CLUSTER__DNS__/10.96.0.10/g&apos; nodelocaldns.yaml &amp;amp;&amp;amp; \
sed -i &apos;s/__PILLAR__UPSTREAM__SERVERS__/\/etc\/resolv.conf/g&apos; nodelocaldns.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;最终 yaml 文件如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

apiVersion: v1
kind: ServiceAccount
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    kubernetes.io/cluster-service: &quot;true&quot;
    addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
  name: kube-dns-upstream
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: &quot;true&quot;
    addonmanager.kubernetes.io/mode: Reconcile
    kubernetes.io/name: &quot;KubeDNSUpstream&quot;
spec:
  ports:
    - name: dns
      port: 53
      protocol: UDP
      targetPort: 53
    - name: dns-tcp
      port: 53
      protocol: TCP
      targetPort: 53
  selector:
    k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
  Corefile: |
    cluster.local:53 {
        errors
        cache {
                success 9984 30
                denial 9984 5
        }
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        health 169.254.20.10:8080
        }
    in-addr.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    ip6.arpa:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . 10.96.0.10 {
                force_tcp
        }
        prometheus :9253
        }
    .:53 {
        errors
        cache 30
        reload
        loop
        bind 169.254.20.10 10.96.0.10
        forward . /etc/resolv.conf {
                force_tcp
        }
        prometheus :9253
        }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-local-dns
  namespace: kube-system
  labels:
    k8s-app: node-local-dns
    kubernetes.io/cluster-service: &quot;true&quot;
    addonmanager.kubernetes.io/mode: Reconcile
spec:
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 10%
  selector:
    matchLabels:
      k8s-app: node-local-dns
  template:
    metadata:
      labels:
        k8s-app: node-local-dns
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: node-local-dns
      hostNetwork: true
      dnsPolicy: Default # Don&apos;t use cluster DNS.
      tolerations:
        - key: &quot;CriticalAddonsOnly&quot;
          operator: &quot;Exists&quot;
      containers:
        - name: node-cache
          image: k8s.gcr.io/k8s-dns-node-cache:1.15.7
          resources:
            requests:
              cpu: 25m
              memory: 5Mi
          args:
            [
              &quot;-localip&quot;,
              &quot;169.254.20.10,10.96.0.10&quot;,
              &quot;-conf&quot;,
              &quot;/etc/Corefile&quot;,
              &quot;-upstreamsvc&quot;,
              &quot;kube-dns-upstream&quot;,
            ]
          securityContext:
            privileged: true
          ports:
            - containerPort: 53
              name: dns
              protocol: UDP
            - containerPort: 53
              name: dns-tcp
              protocol: TCP
            - containerPort: 9253
              name: metrics
              protocol: TCP
          livenessProbe:
            httpGet:
              host: 169.254.20.10
              path: /health
              port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
          volumeMounts:
            - mountPath: /run/xtables.lock
              name: xtables-lock
              readOnly: false
            - name: config-volume
              mountPath: /etc/coredns
            - name: kube-dns-config
              mountPath: /etc/kube-dns
      volumes:
        - name: xtables-lock
          hostPath:
            path: /run/xtables.lock
            type: FileOrCreate
        - name: kube-dns-config
          configMap:
            name: kube-dns
            optional: true
        - name: config-volume
          configMap:
            name: node-local-dns
            items:
              - key: Corefile
                path: Corefile.base
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 yaml 可以看到几个细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;部署类型是使用的&lt;code&gt;DaemonSet&lt;/code&gt;，即在每个 k8s node 节点上运行一个 dns 服务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostNetwork&lt;/code&gt;属性为&lt;code&gt;true&lt;/code&gt;，即直接使用 node 物理机的网卡进行端口绑定，这样在此 node 节点中的 pod 可以直接访问 dns 服务，不通过 service 进行转发，也就不会有 DNAT&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dnsPolicy&lt;/code&gt;属性为&lt;code&gt;Default&lt;/code&gt;，不使用 cluster DNS，在解析外网域名时直接使用本地的 DNS 设置&lt;/li&gt;
&lt;li&gt;绑定在 node 节点&lt;code&gt;169.254.20.10&lt;/code&gt;和&lt;code&gt;10.96.0.10&lt;/code&gt;IP 上，这样节点下面的 pod 只需要将 dns 设置为&lt;code&gt;169.254.20.10&lt;/code&gt;即可直接访问宿主机上的 dns 服务。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;测试&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;/etc/resolv.conf&lt;/code&gt;文件中的 nameserver：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nameserver 169.254.20.10
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进行压测：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 200个并发,持续30秒,记录超过5s的请求个数
./dns -host {service}.{namespace} -c 200 -d 30 -l 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：
&lt;img src=&quot;k8s-dns-lookup-timeout/2019-12-11-17-13-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;结论&lt;/h4&gt;
&lt;p&gt;通过压测发现并没有解决超时的问题，按理说没有&lt;code&gt;conntrack&lt;/code&gt;冲突应该表现出的情况与方案(二)类似才对，也可能是我使用的姿势不对，不过虽然这个问题还存在，但是通过&lt;code&gt;DaemonSet&lt;/code&gt;将 dns 请求压力分散到各个 node 节点，也可以有效的缓解域名解析超时问题。&lt;/p&gt;
&lt;h4&gt;实施&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;方案（一）：通过修改 pod 的 template.spec.dnsConfig 来设置，并将&lt;code&gt;dnsPolicy&lt;/code&gt;设置为&lt;code&gt;None&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;template:
  spec:
    dnsConfig:
      nameservers:
        - 169.254.20.10
      searches:
        - public.svc.cluster.local
        - svc.cluster.local
        - cluster.local
      options:
        - name: ndots
        value: &quot;5&quot;
    dnsPolicy: None
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;方案（二）：修改默认的&lt;code&gt;cluster-dns&lt;/code&gt;，在 node 节点上将&lt;code&gt;/etc/systemd/system/kubelet.service.d/10-kubeadm.conf&lt;/code&gt;文件中的&lt;code&gt;--cluster-dns&lt;/code&gt;参数值修改为&lt;code&gt;169.254.20.10&lt;/code&gt;，然后重启&lt;code&gt;kubelet&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl restart kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;注&lt;/code&gt;：配置文件路径也可能是&lt;code&gt;/etc/kubernetes/kubelet&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;最终解决方案&lt;/h2&gt;
&lt;p&gt;最后还是决定使用&lt;code&gt;方案(二)+方案(三)&lt;/code&gt;配合使用，来最大程度的优化此问题，并且将线上所有的基础镜像都替换为非&lt;code&gt;apline&lt;/code&gt;的镜像版本，至此问题基本解决，也希望 K8S 官方能早日将此功能直接集成进去。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>通过nginx反向代理来调试代码</title><link>https://monkeywie.cn/posts/debug-use-nginx-proxy</link><guid isPermaLink="true">https://monkeywie.cn/posts/debug-use-nginx-proxy</guid><pubDate>Mon, 30 Dec 2019 17:17:28 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;现在公司项目都是前后端分离的方式开发，有些时候由于某些新需求开发或者 bug 修改，想要让前端直接连到我本地开发环境进行调试，而前端代码我并没有，只能通过前端部署的测试环境进行测试，最简单的办法就是直接改 host 把后端测试环境的域名指向我本地的 IP，这对于 HTTP 协议的服务来说是很轻易做到的，不过公司的测试环境全部上了 HTTPS，而我本地的服务是 HTTP 协议这样就算是改了 host 也会由于协议不同导致请求失败，所以需要将本地的服务升级成 HTTPS 才行。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;方案&lt;/h2&gt;
&lt;p&gt;其实 springboot 本身就支持 HTTPS(&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/html/howto.html#howto-configure-ssl&quot;&gt;howto-configure-ssl&lt;/a&gt;)，但是这需要改项目代码不太优雅，于是就想直接用&lt;code&gt;nginx&lt;/code&gt;反向代理到本地服务，这样在&lt;code&gt;nginx&lt;/code&gt;层面做 HTTPS 就不需要改代码了，只需修改 host 将&lt;code&gt;后端测试环境域名&lt;/code&gt;指向 &lt;code&gt;nginx&lt;/code&gt; 服务的 IP 即可，而且可以适用于其它的 HTTP 服务开发调试。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;debug-use-nginx-proxy/2019-12-31-14-16-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;签发证书&lt;/h3&gt;
&lt;p&gt;首先要生成一套证书用于 nginx 的 ssl 配置，直接使用&lt;code&gt;openssl&lt;/code&gt;工具生成一套&lt;code&gt;根证书&lt;/code&gt;和对应的&lt;code&gt;服务证书&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意将命令行中的&lt;code&gt;xxx.com&lt;/code&gt;替换成真实的域名。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;根证书生成&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 生成一个RSA私钥
openssl genrsa -out root.key 2048
# 通过私钥生成一个根证书
openssl req -sha256 -new -x509 -days 365 -key root.key -out root.crt \
    -subj &quot;/C=CN/ST=GD/L=SZ/O=lee/OU=work/CN=fakerRoot&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;服务器证书生成&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 生成一个RSA私钥
openssl genrsa -out server.key 2048
# 生成一个带SAN扩展的证书签名请求文件
openssl req -new \
    -sha256 \
    -key server.key \
    -subj &quot;/C=CN/ST=GD/L=SZ/O=lee/OU=work/CN=xxx.com&quot; \
    -reqexts SAN \
    -config &amp;lt;(cat /etc/pki/tls/openssl.cnf \
        &amp;lt;(printf &quot;[SAN]\nsubjectAltName=DNS:*.xxx.com,DNS:*.test.xxx.com&quot;)) \
    -out server.csr
# 使用之前生成的根证书做签发
openssl ca -in server.csr \
    -md sha256 \
    -keyfile root.key \
    -cert root.crt \
    -extensions SAN \
    -config &amp;lt;(cat /etc/pki/tls/openssl.cnf \
        &amp;lt;(printf &quot;[SAN]\nsubjectAltName=DNS:xxx.com,DNS:*.test.xxx.com&quot;)) \
    -out server.crt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果上面步骤发送错误，可以&lt;a href=&quot;https://monkeywie.github.io/2019/11/15/create-ssl-cert-with-san/#%E5%B8%B8%E8%A7%81%E9%94%99%E8%AF%AF&quot;&gt;参考&lt;/a&gt;常见错误解决办法，成功之后就得到了三个关键文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root.crt&lt;/code&gt;:根证书&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.key&lt;/code&gt;:服务证书私钥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.crt&lt;/code&gt;:服务证书&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;注：生成的服务器证书域名要支持测试环境访问的域名，否则浏览器会提示证书不安全。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;nginx 配置&lt;/h3&gt;
&lt;p&gt;为了方便，直接使用&lt;code&gt;docker&lt;/code&gt;启动了一个 nginx 容器进行访问，并将证书和配置文件挂载到对应的目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nginx.conf&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 443 ssl;
    server_name _;
    ssl_certificate &quot;/usr/local/nginx/ssl/server.crt&quot;;
    ssl_certificate_key &quot;/usr/local/nginx/ssl/server.key&quot;;
    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;upgrade&quot;;
        proxy_pass http://127.0.0.1:3000;
        proxy_redirect off;
        proxy_http_version 1.1;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过配置&lt;code&gt;ssl_certificate&lt;/code&gt;和&lt;code&gt;ssl_certificate_key&lt;/code&gt;来指定服务器的证书和私钥，&lt;code&gt;proxy_pass&lt;/code&gt;指定开发环境的访问地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name https -p 443:443 -v ~/forword/ssl:/usr/local/nginx/ssl -v ~/forword/config/nginx.conf:/etc/nginx/conf.d/default.conf  nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 nginx 配置和证书相关文件挂载至对应的目录，并暴露 443 端口，这样服务启动后即可通过 https 访问到本地开发环境了。&lt;/p&gt;
&lt;h3&gt;安装根证书&lt;/h3&gt;
&lt;p&gt;由于服务证书是自己签发的，并不会被浏览器所信任，所以需要将&lt;code&gt;根证书&lt;/code&gt;安装至操作系统中。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开 chrome 浏览器-&amp;gt;设置-&amp;gt;高级-&amp;gt;管理证书
&lt;img src=&quot;debug-use-nginx-proxy/2019-12-31-14-50-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;受信任的根证书颁发机构-&amp;gt;导入
&lt;img src=&quot;debug-use-nginx-proxy/2019-12-31-14-51-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选择之前生成的根证书&lt;code&gt;root.crt&lt;/code&gt;导入即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;修改 host&lt;/h3&gt;
&lt;p&gt;在需要调试时，只需要将本地服务启动，再将 host 中将要测试的域名解析到&lt;code&gt;nginx&lt;/code&gt;服务器的 IP，即可将前端请求转发到开发环境上，通过浏览器地址栏的&lt;code&gt;小锁图标&lt;/code&gt;可以看到证书，已验证服务已经部署成功。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;debug-use-nginx-proxy/2019-12-31-14-57-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;debug-use-nginx-proxy/2019-12-31-14-54-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;本文中其实已经提到了两种解决方案了，其实还有其它的解决方案，例如使用&lt;code&gt;fidder&lt;/code&gt;这种中间人攻击的方式来实现，这里就不做多叙了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>聊聊HTTP协议的keep-alive</title><link>https://monkeywie.cn/posts/talk-http-keep-alive</link><guid isPermaLink="true">https://monkeywie.cn/posts/talk-http-keep-alive</guid><pubDate>Mon, 13 Jan 2020 12:01:13 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;HTTP 协议里的&lt;code&gt;keep-alive&lt;/code&gt;机制和长连接协议的&lt;code&gt;keep-alive&lt;/code&gt;机制有所不同，HTTP 中的作用是为了复用 TCP 连接，而长连接中大多数作用是为了保活，例如 TCP 通过 keep-alive 心跳包来检测对方是否存活。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;HTTP/1.1&lt;/code&gt;版本中&lt;code&gt;keep-alive&lt;/code&gt;默认是开启的，通过复用 TCP 连接，可以有效的降低 TCP 连接创建的开销，大多数浏览器只允许同时对同一个域名建立 6 个 TCP 连接。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;HTTP/1.0&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;HTTP/1.0&lt;/code&gt;版本中是没有&lt;code&gt;keep-alive&lt;/code&gt;机制的，意味着每次 HTTP 请求都会创建一个新的 TCP 连接，在响应完成后关闭当前 TCP 连接，为了验证我使用&lt;code&gt;wireshark&lt;/code&gt;来监听网卡上的 HTTP 报文。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;curl&lt;/code&gt;指定 HTTP/1.0 版本&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;curl --http1.0 http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;报文截图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;talk-http-keep-alive/2020-01-15-14-16-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到在服务器发送完响应之后就主动发送了&lt;code&gt;FIN包&lt;/code&gt;来关闭连接，验证了之前的内容。&lt;/p&gt;
&lt;h2&gt;HTTP/1.1&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;HTTP/1.0&lt;/code&gt;时代，由于 TCP 连接创建成本很高，很多服务器和浏览器使用了了一套非标准的&lt;code&gt;keep-alive&lt;/code&gt;机制，用于复用 TCP 连接，当然最后&lt;code&gt;HTTP/1.1&lt;/code&gt;将这套东西纳入到了标准中，这个标准就是&lt;code&gt;Connection&lt;/code&gt;头，用于客户端和服务端协商是否要复用 TCP 连接，在 HTTP/1.1 版本中默认值就是&lt;code&gt;keep-alive&lt;/code&gt;，即保持连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Connection: keep-alive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;客户端或服务器发现对方一段时间没有活动，就可以主动关闭连接。不过，规范的做法是，客户端在最后一个请求时，发送 Connection: close，明确要求服务器关闭 TCP 连接。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Connection: close
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的为了验证以上内容，需要抓个包来看看。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;curl&lt;/code&gt;连续访问同一地址&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;curl http://www.baidu.com http://www.baidu.com
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;报文截图&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;talk-http-keep-alive/2020-01-15-14-44-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过报文可以看到在建立了 TCP 连接之后，发生了两次 HTTP 请求之后由客户端发送&lt;code&gt;FIN&lt;/code&gt;包关闭连接，说明这两次 HTTP 请求是在同一个 TCP 连接上进行的。&lt;/p&gt;
&lt;h3&gt;一些细节&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;HTTP/1.0&lt;/code&gt;版本中，服务器不用返回&lt;code&gt;Content-Length&lt;/code&gt;响应头来标识响应体的报文长度，因为每次请求之后都会主动断开 TCP 连接，所以客户端在接收到&lt;code&gt;EOF&lt;/code&gt;(报文结束标识:-1)时，就说明响应已经全部接收完了。&lt;/p&gt;
&lt;p&gt;而在&lt;code&gt;HTTP/1.1&lt;/code&gt;版本中，由于 TCP 连接的复用，服务器必须得通过&lt;code&gt;Content-Length&lt;/code&gt;来告诉客户端响应体报文应该读到哪里，不过&lt;code&gt;Content-Length&lt;/code&gt;需要提前知道响应体的报文长度，对应一些很耗时的动态操作来说(例如：&lt;a href=&quot;https://monkeywie.github.io/2019/08/08/server-push-and-websocket/#more&quot;&gt;HTTP 服务器推送&lt;/a&gt;)，服务器要等到所有操作完成，才能发送数据，显然这样的效率不高。&lt;/p&gt;
&lt;p&gt;对于这种情况 HTTP 协议提出了另一种&lt;code&gt;Chunked&lt;/code&gt;编码来用于 HTTP 报文的传输，有一点数据就发送一点数据，直到数据发送完成，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked

5
hello
4
word
0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报文格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;length&amp;gt;\r\n
&amp;lt;data&amp;gt;\r\n
&amp;lt;length&amp;gt;\r\n
&amp;lt;data&amp;gt;\r\n
0\r\n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即&lt;code&gt;数据块长度(16进制)&lt;/code&gt;+&lt;code&gt;\r\n&lt;/code&gt;+&lt;code&gt;数据块&lt;/code&gt;+&lt;code&gt;\r\n&lt;/code&gt;以此循环，直到&lt;code&gt;数据块长度=0&lt;/code&gt;+&lt;code&gt;\r\n&lt;/code&gt;结束&lt;/p&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;p&gt;虽然&lt;code&gt;keep-alive&lt;/code&gt;是复用了 TCP 连接，但是由于&lt;code&gt;HTTP1/1&lt;/code&gt;协议是不支持&lt;code&gt;并行&lt;/code&gt;的，如果同一个 TCP 连接上需要完成多个 HTTP 请求，那么后一个就会被前一个 HTTP 请求阻塞着，如果前一个请求响应的特别慢，那么后面的请求就会等待的越久，在许多&lt;code&gt;HTTP客户端&lt;/code&gt;中为了避免这个问题，都会允许启用多个 TCP 连接来处理，当然在应用层也可以通过合并请求的方式来减少请求数(例如：多个小图片合成一个大图)，为了解决这些问题又衍生出了&lt;code&gt;HTTP/2&lt;/code&gt;版本，这里就不做过多描述了。&lt;/p&gt;
&lt;h2&gt;衍生&lt;/h2&gt;
&lt;p&gt;我顺带测试了下&lt;code&gt;java&lt;/code&gt;和&lt;code&gt;go&lt;/code&gt;的 http client，发现其实它们也都实现了&lt;code&gt;keep-alive&lt;/code&gt;，
，之前从代码的上来看，一直以为是请求完之后会直接关闭 TCP 连接，要设置一个连接池之类的东西去实现，没想到底层全部实现好了。&lt;/p&gt;
&lt;h3&gt;java 测试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;测试代码
使用&lt;code&gt;URL&lt;/code&gt;库发起三个 HTTP 请求，并抓包分析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public static void main(String[] args) throws IOException {
    for (int i = 0; i &amp;lt; 3; i++) {
        URL url = new URL(&quot;http://www.baidu.com&quot;);
        URLConnection urlConnection = url.openConnection();
        try (InputStream inputStream = urlConnection.getInputStream()) {
            byte[] temp = new byte[8192];
            // 空读
            while (inputStream.read(temp) != -1) {
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;报文截图
通过第一列的连线可以观察到是同一个 TCP 连接
&lt;img src=&quot;talk-http-keep-alive/2020-01-16-15-30-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;go 测试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;测试代码
使用&lt;code&gt;http&lt;/code&gt;标准库来测试，发起三个 HTTP 请求并抓包分析。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
  for i := 0; i &amp;lt; 3; i++ {
    resp, err := http.Get(&quot;http://www.baidu.com&quot;)
    if err != nil {
      panic(err)
    }
    // 空读，注：golang里一定要把数据读完，否则不会复用TCP连接
    io.Copy(ioutil.Discard, resp.Body)
    resp.Body.Close()
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;报文截图
结果也是一样，复用了一个 TCP 连接
&lt;img src=&quot;talk-http-keep-alive/2020-01-16-15-38-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>github镜像站</title><link>https://monkeywie.cn/posts/github-mirror</link><guid isPermaLink="true">https://monkeywie.cn/posts/github-mirror</guid><pubDate>Tue, 11 Feb 2020 12:47:31 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;github release&lt;/code&gt;一直下载不动，或者干脆直接下载失败，搜索一番发现 github 竟然还有镜像站点，下载速度还不错，地址：&lt;s&gt;&lt;a href=&quot;http://github-mirror.bugkiller.org&quot;&gt;http://github-mirror.bugkiller.org&lt;/a&gt;&lt;/s&gt;，新地址：&lt;a href=&quot;https://hub.fastgit.org&quot;&gt;https://hub.fastgit.org&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果有油猴的话推荐直接使用&lt;a href=&quot;https://greasyfork.org/en/scripts/397419-fastgithub-%E9%95%9C%E5%83%8F%E5%8A%A0%E9%80%9F%E8%AE%BF%E9%97%AE-%E5%85%8B%E9%9A%86%E5%92%8C%E4%B8%8B%E8%BD%BD&quot;&gt;FastGithub&lt;/a&gt;，效果图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-mirror/2020-12-21-15-26-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-mirror/2020-12-21-15-26-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>docker清理</title><link>https://monkeywie.cn/posts/docker-clean</link><guid isPermaLink="true">https://monkeywie.cn/posts/docker-clean</guid><pubDate>Fri, 21 Feb 2020 09:42:11 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;docker 在使用过程中，可能会产生很多冗余无用的数据，这些数据会占用大量硬盘空间，这里记录下如何清理 docker。&lt;/p&gt;
&lt;h3&gt;容器清理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;删除所有关闭的容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker rm $(docker ps -a -f status=exited -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关闭并删除所有容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker stop $(docker ps -aq)
docker rm $(docker ps -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;镜像清理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;删除 dangling images&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker image prune
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;删除所有镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker rmi $(docker images -q)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;挂载清理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;删除 dangling volmue&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker volume rm $(docker volume ls -f dangling=true -q)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>通过docker-compose快速搭建mongodb副本集</title><link>https://monkeywie.cn/posts/mongodb-replica-set</link><guid isPermaLink="true">https://monkeywie.cn/posts/mongodb-replica-set</guid><pubDate>Tue, 10 Mar 2020 11:09:10 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;mongoDB 提供的副本集是将数据同步至多个节点，提供了数据冗余备份和节点故障的情况下可以自动转移的高可用特性，架构图如下：
&lt;img src=&quot;mongodb-replica-set/2020-03-11-09-53-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有时候需要要在本地搭建一套 mongoDB 副本集环境来做测试，如果用虚拟机的话还是比较麻烦的，这里记录下如何用 docker-compose 快速搭建 mongoDB 副本集。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;构建镜像&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;准备秘钥用于节点之间的认证&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;openssl rand -base64 756 &amp;gt; auth.key
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;编写 Dockerfile&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;调整 mongo 官方提供的镜像，像里面添加秘钥文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM mongo:3.4.10
#将秘钥文件复制到镜像中
COPY auth.key /app/auth.key
RUN chown -R mongodb:mongodb /app/auth.key
#设置秘钥文件权限，这一步非常关键
RUN chmod 600 /app/auth.key
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;构建镜像&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker build -t mongo-replset .
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;编写 docker-compose&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;开启三个容器，组成一主两从的副本集集群，另外再开启一个容器监听和初始化副本集，这里我的实现方式并不优雅，就是一直重试直到前面三个 mongod 进程启动完成(官方有个&lt;a href=&quot;https://docs.docker.com/compose/startup-order/&quot;&gt;解决方案&lt;/a&gt;来检测依赖服务的启动，不过有点麻烦这里就略过了)，启动完成之后执行副本集初始化命令：&lt;code&gt;mongo mongodb://root:123@mongo-1:27011/admin --eval &apos;rs.initiate({ _id: &quot;rs&quot;, members: [{_id:1,host:&quot;mongo-1:27011&quot;},{_id:2,host:&quot;mongo-2:27012&quot;},{_id:3,host:&quot;mongo-3:27013&quot;}]})&apos;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.1&quot;
services:
  mongo-1:
    image: mongo-replset
    hostname: mongo-1
    restart: always
    ports:
      - 27011:27011
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: 123
    command:
      - --port
      - &quot;27011&quot;
      - --replSet
      - rs
      - --keyFile
      - /app/auth.key
  mongo-2:
    image: mongo-replset
    hostname: mongo-2
    restart: always
    ports:
      - 27012:27012
    command:
      - --port
      - &quot;27012&quot;
      - --replSet
      - rs
      - --keyFile
      - /app/auth.key
  mongo-3:
    image: mongo-replset
    hostname: mongo-3
    restart: always
    ports:
      - 27013:27013
    command:
      - --port
      - &quot;27013&quot;
      - --replSet
      - rs
      - --keyFile
      - /app/auth.key
  mongo-init:
    image: mongo:3.4.10
    depends_on:
      - mongo-1
      - mongo-2
      - mongo-3
    restart: on-failure:5
    command:
      - mongo
      - mongodb://root:123@mongo-1:27011/admin
      - --eval
      - &apos;rs.initiate({ _id: &quot;rs&quot;, members: [{_id:1,host:&quot;mongo-1:27011&quot;},{_id:2,host:&quot;mongo-2:27012&quot;},{_id:3,host:&quot;mongo-3:27013&quot;}]})&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
Creating mongo-replica_mongo-1_1 ... done
Creating mongo-replica_mongo-3_1 ... done
Creating mongo-replica_mongo-2_1 ... done
Creating mongo-replica_mongo-init_1 ... done
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;查看副本集状态&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker-compose exec mongo-1 mongo mongodb://root:123@mongo-1:27011/admin --eval &apos;rs.status()&apos;
MongoDB shell version v3.4.10
connecting to: mongodb://root:123@mongo-1:27011/admin
MongoDB server version: 3.4.10
{
        &quot;set&quot; : &quot;rs&quot;,
        &quot;date&quot; : ISODate(&quot;2020-03-10T07:01:59.764Z&quot;),
        &quot;myState&quot; : 1,
        &quot;term&quot; : NumberLong(1),
        &quot;heartbeatIntervalMillis&quot; : NumberLong(2000),
        &quot;optimes&quot; : {
                &quot;lastCommittedOpTime&quot; : {
                        &quot;ts&quot; : Timestamp(1583823710, 1),
                        &quot;t&quot; : NumberLong(1)
                },
                &quot;appliedOpTime&quot; : {
                        &quot;ts&quot; : Timestamp(1583823710, 1),
                        &quot;t&quot; : NumberLong(1)
                },
                &quot;durableOpTime&quot; : {
                        &quot;ts&quot; : Timestamp(1583823710, 1),
                        &quot;t&quot; : NumberLong(1)
                }
        },
        &quot;members&quot; : [
                {
                        &quot;_id&quot; : 1,
                        &quot;name&quot; : &quot;mongo-1:27011&quot;,
                        &quot;health&quot; : 1,
                        &quot;state&quot; : 1,
                        &quot;stateStr&quot; : &quot;PRIMARY&quot;,
                        &quot;uptime&quot; : 263,
                        &quot;optime&quot; : {
                                &quot;ts&quot; : Timestamp(1583823710, 1),
                                &quot;t&quot; : NumberLong(1)
                        },
                        &quot;optimeDate&quot; : ISODate(&quot;2020-03-10T07:01:50Z&quot;),
                        &quot;electionTime&quot; : Timestamp(1583823469, 1),
                        &quot;electionDate&quot; : ISODate(&quot;2020-03-10T06:57:49Z&quot;),
                        &quot;configVersion&quot; : 1,
                        &quot;self&quot; : true
                },
                {
                        &quot;_id&quot; : 2,
                        &quot;name&quot; : &quot;mongo-2:27012&quot;,
                        &quot;health&quot; : 1,
                        &quot;state&quot; : 2,
                        &quot;stateStr&quot; : &quot;SECONDARY&quot;,
                        &quot;uptime&quot; : 261,
                        &quot;optime&quot; : {
                                &quot;ts&quot; : Timestamp(1583823710, 1),
                                &quot;t&quot; : NumberLong(1)
                        },
                        &quot;optimeDurable&quot; : {
                                &quot;ts&quot; : Timestamp(1583823710, 1),
                                &quot;t&quot; : NumberLong(1)
                        },
                        &quot;optimeDate&quot; : ISODate(&quot;2020-03-10T07:01:50Z&quot;),
                        &quot;optimeDurableDate&quot; : ISODate(&quot;2020-03-10T07:01:50Z&quot;),
                        &quot;lastHeartbeat&quot; : ISODate(&quot;2020-03-10T07:01:59.120Z&quot;),
                        &quot;lastHeartbeatRecv&quot; : ISODate(&quot;2020-03-10T07:01:58.109Z&quot;),
                        &quot;pingMs&quot; : NumberLong(0),
                        &quot;syncingTo&quot; : &quot;mongo-1:27011&quot;,
                        &quot;configVersion&quot; : 1
                },
                {
                        &quot;_id&quot; : 3,
                        &quot;name&quot; : &quot;mongo-3:27013&quot;,
                        &quot;health&quot; : 1,
                        &quot;state&quot; : 2,
                        &quot;stateStr&quot; : &quot;SECONDARY&quot;,
                        &quot;uptime&quot; : 261,
                        &quot;optime&quot; : {
                                &quot;ts&quot; : Timestamp(1583823710, 1),
                                &quot;t&quot; : NumberLong(1)
                        },
                        &quot;optimeDurable&quot; : {
                                &quot;ts&quot; : Timestamp(1583823710, 1),
                                &quot;t&quot; : NumberLong(1)
                        },
                        &quot;optimeDate&quot; : ISODate(&quot;2020-03-10T07:01:50Z&quot;),
                        &quot;optimeDurableDate&quot; : ISODate(&quot;2020-03-10T07:01:50Z&quot;),
                        &quot;lastHeartbeat&quot; : ISODate(&quot;2020-03-10T07:01:59.120Z&quot;),
                        &quot;lastHeartbeatRecv&quot; : ISODate(&quot;2020-03-10T07:01:58.103Z&quot;),
                        &quot;pingMs&quot; : NumberLong(0),
                        &quot;syncingTo&quot; : &quot;mongo-1:27011&quot;,
                        &quot;configVersion&quot; : 1
                }
        ],
        &quot;ok&quot; : 1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;使用 docker-compose 搭建的 mongodb 副本集就完成了，以后可以基于此快速搭建一套 mongodb 副本集环境进行测试。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>设计一种无状态的验证码</title><link>https://monkeywie.cn/posts/stateless-captcha</link><guid isPermaLink="true">https://monkeywie.cn/posts/stateless-captcha</guid><pubDate>Thu, 26 Mar 2020 11:06:02 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;通常验证码都是通过&lt;code&gt;session&lt;/code&gt;来实现，在服务端生成一个随机字符串作为验证码，将该字符串存到&lt;code&gt;session&lt;/code&gt;中，然后将验证码图片渲染到前端，前端提交之后通过&lt;code&gt;session&lt;/code&gt;中存放的正确验证码进行对比从而验证输入的正确性。&lt;/p&gt;
&lt;p&gt;上面是一个典型的验证码实现的流程，但是这种方案存在非常多的弊端，例如：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;分布式应用：大家知道&lt;code&gt;session&lt;/code&gt;是有状态的，当服务器存在多个时，需要去处理&lt;code&gt;session&lt;/code&gt;丢失的问题。&lt;/li&gt;
&lt;li&gt;跨域问题：现在前后端分离大行其道，&lt;code&gt;cookie&lt;/code&gt;跨域问题会导致&lt;code&gt;session id&lt;/code&gt;无法正确传递，需要去处理&lt;code&gt;cookie&lt;/code&gt;跨域的问题。&lt;/li&gt;
&lt;li&gt;开销问题：维护&lt;code&gt;session&lt;/code&gt;需要消耗一定服务器的资源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;无状态验证码&lt;/h2&gt;
&lt;p&gt;为了解决上面的问题，我想了一个解决方案，核心思想是将&lt;code&gt;真实的验证码字符串&lt;/code&gt;存储在前端，当然是经过&lt;code&gt;加密&lt;/code&gt;的字符串，流程图如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;stateless-captcha/2020-03-26-11-49-02.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先前端通过接口获取一个&lt;code&gt;token&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;服务端生成&lt;code&gt;随机字符串&lt;/code&gt;并通过&lt;code&gt;AES&lt;/code&gt;加密，&lt;code&gt;AES KEY&lt;/code&gt;放在服务器保证加密解密是安全的&lt;/li&gt;
&lt;li&gt;客户端通过&lt;code&gt;token&lt;/code&gt;访问一个验证码图片&lt;/li&gt;
&lt;li&gt;服务器通过&lt;code&gt;AES&lt;/code&gt;解密拿到之前生成的&lt;code&gt;随机字符串&lt;/code&gt;，然后将字符串渲染成图片返回&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;至此前端已经得到了一个&lt;code&gt;token&lt;/code&gt;和一个&lt;code&gt;验证码图片&lt;/code&gt;，后续的流程图如下：
&lt;img src=&quot;stateless-captcha/2020-03-26-11-54-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;前端发起登录请求，将&lt;code&gt;token&lt;/code&gt;和用户输入的&lt;code&gt;验证码&lt;/code&gt;一起发送到后端。&lt;/li&gt;
&lt;li&gt;服务器通过&lt;code&gt;AES&lt;/code&gt;解密拿到之前生成的&lt;code&gt;随机字符串&lt;/code&gt;，再和用户输入的&lt;code&gt;验证码&lt;/code&gt;做对比校验&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就实现了一个无状态的验证码。&lt;/p&gt;
&lt;h2&gt;安全性&lt;/h2&gt;
&lt;p&gt;上面的验证码存在&lt;code&gt;重放攻击&lt;/code&gt;的风险，即记录一次正确的&lt;code&gt;token&lt;/code&gt;和&lt;code&gt;输入的验证码&lt;/code&gt;，这样就可以一直使用，以此绕过验证码校验。对此可以在&lt;code&gt;token&lt;/code&gt;中加入&lt;code&gt;过期时间&lt;/code&gt;属性，这样&lt;code&gt;token&lt;/code&gt;中其实包含了加密后的&lt;code&gt;正确验证码&lt;/code&gt;和&lt;code&gt;过期时间&lt;/code&gt;，在经过服务器时，首先通过时间检验，这样就可以大大的避免&lt;code&gt;重放攻击&lt;/code&gt;的风险。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;这里后端主要是用&lt;code&gt;springboot&lt;/code&gt;+&lt;code&gt;hutool&lt;/code&gt;来实现，&lt;code&gt;hutool&lt;/code&gt;用于验证码图片的渲染。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;后端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@ApiOperation(value = &quot;获取验证码token&quot;, httpMethod = &quot;GET&quot;)
@GetMapping(&quot;captcha&quot;)
public Result&amp;lt;String&amp;gt; getCaptcha() {
    // 随机生成4位字符串
    String code = RandomUtil.randomString(4);
    // 封装字符串和过期时间
    CaptchaDTO dto = new CaptchaDTO();
    dto.setCode(code);
    // 过期时间为一分钟
    dto.setExp(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1));
    // CAPTCHA_AES_KEY为AES加密中用到的key，存储在服务器中
    // 将dto对象转化成json字符串，再通过aes加密
    // token=aes(key,JSON.toString(dto))
    String token = SecureUtil.aes(Base64.getDecoder().decode(CAPTCHA_AES_KEY)).encryptBase64(JSON.toJSONString(dto));
    return new Result&amp;lt;&amp;gt;(token);
}

@ApiOperation(value = &quot;渲染验证码&quot;, httpMethod = &quot;GET&quot;)
@GetMapping(&quot;captcha/{token}&quot;)
public void showCaptcha(@PathVariable String token, HttpServletResponse response) throws Exception {
    //解码token
    CaptchaDTO dto = decodeCaptcha(token);
    LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(100, 40, 4, 50);
    //渲染验证码
    lineCaptcha.createImage(dto.getCode());
    response.setContentType(&quot;image/png&quot;);
    lineCaptcha.write(response.getOutputStream());
}

@ApiOperation(value = &quot;登录&quot;, httpMethod = &quot;POST&quot;)
@PostMapping(&quot;login&quot;)
public Result&amp;lt;UserDTO&amp;gt; login(@RequestBody LoginVO loginVO) throws Exception {
    CaptchaDTO dto;
    try {
        //解码token
        dto = decodeCaptcha(loginVO.getToken());
    } catch (Exception e) {
        throw new BizException(&quot;验证码数据异常&quot;);
    }
    // 校验时间和验证码输入
    if (System.currentTimeMillis() &amp;gt; dto.getExp()
            || StrUtil.isBlank(loginVO.getCode())
            || !dto.getCode().equalsIgnoreCase(loginVO.getCode())) {
        throw new BizException(&quot;验证码校验不通过&quot;);
    }
    // 处理登录逻辑
    UserDTO user = userService.login(loginVO);
    return new Result&amp;lt;&amp;gt;(user);
}

 private CaptchaDTO decodeCaptcha(String token) throws UnsupportedEncodingException {
    // 解码token，注意要做一次url decode，因为前端通过url传递时需要做url encode
    String jsonStr = SecureUtil.aes(Base64.getDecoder().decode(CAPTCHA_AES_KEY)).decryptStrFromBase64(URLDecoder.decode(token, &quot;UTF-8&quot;));
    return JSON.parseObject(jsonStr, CaptchaDTO.class);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;前端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export default {
  name: &quot;Login&quot;,
  data: () =&amp;gt; {
    return {
      loginForm: { username: &quot;&quot;, password: &quot;&quot;, token: &quot;&quot;, code: &quot;&quot; },
      captchaUrl: &quot;&quot;
    };
  },
  mounted() {
    this.getCaptcha();
  },
  methods: {
    //加载验证码
    async getCaptcha() {
      //获取验证码token
      this.loginForm.token = encodeURIComponent(await getCaptcha());
      //这里其实对token做了两次encodeURIComponent，因为img标签的get请求浏览器默认会做一次decode，不做两次encode会请求失败
      this.captchaUrl = `${api}/manager/users/captcha/${encodeURIComponent(
        this.loginForm.token
      )}`;
    },
    async login() {
      const result = await login(this.loginForm);
      //登录后处理...
    }
  }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;虽然在 &lt;code&gt;token&lt;/code&gt; 中加入了一分钟的过期时间，但是在这一分钟内其实够干很多事了，比如注册的业务使用这种验证码方式，一分钟内可以模拟大量的请求来进行注册，所以无状态验证码方案并不适合所有的业务场景，还是需要根据业务情况来进行实施。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言方法复用</title><link>https://monkeywie.cn/posts/go-reuse-method</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-reuse-method</guid><pubDate>Tue, 31 Mar 2020 16:00:06 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;用过&lt;code&gt;OOP&lt;/code&gt;的都知道，子类重写父类的方法可以优雅的实现代码的复用，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class People {
    String name;
    int age;

    public People(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void hello() {
        System.out.printf(&quot;name:%s age:%d sex:%s\n&quot;, name, age, sex());
    }

    public abstract String sex();

    static class Male extends People {

        public Male(String name, int age) {
            super(name, age);
        }

        @Override
        public String sex() {
            return &quot;M&quot;;
        }
    }

    static class Female extends People {

        public Female(String name, int age) {
            super(name, age);
        }

        @Override
        public String sex() {
            return &quot;F&quot;;
        }
    }

    public static void main(String[] args) {
        new Male(&quot;小明&quot;,20).hello();
        new Female(&quot;小红&quot;,18).hello();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name:小明 age:20 sex:M
name:小红 age:18 sex:F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是&lt;code&gt;go&lt;/code&gt;不支持&lt;code&gt;OOP&lt;/code&gt;，那么在&lt;code&gt;go&lt;/code&gt;中要类似情况的应该怎么实现？&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;错误的示范&lt;/h3&gt;
&lt;p&gt;一开始我想通过&lt;code&gt;go&lt;/code&gt;提供的组合模拟继承来实现，于是有了以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type People struct {
	name string
	age  int
}

func (p *People) say() {
	fmt.Printf(&quot;name:%s age:%d sex:%s\n&quot;, p.name, p.age, p.sex())
}

func (p *People) sex() string {
	return &quot;unknown&quot;
}

type Male struct {
	People
}

func (m *Male) sex() string {
	return &quot;M&quot;
}

type Female struct {
	People
}

func (f *Female) sex() string {
	return &quot;F&quot;
}

func main() {
	m := &amp;amp;Male{People{name: &quot;小明&quot;, age: 20}}
	m.say()
	f := &amp;amp;Female{People{name: &quot;小红&quot;, age: 18}}
	f.say()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面代码中分别&lt;code&gt;Male&lt;/code&gt;和&lt;code&gt;Female&lt;/code&gt;都重写了&lt;code&gt;sex()&lt;/code&gt;方法，来进行方法重用，然而运行的结果却和预料的不一样，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name:小明 age:20 sex:unknown
name:小红 age:18 sex:unknown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从输出结果可以看到，依旧是调用的&lt;code&gt;people结构体的sex方法&lt;/code&gt;，因为&lt;code&gt;go&lt;/code&gt;并不支持&lt;code&gt;OOP&lt;/code&gt;，组合类型其实只是某种形式上的语法弹，并不会改变&lt;code&gt;&quot;父类&quot;&lt;/code&gt;中调用的方法。&lt;/p&gt;
&lt;h3&gt;通过接口实现&lt;/h3&gt;
&lt;p&gt;抽象一个&lt;code&gt;Sex&lt;/code&gt;接口出来，由&lt;code&gt;Male&lt;/code&gt;和&lt;code&gt;Female&lt;/code&gt;来具体实现，直接上代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Sex interface {
	sex() string
}

type People struct {
	name string
	age  int
	Sex
}

func (p *People) say() {
	fmt.Printf(&quot;name:%s age:%d sex:%s\n&quot;, p.name, p.age, p.sex())
}

type Male struct {
}

func (m *Male) sex() string {
	return &quot;M&quot;
}

func (m *Male) play(){
	fmt.Println(&quot;男生爱打游戏&quot;)
}

type Female struct {
}

func (f *Female) sex() string {
	return &quot;F&quot;
}

func (f *Female) sing() {
	fmt.Println(&quot;女生爱唱歌&quot;)
}

func main() {
	m := &amp;amp;People{name: &quot;小明&quot;, age: 20, Sex: &amp;amp;Male{}}
	m.say()
	f := &amp;amp;People{name: &quot;小红&quot;, age: 18, Sex: &amp;amp;Female{}}
	f.say()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name:小明 age:20 sex:M
name:小红 age:18 sex:F
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过这样也引入了一个新的问题，&lt;code&gt;m&lt;/code&gt;和&lt;code&gt;f&lt;/code&gt;变量都是&lt;code&gt;People&lt;/code&gt;类型，比如现在有个方法需要通过性别来做不同的处理，那么就要使用&lt;code&gt;类型断言&lt;/code&gt;来实现了，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func handle(people *People){
    switch sex := people.Sex.(type) {
	case *Male:
		sex.play()
	case *Female:
		sex.sing()
	default:
		panic(&quot;error&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>win10彻底关闭windows defender</title><link>https://monkeywie.cn/posts/win10-disabled-windows-defender</link><guid isPermaLink="true">https://monkeywie.cn/posts/win10-disabled-windows-defender</guid><pubDate>Tue, 31 Mar 2020 18:23:19 GMT</pubDate><content:encoded>&lt;h3&gt;前言&lt;/h3&gt;
&lt;p&gt;最近把 win10 版本升级到了&lt;code&gt;1909&lt;/code&gt;,然后发现在有个&lt;code&gt;Windows Defender Antivirus Server&lt;/code&gt;的服务会占用大量的 cpu 和内存，网上找了好多办法都关不掉它，现在终于找了个靠谱的办法在这里记录下。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开注册表，找到&lt;code&gt;HKEY_LOCAL_MACHINE/SOFTWARE/Policies/Microsoft/Windows Defender&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右边窗口新建一个名为&lt;code&gt;DisableAntiSpyware&lt;/code&gt;类型为&lt;code&gt;DWORD(32 位)值&lt;/code&gt;的键，并将值设为&lt;code&gt;1&lt;/code&gt;保存。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;win10-disabled-windows-defender/2020-04-01-09-08-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;左边窗口选中&lt;code&gt;Windows Defender&lt;/code&gt;项，再其里面新建一个名为&lt;code&gt;Real-Time Protection&lt;/code&gt;的&lt;code&gt;项&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在刚刚创建的&lt;code&gt;Real-Time Protection&lt;/code&gt;项中新建一个名为&lt;code&gt;DisableScanOnRealtimeEnable&lt;/code&gt;类型为&lt;code&gt;DWORD(32 位)值&lt;/code&gt;的键，并将值设为&lt;code&gt;1&lt;/code&gt;保存。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;win10-disabled-windows-defender/2020-04-01-09-13-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最重要的一步，禁用&lt;code&gt;Windows Defender&lt;/code&gt;启动，找到&lt;code&gt;\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SecurityHealthService&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将右边窗口的&lt;code&gt;Start&lt;/code&gt;键值由&lt;code&gt;2&lt;/code&gt;改为&lt;code&gt;4&lt;/code&gt;，如果后续要重新启用再改回来即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;win10-disabled-windows-defender/2020-04-01-09-16-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>IDEA搭建scala开发环境</title><link>https://monkeywie.cn/posts/setup-scala-on-idea</link><guid isPermaLink="true">https://monkeywie.cn/posts/setup-scala-on-idea</guid><pubDate>Tue, 14 Apr 2020 15:46:22 GMT</pubDate><content:encoded>&lt;h3&gt;环境信息&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;windows 10&lt;/li&gt;
&lt;li&gt;IDEA 2019.3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不用下载&lt;code&gt;scala&lt;/code&gt;和&lt;code&gt;sbt&lt;/code&gt;，直接使用 IDEA 插件中自带的就行。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;操作步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;IDEA 安装&lt;code&gt;scala&lt;/code&gt;插件
&lt;img src=&quot;setup-scala-on-idea/2020-04-14-15-53-21.png&quot; alt=&quot;&quot; /&gt;
国内网络环境安装可能比较慢，如果失败的话可以通过&lt;code&gt;设置代理&lt;/code&gt;或者&lt;a href=&quot;https://plugins.jetbrains.com/plugin/1347-scala/versions&quot;&gt;离线下载&lt;/a&gt;的方式安装。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置&lt;code&gt;sbt&lt;/code&gt;环境
这一步是因为国内访问 sbt 仓库太慢，需要配置国内加速镜像。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;~/.sbt&lt;/code&gt;目录下创建&lt;code&gt;repositories&lt;/code&gt;文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[repositories]
local
nexus-aliyun:https://maven.aliyun.com/nexus/content/groups/public
huaweicloud-maven: https://repo.huaweicloud.com/repository/maven/
maven-central: https://repo1.maven.org/maven2/
sbt-plugin-repo: https://repo.scala-sbt.org/scalasbt/sbt-plugin-releases, [organization]/[module]/(scala_[scalaVersion]/)(sbt_[sbtVersion]/)[revision]/[type]s/[artifact](-[classifier]).[ext]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 IDEA 设置中搜索&lt;code&gt;sbt&lt;/code&gt;，然后修改&lt;code&gt;VM parameters&lt;/code&gt;，填入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-Dsbt.override.build.repos=true
-Dsbt.repository.config=${用户目录}\.sbt\repositories
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;setup-scala-on-idea/2020-04-14-16-05-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设置完毕就可以畅快使用&lt;code&gt;sbt&lt;/code&gt;了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建项目
IDEA 创建项目时选择&lt;code&gt;scala&lt;/code&gt;+&lt;code&gt;sbt&lt;/code&gt;
&lt;img src=&quot;setup-scala-on-idea/2020-04-14-16-08-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击&lt;code&gt;Next&lt;/code&gt;到下一步，选择对应的&lt;code&gt;sbt&lt;/code&gt;和&lt;code&gt;scala&lt;/code&gt;版本
&lt;img src=&quot;setup-scala-on-idea/2020-04-14-16-20-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后一直等到&lt;code&gt;sync finished&lt;/code&gt;就可以了
&lt;img src=&quot;setup-scala-on-idea/2020-04-14-16-21-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Levi</author></item><item><title>CDH6.2离线安装</title><link>https://monkeywie.cn/posts/cdh6-2-install</link><guid isPermaLink="true">https://monkeywie.cn/posts/cdh6-2-install</guid><pubDate>Tue, 14 Apr 2020 17:02:37 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在公司搭建了一个 CDH 测试环境集群，从官网得知 CDH 在 6.3.3 版本开始不再提供免费版本了，于是选择了 6.2.x 版本进行安装，这里记录一下正确的&lt;code&gt;离线安装&lt;/code&gt;步骤，避免下次安装时又踩坑。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;CM 和 CDH 简介&lt;/h2&gt;
&lt;p&gt;在安装之前先要理清一下 CM(Cloudera Manager)和 CDH(Cloudera Distribution Hadoop)的区别。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CDH
CDH 是由 Cloudera 公司发行的一套 hadoop 软件包，里面包含了&lt;code&gt;hadoop、hdfs、yarn、hive、spark&lt;/code&gt;等等一系列&lt;code&gt;稳定的&lt;/code&gt;、&lt;code&gt;版本兼容的&lt;/code&gt;大数据套件。&lt;/li&gt;
&lt;li&gt;CM
CM 也是 Cloudera 公司开发的一套用于管理和监控 CDH 集群的软件，只要通过 CM 提供的 web 管理页面操作就可以轻松的管理和监控 CDH 集群环境。
当然也可以手动使用 CDH 来搭建集群，在服务器数量较少的情况下还可以接受，但是试想下如果集群有上百台或上千台机器，每个机器都手动安装，这是人做的事吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装前准备&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;注：不知道什么时候开始CDH官网已经把离线下载资源入口都关掉了，不过好在我之前下载的文件都还在，现在分享到&lt;code&gt;google driver&lt;/code&gt;里了，链接：https://drive.google.com/drive/folders/1KZLRvYeSeygmPkoJWsNhQvLK4MgPiR5D?usp=sharing&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;服务器&lt;/h3&gt;
&lt;p&gt;准备好 N 台服务器，我这里用了 3 台机器，分别如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;内存&lt;/th&gt;
&lt;th&gt;硬盘&lt;/th&gt;
&lt;th&gt;系统&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.135&lt;/td&gt;
&lt;td&gt;4 核&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;50G&lt;/td&gt;
&lt;td&gt;CentOS7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.136&lt;/td&gt;
&lt;td&gt;4 核&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;50G&lt;/td&gt;
&lt;td&gt;CentOS7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.137&lt;/td&gt;
&lt;td&gt;4 核&lt;/td&gt;
&lt;td&gt;16G&lt;/td&gt;
&lt;td&gt;50G&lt;/td&gt;
&lt;td&gt;CentOS7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;注&lt;/code&gt;：所有机器的 root 用户登录密码要保持一致&lt;/p&gt;
&lt;h3&gt;域名设置&lt;/h3&gt;
&lt;p&gt;为三台机器设置好对应的域名，并在&lt;code&gt;hosts&lt;/code&gt;中做好对应的 IP 解析。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;th&gt;域名&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.135&lt;/td&gt;
&lt;td&gt;master.cdh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.136&lt;/td&gt;
&lt;td&gt;node1.cdh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192.168.200.137&lt;/td&gt;
&lt;td&gt;node2.cdh&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;修改所有机器的&lt;code&gt;/etc/hosts&lt;/code&gt;文件，在后面加入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;192.168.200.135 master.cdh
192.168.200.136 node1.cdh
192.168.200.137 node2.cdh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分别设置机器的主机名，执行以下命令&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;192.168.200.135&lt;pre&gt;&lt;code&gt;hostnamectl set-hostname master.cdh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;192.168.200.136&lt;pre&gt;&lt;code&gt;hostnamectl set-hostname node1.cdh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;192.168.200.137&lt;pre&gt;&lt;code&gt;hostnamectl set-hostname node2.cdh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;关闭防火墙和 SELinux&lt;/h3&gt;
&lt;p&gt;关闭所有机器的防火墙和 SELinux，执行以下命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭防火墙&lt;pre&gt;&lt;code&gt;systemctl stop firewalld
systemctl disable firewalld
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;关闭 SELinux
编辑&lt;code&gt;/etc/selinux/config&lt;/code&gt;文件，将&lt;code&gt;SELINUX=enforcing&lt;/code&gt;修改为&lt;code&gt;SELINUX=permissive&lt;/code&gt;。
执行命令&lt;code&gt;setenforce 0&lt;/code&gt;临时生效，或者重启机器永久生效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;搭建本地 yum 源&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;master&lt;/code&gt;节点上搭建本地源用于离线安装&lt;code&gt;CM&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;httpd&lt;/code&gt;作为本地源服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y httpd
systemctl start httpd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下载 CM 离线包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://archive.cloudera.com/cm6/6.2.1/repo-as-tarball/cm6.2.1-redhat7.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载慢的话可以用&lt;code&gt;ProxeeDown&lt;/code&gt;或者&lt;code&gt;IDM&lt;/code&gt;这类高并发下载器进行下载，再复制到&lt;code&gt;master&lt;/code&gt;节点。
还是下载不动的话只能挂梯子下了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解压到对应目录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /var/www/html/cloudera-repos/cm6
tar xvfz cm6.2.1-redhat7.tar.gz -C /var/www/html/cloudera-repos/cm6 --strip-components=1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;验证
浏览器访问&lt;code&gt;http://master.cdh/cloudera-repos/cm6&lt;/code&gt;，安装成功的话可以看到下图：
&lt;img src=&quot;cdh6-2-install/2020-04-16-12-14-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;源配置
只需要在&lt;code&gt;master.cdh&lt;/code&gt;节点上配置即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch vi /etc/yum.repos.d/cloudera-repo.repo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在文件中写入以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[cloudera-repo]
name=cloudera-repo
baseurl=http://master.cdh/cloudera-repos/cm6
enabled=1
gpgcheck=0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安装 JDK&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;所有机器&lt;/code&gt;上安装好 jdk，这里我安装的是&lt;code&gt;openjdk1.8&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y java-1.8.0-openjdk-devel
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 CM&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;master&lt;/code&gt;节点安装 CM&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y cloudera-manager-daemons cloudera-manager-agent cloudera-manager-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之前配置好了本地 yum 源，现在通过 yum 安装 CM 会非常的快，直接使用本地的包&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cdh6-2-install/2020-04-16-17-14-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;安装 MariaDB&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;master&lt;/code&gt;节点上安装一个数据库，这里我安装的是&lt;code&gt;MariaDB&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install -y mariadb-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改数据库配置文件&lt;code&gt;/etc/my.cnf&lt;/code&gt;，使用官方提供的配置进行替换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
transaction-isolation = READ-COMMITTED
# Disabling symbolic-links is recommended to prevent assorted security risks;
# to do so, uncomment this line:
symbolic-links = 0
# Settings user and group are ignored when systemd is used.
# If you need to run mysqld under a different user or group,
# customize your systemd unit file for mariadb according to the
# instructions in http://fedoraproject.org/wiki/Systemd

key_buffer = 16M
key_buffer_size = 32M
max_allowed_packet = 32M
thread_stack = 256K
thread_cache_size = 64
query_cache_limit = 8M
query_cache_size = 64M
query_cache_type = 1

max_connections = 550
#expire_logs_days = 10
#max_binlog_size = 100M

#log_bin should be on a disk with enough free space.
#Replace &apos;/var/lib/mysql/mysql_binary_log&apos; with an appropriate path for your
#system and chown the specified folder to the mysql user.
log_bin=/var/lib/mysql/mysql_binary_log

#In later versions of MariaDB, if you enable the binary log and do not set
#a server_id, MariaDB will not start. The server_id must be unique within
#the replicating group.
server_id=1

binlog_format = mixed

read_buffer_size = 2M
read_rnd_buffer_size = 16M
sort_buffer_size = 8M
join_buffer_size = 8M

# InnoDB settings
innodb_file_per_table = 1
innodb_flush_log_at_trx_commit  = 2
innodb_log_buffer_size = 64M
innodb_buffer_pool_size = 4G
innodb_thread_concurrency = 8
innodb_flush_method = O_DIRECT
innodb_log_file_size = 512M

[mysqld_safe]
log-error=/var/log/mariadb/mariadb.log
pid-file=/var/run/mariadb/mariadb.pid

#
# include all files from the config directory
#
!includedir /etc/my.cnf.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start mariadb
systemctl enable mariadb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化数据库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/bin/mysql_secure_installatio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化&lt;code&gt;root&lt;/code&gt;用户密码和其它一些选项&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[...]
Enter current password for root (enter for none):
OK, successfully used password, moving on...
[...]
Set root password? [Y/n] Y
New password:
Re-enter new password:
[...]
Remove anonymous users? [Y/n] Y
[...]
Disallow root login remotely? [Y/n] N
[...]
Remove test database and access to it [Y/n] Y
[...]
Reload privilege tables now? [Y/n] Y
[...]
All done!  If you&apos;ve completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载 jdbc 驱动&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;master&lt;/code&gt;节点上下载，因为 Maria 兼容 MySql ，所以直接下载 mysql 的 jdbc 驱动就行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.46.tar.gz
mkdir -p /usr/share/java/
cp mysql-connector-java-5.1.46/mysql-connector-java-5.1.46-bin.jar /usr/share/java/mysql-connector-java.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建数据库&lt;/h3&gt;
&lt;p&gt;登录 DB 并创建对应的数据库和用户，用于后续 CM 程序使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行语句&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE DATABASE scm DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON scm.* TO &apos;scm&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE amon DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON amon.* TO &apos;amon&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE rman DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON rman.* TO &apos;rman&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE hue DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON hue.* TO &apos;hue&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE metastore DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON metastore.* TO &apos;hive&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE sentry DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON sentry.* TO &apos;sentry&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE nav DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON nav.* TO &apos;nav&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE navms DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON navms.* TO &apos;navms&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;
CREATE DATABASE oozie DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
GRANT ALL ON oozie.* TO &apos;oozie&apos;@&apos;%&apos; IDENTIFIED BY &apos;123456&apos;;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认是否创建成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MariaDB [(none)]&amp;gt; SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| amon               |
| hue                |
| metastore          |
| mysql              |
| nav                |
| navms              |
| oozie              |
| performance_schema |
| rman               |
| scm                |
| sentry             |
+--------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设置 CM 数据库&lt;/h3&gt;
&lt;p&gt;官方提供了一个脚本用于初始化 CM 相关的数据，执行如下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/opt/cloudera/cm/schema/scm_prepare_database.sh mysql scm scm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/opt/cloudera/cm/schema/scm_prepare_database.sh mysql scm scm
Enter SCM password:
JAVA_HOME=/usr/lib/jvm/java-openjdk
Verifying that we can write to /etc/cloudera-scm-server
Creating SCM configuration file in /etc/cloudera-scm-server
Executing:  /usr/lib/jvm/java-openjdk/bin/java -cp /usr/share/java/mysql-connector-java.jar:/usr/share/java/oracle-connector-java.jar:/usr/share/java/postgresql-connector-java.jar:/opt/cloudera/cm/schema/../lib/* com.cloudera.enterprise.dbutil.DbCommandExecutor /etc/cloudera-scm-server/db.properties com.cloudera.cmf.db.
[                          main] DbCommandExecutor              INFO  Successfully connected to database.
All done, your SCM database is configured correctly!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;下载 CDH 离线包&lt;/h3&gt;
&lt;p&gt;这里要注意的是，根据不同的操作系统需要下载对应的包，浏览器访问&lt;a href=&quot;https://archive.cloudera.com/cdh6/6.2.1/parcels&quot;&gt;https://archive.cloudera.com/cdh6/6.2.1/parcels&lt;/a&gt;可以看到所有包，由于我这里用的是&lt;code&gt;CentOS7&lt;/code&gt;，所以选择下载&lt;code&gt;CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel&lt;/code&gt;。
&lt;img src=&quot;cdh6-2-install/2020-04-16-13-36-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下载&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wget https://archive.cloudera.com/cdh6/6.2.1/parcels/CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载慢的话参考上一步的解决办法，下载完成后将文件复制到&lt;code&gt;/opt/cloudera/parcel-repo&lt;/code&gt;目录&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复制到&lt;code&gt;/opt/cloudera/parcel-repo&lt;/code&gt;目录&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p /opt/cloudera/parcel-repo
mv CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel /opt/cloudera/parcel-repo/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;生成&lt;code&gt;sha1&lt;/code&gt;签名文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sha1sum CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel | awk &apos;{ print $1 }&apos; &amp;gt; CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel.sha
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改目录所属用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;chown -R cloudera-scm:cloudera-scm /opt/cloudera/parcel-repo/*
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;目录最终如下&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ls -l /opt/cloudera/parcel-repo/
-rw-r--r--. 1 cloudera-scm cloudera-scm 2093332003 4月   9 10:19 CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel
-rw-r--r--. 1 cloudera-scm cloudera-scm         41 4月  16 16:26 CDH-6.2.1-1.cdh6.2.1.p0.1425774-el7.parcel.sha
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动 CM 服务&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;master&lt;/code&gt;节点上运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl start cloudera-scm-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察日志输出，当看到&lt;code&gt;Started Jetty server.&lt;/code&gt;时表示服务已经启动成功了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tail -f /var/log/cloudera-scm-server/cloudera-scm-server.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;cdh6-2-install/2020-04-16-17-27-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;访问 CM 控制台&lt;/h3&gt;
&lt;p&gt;浏览器访问&lt;code&gt;http://master.cdh:7180&lt;/code&gt;，账号密码统一为&lt;code&gt;admin&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cdh6-2-install/2020-04-16-17-29-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开始安装&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;cdh6-2-install/2020-04-16-17-31-10.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-31-54.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-32-11.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-32-37.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-32-53.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-33-48.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-35-26.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-36-16.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-17-38-29.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-08-35.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-13-41.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-14-16.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-14-47.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-15-46.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-16-31.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;cdh6-2-install/2020-04-16-18-21-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后等到安装完成就行了，最终结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;cdh6-2-install/2020-04-17-09-06-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>spark应用调试</title><link>https://monkeywie.cn/posts/spark-application-debug</link><guid isPermaLink="true">https://monkeywie.cn/posts/spark-application-debug</guid><pubDate>Fri, 17 Apr 2020 13:49:49 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;平常开发 spark 应用的时候，为了快速验证程序是否正确一般都会设置&lt;code&gt;master&lt;/code&gt;为&lt;code&gt;local&lt;/code&gt;模式来运行，但是如果想用集群环境来运行的话，就需要打一个 jar 包用&lt;code&gt;spark-submit&lt;/code&gt;进行任务提交，但是在开发过程中频繁打 jar 包提交也是一件麻烦事，查阅相关资料之后发现其实可以在本地运行代码的时候指定集群环境来运行，达到快速调试的目的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;p&gt;每次运行之前还是需要打一个 jar 包，如果有引入 spark 之外的依赖，需要把依赖也打进去，否则会报&lt;code&gt;ClassNotFound&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sbt package
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;spark standalone 集群&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;val conf = new SparkConf()
      .setAppName(&quot;test&quot;)
      //指定spark master地址
      .setMaster(&quot;spark://master:7077&quot;)
      //指定本地jar包路径
      .setJars(List(&quot;file:///E:/code/study/scala/spark-demo/target/scala-2.11/spark-demo_2.11-0.1.jar&quot;))
      //指定本机IP为driver
      .setIfMissing(&quot;spark.driver.host&quot;, &quot;192.168.102.142&quot;)
val spark = SparkSession.builder()
    .config(conf)
    .getOrCreate()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在直接运行代码就可以直接运行在指定的 spark 集群环境上了。&lt;/p&gt;
&lt;h2&gt;spark on yarn 集群&lt;/h2&gt;
&lt;p&gt;这种集群方式稍微有点麻烦，需要先手动把 spark 中的 jar 包上传到 hdfs 中，然后指定 yarn 运行环境的 spark jars 路径。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;上传 jar 包至 hdfs
把集群中&lt;code&gt;${SPARK_HOME}/jars&lt;/code&gt;目录下的所有文件上传到 hdfs 中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hadoop fs -put ./jars/* /user/spark/share/lib/2.4.5/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;注：如果是使用的cdh安装的spark集群，不能使用cdh中的spark目录下的jar包，因为cdh和apache官方提供的jar包不一致，而开发的时候引入的依赖一般都是apache提供的jar包，这样运行的时候会报错，需要自行从apache官网下载对应的spark发行包然后进行上传，总而言之待上传的spark环境需要和本地开发环境保持一致即可。&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;编写代码&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;val conf = new SparkConf()
      .setAppName(&quot;test&quot;)
      //设置为yarn模式提交
      .setMaster(&quot;yarn&quot;)
      //设置yarn域名(必需，不然job状态一直ACCEPTED)
      .set(&quot;spark.hadoop.yarn.resourcemanager.hostname&quot;, &quot;master&quot;)
      //设置yarn提交地址
      .set(&quot;spark.hadoop.yarn.resourcemanager.address&quot;, &quot;master:8032&quot;)
      //设置stagingDir，用于存放任务运行时的临时文件
      .set(&quot;spark.yarn.stagingDir&quot;, &quot;hdfs://master:8020/user/root/spark/test&quot;)
      //设置yarn jars，填入上一步上传的hdfs地址
      .set(&quot;spark.yarn.jars&quot;, &quot;hdfs://master:8020/user/spark/share/lib/2.4.5/*.jar&quot;)
      //设置本地jar包地址
      .setJars(List(&quot;file:///E:/code/study/scala/spark-demo/target/scala-2.11/spark-demo_2.11-0.1.jar&quot;))
      //指定本机IP为driver
      .setIfMissing(&quot;spark.driver.host&quot;, &quot;192.168.102.142&quot;)
val spark = SparkSession.builder()
    .config(conf)
    .getOrCreate()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关于 setJars&lt;/h2&gt;
&lt;p&gt;前面说了每次运行之前都需要重新构建一次 jar 包，但其实也不一定，这个 jar 包的作用是为了能将参与 spark 运算的&lt;code&gt;匿名函数&lt;/code&gt;的反序列化。&lt;/p&gt;
&lt;p&gt;所以在没有修改&lt;code&gt;运算逻辑&lt;/code&gt;的时候，可以不需要重新构建 jar 包，举个例子来证明：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;第一次代码如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spark.sparkContext
      .parallelize(List(&quot;hello word&quot;, &quot;test word&quot;, &quot;hello haha&quot;, &quot;ok&quot;))
      .flatMap(_.split(&quot; &quot;))
      .map((_, 1))
      .take(10)
      .foreach(kv =&amp;gt; println(kv._1 + &quot;:&quot; + kv._2))
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;构建 jar 包&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sbt package
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行代码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;输出结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:1
ok:1
haha:1
hello:2
word:2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改代码，把数据改一改&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spark.sparkContext
      .parallelize(List(&quot;hello scala&quot;, &quot;test scala&quot;, &quot;hello haha&quot;, &quot;ok&quot;))
      .flatMap(_.split(&quot; &quot;))
      .map((_, 1))
      .countByKey()
      .take(10)
      .foreach(kv =&amp;gt; println(kv._1 + &quot;:&quot; + kv._2))
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;不重新构建 jar 包，直接运行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:1
scala:2
ok:1
haha:1
hello:2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现没有重新构建 jar 包，结果也边了，说明是运行的刚刚修改的代码。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;修改算子运行&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spark.sparkContext
      .parallelize(List(&quot;hello scala&quot;, &quot;test scala&quot;, &quot;hello haha&quot;, &quot;ok&quot;))
      .flatMap(_.split(&quot; &quot;))
      .map((_, 2)) //注意这里从1改成了2
      .countByKey()
      .take(10)
      .foreach(kv =&amp;gt; println(kv._1 + &quot;:&quot; + kv._2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不重新构建 jar 包，直接运行，结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test:1
scala:2
ok:1
haha:1
hello:2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算结果和之前的一样，没有发生变化，说明在计算的时候，节点是以 jar 中编译好的 class 进行计算。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;继续测试&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修改代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spark.sparkContext
      .parallelize(List(&quot;hello scala&quot;, &quot;test scala&quot;, &quot;hello haha&quot;, &quot;ok&quot;))
      .flatMap(_.split(&quot; &quot;))
      .map((_, 2))
      .countByKey()
      .take(10)
      .foreach(kv =&amp;gt; println(kv._1 + &quot;=&quot; + kv._2)) //注意这里将:换成了=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接运行，结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;test=1
scala=2
ok=1
haha=1
hello=2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到结果发生变化了，同样是匿名函数的实现修改，为什么这里又可以直接生效呢，接着往下。&lt;/p&gt;
&lt;h2&gt;setJars 原理&lt;/h2&gt;
&lt;p&gt;通过上面的示例，可以指定在这个例子中 spark 从 jar 包里主要拿到下面两个&lt;code&gt;匿名函数&lt;/code&gt;反序列化之后的类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.flatMap(_.split(&quot; &quot;))
.map((_, 1))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把 jar 包打开看一看，里面有三个内部类，分别对应代码中的三个&lt;code&gt;匿名函数&lt;/code&gt;
&lt;img src=&quot;spark-application-debug/2020-04-17-17-29-04.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//main$1.class
.flatMap(_.split(&quot; &quot;))
//main$2.class
.map((_, 1))
//main$3.class
.foreach(kv =&amp;gt; println(kv._1 + &quot;:&quot; + kv._2))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在将 rdd 分发到各个计算节点时，都是通过 jar 包中的 class 来&lt;code&gt;反序列化&lt;/code&gt;出对应的&lt;code&gt;匿名函数&lt;/code&gt;，所以在没有重新构建 jar 包的情况下修改代码不会生效，但是由于&lt;code&gt;.foreach(kv =&amp;gt; println(kv._1 + &quot;:&quot; + kv._2))&lt;/code&gt;在&lt;code&gt;take()&lt;/code&gt;方法之后调用，take 这个方法是将计算结果取回到&lt;code&gt;driver&lt;/code&gt;中，是使用本地运行时编译的 class，所以这里代码修改的话不需要重新构建 jar 也能及时生效。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;本来只是想要通过代码直接提交任务至 spark 集群环境，却意外研究了&lt;code&gt;setJars&lt;/code&gt;相关的知识，让我对 spark 计算过程有了更深刻的了解，甚是美哉。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stackoverflow.com/a/52164371/8129004&quot;&gt;https://stackoverflow.com/a/52164371/8129004&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>chrome清除HSTS记录</title><link>https://monkeywie.cn/posts/chrome-clear-hsts</link><guid isPermaLink="true">https://monkeywie.cn/posts/chrome-clear-hsts</guid><pubDate>Thu, 23 Apr 2020 10:08:44 GMT</pubDate><content:encoded>&lt;h2&gt;HSTS 简介&lt;/h2&gt;
&lt;p&gt;HSTS(HTTP Strict Transport Security)是一套由互联网工程任务组发布的互联网安全策略机制。网站可以选择使用 HSTS 策略，来让浏览器强制使用 HTTPS 与网站进行通信，以减少会话劫持风险。&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;有时候由于某些开发的需要，想访问 http 协议的接口，但是由于 HSTS 的机制，浏览器一直会强制跳转到 https，没办法调试，所以得把 HSTS 记录清除掉。&lt;/p&gt;
&lt;h2&gt;清除步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;浏览器访问&lt;code&gt;chrome://net-internals/#hsts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;找到&lt;code&gt;Delete domain security policies&lt;/code&gt;选项，输入对应的域名点击删除即可
&lt;img src=&quot;chrome-clear-hsts/2020-04-23-10-17-33.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Levi</author></item><item><title>做个优雅的CRUD boy</title><link>https://monkeywie.cn/posts/crud-gracefully</link><guid isPermaLink="true">https://monkeywie.cn/posts/crud-gracefully</guid><pubDate>Mon, 25 May 2020 18:15:36 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;网上都在调侃&lt;code&gt;CRUD&lt;/code&gt;没有技术含量，但是不可否认的是在工作中无可避免的要做大量的&lt;code&gt;CRUD&lt;/code&gt;，这里面会存在大量的重复工作，意味着可能会写大量的冗余代码，秉着能少写一行代码绝不多写一行的原则，不应该把时间浪费在这些重复的工作中的，在这里分享两个方案来用尽量少的代码实现&lt;code&gt;CRUD&lt;/code&gt;,一个是&lt;code&gt;spring-data-rest&lt;/code&gt;还有一个是我自己封装的一套框架&lt;code&gt;monkey-spring-boot-starter&lt;/code&gt;，下面一一进行介绍。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;spring-data-rest&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;Spring Data REST 是 Spring Data 项目的一部分，可轻松在 Spring Data repository 上构建 REST 服务，目前支持&lt;code&gt;JPA&lt;/code&gt;、&lt;code&gt;MongoDB&lt;/code&gt;、&lt;code&gt;Neo4j&lt;/code&gt;、&lt;code&gt;Solr&lt;/code&gt;、&lt;code&gt;Cassandra&lt;/code&gt;、&lt;code&gt;Gemfire&lt;/code&gt;，只需要定义一个&lt;code&gt;repository&lt;/code&gt;就可以自动转换成 REST 服务。&lt;/p&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;p&gt;先通过一个例子看看，只需要两个类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Data
@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;UserRepository.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public interface UserRepository extends JpaRepository&amp;lt;User,Long&amp;gt; {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这样&lt;code&gt;User表&lt;/code&gt;相关的&lt;code&gt;REST接口&lt;/code&gt;就已经生成好了，来测试下看看。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;访问根目录，会列出所有可用的资源列表&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl localhost:8080
{
  &quot;_links&quot; : {
    &quot;users&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users{?page,size,sort}&quot;,
      &quot;templated&quot; : true
    },
    &quot;profile&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/profile&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据上面的响应，可以看到&lt;code&gt;User资源&lt;/code&gt;对应的接口地址，接着继续测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl -X POST -i -H &quot;Content-Type:application/json&quot; -d &apos;{&quot;name&quot;:&quot;lee&quot;,&quot;age&quot;:18}&apos; localhost:8080/users
{
  &quot;name&quot; : &quot;lee&quot;,
  &quot;age&quot; : 18,
  &quot;_links&quot; : {
    &quot;self&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    },
    &quot;user&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查询用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl localhost:8080/users
{
  &quot;_embedded&quot; : {
    &quot;users&quot; : [ {
      &quot;name&quot; : &quot;lee&quot;,
      &quot;age&quot; : 18,
      &quot;_links&quot; : {
        &quot;self&quot; : {
          &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
        },
        &quot;user&quot; : {
          &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
        }
      }
    } ]
  },
  &quot;_links&quot; : {
    &quot;self&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users&quot;
    },
    &quot;profile&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/profile/users&quot;
    }
  },
  &quot;page&quot; : {
    &quot;size&quot; : 20,
    &quot;totalElements&quot; : 1,
    &quot;totalPages&quot; : 1,
    &quot;number&quot; : 0
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;分页查询用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl &quot;localhost:8080/users?page=0&amp;amp;size=10&quot;
{
  &quot;_embedded&quot; : {
    &quot;users&quot; : [ {
      &quot;name&quot; : &quot;lee&quot;,
      &quot;age&quot; : 18,
      &quot;_links&quot; : {
        &quot;self&quot; : {
          &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
        },
        &quot;user&quot; : {
          &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
        }
      }
    } ]
  },
  &quot;_links&quot; : {
    &quot;self&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users?page=0&amp;amp;size=10&quot;
    },
    &quot;profile&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/profile/users&quot;
    }
  },
  &quot;page&quot; : {
    &quot;size&quot; : 10,
    &quot;totalElements&quot; : 1,
    &quot;totalPages&quot; : 1,
    &quot;number&quot; : 0
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl -X PUT -H &quot;Content-Type:application/json&quot; -d &apos;{&quot;name&quot;:&quot;hello&quot;,&quot;age&quot;:20}&apos; localhost:8080/users/1
{
  &quot;name&quot; : &quot;hello&quot;,
  &quot;age&quot; : 20,
  &quot;_links&quot; : {
    &quot;self&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    },
    &quot;user&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ curl -X PATCH -H &quot;Content-Type:application/json&quot; -d &apos;{&quot;age&quot;:18}&apos; localhost:8080/users/1
{
  &quot;name&quot; : &quot;hello&quot;,
  &quot;age&quot; : 18,
  &quot;_links&quot; : {
    &quot;self&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    },
    &quot;user&quot; : {
      &quot;href&quot; : &quot;http://localhost:8080/users/1&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;删除用户&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ curl -i -X DELETE localhost:8080/users/1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码只有十几行就完成了一个基本的&lt;code&gt;CRUD&lt;/code&gt;功能，在开发小项目的时候效率非常的高，但是由于屏蔽了&lt;code&gt;controller&lt;/code&gt;层，如果有基于&lt;code&gt;拦截器&lt;/code&gt;或者&lt;code&gt;AOP&lt;/code&gt;做一些定制化的功能就比较麻烦了，例如：&lt;code&gt;日志审计&lt;/code&gt;、&lt;code&gt;权限校验&lt;/code&gt;之类的。&lt;/p&gt;
&lt;h2&gt;未完待续&lt;/h2&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言HTTP请求流式写入body</title><link>https://monkeywie.cn/posts/go-http-request-body-stream-writer</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-http-request-body-stream-writer</guid><pubDate>Mon, 01 Jun 2020 18:45:16 GMT</pubDate><content:encoded>&lt;h3&gt;背景&lt;/h3&gt;
&lt;p&gt;最近在开发一个功能时，需要通过 http 协议上报大量的日志内容，但是在 Go 标准库里的 http client 的 API 是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.NewRequest(method, url string, body io.Reader)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;body 是通过&lt;code&gt;io.Reader&lt;/code&gt;接口来传递，并没有暴露一个&lt;code&gt;io.Writer&lt;/code&gt;接口来提供写入的办法，先来看看正常情况下怎么写入一个&lt;code&gt;body&lt;/code&gt;，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buf := bytes.NewBuffer([]byte(&quot;hello&quot;))
http.Post(&quot;localhost:8099/report&quot;,&quot;text/pain&quot;,buf)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要先把要写入的数据放在&lt;code&gt;Buffer&lt;/code&gt;中，放内存缓存着，但是我需要写入&lt;code&gt;大量&lt;/code&gt;的数据，如果都放内存里肯定要 OOM 了，http client 并没有提供&lt;code&gt;流式写入&lt;/code&gt;的方法，我这么大的数据量直接用&lt;code&gt;Buffer&lt;/code&gt;肯定是不行的，最后在 google 了一番之后找到了解决办法。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;使用 io.pipe&lt;/h3&gt;
&lt;p&gt;调用&lt;code&gt;io.pipe()&lt;/code&gt;方法会返回&lt;code&gt;Reader&lt;/code&gt;和&lt;code&gt;Writer&lt;/code&gt;接口实现对象，通过&lt;code&gt;Writer&lt;/code&gt;写数据，&lt;code&gt;Reader&lt;/code&gt;就可以读到，利用这个特性就可以实现流式的写入，开一个协程来写，然后把&lt;code&gt;Reader&lt;/code&gt;传递到方法中，就可以实现 http client body 的流式写入了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;代码示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pr, pw := io.Pipe()
// 开协程写入大量数据
go func(){
    for i := 0; i &amp;lt; 100000; i++ {
        pw.Write([]byte(fmt.Sprintf(&quot;line:%d\r\n&quot;, i)))
    }
    pw.Close()
}()
// 传递Reader
http.Post(&quot;localhost:8099/report&quot;,&quot;text/pain&quot;,pr)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;源码阅读&lt;/h3&gt;
&lt;h4&gt;目的&lt;/h4&gt;
&lt;p&gt;了解 go 中 http client 对于 body 的传输是如何处理的。&lt;/p&gt;
&lt;h4&gt;开始&lt;/h4&gt;
&lt;p&gt;在构建 Request 的时候，会断言 body 参数的类型，当类型为&lt;code&gt;*bytes.Buffer&lt;/code&gt;、&lt;code&gt;*bytes.Reader&lt;/code&gt;、&lt;code&gt;*strings.Reader&lt;/code&gt;的时候，可以直接通过&lt;code&gt;Len()&lt;/code&gt;方法取出长度，用于&lt;code&gt;Content-Length&lt;/code&gt;请求头，相关代码&lt;a href=&quot;https://github.com/golang/go/blob/6be4a5eb4898c7b5e7557dda061cc09ba310698b/src/net/http/request.go#L872-L914&quot;&gt;net/http/request.go#L872-L914&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if body != nil {
    switch v := body.(type) {
    case *bytes.Buffer:
        req.ContentLength = int64(v.Len())
        buf := v.Bytes()
        req.GetBody = func() (io.ReadCloser, error) {
            r := bytes.NewReader(buf)
            return ioutil.NopCloser(r), nil
        }
    case *bytes.Reader:
        req.ContentLength = int64(v.Len())
        snapshot := *v
        req.GetBody = func() (io.ReadCloser, error) {
            r := snapshot
            return ioutil.NopCloser(&amp;amp;r), nil
        }
    case *strings.Reader:
        req.ContentLength = int64(v.Len())
        snapshot := *v
        req.GetBody = func() (io.ReadCloser, error) {
            r := snapshot
            return ioutil.NopCloser(&amp;amp;r), nil
        }
    default:
    }
    if req.GetBody != nil &amp;amp;&amp;amp; req.ContentLength == 0 {
        req.Body = NoBody
        req.GetBody = func() (io.ReadCloser, error) { return NoBody, nil }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在链接建立的时候，会通过&lt;code&gt;body&lt;/code&gt;和上一步中得到的&lt;code&gt;ContentLength&lt;/code&gt;来进行判断，如果&lt;code&gt;body!=nil&lt;/code&gt;并且&lt;code&gt;ContentLength==0&lt;/code&gt;时，可能就会启用&lt;code&gt;Chunked&lt;/code&gt;编码进行传输，相关代码&lt;a href=&quot;https://github.com/golang/go/blob/6be4a5eb4898c7b5e7557dda061cc09ba310698b/src/net/http/transfer.go#L82-L96&quot;&gt;net/http/transfer.go#L82-L96&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case *Request:
    if rr.ContentLength != 0 &amp;amp;&amp;amp; rr.Body == nil {
        return nil, fmt.Errorf(&quot;http: Request.ContentLength=%d with nil Body&quot;, rr.ContentLength)
    }
    t.Method = valueOrDefault(rr.Method, &quot;GET&quot;)
    t.Close = rr.Close
    t.TransferEncoding = rr.TransferEncoding
    t.Header = rr.Header
    t.Trailer = rr.Trailer
    t.Body = rr.Body
    t.BodyCloser = rr.Body
    // 当body为非nil，并且ContentLength==0时，这里返回-1
    t.ContentLength = rr.outgoingLength()
    // TransferEncoding没有手动设置，并且请求方法为PUT、POST、PATCH时，会启用chunked编码传输
    if t.ContentLength &amp;lt; 0 &amp;amp;&amp;amp; len(t.TransferEncoding) == 0 &amp;amp;&amp;amp; t.shouldSendChunkedRequestBody() {
        t.TransferEncoding = []string{&quot;chunked&quot;}
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;验证(一)&lt;/h4&gt;
&lt;p&gt;按照对源码的理解，可以得知在使用&lt;code&gt;io.pipe()&lt;/code&gt;方法进行流式传输时，会使用&lt;code&gt;chunked&lt;/code&gt;编码进行传输，通过以下代码进行验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func main(){
	http.HandleFunc(&quot;/report&quot;, func(writer http.ResponseWriter, request *http.Request) {

	})
	http.ListenAndServe(&quot;:8099&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func main(){
    pr, rw := io.Pipe()
    go func(){
        for i := 0; i &amp;lt; 100; i++ {
            rw.Write([]byte(fmt.Sprintf(&quot;line:%d\r\n&quot;, i)))
        }
        rw.Close()
    }()
    http.Post(&quot;localhost:8099/report&quot;,&quot;text/pain&quot;,buf)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先运行服务端，然后运行客户端，并且使用&lt;code&gt;WireShake&lt;/code&gt;进行抓包分析，结果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;go-http-request-body-stream-writer/2020-06-02-14-26-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到和预想的结果一样。&lt;/p&gt;
&lt;h4&gt;验证(二)&lt;/h4&gt;
&lt;p&gt;在数据量大的时候&lt;code&gt;chunked&lt;/code&gt;编码会增加额外的开销，包括编解码和额外的报文开销，能不能不用&lt;code&gt;chunked&lt;/code&gt;编码来进行&lt;code&gt;流式传输&lt;/code&gt;呢？通过源码可以得知，当&lt;code&gt;ContentLength&lt;/code&gt;不为 0 时，如果能预先计算出待传输的&lt;code&gt;body size&lt;/code&gt;，是不是就能避免&lt;code&gt;chunked&lt;/code&gt;编码呢？思路就到这，接着就是写代码验证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func main(){
	http.HandleFunc(&quot;/report&quot;, func(writer http.ResponseWriter, request *http.Request) {

	})
	http.ListenAndServe(&quot;:8099&quot;, nil)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;客户端&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;count := 100
line := []byte(&quot;line\r\n&quot;)
pr, rw := io.Pipe()
go func() {
    for i := 0; i &amp;lt; count; i++ {
        rw.Write(line)
    }
    rw.Close()
}()
// 构造request对象
request, err := http.NewRequest(&quot;POST&quot;, &quot;http://localhost:8099/report&quot;, pr)
if err != nil {
    log.Fatal(err)
}
// 提前计算出ContentLength
request.ContentLength = int64(len(line) * count)
// 发起请求
http.DefaultClient.Do(request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抓包结果：
&lt;img src=&quot;go-http-request-body-stream-writer/2020-06-02-14-44-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到确实直接使用的&lt;code&gt;Content-Length&lt;/code&gt;进行传输，没有进行&lt;code&gt;chunked&lt;/code&gt;编码了。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;本文的目的主要是记录 go 语言中&lt;code&gt;http client&lt;/code&gt;如何进行流式的写入，并通过阅读源码了解&lt;code&gt;http client&lt;/code&gt;内部对 body 的写入是如何进行处理的，通过两个验证可以得知，如果能提前计算出&lt;code&gt;ContentLength&lt;/code&gt;并且对性能要求比较苛刻的情况下，可以通过手动设置&lt;code&gt;ContentLength&lt;/code&gt;来优化性能。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>自定义SpringMVC中的RequestMappingHandlerMapping</title><link>https://monkeywie.cn/posts/custom-springmvc-requestmappinghandlermapping</link><guid isPermaLink="true">https://monkeywie.cn/posts/custom-springmvc-requestmappinghandlermapping</guid><pubDate>Mon, 22 Jun 2020 11:29:58 GMT</pubDate><content:encoded>&lt;h2&gt;RequestMappingHandlerMapping 介绍&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;是&lt;code&gt;SpringMVC&lt;/code&gt;中的一个重要组件，作用是扫描&lt;code&gt;@Controller&lt;/code&gt;、&lt;code&gt;@RequestMapping&lt;/code&gt;注解修饰的类，然后生成&lt;code&gt;请求&lt;/code&gt;与&lt;code&gt;方法&lt;/code&gt;的对应关系，当有一个 HTTP 请求进入 SpringMVC 时，就会通过请求找到对应的方法进行执行。&lt;/p&gt;
&lt;p&gt;可以简单的想象一下，在&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;会维护一个&lt;code&gt;Map&amp;lt;String,Handle&amp;gt;&lt;/code&gt;，key 存放的是&lt;code&gt;URI&lt;/code&gt;，value 存放的是对应处理的&lt;code&gt;handle&lt;/code&gt;，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map.put(&quot;GET /user&quot;,UserController#get)
map.put(&quot;POST /user&quot;,UserController#create)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样通过解析请求就可以很快的找到对应的方法去执行，当然 SpringMVC 的实现肯定不会像上面一样这么简单，不过思路是差不多的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;加载流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;流程图&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;custom-springmvc-requestmappinghandlermapping/2020-06-22-16-57-20.png&quot; alt=&quot;IDEA SequenceDiagram插件生成&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;实现了&lt;code&gt;InitializingBean&lt;/code&gt;接口，在应用启动时会触发&lt;code&gt;afterPropertiesSet&lt;/code&gt;方法。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;initHandlerMethods&lt;/code&gt;方法中，会遍历所有候选的 Bean，并通过&lt;code&gt;processCandidateBean&lt;/code&gt;方法进行处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbstractHandlerMethodMapping.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected void initHandlerMethods() {
    //遍历所有候选的Bean name
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            //处理Bean name
            processCandidateBean(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在&lt;code&gt;processCandidateBean&lt;/code&gt;方法中，会通过&lt;code&gt;isHandler&lt;/code&gt;判断&lt;code&gt;Bean&lt;/code&gt;是否为&lt;code&gt;@Controller&lt;/code&gt;、&lt;code&gt;@RequestMapping&lt;/code&gt;注解修饰的类，是的话调用&lt;code&gt;detectHandlerMethods&lt;/code&gt;来检查类中的&lt;code&gt;Handler method&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;detectHandlerMethods&lt;/code&gt;中会遍历类中所有方法，通过&lt;code&gt;getMappingForMethod&lt;/code&gt;方法筛选出&lt;code&gt;@RequestMapping&lt;/code&gt;注解修饰的方法，然后解析成&lt;code&gt;method&lt;/code&gt;-&amp;gt;&lt;code&gt;mapping&lt;/code&gt;的 Map 结构存起来，再遍历使用&lt;code&gt;registerHandlerMethod&lt;/code&gt;方法注册到 SpringMVC 中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbstractHandlerMethodMapping.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;protected void detectHandlerMethods(Object handler) {
    Class&amp;lt;?&amp;gt; handlerType = (handler instanceof String ?
            obtainApplicationContext().getType((String) handler) : handler.getClass());

    if (handlerType != null) {
        Class&amp;lt;?&amp;gt; userType = ClassUtils.getUserClass(handlerType);
        //查询Class中的方法
        Map&amp;lt;Method, T&amp;gt; methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup&amp;lt;T&amp;gt;) method -&amp;gt; {
                    //通过匿名内部类的方式来进行method的过滤，没有通过@RequestMapping修饰的方法会返回null
                    try {
                        return getMappingForMethod(method, userType);
                    }
                    catch (Throwable ex) {
                        throw new IllegalStateException(&quot;Invalid mapping on handler class [&quot; +
                                userType.getName() + &quot;]: &quot; + method, ex);
                    }
                });
        if (logger.isTraceEnabled()) {
            logger.trace(formatMappings(userType, methods));
        }
        //遍历methods进行注册
        methods.forEach((method, mapping) -&amp;gt; {
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;registerHandlerMethod&lt;/code&gt;将对应的关系存放到&lt;code&gt;mappingRegistry&lt;/code&gt;对象中，里面有很多的 Map 用于存储映射关系&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;AbstractHandlerMethodMapping.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//封装HandlerMethod，实际上就是bean name+method，在拦截器中就是暴露的这个对象
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);
//将mapping对象和handlerMethod关系存放至mappingLookup
this.mappingLookup.put(mapping, handlerMethod);

List&amp;lt;String&amp;gt; directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
    //将非通配符形式的路径与mapping对象关系存放至urlLookup
    this.urlLookup.add(url, mapping);
}

String name = null;
if (getNamingStrategy() != null) {
    name = getNamingStrategy().getName(handlerMethod, mapping);
    addMappingName(name, handlerMethod);
}

CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
    this.corsLookup.put(handlerMethod, corsConfig);
}

this.registry.put(mapping, new MappingRegistration&amp;lt;&amp;gt;(mapping, handlerMethod, directUrls, name));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过源码可以得知，目前有这两个&lt;code&gt;mappingLookup&lt;/code&gt;和&lt;code&gt;urlLookup&lt;/code&gt;对象存放了请求映射关系，在请求到来的时候就会通过这两个&lt;code&gt;Map&lt;/code&gt;去寻找要执行的方法。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;请求流程&lt;/h3&gt;
&lt;p&gt;先上一张 springMVC 流程图：
&lt;img src=&quot;custom-springmvc-requestmappinghandlermapping/2020-06-23-14-11-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;入口由&lt;code&gt;DispatcherServlet&lt;/code&gt;统一接管，然后通过上一步生成好的&lt;code&gt;HandlerMapping&lt;/code&gt;映射关系来查找请求对应的处理方法。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DispatcherServlet.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 寻找当前请求的处理方法
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;getHandler&lt;/code&gt;方法中就是对应的逻辑了，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        //遍历handlerMappings,只要能根据请求匹配到一个handler就返回
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里值得一提的是&lt;code&gt;handlerMappings&lt;/code&gt;是一组&lt;code&gt;HandlerMapping&lt;/code&gt;接口的实现，&lt;code&gt;SpringMVC&lt;/code&gt;默认提供的是&lt;code&gt;org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping&lt;/code&gt;，如果有需要我们也可以自定义一个&lt;code&gt;HandlerMapping&lt;/code&gt;实现来处理请求。&lt;/p&gt;
&lt;p&gt;接着一路跟踪源码，直到&lt;code&gt;AbstractHandlerMethodMapping#lookupHandlerMethod(String lookupPath, HttpServletRequest request)&lt;/code&gt;方法，就可以看到具体的实现了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AbstractHandlerMethodMapping.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//先直接使用URI进行匹配，适用于没使用通配符修饰的接口路径，对应urlLookup
List&amp;lt;T&amp;gt; directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
    //路径匹配到之后，还要根据method、header、consume、produce等等条件继续进行匹配
    addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
    //如果没匹配到，再通过通配符的方式去匹配，对应mappingLookup
    addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此与 RequestMappingHandlerMapping 有关的请求流程就已经介绍完了，最后再附上一张类图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-springmvc-requestmappinghandlermapping/2020-06-23-15-38-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;大部分的实现逻辑都在父类&lt;code&gt;AbstractHandlerMethodMapping&lt;/code&gt;中。&lt;/p&gt;
&lt;h2&gt;自定义 RequestMappingHandlerMapping&lt;/h2&gt;
&lt;p&gt;终于步入主题了，在了解&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;的大概的原理之后，就很清楚的如何来魔改&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;需求&lt;/h3&gt;
&lt;p&gt;项目中有一个&lt;code&gt;BaseController&lt;/code&gt;基础类，当有新的需求开发时只需要继承该类就会拥有对应的&lt;code&gt;CRUD&lt;/code&gt;接口，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BaseController.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BaseController&amp;lt;T&amp;gt; {
    @PostMapping
    public Result&amp;lt;T&amp;gt; insert(@Validated @RequestBody T vo) {
        //...
    }

    @PutMapping(&quot;{id}&quot;)
    public Result&amp;lt;T&amp;gt; update(@PathVariable @NotNull String id, @RequestBody @Validated T vo) {
        //...
    }

    @DeleteMapping(&quot;{id}&quot;)
    public Result&amp;lt;T&amp;gt; delete(@PathVariable @NotNull String id) {
        //...
    }

    @GetMapping(&quot;{id}&quot;)
    public Result&amp;lt;T&amp;gt; get(@PathVariable @NotNull String id) {
        //...
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;AppController.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/app&quot;)
public class AppController extends BaseController&amp;lt;App&amp;gt;{

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样&lt;code&gt;AppController&lt;/code&gt;就拥有了基本的&lt;code&gt;CRUD&lt;/code&gt;接口功能，但是在某些情况的时候我需要屏蔽掉某个接口，可以通过重写方法来实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AppController.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/app&quot;)
public class AppController extends BaseController&amp;lt;App&amp;gt;{

    //屏蔽get接口
    @Override
    @GetMapping(&quot;{id}&quot;)
    public Result&amp;lt;T&amp;gt; get(@PathVariable @NotNull String id) {
        throw new UnsupportedOperationException();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样实现其实也没啥问题，不过会占用一个路由，如果想重写这个接口，并且返回不同的响应体，就实现不了了，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重写父类方法编译不通过，因为泛型不兼容&lt;code&gt;Result&amp;lt;App&amp;gt;!=Result&amp;lt;AppDetailDTO&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//返回特殊的AppDetailDTO
@Override
@GetMapping(&quot;{id}&quot;)
public Result&amp;lt;AppDetailDTO&amp;gt; get(@PathVariable @NotNull String id) {
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;屏蔽父类接口，并声明一个新的方法来实现&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//屏蔽get接口
@Override
@GetMapping(&quot;{id}&quot;)
public Result&amp;lt;T&amp;gt; get(@PathVariable @NotNull String id) {
    throw new UnsupportedOperationException();
}

//声明一个新方法来实现
@GetMapping(&quot;/detail/{id}&quot;)
public Result&amp;lt;AppDetailDTO&amp;gt; getDetail(@PathVariable @NotNull String id) {
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过重新定义一个新的&lt;code&gt;路由&lt;/code&gt;来实现，虽然说可以达到目的，但是感觉不够优雅，&lt;code&gt;/{id}&lt;/code&gt;路由白白就浪费了，这个时候就只能通过自定义&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;来实现了。&lt;/p&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;通过上面的分析可以得知，在应用启动时&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;会去扫描所有的&lt;code&gt;handle&lt;/code&gt;进行关系映射，可不可以实现一个注解，在扫描某个方法时，如果有该注解修饰的时候就跳过。&lt;/p&gt;
&lt;p&gt;根据源码可以得知&lt;code&gt;getMappingForMethod&lt;/code&gt;，是扫描&lt;code&gt;method&lt;/code&gt;的处理入口，方法签名如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;protected RequestMappingInfo getMappingForMethod(Method method, Class&amp;lt;?&amp;gt; handlerType)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法可以拿到&lt;code&gt;Method&lt;/code&gt;，只有重写该方法并且判断&lt;code&gt;Method&lt;/code&gt;上有自定义的注解修饰直接返回 null 就可以达到取消路由注册的目的了。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;定义一个&lt;code&gt;@Disable&lt;/code&gt;注解，用于标识方法不进行路由注册：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Disable {
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过实现&lt;code&gt;WebMvcRegistrations&lt;/code&gt;接口来自定义&lt;code&gt;RequestMappingHandlerMapping&lt;/code&gt;类，并重写&lt;code&gt;getMappingForMethod&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
@ConditionalOnWebApplication
public class WebAutoConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new RequestMappingHandlerMapping() {
            @Override
            @Nullable
            protected RequestMappingInfo getMappingForMethod(Method method, Class&amp;lt;?&amp;gt; handlerType) {
                //如果方法上有@Disable注解，直接返回null
                if (AnnotationUtils.findAnnotation(method, Disable.class) != null) {
                    return null;
                }
                //否则还是按照以前的逻辑进行处理
                return super.getMappingForMethod(method, handlerType);
            }
        };
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样之前的需求就可以解决了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//屏蔽get接口
@Disable
@Override
@GetMapping(&quot;{id}&quot;)
public Result&amp;lt;T&amp;gt; get(@PathVariable @NotNull String id) {
    throw new UnsupportedOperationException();
}

//声明一个新方法来实现，并且路由不变
@GetMapping(&quot;{id}&quot;)
public Result&amp;lt;AppDetailDTO&amp;gt; getDetail(@PathVariable @NotNull String id) {
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;父类的方法用&lt;code&gt;@Disable&lt;/code&gt;注解修饰了，SpringMVC 并不会加载这个路由，在项目重启的时候就不会报错提示有两个相同的路由存在。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;不要为了看源码而看源码，而是带着问题去看框架的源码才是有意义的。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>k8s通过coredns配置CNAME</title><link>https://monkeywie.cn/posts/k8s-coredns-cname</link><guid isPermaLink="true">https://monkeywie.cn/posts/k8s-coredns-cname</guid><pubDate>Sun, 28 Jun 2020 13:51:38 GMT</pubDate><content:encoded>&lt;h3&gt;背景&lt;/h3&gt;
&lt;p&gt;在一次升级阿里云 k8s 版本之后暴露出来一个问题，一般在 k8s 集群中都会使用&lt;code&gt;service域名&lt;/code&gt;来进行服务之间访问，但是为了在本地开发时能访问到这些服务，又会通过&lt;code&gt;ingress&lt;/code&gt;暴露在外网中，这样在开发的时候就可以直接使用的&lt;code&gt;ingress&lt;/code&gt;暴露的外网域名进行访问。&lt;/p&gt;
&lt;p&gt;按理说本地开发环境的时候使用&lt;code&gt;外网域名&lt;/code&gt;，在部署到 k8s 的时候应该使用 k8s 内部的&lt;code&gt;service域名&lt;/code&gt;就什么事都没有了，然而在没有强约束的情况下很多项目&lt;code&gt;调用集群内部服务的时候还是使用的外网域名&lt;/code&gt;，然而这样可能会导致服务调用失败，没想到吧。对此我们咨询了阿里云，给我们的回答是：
&lt;img src=&quot;k8s-coredns-cname/2020-06-28-14-26-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;好吧，按着提示把&lt;code&gt;externalTrafficPolicy&lt;/code&gt;选项修改为&lt;code&gt;Cluster&lt;/code&gt;，确实问题解决了，但是这样一改又把&lt;code&gt;源IP&lt;/code&gt;丢失了，在后端服务中获取的&lt;code&gt;IP&lt;/code&gt;都变成了&lt;code&gt;Node IP&lt;/code&gt;，这肯定也不行。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;所以目前只有一种解决方案，就是把所有项目中通过&lt;code&gt;外网域名&lt;/code&gt;访问内部服务的域名全部修改成 k8s 内部的&lt;code&gt;service域名&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对此有两种实施方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把所有项目都与对应的开发一一对接，然后通过环境变量将&lt;code&gt;外网域名&lt;/code&gt;设置为&lt;code&gt;service域名&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;通过配置&lt;code&gt;coredns&lt;/code&gt;，实现将外网域名&lt;code&gt;CNAME&lt;/code&gt;到&lt;code&gt;service域名&lt;/code&gt;上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;方案一对于项目少的情况比较好实施，由于我们集群的服务比较多，施成本会非常高，所以还是去调研了方案二。&lt;/p&gt;
&lt;h3&gt;coredns 配置&lt;/h3&gt;
&lt;p&gt;在 coredns 官网查阅资料之后，得知可以使用&lt;a href=&quot;https://coredns.io/plugins/rewrite/&quot;&gt;rewrite&lt;/a&gt;插件来实现&lt;code&gt;CNAME&lt;/code&gt;，测试如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置&lt;code&gt;www.baidu.com&lt;/code&gt; CNAME 到&lt;code&gt;www.taobao.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;.:53 {
    rewrite name www.baidu.com www.taobao.com
    forward . 114.114.114.114
    reload
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;效果&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;dig @localhost www.baidu.com

; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.11.3-1ubuntu1.11-Ubuntu &amp;lt;&amp;lt;&amp;gt;&amp;gt; @localhost www.baidu.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NOERROR, id: 45042
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;www.baidu.com.                 IN      A

;; ANSWER SECTION:
www.baidu.com.          364     IN      CNAME   www.taobao.com.danuoyi.tbcache.com.
www.baidu.com.          33      IN      A       113.96.109.101
www.baidu.com.          33      IN      A       113.96.109.100

;; Query time: 39 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Sun Jun 28 14:47:34 CST 2020
;; MSG SIZE  rcvd: 161
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到已经成功实现了。&lt;/p&gt;
&lt;h3&gt;实施&lt;/h3&gt;
&lt;p&gt;接着就是配置 k8s 集群中的&lt;code&gt;coredns&lt;/code&gt;来实现目的了，&lt;code&gt;coredns&lt;/code&gt;通过配置项&lt;code&gt;coredns&lt;/code&gt;中 key 为&lt;code&gt;Corefile&lt;/code&gt;的配置来挂载成&lt;code&gt;Corefile&lt;/code&gt;，所以只需要修改该配置项即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.:53 {
    cache 30
    errors
    health
    kubernetes cluster.local in-addr.arpa ip6.arpa {
        pods insecure
        fallthrough in-addr.arpa ip6.arpa
    }
    rewrite name saas.test.xxx.cn backend-api.saas-test.svc.cluster.local
    loadbalance
    loop
    prometheus :9153
    forward . /etc/resolv.conf
    reload
    ready
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意第 9 行，&lt;code&gt;saas.test.xxx.cn&lt;/code&gt;是一个外网域名，对应的 service 域名是&lt;code&gt;backend-api.saas-test.svc.cluster.local&lt;/code&gt;，修改好之后等待一分钟左右，&lt;code&gt;coredns&lt;/code&gt;的 reload 插件会自动进行热加载，然后进入 pod 中测试看看是否生效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;k8s-coredns-cname/2020-06-28-14-58-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到&lt;code&gt;backend-api.saas-test.svc.cluster.local&lt;/code&gt;解析出来的 ip 是&lt;code&gt;10.21.7.203&lt;/code&gt;，&lt;code&gt;saas.test.xxx.cn&lt;/code&gt;解析出来的 ip 也是&lt;code&gt;10.21.7.203&lt;/code&gt;，这样的话在集群里通过外网域名请求其实也和直接访问 service 域名一致了。&lt;/p&gt;
&lt;p&gt;接下来只需通过命令&lt;code&gt;kubectl get ingress --all-namespaces&lt;/code&gt;把所有 ingress 暴露的外网域名和对应的 service 进行关联，并生成对应的配置加入&lt;code&gt;Corefile&lt;/code&gt;中即可。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>linux下的全局代理工具proxychain</title><link>https://monkeywie.cn/posts/linux-global-proxy-tool-proxychain</link><guid isPermaLink="true">https://monkeywie.cn/posts/linux-global-proxy-tool-proxychain</guid><pubDate>Mon, 06 Jul 2020 11:52:35 GMT</pubDate><content:encoded>&lt;h2&gt;proxychain 介绍&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;本文介绍的是&lt;a href=&quot;https://github.com/rofl0r/proxychains-ng&quot;&gt;proxychains-ng&lt;/a&gt;项目&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 linux 上运行一些命令的时候，经常访问到国外的网站，速度非常的慢，例如用&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;wget&lt;/code&gt;等等，这个时候就可以通过&lt;code&gt;proxychain&lt;/code&gt;工具来使用代理进行网络访问，使用教程如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone git@github.com:rofl0r/proxychains-ng.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在所有要运行的命令行之前加上&lt;code&gt;proxychains4&lt;/code&gt;就可以通过代理进行网络访问了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;h3&gt;Ubuntu&lt;/h3&gt;
&lt;p&gt;直接通过 apt 包管理工具就可以安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install -y proxychains4
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;源码构建&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:rofl0r/proxychains-ng.git
sudo make
sudo make install
sudo make install-config
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;安装完之后可以找到&lt;code&gt;/etc/proxychains.conf&lt;/code&gt;或&lt;code&gt;/etc/proxychains4.conf&lt;/code&gt;文件进行修改，一般请求下翻到最后一段修改代理服务器配置即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ProxyList]
# add proxy here ...
# meanwile
# defaults set to &quot;tor&quot;
socks5  192.168.56.1 1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我设置的 socks5 代理，还支持&lt;code&gt;http&lt;/code&gt;、&lt;code&gt;socks4&lt;/code&gt;协议的代理，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#        Examples:
#
#               socks5  192.168.67.78   1080    lamer   secret
#               http    192.168.89.3    8080    justu   hidden
#               socks4  192.168.1.49    1080
#               http    192.168.39.93   8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然以上内容在&lt;code&gt;/etc/proxychains.conf&lt;/code&gt;中都可以看到。&lt;/p&gt;
&lt;h2&gt;设置别名&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;proxychains4&lt;/code&gt;这个命令比较长不太好记，我通过&lt;code&gt;alias&lt;/code&gt;给它设置了一个别名&lt;code&gt;pc&lt;/code&gt;，修改&lt;code&gt;~/.profile&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias pc=proxychains4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷新 profile&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source ~/.profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pc curl -I https://www.google.com
[proxychains] config file found: /etc/proxychains.conf
[proxychains] preloading /usr/lib/libproxychains4.so
[proxychains] DLL init: proxychains-ng 4.14-git-8-gb8fa2a7
[proxychains] Strict chain  ...  192.168.56.1:1080  ...  www.google.com:443  ...  OK
HTTP/2 200
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到已经能够成功访问&lt;code&gt;google&lt;/code&gt;了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>IDEA插件推荐之Maven-Helper</title><link>https://monkeywie.cn/posts/idea-maven-helper-plugin</link><guid isPermaLink="true">https://monkeywie.cn/posts/idea-maven-helper-plugin</guid><pubDate>Mon, 13 Jul 2020 16:56:38 GMT</pubDate><content:encoded>&lt;h3&gt;Maven-Helper 插件介绍&lt;/h3&gt;
&lt;p&gt;这个插件能可以通过 UI 界面的方式来查看 maven 项目的依赖关系，当然还有最重要的功能&lt;code&gt;解决依赖冲突&lt;/code&gt;，使用起来非常的方便，效果图：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-01-29.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在开发 JAVA 项目的时候，经常会由于 maven 依赖冲突导致项目启动失败，这个时候往往会懵逼，到底哪个依赖冲突了，需要排除哪个子依赖，有了这个插件之后就可以很快的定位到冲突的组件，并进行排除。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;首先进入 IDEA plugins 市场，搜索&lt;code&gt;Maven Helper&lt;/code&gt;进行安装：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-08-12.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里需要注意的是国内网络连接 jetbrains 服务器会比较慢，可能需要梯子，通过右上角的设置按钮进行代理设置：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-08-56.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-09-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;安装完成之后重启 IDEA，打开项目里的&lt;code&gt;pom.xml&lt;/code&gt;文件，就可以看到左下角有一个&lt;code&gt;Dependency Analyzer&lt;/code&gt;选项卡，点进去就可以看到界面了：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-10-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;排除冲突&lt;/h3&gt;
&lt;p&gt;选中&lt;code&gt;Conflicts&lt;/code&gt;插件就会列出有冲突的依赖，然后选择某个依赖就可以看到详细信息：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-13-41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到这个例子，在此项目中有两个&lt;code&gt;hutool-all&lt;/code&gt;版本，如果项目启动失败提示跟此依赖有关的话，就可以通过右键指定版本进行排除：
&lt;img src=&quot;idea-maven-helper-plugin/2020-07-20-12-15-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击&lt;code&gt;Exclude&lt;/code&gt;之后，插件自动会在&lt;code&gt;pom.xml&lt;/code&gt;文件中添加&lt;code&gt;&amp;lt;exclusion&amp;gt;&lt;/code&gt;相关代码，进行子依赖的排除。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Spring AOP调用本类方法没有生效的问题</title><link>https://monkeywie.cn/posts/spring-aop-call-self-method</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-aop-call-self-method</guid><pubDate>Wed, 22 Jul 2020 17:54:25 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;首先请思考一下以下代码执行的结果：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LogAop.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//声明一个AOP拦截service包下的所有方法
@Aspect
public class LogAop {

  @Around(&quot;execution(* com.demo.service.*.*(..))&quot;)
  public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
      try {
          MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
          Method method = methodSignature.getMethod();
          Object ret = joinPoint.proceed();
          //执行完目标方法之后打印
          System.out.println(&quot;after execute method:&quot;+method.getName());
          return ret;
      } catch (Throwable throwable) {
          throw throwable;
      }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UserService.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService{

  public User save(User user){
    //省略代码
  }

  public void sendEmail(User user){
    //省略代码
  }

  //注册
  public void register(User user){
    //保存用户
    this.save(user);
    //发送邮件给用户
    this.sendEmail(user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;UserServiceTest.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@SpringBootTest
public class UserServiceTest{

  @Autowired
  private UserService userService;

  @Test
  public void save(){
    userService.save(new User());
  }

  @Test
  public void sendEmail(){
    userService.sendEmail(new User());
  }

  @Test
  public void register(){
    userService.register(new User());
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行&lt;code&gt;save&lt;/code&gt;方法后，控制台输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在执行&lt;code&gt;sendEmail&lt;/code&gt;方法后，控制台输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:sendEmail
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请问在执行&lt;code&gt;register()&lt;/code&gt;方法后会打印出什么内容？&lt;/p&gt;
&lt;h2&gt;反直觉&lt;/h2&gt;
&lt;p&gt;这个时候可能很多人都会和我之前想的一样，在&lt;code&gt;register&lt;/code&gt;方法里调用了&lt;code&gt;save&lt;/code&gt;和&lt;code&gt;sendEmail&lt;/code&gt;，那 AOP 会处理&lt;code&gt;save&lt;/code&gt;和&lt;code&gt;sendEmail&lt;/code&gt;，输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:save
after execute method:sendEmail
after execute method:register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而事实并不是这样的，而是输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种认知的情况下，很可能就会写出有&lt;code&gt;bug&lt;/code&gt;的代码，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService{
  //用户下单一个商品
  public void order(User user,String orderId){
    Order order = findOrder(orderId);
    pay(user,order);
  }

  @Transactional
  public void pay(User user,Order order){
    //扣款
    user.setMoney(user.getMoney()-order.getPrice());
    save(user);
    //...其它处理
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当用户下单时调用的&lt;code&gt;order&lt;/code&gt;方法，在该方法里面调用了&lt;code&gt;@Transactional&lt;/code&gt;注解修饰的&lt;code&gt;pay&lt;/code&gt;方法，这个时候&lt;code&gt;pay&lt;/code&gt;方法的事务管理已经不生效了，在发生异常时就会出现问题。&lt;/p&gt;
&lt;h2&gt;理解 AOP&lt;/h2&gt;
&lt;p&gt;我们知道 Spring AOP 默认是基于动态代理来实现的，那么先化繁为简，只要搞懂最基本的动态代理自然就明白之前的原因了，这里直接以 JDK 动态代理为例来演示一下上面的情况。&lt;/p&gt;
&lt;p&gt;由于 JDK 动态代理一定需要接口类，所以首先声明一个&lt;code&gt;IUserService&lt;/code&gt;接口&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IUserService.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public interface IUserService{
  User save(User user);
  void sendEmail(User user);
  User register(User user);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写实现类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UserService.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UserService implements IUserService{

  @Override
  public User save(User user){
    //省略代码
  }

  @Override
  public void sendEmail(User user){
    //省略代码
  }

  //注册
  @Override
  public void register(User user){
    //保存用户
    this.save(user);
    //发送邮件给用户
    this.sendEmail(user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写日志处理动态代理实现&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ServiceLogProxy.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static class ServiceLogProxy {
    public static Object getProxy(Class&amp;lt;?&amp;gt; clazz, Object target) {
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{clazz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Object ret = method.invoke(target, args);
                    System.out.println(&quot;after execute method:&quot; + method.getName());
                    return ret;
                }
            });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行代码&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Main.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Main{
    public static void main(String[] args) {
        //获取代理类
        IUserService userService = (IUserService) ServiceLogProxy.getProxy(IUserService.class, new UserService());
        userService.save(new User());
        userService.sendEmail(new User());
        userService.register(new User());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:save
after execute method:sendEmail
after execute method:register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以发现和之前 Spring AOP 的情况一样，&lt;code&gt;register&lt;/code&gt;方法中调用的&lt;code&gt;save&lt;/code&gt;和&lt;code&gt;sendEmail&lt;/code&gt;方法同样的没有被动态代理拦截到，这是为什么呢，接下来就看看下动态代理的底层实现。&lt;/p&gt;
&lt;h2&gt;动态代理原理&lt;/h2&gt;
&lt;p&gt;其实动态代理就是在运行期间动态的生成了一个&lt;code&gt;class&lt;/code&gt;在 jvm 中，然后通过这个&lt;code&gt;class&lt;/code&gt;的实例调用真正的实现类的方法，伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class $Proxy0 implements IUserService{

  //这个类就是之前动态代理里的new InvocationHandler(){}对象
  private InvocationHandler h;
  //从接口中拿到的register Method
  private Method registerMethod;

  @Override
  public void register(User user){
    //执行前面ServiceLogProxy编写好的invoke方法，实现代理功能
    h.invoke(this,registerMethod,new Object[]{user})
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回到刚刚的&lt;code&gt;main&lt;/code&gt;方法，那个&lt;code&gt;userService&lt;/code&gt;变量的实例类型其实就是动态生成的类，可以把它的 class 打印出来看看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IUserService userService = (IUserService) ServiceLogProxy.getProxy(IUserService.class, new UserService());
System.out.println(userService.getClass());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xxx.xxx.$Proxy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在了解这个原理之后，再接着解答之前的疑问，可以看到通过&lt;code&gt;代理类的实例&lt;/code&gt;执行的方法才会进入到拦截处理中，而在&lt;code&gt;InvocationHandler#invoke()&lt;/code&gt;方法中，是这样执行目标方法的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//注意这个target是new UserService()实例对象
Object ret = method.invoke(target, args);
System.out.println(&quot;after execute method:&quot; + method.getName());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;register&lt;/code&gt;方法中调用&lt;code&gt;this.save&lt;/code&gt;和&lt;code&gt;this.sendEmail&lt;/code&gt;方法时，&lt;code&gt;this&lt;/code&gt;是指向本身&lt;code&gt;new UserService()&lt;/code&gt;实例，所以本质上就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;User user = new User();
UserService userService = new UserService();
userService.save(user);
userService.sendEmail(user);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不是动态代理生成的类去执行目标方法，那必然不会进行动态代理的拦截处理中，明白这个之后原理之后，就可以改造下之前的方法，让方法内调用本类方法也能使动态代理生效，就是用动态代理生成的类去调用方法就好了，改造如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;UserService.java&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class UserService implements IUserService{

  //注册
  @Override
  public void register(User user){
    //获取到代理类
    IUserService self = (IUserService) ServiceLogProxy.getProxy(IUserService.class, this);
    //通过代理类保存用户
    self.save(user);
    //通过代理类发送邮件给用户
    self.sendEmail(user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;main&lt;/code&gt;方法，结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;after execute method:save
after execute method:sendEmail
after execute method:save
after execute method:sendEmail
after execute method:register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到已经达到预期效果了。&lt;/p&gt;
&lt;h2&gt;Spring AOP 中方法调用本类方法的解决方案&lt;/h2&gt;
&lt;p&gt;同样的，只要使用代理类来执行目标方法就行，而不是用&lt;code&gt;this&lt;/code&gt;引用，修改后如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserService{

  //拿到代理类
  @Autowired
  private UserService self;

  //注册
  public void register(User user){
    //通过代理类保存用户
    self.save(user);
    //通过代理类发送邮件给用户
    self.sendEmail(user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好了，问题到此就解决了，但是需要注意的是&lt;code&gt;Spring&lt;/code&gt;官方是不提倡这样的做法的，官方提倡的是使用一个新的类来解决此类问题，例如创建一个&lt;code&gt;UserRegisterService&lt;/code&gt;类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Service
public class UserRegisterService{
  @Autowired
  private UserService userService;

  //注册
  public void register(User user){
    //通过代理类保存用户
    userService.save(user);
    //通过代理类发送邮件给用户
    userService.sendEmail(user);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://monkeywie.github.io/2018/07/25/jvm-dump-class/&quot;&gt;从JVM中拿到动态代理生成的class文件&lt;/a&gt;
&lt;a href=&quot;https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-understanding-aop-proxies&quot;&gt;aop-understanding-aop-proxies&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>给博客的next主题升个级</title><link>https://monkeywie.cn/posts/upgrade-hexo-next</link><guid isPermaLink="true">https://monkeywie.cn/posts/upgrade-hexo-next</guid><pubDate>Fri, 07 Aug 2020 10:08:15 GMT</pubDate><content:encoded>&lt;h2&gt;为什么升级&lt;/h2&gt;
&lt;p&gt;最近突然想把博客的评论系统换掉，因为之前用的&lt;code&gt;valine&lt;/code&gt;评论插件，起初是觉得方便并且可以支持匿名评论，但是在评论之后没有通知，很多评论都是过了很久之后我才知道，所以想换一个能有通知的评论系统，起初的选型是：&lt;code&gt;Gitment&lt;/code&gt;和&lt;code&gt;Gitalk&lt;/code&gt;，但是发现一个更好的&lt;code&gt;utterances&lt;/code&gt;，查了一下 next 高版本已经可以支持&lt;code&gt;utterances&lt;/code&gt;了，这样升级 next 就直接接入了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;为什么选择 utterances&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;utterances&lt;/code&gt;这款评论组件和&lt;code&gt;Gitalk&lt;/code&gt;、&lt;code&gt;Gitment&lt;/code&gt;一样都是基于 Github 的 issues 实现的，但是与之不同的是，&lt;code&gt;Gitalk&lt;/code&gt;、&lt;code&gt;Gitment&lt;/code&gt;是通过&lt;code&gt;OAuth Apps&lt;/code&gt;实现的，在使用时会申请评论者对所有公共仓库的读写权限：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;upgrade-hexo-next/2020-08-07-10-52-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;就相当于只要评论者在确认授权之后，就可以使用评论者的 github 账号来对公共仓库做任何的事情，这风险是非常高点。&lt;/p&gt;
&lt;p&gt;平常我想评论别人博客的时候，在授权时看到上面图片的提示，就会直接放弃评论了，因为无法确定别人网站里会不会偷偷摸摸的做一些坏事，例如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;upgrade-hexo-next/2020-08-07-11-28-16.png&quot; alt=&quot;来自：https://www.v2ex.com/t/534800&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;utterances&lt;/code&gt;是通过&lt;code&gt;Github Apps&lt;/code&gt;实现的，权限粒度划分的很细，可以单独对某个仓库进行授权，并且只对&lt;code&gt;issues模块授权&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;upgrade-hexo-next/2020-08-07-11-56-41.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样的话安全性就有保障。&lt;/p&gt;
&lt;h2&gt;升级步骤&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;删除&lt;code&gt;./themes/next&lt;/code&gt;目录&lt;/li&gt;
&lt;li&gt;下载最新版本的&lt;code&gt;next&lt;/code&gt;主题，这里我使用的&lt;code&gt;v7.8.0&lt;/code&gt;版本：&lt;pre&gt;&lt;code&gt;git clone --depth 1 --branch v7.8.0 git@github.com:theme-next/hexo-theme-next.git themes/next
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;删除&lt;code&gt;.git&lt;/code&gt;目录：&lt;pre&gt;&lt;code&gt;rm -rf themes/next/.git
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;themes/next/_config.yml&lt;/code&gt;文件，如果之前有配置或定制一些功能的话需要按照新版本的规范重新设置一次&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;utterances 安装&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;next-utterances&lt;/code&gt;插件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install github:theme-next/hexo-next-utteranc
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：插件还未发布到 npm 仓库，所以指定从 github 中拉取&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;next-util&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install next-util --registry=https://registry.npmjs.org
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：一定要使用 npm 官方源进行安装，使用淘宝源下载不到，详见：&lt;a href=&quot;https://github.com/cnpm/npm.taobao.org/issues/63&quot;&gt;https://github.com/cnpm/npm.taobao.org/issues/63&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 GitHub 上安装&lt;code&gt;utterances&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;访问：&lt;a href=&quot;https://github.com/apps/utterances&quot;&gt;https://github.com/apps/utterances&lt;/a&gt;进行安装
&lt;img src=&quot;upgrade-hexo-next/2020-08-07-16-29-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;授权时只选择存放评论的仓库
&lt;img src=&quot;upgrade-hexo-next/2020-08-07-16-33-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改&lt;code&gt;themes/next/_config.yml&lt;/code&gt;，添加以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;utteranc:
  enable: true
  repo: monkeyWie/monkeywie.github.io
  pathname: pathname
  # theme: github-light,github-dark,github-dark-orange
  theme: github-light
  cdn: https://utteranc.es/client.js
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：只需要修改&lt;code&gt;repo&lt;/code&gt;即可&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就升级完成了，重新部署博客就可以看到评论组件了。
&lt;img src=&quot;upgrade-hexo-next/2020-08-07-16-57-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;最后谈一谈这个组件的缺点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;由于&lt;code&gt;utteranc.es&lt;/code&gt;网站是国外的，国内访问的时候评论组件加载速度比较慢&lt;/li&gt;
&lt;li&gt;如果这个网站关闭走人，那么评论组件就失效了，不过所有评论都存在 GitHub 上，所以数据是不会丢失的&lt;/li&gt;
&lt;li&gt;目前该评论组件只支持英文，并且不支持国际化，但是由于内容也不多，所以可以忽略&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Levi</author></item><item><title>我云了，原来wireshark可以抓HTTPS明文包</title><link>https://monkeywie.cn/posts/wireshark-capture-https</link><guid isPermaLink="true">https://monkeywie.cn/posts/wireshark-capture-https</guid><pubDate>Fri, 07 Aug 2020 17:30:49 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;以前在使用&lt;code&gt;wireshark&lt;/code&gt;做协议分析的时候，一直以为它只能抓 HTTP 的报文，所以在抓 HTTPS 包的时候一直是用的&lt;code&gt;Fiddler&lt;/code&gt;，然而有一天我突然想抓一下&lt;code&gt;HTTP2&lt;/code&gt;的报文看一看，&lt;code&gt;Fiddler&lt;/code&gt;就不行了，于是在一番 google 之后发现&lt;code&gt;wireshark&lt;/code&gt;是可以支持的，只不过需要在特定的条件下才可以。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;Fiddler 存在的问题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Fiddler&lt;/code&gt;目前还不支持&lt;code&gt;HTTP2&lt;/code&gt;协议，无法看到真正的&lt;code&gt;HTTP2&lt;/code&gt;报文，这里有些人可能会有疑问，说我明明就用&lt;code&gt;Fiddler&lt;/code&gt;抓到了&lt;code&gt;HTTP2&lt;/code&gt;协议的报文啊，那是因为&lt;code&gt;Fiddler&lt;/code&gt;中间人攻击服务器通过协商把协议降级成了&lt;code&gt;HTTP1&lt;/code&gt;协议，所以实际上看到的还是的&lt;code&gt;HTTP1&lt;/code&gt;的报文，通过下面两个图片可以直观的感受到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不通过代理，直接访问支持 HTTP2 的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;wireshark-capture-https/2020-08-10-10-10-24.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过代理访问，进行抓包&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;wireshark-capture-https/2020-08-10-10-11-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到在通过代理抓包的时候，协议变成了&lt;code&gt;http/1.1&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;使用 wireshark 抓取&lt;/h2&gt;
&lt;p&gt;现在市面上的主流浏览器实现的 HTTP2 都是基于&lt;code&gt;TLS&lt;/code&gt;的，也就是说要分析&lt;code&gt;HTTP2&lt;/code&gt;报文得先过了&lt;code&gt;TLS&lt;/code&gt;这一关，不然只能分析一堆加密的乱码。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wireshark&lt;/code&gt;支持两种方式来解密&lt;code&gt;SSL/TLS&lt;/code&gt;报文：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过网站的私钥&lt;/li&gt;
&lt;li&gt;通过浏览器的将 TLS 对称加密秘保存在外部文件中，以供 wireshark 加解密&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面我来一一进行演示&lt;/p&gt;
&lt;h3&gt;1. 通过网站的私钥&lt;/h3&gt;
&lt;p&gt;如果你想抓取的网站是你自己的，那么可以利用这种方式，因为这需要使用网站生成证书使用的私钥进行解密，就是那个 nginx 上&lt;code&gt;ssl_certificate_key&lt;/code&gt;配置对应的私钥文件，把它添加到 wireshark 配置中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wireshark-capture-https/2020-08-10-10-41-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后通过&lt;code&gt;wireshark&lt;/code&gt;就可以看到明文了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wireshark-capture-https/2020-08-10-11-10-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过上图可以看到，我通过&lt;code&gt;curl&lt;/code&gt;访问的 https 协议的 URL，在配置了该服务器对应的私钥后可以抓取到对应的 HTTP 明文。&lt;/p&gt;
&lt;p&gt;不过缺点也非常明显，只能分析自己持有私钥的网站，如果别人的网站就分析不了了，所幸的是还有第二种方案来支持。&lt;/p&gt;
&lt;h3&gt;2. 通过浏览器的 SSL 日志功能&lt;/h3&gt;
&lt;p&gt;目前该方案只支持&lt;code&gt;Chrome&lt;/code&gt;和&lt;code&gt;Firefox&lt;/code&gt;浏览器，通过设置&lt;code&gt;SSLKEYLOGFILE&lt;/code&gt;环境变量，可以指定浏览器在访问&lt;code&gt;SSL/TLS&lt;/code&gt;网站时将对应的密钥保存到本地文件中，有了这个日志文件之后&lt;code&gt;wireshake&lt;/code&gt;就可以将报文进行解密了。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先设置&lt;code&gt;SSLKEYLOGFILE&lt;/code&gt;环境变量：
&lt;img src=&quot;wireshark-capture-https/2020-08-10-11-19-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：这是在 windows 系统上进行操作的，其它操作系统同理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置&lt;code&gt;wireshake&lt;/code&gt;，首选项-&amp;gt;Protocls-&amp;gt;TLS：
&lt;img src=&quot;wireshark-capture-https/2020-08-10-11-23-31.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将第一步中指定的文件路径配置好&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启浏览器，进行抓包：
&lt;img src=&quot;wireshark-capture-https/2020-08-10-11-30-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同样的可以抓取到 HTTP 明文。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：不抓包时记得把环境变量删掉，以避免性能浪费和安全性问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;方案二的优点非常明显，可以抓取任意网站的&lt;code&gt;SSL/TLS&lt;/code&gt;加密的报文，唯一的缺点就是只能是浏览器支持的情况才行，而方案一可以针对任何 HTTP 客户端进行抓包。&lt;/p&gt;
&lt;h2&gt;通过 wireshake 抓取 HTTP2 报文&lt;/h2&gt;
&lt;p&gt;上面都是针对&lt;code&gt;TLS+HTTP1&lt;/code&gt;进行的抓包，市面上主流的浏览器的&lt;code&gt;HTTP2&lt;/code&gt;都是基于&lt;code&gt;TLS&lt;/code&gt;实现的，所以也是一样的，把&lt;code&gt;TLS&lt;/code&gt;这层解密了自然看到的就是最原始的明文。&lt;/p&gt;
&lt;p&gt;这里以分析&lt;code&gt;https://www.qq.com&lt;/code&gt;为例，为什么不是经典&lt;code&gt;htts://www.baidu.com&lt;/code&gt;，因为百度首页至今还是&lt;code&gt;HTTP/1.1&lt;/code&gt;协议。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用上面的第二种方案配置好&lt;code&gt;wiresharke&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;http2&lt;/code&gt;关键字做过滤&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;浏览器访问&lt;code&gt;https://www.qq.com&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看&lt;code&gt;HTTP2&lt;/code&gt;报文：
&lt;img src=&quot;wireshark-capture-https/2020-08-10-11-47-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样就抓取到了&lt;code&gt;HTTP2&lt;/code&gt;报文了，HTTP2 协议非常复杂，我也还在学习阶段，这里就不多说啥了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;wireshake 真的是一款非常强大的网络分析工具，在&lt;code&gt;HTTPS&lt;/code&gt;和&lt;code&gt;HTTP2&lt;/code&gt;日渐成为主流的时候，可以用它来帮助我们加深对这些协议的理解，以便迎接新的机遇与挑战。&lt;/p&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://imququ.com/post/http2-traffic-in-wireshark.html&quot;&gt;使用 Wireshark 调试 HTTP/2 流量&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>使用cloudflare免费加速github page</title><link>https://monkeywie.cn/posts/fast-github-page-with-cloudflare</link><guid isPermaLink="true">https://monkeywie.cn/posts/fast-github-page-with-cloudflare</guid><pubDate>Thu, 20 Aug 2020 17:45:18 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;github page 在国内访问速度非常慢，而且近期 github.io 的域名经常被干扰解析成&lt;code&gt;127.0.0.1&lt;/code&gt;，迫于无奈在网上找到了一个能白嫖加速 github page 的办法，就是套一层 cloudflare CDN，虽然它在国内没有 CDN 节点，但是整体效果是完爆 github.io，不过要注意的是免费版本是有请求次数限制的，每天 10W 次，当然这足够我的小博客使用了，这里记录一下操作步骤。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;准备&lt;/h2&gt;
&lt;h3&gt;准备域名&lt;/h3&gt;
&lt;p&gt;虽然说是白嫖，但是还是得买个域名，不过域名很便宜，我的域名&lt;code&gt;monkeywie.cn&lt;/code&gt;买了 10 年也就 300 多块钱，买好域名之后现在一般会要求实名认证，所以先完成实名认证，示例图：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-08-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;设置 github page&lt;/h3&gt;
&lt;p&gt;通过 github 仓库中的设置页面找到对应的设置，把要用到的域名配置上去，示例图：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-10-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;保存之后 github 会&lt;code&gt;自动&lt;/code&gt;的在仓库根目录里生成一个&lt;code&gt;CNAME&lt;/code&gt;文件，里面存储着域名配置信息，示例图：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-12-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;设置域名解析&lt;/h3&gt;
&lt;p&gt;通过域名提供商，修改刚刚的域名解析，通过 A 记录分别解析到以下 4 个 IP：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例图：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-21-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;设置完之后通过命令行验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dig monkeywie.cn +noall +answer
&amp;gt; monkeywie.cn     3600    IN A     185.199.108.153
&amp;gt; monkeywie.cn     3600    IN A     185.199.109.153
&amp;gt; monkeywie.cn     3600    IN A     185.199.110.153
&amp;gt; monkeywie.cn     3600    IN A     185.199.111.153
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当记录全部解析生效时，就可以通过&lt;code&gt;http://monkeywie.cn&lt;/code&gt;访问到博客了，这个时候再开启&lt;code&gt;HTTPS&lt;/code&gt;，示例图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-24-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后 github 会自动签发提供给&lt;code&gt;monkeywie.cn&lt;/code&gt;域名使用的 SSL 证书，等待一段时间后，就可以通过&lt;code&gt;HTTPS&lt;/code&gt;访问博客了。&lt;/p&gt;
&lt;h3&gt;使用 cloudflare CDN&lt;/h3&gt;
&lt;p&gt;上面的步骤全部就绪之后，就可以开始白嫖之路了&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先通过&lt;a href=&quot;https://dash.cloudflare.com/sign-up&quot;&gt;https://dash.cloudflare.com/sign-up&lt;/a&gt;链接进行注册&lt;/li&gt;
&lt;li&gt;添加站点，把对应的域名填写进去：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-31-57.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;提交之后会自动扫描域名对应的解析记录：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-32-34.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;查看 cloudfalre 对应的 NS 记录：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-33-46.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;通过域名的运营商修改对应的 NS 记录，这里每个运营商的修改方式都不一样，我这里是用的阿里云的：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-36-57.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-37-25.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样就设置完毕了，等一段时间再用命令行验证一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dig monkeywie.cn +noall +answer
&amp;gt; monkeywie.cn.		600	IN	A	104.28.28.212
&amp;gt; monkeywie.cn.		600	IN	A	172.67.169.202
&amp;gt; monkeywie.cn.		600	IN	A	104.28.29.212
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 dns 解析的 ip 已经变了，已经被 cloudflare 接管了，
然后清除下浏览器 DNS 缓存，chrome 浏览器输入&lt;code&gt;chrome://net-internals/#dns&lt;/code&gt;进入清除页：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-41-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再次访问&lt;code&gt;https://monkeywie.cn&lt;/code&gt;，F12 打开网络面板可以看到已经用上了 CDN 了：
&lt;img src=&quot;fast-github-page-with-cloudflare/2020-08-20-18-43-05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;一直白嫖一直爽，但是&lt;code&gt;cloudflare&lt;/code&gt;不一定一直会提供免费版的，如果有一天它挂了，只需要把 DNS 的 NS 解析记录再还原回去就行了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>debian快速设置apt源</title><link>https://monkeywie.cn/posts/debian-apt-mirros</link><guid isPermaLink="true">https://monkeywie.cn/posts/debian-apt-mirros</guid><pubDate>Wed, 02 Sep 2020 15:50:18 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;使用 docker 的时候为了排查问题经常需要下载一些软件包，但是一般镜像中都没有&lt;code&gt;vim&lt;/code&gt;，如果直接用&lt;code&gt;apt&lt;/code&gt;官方源去下载，基本上就是下面这样：
&lt;img src=&quot;debian-apt-mirros/2020-09-02-15-54-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是因为没有更新源，需要通过&lt;code&gt;apt-get update&lt;/code&gt;进行更新，但是国内访问官方源的速度实在是太慢，要修改成国内的源镜像去加速，这里记录下没有&lt;code&gt;vim&lt;/code&gt;的情况下如何快速修改源地址。&lt;/p&gt;
&lt;h2&gt;使用清华大学镜像&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sed -i &apos;s/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g&apos; /etc/apt/sources.list
sed -i &apos;s|security.debian.org/debian-security|mirrors.tuna.tsinghua.edu.cn/debian-security|g&apos; /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;更新和安装软件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;apt-get update
apt-get install -y net-tools
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>为什么HTTPS是安全的</title><link>https://monkeywie.cn/posts/why-https-secure</link><guid isPermaLink="true">https://monkeywie.cn/posts/why-https-secure</guid><pubDate>Mon, 07 Sep 2020 13:40:06 GMT</pubDate><content:encoded>&lt;h2&gt;1. HTTP 协议&lt;/h2&gt;
&lt;p&gt;在谈论 HTTPS 协议之前，先来回顾一下 HTTP 协议的概念。&lt;/p&gt;
&lt;h3&gt;1.1 HTTP 协议介绍&lt;/h3&gt;
&lt;p&gt;HTTP 协议是一种基于文本的传输协议，它位于 OSI 网络模型中的&lt;code&gt;应用层&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-42-05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;HTTP 协议是通过客户端和服务器的请求应答来进行通讯，目前协议由之前的 &lt;a href=&quot;https://tools.ietf.org/html/rfc2616&quot;&gt;RFC 2616&lt;/a&gt; 拆分成立六个单独的协议说明（&lt;a href=&quot;https://tools.ietf.org/html/rfc7230&quot;&gt;RFC 7230&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/rfc7231&quot;&gt;RFC 7231&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/rfc7232&quot;&gt;RFC 7232&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/rfc7233&quot;&gt;RFC 7233&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/rfc7234&quot;&gt;RFC 7234&lt;/a&gt;、&lt;a href=&quot;https://tools.ietf.org/html/rfc7235&quot;&gt;RFC 7235&lt;/a&gt;），通讯报文如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;POST http://www.baidu.com HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Content-Length: 7
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36

wd=HTTP
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;响应&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Thu, 14 Feb 2019 07:23:49 GMT
Transfer-Encoding: chunked

&amp;lt;html&amp;gt;...&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;1.2 HTTP 中间人攻击&lt;/h3&gt;
&lt;p&gt;HTTP 协议使用起来确实非常的方便，但是它存在一个致命的缺点：&lt;code&gt;不安全&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们知道 HTTP 协议中的报文都是以明文的方式进行传输，不做任何加密，这样会导致什么问题呢？下面来举个例子：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;小明在 JAVA 贴吧发帖，内容为&lt;code&gt;我爱JAVA&lt;/code&gt;：
&lt;img src=&quot;why-https-secure/2020-09-07-13-42-15.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;被中间人进行攻击，内容修改为&lt;code&gt;我爱PHP&lt;/code&gt;
&lt;img src=&quot;why-https-secure/2020-09-07-13-42-23.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;小明被群嘲(手动狗头)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以看到在 HTTP 传输过程中，中间人能看到并且修改 HTTP 通讯中所有的请求和响应内容，所以使用 HTTP 是非常的不安全的。&lt;/p&gt;
&lt;h3&gt;1.3 防止中间人攻击&lt;/h3&gt;
&lt;p&gt;这个时候可能就有人想到了，既然内容是明文那我使用&lt;code&gt;对称加密&lt;/code&gt;的方式将报文加密这样中间人不就看不到明文了吗，于是如下改造：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;双方约定加密方式
&lt;img src=&quot;why-https-secure/2020-09-07-13-43-01.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;使用 AES 加密报文
&lt;img src=&quot;why-https-secure/2020-09-07-13-43-09.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样看似中间人获取不到明文信息了，但其实在通讯过程中还是会以明文的方式暴露加密方式和秘钥，如果第一次通信被拦截到了，那么秘钥就会泄露给中间人，中间人仍然可以解密后续的通信：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-43-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么对于这种情况，我们肯定就会考虑能不能将秘钥进行加密不让中间人看到呢？答案是有的，采用&lt;code&gt;非对称加密&lt;/code&gt;，我们可以通过 RSA 算法来实现。&lt;/p&gt;
&lt;p&gt;在约定加密方式的时候由服务器生成一对&lt;code&gt;公私钥&lt;/code&gt;，服务器将&lt;code&gt;公钥&lt;/code&gt;返回给客户端，客户端本地生成一串秘钥(&lt;code&gt;AES_KEY&lt;/code&gt;)用于&lt;code&gt;对称加密&lt;/code&gt;，并通过服务器发送的&lt;code&gt;公钥&lt;/code&gt;进行加密得到(&lt;code&gt;AES_KEY_SECRET&lt;/code&gt;)，之后返回给服务端，服务端通过&lt;code&gt;私钥&lt;/code&gt;将客户端发送的&lt;code&gt;AES_KEY_SECRET&lt;/code&gt;进行解密得到&lt;code&gt;AEK_KEY&lt;/code&gt;,最后客户端和服务器通过&lt;code&gt;AEK_KEY&lt;/code&gt;进行报文的加密通讯，改造如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-43-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到这种情况下中间人是窃取不到用于&lt;code&gt;AES加密&lt;/code&gt;的秘钥，所以对于后续的通讯是肯定无法进行解密了，那么这样做就是绝对安全了吗？&lt;/p&gt;
&lt;p&gt;所谓道高一尺魔高一丈，中间人为了对应这种加密方法又想出了一个新的破解方案，既然拿不到&lt;code&gt;AES_KEY&lt;/code&gt;，那我就把自己模拟成一个客户端和服务器端的结合体，在&lt;code&gt;用户-&amp;gt;中间人&lt;/code&gt;的过程中中间人模拟服务器的行为，这样可以拿到用户请求的明文，在&lt;code&gt;中间人-&amp;gt;服务器&lt;/code&gt;的过程中中间人模拟客户端行为，这样可以拿到服务器响应的明文，以此来进行中间人攻击：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-43-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这一次通信再次被中间人截获，中间人自己也伪造了一对公私钥，并将公钥发送给用户以此来窃取客户端生成的&lt;code&gt;AES_KEY&lt;/code&gt;，在拿到&lt;code&gt;AES_KEY&lt;/code&gt;之后就能轻松的进行解密了。&lt;/p&gt;
&lt;p&gt;中间人这样为所欲为，就没有办法制裁下吗，当然有啊，接下来我们看看 HTTPS 是怎么解决通讯安全问题的。&lt;/p&gt;
&lt;h2&gt;2. HTTPS 协议&lt;/h2&gt;
&lt;h3&gt;2.1 HTTPS 简介&lt;/h3&gt;
&lt;p&gt;HTTPS 其实是&lt;code&gt;SSL+HTTP&lt;/code&gt;的简称,当然现在&lt;code&gt;SSL&lt;/code&gt;基本已经被&lt;code&gt;TLS&lt;/code&gt;取代了，不过接下来我们还是统一以&lt;code&gt;SSL&lt;/code&gt;作为简称，&lt;code&gt;SSL&lt;/code&gt;协议其实不止是应用在&lt;code&gt;HTTP&lt;/code&gt;协议上，还在应用在各种应用层协议上，例如：&lt;code&gt;FTP&lt;/code&gt;、&lt;code&gt;WebSocket&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;其实&lt;code&gt;SSL&lt;/code&gt;协议大致就和上一节&lt;code&gt;非对称加密&lt;/code&gt;的性质一样，握手的过程中主要也是为了交换秘钥，然后再通讯过程中使用&lt;code&gt;对称加密&lt;/code&gt;进行通讯，大概流程如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-44-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我只是画了个示意图，其实真正的 SSL 握手会比这个复杂的多，但是性质还是差不多，而且我们这里需要关注的重点在于 HTTPS 是如何防止中间人攻击的。&lt;/p&gt;
&lt;p&gt;通过上图可以观察到，服务器是通过 SSL 证书来传递&lt;code&gt;公钥&lt;/code&gt;，客户端会对 SSL 证书进行验证，其中证书认证体系就是确保&lt;code&gt;SSL&lt;/code&gt;安全的关键，接下来我们就来讲解下&lt;code&gt;CA 认证体系&lt;/code&gt;，看看它是如何防止中间人攻击的。&lt;/p&gt;
&lt;h3&gt;2.2 CA 认证体系&lt;/h3&gt;
&lt;p&gt;上一节我们看到客户端需要对服务器返回的 SSL 证书进行校验，那么客户端是如何校验服务器 SSL 证书的安全性呢。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;权威认证机构
在 CA 认证体系中，所有的证书都是由权威机构来颁发，而权威机构的 CA 证书都是已经在操作系统中内置的，我们把这些证书称之为&lt;code&gt;CA根证书&lt;/code&gt;：
&lt;img src=&quot;why-https-secure/2020-09-07-13-44-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;签发证书
我们的应用服务器如果想要使用 SSL 的话，需要通过权威认证机构来签发&lt;code&gt;CA证书&lt;/code&gt;，我们将服务器生成的公钥和站点相关信息发送给&lt;code&gt;CA签发机构&lt;/code&gt;，再由&lt;code&gt;CA签发机构&lt;/code&gt;通过服务器发送的相关信息用&lt;code&gt;CA签发机构&lt;/code&gt;进行加签，由此得到我们应用服务器的证书，证书会对应的生成证书内容的&lt;code&gt;签名&lt;/code&gt;，并将该&lt;code&gt;签名&lt;/code&gt;使用&lt;code&gt;CA签发机构&lt;/code&gt;的私钥进行加密得到&lt;code&gt;证书指纹&lt;/code&gt;，并且与上级证书生成关系链。&lt;/p&gt;
&lt;p&gt;这里我们把百度的证书下载下来看看：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;why-https-secure/2020-09-07-13-44-26.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;why-https-secure/2020-09-07-13-47-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到百度是受信于&lt;code&gt;GlobalSign G2&lt;/code&gt;，同样的&lt;code&gt;GlobalSign G2&lt;/code&gt;是受信于&lt;code&gt;GlobalSign R1&lt;/code&gt;，当客户端(浏览器)做证书校验时，会一级一级的向上做检查，直到最后的&lt;code&gt;根证书&lt;/code&gt;，如果没有问题说明&lt;code&gt;服务器证书&lt;/code&gt;是可以被信任的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何验证服务器证书
那么客户端(浏览器)又是如何对&lt;code&gt;服务器证书&lt;/code&gt;做校验的呢，首先会通过层级关系找到上级证书，通过上级证书里的&lt;code&gt;公钥&lt;/code&gt;来对服务器的&lt;code&gt;证书指纹&lt;/code&gt;进行解密得到&lt;code&gt;签名(sign1)&lt;/code&gt;，再通过签名算法算出服务器证书的&lt;code&gt;签名(sign2)&lt;/code&gt;，通过对比&lt;code&gt;sign1&lt;/code&gt;和&lt;code&gt;sign2&lt;/code&gt;，如果相等就说明证书是没有被&lt;code&gt;篡改&lt;/code&gt;也不是&lt;code&gt;伪造&lt;/code&gt;的。
&lt;img src=&quot;why-https-secure/2020-09-07-13-45-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里有趣的是，证书校验用的 RSA 是通过私钥加密证书签名，公钥解密来巧妙的验证证书有效性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样通过证书的认证体系，我们就可以避免了中间人窃取&lt;code&gt;AES_KEY&lt;/code&gt;从而发起拦截和修改 HTTP 通讯的报文。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;首先先通过对 HTTP 中间人攻击的来了解到 HTTP 为什么是不安全的，然后再从安全攻防的技术演变一直到 HTTPS 的原理概括，希望能让大家对 HTTPS 有个更深刻的了解。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>linux中访问运行中程序的输出</title><link>https://monkeywie.cn/posts/linux-access-running-process-output</link><guid isPermaLink="true">https://monkeywie.cn/posts/linux-access-running-process-output</guid><pubDate>Tue, 29 Sep 2020 09:07:32 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 linux 中我们经常会使用&lt;code&gt;&amp;amp;&lt;/code&gt;符号让进程在后台运行，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nohup java -jar app.jar &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这样的话在终端就看不到输出了，有时候临时需要排查问题看不到输出就 GG 了。&lt;/p&gt;
&lt;h2&gt;解决办法&lt;/h2&gt;
&lt;p&gt;其实可以利用&lt;code&gt;proc&lt;/code&gt;系统文件来访问程序对应的输出：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先获取到进程对应的&lt;code&gt;PID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;tail&lt;/code&gt;命令读取输出：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#获取标准输出
tail -f /proc/&amp;lt;PID&amp;gt;/fd/1
#获取错误输出
tail -f /proc/&amp;lt;PID&amp;gt;/fd/2
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言HTTP服务生命周期</title><link>https://monkeywie.cn/posts/go-http-server-life-cycle</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-http-server-life-cycle</guid><pubDate>Wed, 21 Oct 2020 09:59:45 GMT</pubDate><content:encoded>&lt;p&gt;在 go 语言里启动一个 http 服务非常简单，只需要一行代码&lt;code&gt;http.ListenAndServe()&lt;/code&gt;就可以搞定，这个方法会一直阻塞着直到进程关闭，如果这个时候来了些特殊的需求比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;监听服务启动&lt;/li&gt;
&lt;li&gt;手动关闭服务&lt;/li&gt;
&lt;li&gt;监听服务关闭&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 go 中应该怎么实现呢？下面来一一举例。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;监听服务启动&lt;/h2&gt;
&lt;h3&gt;方法一（推荐）&lt;/h3&gt;
&lt;p&gt;将&lt;code&gt;Listen&lt;/code&gt;步骤拆分出来，先监听端口，再绑定到&lt;code&gt;server&lt;/code&gt;上，代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;l, _ := net.Listen(&quot;tcp&quot;, &quot;:8080&quot;)
// 服务启动成功，进行初始化
doInit()
// 绑定到server上
http.Serve(l, nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法二&lt;/h3&gt;
&lt;p&gt;通过一个&lt;code&gt;协程&lt;/code&gt;去轮询监听服务启动状态，代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go func() {
    for {
        if _, err := net.Dial(&quot;tcp&quot;, &quot;127.0.0.1:8080&quot;); err == nil {
            // 服务启动成功，进行初始化
            doInit()
            //退出协程
            break
        }
        // 每隔一秒检查一次服务是否启动成功
        time.Sleep(time.Second)
    }
}()
http.ListenAndServe(&quot;:8080&quot;, nil)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;手动关闭服务&lt;/h2&gt;
&lt;h3&gt;优雅关闭（推荐）&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;http&lt;/code&gt;包中并没有暴露服务的关闭方法，通过&lt;code&gt;http.ListenAndServe()&lt;/code&gt;方法启动的 http 服务默认帮我们创建了一个&lt;code&gt;*http.Server&lt;/code&gt;对象，源码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func ListenAndServe(addr string, handler Handler) error {
    server := &amp;amp;Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上在&lt;code&gt;*http.Server&lt;/code&gt;中是有提供&lt;code&gt;Shutdown&lt;/code&gt;方法的，所以我们只需要手动构造一个&lt;code&gt;*http.Server&lt;/code&gt;对象，就可以进行优雅关闭了，代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;srv := &amp;amp;http.Server{Addr: &quot;:8080&quot;}
go func(){
    // 10秒之后关闭服务
    time.Sleep(time.Second * 10)
    srv.Shutdown(context.TODO())
}()
// 启动服务
srv.ListenAndServe()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;强制关闭&lt;/h3&gt;
&lt;p&gt;强制关闭和上面步骤是一样的，只是调用的方法换成了&lt;code&gt;srv.Close()&lt;/code&gt;，这会导致所有的请求立即中断，所以需要特别注意。&lt;/p&gt;
&lt;h2&gt;监听服务关闭&lt;/h2&gt;
&lt;p&gt;当我们手动将服务关闭之后，&lt;code&gt;srv.ListenAndServe()&lt;/code&gt;方法就会立即返回，这里需要注意的是该方法会返回一个&lt;code&gt;error&lt;/code&gt;，当然这个&lt;code&gt;error&lt;/code&gt;是一个特殊的 error &lt;code&gt;http.ErrServerClosed&lt;/code&gt;，帮助我们区分是否为正常的服务关闭，所以需要对它特殊处理下，代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if err := server.ListenAndServe(); err != nil {
    // 服务关闭，进行处理
    doShutdown()
    if err != http.ErrServerClosed{
        // 异常宕机，打印错误信息
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/39320025/how-to-stop-http-listenandserve&quot;&gt;how-to-stop-http-listenandserve&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/32738188/go-how-can-i-start-the-browser-after-the-server-started-listening&quot;&gt;go-how-can-i-start-the-browser-after-the-server-started-listening&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>JAVA枚举最佳实践</title><link>https://monkeywie.cn/posts/java-enum-best-practice</link><guid isPermaLink="true">https://monkeywie.cn/posts/java-enum-best-practice</guid><pubDate>Tue, 03 Nov 2020 15:03:29 GMT</pubDate><content:encoded>&lt;p&gt;在业务开发中经常会使用枚举来定义一些业务常量，这确实可以让我们的代码变的干净和优雅，但是如果枚举使用不当的话很可能就会发生问题，这里分享一些关于枚举最佳实践。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;使用枚举定义数据库表字段&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;JPA&lt;/code&gt;或者&lt;code&gt;ORM&lt;/code&gt;框架中，一般都是支持通过枚举来定义一个表字段，例如有一个用户表(user)，其中有一个性别字段(sex)，这个时候我们就可以通过枚举来定义 sex，代码如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义枚举类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public enum Sex{
  M,F
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;定义实体类&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class User{
  private Sex sex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以提供在编译时检测，确保&lt;code&gt;sex&lt;/code&gt;字段值只能是&lt;code&gt;M&lt;/code&gt;或&lt;code&gt;F&lt;/code&gt;，从而提升代码的健壮性。&lt;/p&gt;
&lt;h2&gt;使用==来比较枚举值&lt;/h2&gt;
&lt;p&gt;很多时候我们在对枚举值做判断的时候可能是用的&lt;code&gt;equals&lt;/code&gt;，但是这样其实是有风险的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 根据性别枚举显示对应的中文描述
if (Sex.M.equals(user.getSex())) {
  return &quot;男&quot;;
} else {
  return &quot;女&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这段代码正常情况下是没有任何问题的，但是如果有一天需要把&lt;code&gt;User&lt;/code&gt;类的&lt;code&gt;sex&lt;/code&gt;属性类型调整为&lt;code&gt;String&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class User{
  //类型替换为String
  private String sex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为&lt;code&gt;equals&lt;/code&gt;方法的参数是&lt;code&gt;Object&lt;/code&gt;类型，导致类型重构后根本得不到编译器的反馈，上面那判断逻辑代码并不会报错，这会极容易导致遗漏修改，然后所有人都成了性别&lt;code&gt;女&lt;/code&gt;，为了避免类似这种问题，所以推荐使用&lt;code&gt;==&lt;/code&gt;去做枚举比较，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (Sex.M == user.getSex()) {
  return &quot;男&quot;;
} else {
  return &quot;女&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样在类型重构的时候就会得到编译时的报错反馈，从而确保重构之后的代码正确性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;java-enum-best-practice/2020-11-06-15-14-31.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用枚举实现单例&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;《effective java》&lt;/code&gt;作者极力推荐使用枚举来实现单例，因为它具备以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写法简洁，代码短小精悍&lt;/li&gt;
&lt;li&gt;线程安全&lt;/li&gt;
&lt;li&gt;防止反序列化和反射的破坏&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public enum EnumSingleton {
    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    public void hello(){
        System.out.println(&quot;hello world&quot;);
    }
}

public static void main(String[] args) {
    EnumSingleton.INSTANCE.hello();
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>通过GitHub Action自动部署Maven项目</title><link>https://monkeywie.cn/posts/maven-deploy-to-nexus-by-github-action</link><guid isPermaLink="true">https://monkeywie.cn/posts/maven-deploy-to-nexus-by-github-action</guid><pubDate>Fri, 06 Nov 2020 15:37:47 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;要把自己的 JAVA 项目发布到 Maven 中央仓库上，这个过程非常的麻烦，而且由于 Maven 中央仓库的严谨性，每次发布都需要登录到&lt;code&gt;Nexus&lt;/code&gt;网站手动进行流程确认，并不支持纯命令行式的部署，导致无法做到真正的&lt;code&gt;CI/CD&lt;/code&gt;，为了弥补这一点，我抓包分析了一下&lt;code&gt;Nexus API&lt;/code&gt;并且开发了一个&lt;code&gt;Github Action&lt;/code&gt;(&lt;a href=&quot;https://github.com/monkeyWie/maven-nexus-release&quot;&gt;maven-nexus-release&lt;/a&gt;)用于自动的&lt;code&gt;Close&lt;/code&gt;和&lt;code&gt;Release&lt;/code&gt;，从而达到真正的全自动部署。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;效果图
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-10-11-52.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;已经有发布 jar 包到中央仓库的老司机应该都明白发布 jar 包有多麻烦，没有发布过但是想把自己开源项目发布到&lt;code&gt;Maven&lt;/code&gt;中央仓库的可以先参考下我之前的一篇文章：&lt;a href=&quot;https://monkeywie.cn/2018/07/23/publish-jar-to-maven&quot;&gt;发布 jar 包到 maven 中央仓库&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;使用&lt;/h2&gt;
&lt;p&gt;首先最好是对 Github Action 有一定的了解，如果不了解也没关系，可以通过我之前的文章快速过一遍：&lt;a href=&quot;https://monkeywie.cn/2019/10/29/hello-github-actions&quot;&gt;Github Actions 尝鲜&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;准备&lt;/h3&gt;
&lt;h4&gt;托管在 Github 上的 Maven 项目&lt;/h4&gt;
&lt;p&gt;需要调整&lt;code&gt;pom.xml&lt;/code&gt;中&lt;code&gt;maven-gpg-plugin&lt;/code&gt;插件的配置，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; &amp;lt;plugin&amp;gt;
   &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
   &amp;lt;artifactId&amp;gt;maven-gpg-plugin&amp;lt;/artifactId&amp;gt;
   &amp;lt;executions&amp;gt;
       &amp;lt;execution&amp;gt;
           &amp;lt;id&amp;gt;sign-artifacts&amp;lt;/id&amp;gt;
           &amp;lt;phase&amp;gt;verify&amp;lt;/phase&amp;gt;
           &amp;lt;goals&amp;gt;
               &amp;lt;goal&amp;gt;sign&amp;lt;/goal&amp;gt;
           &amp;lt;/goals&amp;gt;
       &amp;lt;/execution&amp;gt;
   &amp;lt;/executions&amp;gt;
   &amp;lt;configuration&amp;gt;
       &amp;lt;!-- 这个configuration必须配置，用于gpg非交互式密码输入 --&amp;gt;
       &amp;lt;gpgArguments&amp;gt;
           &amp;lt;arg&amp;gt;--pinentry-mode&amp;lt;/arg&amp;gt;
           &amp;lt;arg&amp;gt;loopback&amp;lt;/arg&amp;gt;
       &amp;lt;/gpgArguments&amp;gt;
   &amp;lt;/configuration&amp;gt;
 &amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Nexus 用户名和密码&lt;/h4&gt;
&lt;p&gt;登录到&lt;code&gt;https://oss.sonatype.org&lt;/code&gt;的账号和密码。&lt;/p&gt;
&lt;h4&gt;gpg private key&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Base64&lt;/code&gt;编码的 gpg 私钥，通过命令行导出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;列出秘钥&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;gpg --list-secret-keys --keyid-format LONG
------------------------------------------------
sec   rsa4096/2A6B618785DD7899 2020-11-05 [SC]
      992BB9305698C72B846EF4982A6B618785DD7899
uid                 [ultimate] monkeyWie &amp;lt;liwei-8466@qq.com&amp;gt;
ssb   rsa4096/F8E9F8CBD90028C5 2020-11-05 [E]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;找到用于发布 jar 包的 key，这里示例中的是&lt;code&gt;2A6B618785DD7899&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导出私钥&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;gpg --armo --export-secret-keys 2A6B618785DD7899
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意私钥是从&lt;code&gt;-----BEGIN PGP PRIVATE KEY BLOCK-----&lt;/code&gt;一直到&lt;code&gt;-----END PGP PRIVATE KEY BLOCK-----&lt;/code&gt;，而不是仅仅是中间这一段文本。&lt;/p&gt;
&lt;h4&gt;gpg passphrase&lt;/h4&gt;
&lt;p&gt;在生成 gpg 秘钥的时候会需要输入一个短密码，应该还记得吧。&lt;/p&gt;
&lt;h3&gt;将秘钥配置到 Github Secrets 中&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;进入 Github 项目主页，然后找到 Settings 选项。
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-10-55-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进入&lt;code&gt;Secrets&lt;/code&gt;菜单
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-10-56-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把刚刚准备好的秘钥一一创建
在右边有&lt;code&gt;New secret&lt;/code&gt;按钮用于创建秘钥，将刚刚的秘钥内容创建并给定对应的名称，示例：
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-11-01-06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最终 Secrets 如下：
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-10-59-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;编写 Github Action 配置文件&lt;/h3&gt;
&lt;p&gt;在项目根目录下新建&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: deploy

on:
  # 支持手动触发构建
  workflow_dispatch:
  release:
    # 创建release的时候触发
    types: [published]
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      # 拉取源码
      - uses: actions/checkout@v2
      # 安装JDK环境
      - name: Set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      # 设置Maven中央仓库配置
      - name: Set up Apache Maven Central
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
          server-id: releases
          # Nexus用户名环境变量
          server-username: MAVEN_USERNAME
          # Nexus密码环境变量
          server-password: MAVEN_CENTRAL_TOKEN
          # gpg短密码环境变量
          gpg-passphrase: MAVEN_GPG_PASSPHRASE
          # gpg私钥
          gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
      # 推送jar包至maven中央仓库
      - name: Publish to Apache Maven Central
        # 执行maven deploy命令
        run: mvn clean deploy
        # 环境变量设置
        env:
          # Nexus用户名,如果觉得不想暴露也可以配置到secrets中
          MAVEN_USERNAME: xxx
          # Nexus密码
          MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
          # gpg短密码
          MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
      # Nexus自动部署
      - name: Release on nexus
        uses: monkeyWie/maven-nexus-release@v1
        with:
          # Nexus用户名
          maven-repo-server-username: xxx
          # Nexus密码
          maven-repo-server-password: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把代码推送到 Github 上，就可以看到对应的&lt;code&gt;Action&lt;/code&gt;了，上面示例中有两种方式来触发构建：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;手动触发
通过 Github 可以手动的触发构建，方便测试，操作如下图：
&lt;img src=&quot;maven-deploy-to-nexus-by-github-action/2020-11-09-11-15-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发布 release 时自动触发
在 Github 项目中创建 release，会自动的触发构建，适用于项目稳定之后。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;以上步骤都在我的项目&lt;a href=&quot;https://github.com/monkeyWie/proxyee&quot;&gt;proxyee&lt;/a&gt;中通过验证，另外&lt;a href=&quot;https://github.com/monkeyWie/maven-nexus-release&quot;&gt;maven-nexus-release&lt;/a&gt;项目还是刚起步，功能可能不够完善，大家如果有什么好的想法和建议欢迎提出 issue 和 pr。&lt;/p&gt;
&lt;p&gt;顺便小小的安利下&lt;code&gt;proxyee&lt;/code&gt;，它是基于&lt;code&gt;netty&lt;/code&gt;编写的 HTTP 代理服务器，支持代理&lt;code&gt;HTTP&lt;/code&gt;+&lt;code&gt;HTTPS&lt;/code&gt;+&lt;code&gt;WebSocket&lt;/code&gt;，并且支持&lt;code&gt;HTTP&lt;/code&gt;和&lt;code&gt;HTTPS&lt;/code&gt;抓包，感兴趣的可以 Star 一下。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>面经总结之TCP</title><link>https://monkeywie.cn/posts/interview-tcp</link><guid isPermaLink="true">https://monkeywie.cn/posts/interview-tcp</guid><pubDate>Thu, 17 Dec 2020 10:17:14 GMT</pubDate><content:encoded>&lt;p&gt;最近换工作，面试发现很多公司都爱问 TCP 协议，所以根据自己的理解总结了一份手记仅供面试使用。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h3&gt;TCP 是一种什么协议&lt;/h3&gt;
&lt;p&gt;TCP 是一种建立在 IP 协议上的面向连接、可靠、基于流的传输协议&lt;/p&gt;
&lt;h3&gt;TCP 三次握手流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端生成 ISN，SYN 到服务器&lt;/li&gt;
&lt;li&gt;服务器生成 ISN，SYN 和 ACK 到客户端&lt;/li&gt;
&lt;li&gt;客户端 ACK 到服务器，可以携带数据&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;TCP 的三次握手原因&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;确认双方收发能力&lt;/li&gt;
&lt;li&gt;同步双方的序列号和窗口大小,用于后续的数据传输可靠性和有序性&lt;/li&gt;
&lt;li&gt;阻止重复的历史连接，如果旧的 SYN 比新的 SYN 先到，客户端在第三次握手的时候就通过 seq 可以知道是旧的 SYN，可以直接发 RST 来终止连接 （解释为什么不是两次）&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;TCP 四次挥手流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;客户端发送 FIN FIN_WAIT1&lt;/li&gt;
&lt;li&gt;服务端发送 ACK(服务端不再接收新的数据) TIME_WAIT&lt;/li&gt;
&lt;li&gt;客户端收到 ACK(客户端不再发送新的数据) FIN_WAIT2&lt;/li&gt;
&lt;li&gt;服务端处理完数据发送 FIN&lt;/li&gt;
&lt;li&gt;客户端 ACK CLOSE_WAIT&lt;/li&gt;
&lt;li&gt;服务端接收到 ACK 后关闭连接 CLOSED&lt;/li&gt;
&lt;li&gt;客户端发送完 ACK 之后等待 2MSL 关闭连接 CLOSED&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;MSL 是什么&lt;/h3&gt;
&lt;p&gt;MSL 是指报文在网络上的最大生存时间，超过这个时间一般就可以认为报文已经丢失，linux 上是 30 秒，2MSL 就是 60 秒&lt;/p&gt;
&lt;h3&gt;为什么等 2MSL&lt;/h3&gt;
&lt;p&gt;因为考虑到数据传输需要一来一回，所以要等待 2MSL&lt;/p&gt;
&lt;h3&gt;TIME_WAIT 过多有什么危害&lt;/h3&gt;
&lt;p&gt;服务器发起的主动关闭，导致 FD 不够用，可能无法接受新的连接
客户端发起的主动关闭，导致端口不够用，可能无法发起新的连接&lt;/p&gt;
&lt;h3&gt;TCP 有序性怎么保证&lt;/h3&gt;
&lt;p&gt;通过双方约定的 seq 保证&lt;/p&gt;
&lt;h3&gt;TCP 可靠性怎么保证&lt;/h3&gt;
&lt;p&gt;通过重传机制保证&lt;/p&gt;
&lt;h3&gt;TCP 重传&lt;/h3&gt;
&lt;p&gt;在报文一直没有被 ACK 时，就会触发超时重传，超时时间有一个复杂的公式来动态计算，如果再超时则等待时间翻倍，一直到超时重传限制数，如果任然没有应答说明网络或对方有异常，关闭连接。
超时重传次限制数为 15&lt;/p&gt;
&lt;h3&gt;快速重传&lt;/h3&gt;
&lt;p&gt;接受到三次相同 seq 的 ACK，就会触发快速重传&lt;/p&gt;
&lt;h3&gt;SACK&lt;/h3&gt;
&lt;p&gt;在 ACK 的时候告诉发送方已接受到的 seq，这样发送就知道需要重传哪些数据&lt;/p&gt;
&lt;h3&gt;D-SACK&lt;/h3&gt;
&lt;p&gt;在 ACK 的时候告诉发送放已接受到的 seq，通知对方不需要重传了已经接受到了，只是之前没 ack 到&lt;/p&gt;
&lt;h3&gt;滑动窗口和流量控制&lt;/h3&gt;
&lt;p&gt;接收方通过窗口大小告诉发送方自己能处理的报文数，
为了解决 TCP 每次发送数据都要一答一应的低效问题，滑动窗口为了解决这个问题，设计了可以在请求没被 ACK 的情况下继续发送数据，双方约定各自的窗口大小，这个大小就是无需 ACK 就可以发送数据的大小。&lt;/p&gt;
&lt;h3&gt;窗口大小由哪一方决定&lt;/h3&gt;
&lt;p&gt;窗口大小是接收方告诉发送方自己的缓冲区还有多少数据可以接收，所以窗口大小都是由接收方决定&lt;/p&gt;
&lt;h3&gt;窗口满了怎么处理&lt;/h3&gt;
&lt;p&gt;发送方会定时去检测，如果检测到接收方窗口大小有空闲了就继续发送。&lt;/p&gt;
&lt;h3&gt;拥塞控制&lt;/h3&gt;
&lt;p&gt;流量控制是避免「发送方」的数据填满「接收方」的缓存，但是并不知道网络的中发生了什么。而拥塞控制是为了避免网络出现拥堵时继续发送大量的数据包，导致包延时、丢失等等触发重传进而加大网络的拥堵程度导致网络瘫痪。
拥塞窗口(cwnd)是发送端维护的，和滑动窗口中的接收窗口关联，在发送时在两者中取最小作为发送窗口大小，一个 cwnd 表示可以传 1MSS(1460)大小的数据&lt;/p&gt;
&lt;h3&gt;慢启动&lt;/h3&gt;
&lt;p&gt;每次 ack，cwnd+1，指数级增长，直到慢启动阀值(ssthresh=65535)：1&amp;gt;2,2&amp;gt;4,4&amp;gt;8
拥塞避免算法：
超过阀值之后每次 ack,cwnd+1/cwnd，线性增长&lt;/p&gt;
&lt;h3&gt;拥塞发生&lt;/h3&gt;
&lt;p&gt;发生超时重传时，cwnd 重置为 1，慢启动阀值(ssthresh)设置为当前 cwnd/2，接着又开始慢启动流程
发生快速重传时，TCP 认为这种情况不严重，因为大部分没丢，只丢了一小部分，慢启动阀值(ssthresh)设置为当前 cwnd/2，然后进入快速恢复&lt;/p&gt;
&lt;h3&gt;快速恢复算法&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;cwnd=慢启动阀值(ssthresh)+3(快速重传的 3 次 ack)&lt;/li&gt;
&lt;li&gt;后续如果还有收到重复的 ack，则 cwnd+1&lt;/li&gt;
&lt;li&gt;收到新的 ack 时快恢复结束，将 cwnd 的值设置为慢启动阀值(ssthresh)，重新进入拥塞避免阶段&lt;/li&gt;
&lt;/ol&gt;
</content:encoded><author>Levi</author></item><item><title>最强开源接口管理平台YApi搭建</title><link>https://monkeywie.cn/posts/yapi-setup</link><guid isPermaLink="true">https://monkeywie.cn/posts/yapi-setup</guid><pubDate>Mon, 04 Jan 2021 15:24:27 GMT</pubDate><content:encoded>&lt;h2&gt;YApi 简介&lt;/h2&gt;
&lt;p&gt;YApi 是去哪儿网开源的一个&lt;code&gt;高效&lt;/code&gt;、&lt;code&gt;易用&lt;/code&gt;、&lt;code&gt;功能强大&lt;/code&gt;的 API 管理平台，它拥有接口管理，接口调试，接口测试，Mock 等等一系列特性，并且支持导入和自动同步&lt;code&gt;swagger&lt;/code&gt;文档，可以直接将现有的所有项目&lt;code&gt;swagger&lt;/code&gt;文档无缝迁移到 YApi 上统一管理，真的是不讲武德！&lt;/p&gt;
&lt;p&gt;官方已经部署了一套公有服务进行演示，直接访问&lt;a href=&quot;https://yapi.baidu.com&quot;&gt;https://yapi.baidu.com&lt;/a&gt;即可快速体验。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;部署&lt;/h2&gt;
&lt;p&gt;这里介绍两种部署方式，一种是基于&lt;code&gt;docker&lt;/code&gt;一键部署，还有一种是基于&lt;code&gt;命令行&lt;/code&gt;手动部署，当然我强烈建议使用&lt;code&gt;docker&lt;/code&gt;进行部署，否则容易碰到一些环境相关的问题。&lt;/p&gt;
&lt;h3&gt;基于 docker 部署&lt;/h3&gt;
&lt;h4&gt;1.创建网桥&lt;/h4&gt;
&lt;p&gt;创建一个网桥让 yapi 容器能访问到 mongoDB 容器，命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create yapi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.部署 mongoDB&lt;/h4&gt;
&lt;p&gt;基于 docker 部署非常简单，只需要一行命令就搞定，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name=mongo \
  --network=yapi \
  -e MONGO_INITDB_ROOT_USERNAME=root \
  -e MONGO_INITDB_ROOT_PASSWORD=123456 \
  mongo:3.6.21-xenial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里关键的点就是通过&lt;code&gt;MONGO_INITDB_ROOT_USERNAME&lt;/code&gt;和&lt;code&gt;MONGO_INITDB_ROOT_PASSWORD&lt;/code&gt;环境变量来设置数据库访问的账号密码，在下面部署 yapi 时要用到。&lt;/p&gt;
&lt;h4&gt;3.部署 yapi&lt;/h4&gt;
&lt;p&gt;通过上一步部署的 mongoDB 账号密码进行部署，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d --name yapi \
  --network=yapi \
  -e DB_SERVERNAME=mongo \
  -e DB_DATABASE=admin \
  -e DB_PORT=27017 \
  -e DB_USER=root \
  -e DB_PASS=123456 \
  -p 3000:3000 \
  liwei2633/yapi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署成功的话通过浏览器访问&lt;code&gt;http://127.0.0.1:3000&lt;/code&gt;应该就可以看到以下界面：
&lt;img src=&quot;yapi-setup/2021-01-05-14-58-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后使用默认管理员账号密码：admin@admin.com+ymfe.org登录就能使用了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;yapi-setup/2021-01-05-15-00-37.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;yapi-setup/2021-01-05-15-00-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的镜像是我自己构建上传的，详细可以参考：&lt;a href=&quot;https://github.com/monkeyWie/yapi-docker/blob/master/build/basic/README.md&quot;&gt;basic/README.md&lt;/a&gt;。
然后还提供了一个支持&lt;code&gt;gitlab登录&lt;/code&gt;的镜像，如果有需要的可以参考：&lt;a href=&quot;https://github.com/monkeyWie/yapi-docker/blob/master/build/gitlab/README.md&quot;&gt;gitlab/README.md&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;基于命令行部署&lt;/h3&gt;
&lt;p&gt;上面介绍了基于 docker 的方式安装，现在介绍下基于命令行的方式进行安装。&lt;/p&gt;
&lt;h4&gt;1.安装 mongoDB&lt;/h4&gt;
&lt;p&gt;通过 mongoDB 官网的教程，根据自己的操作系统进行安装即可，地址：&lt;code&gt;https://docs.mongodb.com/v3.6/administration/install-community&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;由于 mongoDB 默认是无需身份验证的，不太安全，所以要创建一个用户用于访问数据库，命令如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use admin

db.createUser(
  {
    user: &quot;root&quot;,
    pwd: &quot;123456&quot;,
    roles: [ { role: &quot;userAdminAnyDatabase&quot;, db: &quot;admin&quot; } ]
  }
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样创建好了一个用户，账号密码为：&lt;code&gt;root&lt;/code&gt;+&lt;code&gt;123456&lt;/code&gt;&lt;/p&gt;
&lt;h4&gt;2.安装 node.js 环境&lt;/h4&gt;
&lt;p&gt;注意不要安装太高版本的 node，否则可能会导致部分依赖下载失败，我测试 node10 版本是没问题的，直接去官网&lt;code&gt;https://nodejs.org/dist/latest-v10.x&lt;/code&gt;，下载对应的 node 版本安装即可。&lt;/p&gt;
&lt;p&gt;安装好之后运行以下命令来验证是否正确安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常的话会输出&lt;code&gt;v10.x.x&lt;/code&gt;对应版本号，接着就可以开始安装 yapi 了。&lt;/p&gt;
&lt;h4&gt;3.安装 yapi&lt;/h4&gt;
&lt;p&gt;选好一个安装目录，在目录下打开终端执行命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install -g yapi-cli --registry https://registry.npm.taobao.org
yapi server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行成功之后终端会提示访问地址，通过浏览器访问&lt;code&gt;http://127.0.0.1:9090&lt;/code&gt;进行安装，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;yapi-setup/2021-01-05-15-25-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;填写好对应的配置信息，然后点击开始部署，接着等待一段时间就部署完成了，然后按&amp;lt;kbd&amp;gt;ctrl&amp;lt;/kbd&amp;gt;+&amp;lt;kbd&amp;gt;c&amp;lt;/kbd&amp;gt;退出。&lt;/p&gt;
&lt;p&gt;再根据安装提示在终端中启动 yapi 服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd &amp;lt;部署路径&amp;gt;
node vendors/server/app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动成功后就可以访问了。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;YApi 使用在我们团队使用了一段时间，体验还是非常不错的，但是很可惜的是这个项目目前基本属于无人维护的状态，最近的一次&lt;code&gt;commit&lt;/code&gt;还是在&lt;code&gt;2020年10月&lt;/code&gt;，如果碰到坑的话就不要指望官方来修复了，只能靠自己，前几天给官方提了个&lt;a href=&quot;https://github.com/YMFE/yapi/pull/2059&quot;&gt;PR&lt;/a&gt;到现在也还没回复，希望官方能把项目再运营起来吧~&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>spring cloud gateway修改请求体和响应体</title><link>https://monkeywie.cn/posts/spring-cloud-gateway-modify-request-and-response-body</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-cloud-gateway-modify-request-and-response-body</guid><pubDate>Tue, 26 Jan 2021 18:50:34 GMT</pubDate><content:encoded>&lt;p&gt;// TODO&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Spring Boot整合feign</title><link>https://monkeywie.cn/posts/spring-boot-integration-feign</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-boot-integration-feign</guid><pubDate>Thu, 28 Jan 2021 15:00:28 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;feign 是一个非常好用的 http 客户端工具，它是一种声明式的 http 客户端，只需要声明好接口即可调用，不需要关注底层的请求细节。
通常情况下都是在 Spring Cloud 项目中使用，这里我把它单独整合到 Spring Boot 中，用来替代&lt;code&gt;RestTemplate&lt;/code&gt;，提高项目可维护性。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;整合 feign&lt;/h2&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.github.openfeign&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;feign-spring4&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;11.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;

&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.github.openfeign&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;feign-gson&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;11.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;声明接口&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;feign-spring4&lt;/code&gt;这个依赖，可以支持使用 SpringMVC 的注解来声明接口，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public interface UserFeign {
    @PostMapping(&quot;/users&quot;)
    Result&amp;lt;Void&amp;gt; add(@RequestBody UserAddDTO dto);

    @GetMapping(&quot;/users&quot;)
    Result&amp;lt;UserVO&amp;gt; find(@RequestParam(&quot;name&quot;) String name);

    @GetMapping(&quot;/test/404&quot;)
    Result&amp;lt;UserVO&amp;gt; error404();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到代码和&lt;code&gt;controller&lt;/code&gt;方法声明基本一致，非常的清晰易懂。&lt;/p&gt;
&lt;h3&gt;配置 feign&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class FeignConfig {

    @Autowired
    private Environment environment;
    @Autowired
    private ObjectMapper objectMapper;

    // 构造user服务的feign客户端
    @Bean
    public UserFeign userFeign() {
        // 这里是因为方便演示，直接调用自身服务的接口
        String userServer = &quot;http://127.0.0.1:&quot; + environment.getProperty(&quot;server.port&quot;);
        return Feign.builder()
                .client(new FeignClient(null, null))
                .encoder(new GsonEncoder())
                .decoder(new GsonDecoder())
                .contract(new SpringContract()) // 这里很关键，要使用SpringMVC注解必须配置这个contract
                .retryer(Retryer.NEVER_RETRY)
                .requestInterceptor(template -&amp;gt; {
                    template.header(&quot;Content-Type&quot;, &quot;application/json&quot;);
                })
                .options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
                .target(UserFeign.class, userServer);
    }

    public class FeignClient extends Client.Default {

        public FeignClient(final SSLSocketFactory sslContextFactory, final HostnameVerifier hostnameVerifier) {
            super(sslContextFactory, hostnameVerifier);
        }

        // 重写execute方法，在接口请求失败时抛出异常
        @Override
        public Response execute(final Request request, final Request.Options options) throws IOException {
            Response response = super.execute(request, options);
            if (response.status() == 200) {
                String body = Util.toString(response.body().asReader(Charset.forName(&quot;UTF-8&quot;)));
                Result result = null;
                try {
                    result = objectMapper.readValue(body, Result.class);
                } catch (Exception e) {

                }
                if (result == null || result.getCode() != 200) {
                    throw new FeignException.FeignServerException(200, &quot;http request fail&quot;, request, body.getBytes());
                }
                // 注意这里因为把响应流读完了，所以要重新把body赋值，不然后续后报错
                response = response.toBuilder()
                        .body(body.getBytes())
                        .build();
            }
            return response;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置好了之后，后续有新的接口只要去接口上声明新的方法就可以了，维护起来也是非常的方便。&lt;/p&gt;
&lt;h3&gt;调用接口&lt;/h3&gt;
&lt;p&gt;前面已经将&lt;code&gt;UserFeign&lt;/code&gt;注册在 spring 容器中了，使用的时候只需要注入到类中，然后和调用本地方法一样使用就行了，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Autowired
private UserFeign userFeign;

public void query() throws Exception {
    // 调用用户服务查找接口
    Result&amp;lt;UserVO&amp;gt; result = userFeign.find(&quot;java&quot;);
    System.out.println(result.toString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;p&gt;本文完整代码放在&lt;a href=&quot;https://github.com/monkeyWie/spring-boot-best-practices/tree/master/integration-feign&quot;&gt;github&lt;/a&gt;。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Spring Boot记录完整请求响应日志</title><link>https://monkeywie.cn/posts/spring-boot-log-request-and-response-body</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-boot-log-request-and-response-body</guid><pubDate>Sat, 27 Feb 2021 10:13:51 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在排查错误时通常都需要通过日志来查看接口的请求参数和响应结果来定位和分析问题，一般我们都会使用一个&lt;code&gt;Filter&lt;/code&gt;来做一些简单的请求日志记录，但是默认情况下 Spring Boot 是不支持记录&lt;code&gt;请求体&lt;/code&gt;和&lt;code&gt;响应体&lt;/code&gt;的，因为请求体和响应体都是以流的方式对外提供调用，如果在&lt;code&gt;Filter&lt;/code&gt;中把请求体和响应体读完了，就会使后续的应用读不到流数据导致异常。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;实现思路&lt;/h2&gt;
&lt;p&gt;如果要记录&lt;code&gt;请求体&lt;/code&gt;和&lt;code&gt;响应体&lt;/code&gt;的话，需要将流使用完之后缓存在内存中，以供后续使用，这个实现起来好像还挺复杂，需要包装&lt;code&gt;HttpServletRequest&lt;/code&gt;、&lt;code&gt;HttpServletResponse&lt;/code&gt;两个类，然后对其中的&lt;code&gt;IO&lt;/code&gt;接口做处理，大概代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public OncePerRequestFilter contentCachingRequestFilter() {
    // 配置一个Filter
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
            // 包装HttpServletRequest，把输入流缓存下来
            CachingRequestWrapper wrappedRequest = new CachingRequestWrapper(request);
            // 包装HttpServletResponse，把输出流缓存下来
            CachingResponseWrapper wrappedResponse = new CachingResponseWrapper(response);
            filterChain.doFilter(wrappedRequest, wrappedResponse);
            LOGGER.info(&quot;http request:{}&quot;, wrappedRequest.getContent());
            LOGGER.info(&quot;http response:{}&quot;, wrappedResponse.getContent());
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 spring 内置包装类&lt;/h2&gt;
&lt;p&gt;有了上面一步的思路应该可以实现记录&lt;code&gt;请求体&lt;/code&gt;和&lt;code&gt;响应体&lt;/code&gt;内容了，然而没必要，&lt;code&gt;spring&lt;/code&gt;官方已经提供了两个类来做这件事，就是&lt;code&gt;ContentCachingRequestWrapper&lt;/code&gt;和&lt;code&gt;ContentCachingResponseWrapper&lt;/code&gt;，使用方法也差不多，代码示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public OncePerRequestFilter contentCachingRequestFilter() {
    // 配置一个Filter
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
            // 包装HttpServletRequest，把输入流缓存下来
            ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
            // 包装HttpServletResponse，把输出流缓存下来
            ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
            filterChain.doFilter(wrappedRequest, wrappedResponse);
            LOGGER.info(&quot;http request:{}&quot;, new String(wrappedRequest.getContentAsByteArray()));
            LOGGER.info(&quot;http response:{}&quot;, new String(wrappedResponse.getContentAsByteArray()));
            // 注意这一行代码一定要调用，不然无法返回响应体
            wrappedResponse.copyBodyToResponse();
        }
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;p&gt;本文完整代码放在&lt;a href=&quot;https://github.com/monkeyWie/spring-boot-best-practices/tree/master/log-body&quot;&gt;github&lt;/a&gt;。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Spring Boot使用JSR-380进行校验</title><link>https://monkeywie.cn/posts/spring-boot-jsr380</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-boot-jsr380</guid><pubDate>Mon, 01 Mar 2021 10:15:18 GMT</pubDate><content:encoded>&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JSR-380&lt;/code&gt;是 J2EE 的一个规范，用于校验实体属性，它是&lt;code&gt;JSR-303&lt;/code&gt;的升级版，在 Spring Boot 中可以基于它优雅实现参数校验。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;示例&lt;/h2&gt;
&lt;p&gt;在没有使用&lt;code&gt;JSR-380&lt;/code&gt;之前，我们一般都会将参数校验硬编码在&lt;code&gt;controller&lt;/code&gt;类中，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public Result add(@RequestBody User user){
    if(StringUtils.isBlank(user.getName())){
        return Result.error(&quot;用户名不能为空&quot;);
    }
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而使用&lt;code&gt;JSR-380&lt;/code&gt;只需要通过添加对应的注解即可实现校验，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Data
public class User{
    @NotBlank
    private String name;
    private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public Result register(@Validated @RequestBody User user){
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样看起来代码是不是清爽了很多，只需要在需要校验的字段上加上对应的校验注解，然后对需要校验的地方加上&lt;code&gt;@Validated&lt;/code&gt;注解，然后框架就会帮我们完成校验。&lt;/p&gt;
&lt;h2&gt;通过全局异常自定义错误响应&lt;/h2&gt;
&lt;p&gt;框架校验失败之后会抛出异常，需要捕获这个异常然后来自定义校验不通过的错误响应，这里直接贴代码，兼容&lt;code&gt;@RequestBody&lt;/code&gt;、&lt;code&gt;@ModelAttribute&lt;/code&gt;、&lt;code&gt;@RequestParam&lt;/code&gt;三种入参的校验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
    public ResponseEntity&amp;lt;Result&amp;gt; methodArgumentNotValidHandler(HttpServletRequest request, Exception e) {
        BindingResult bindingResult;
        if (e instanceof MethodArgumentNotValidException) {
            //@RequestBody参数校验
            bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
        } else {
            //@ModelAttribute参数校验
            bindingResult = ((BindException) e).getBindingResult();
        }
        FieldError fieldError = bindingResult.getFieldError();
        return ResponseEntity.ok(Result.fail(Result.CODE_PARAMS_INVALID, &quot;[&quot; + fieldError.getField() + &quot;]&quot; + fieldError.getDefaultMessage()));
    }

    //@RequestParam参数校验
    @ExceptionHandler(value = {ConstraintViolationException.class, MissingServletRequestParameterException.class})
    public ResponseEntity&amp;lt;Result&amp;gt; constraintViolationHandler(Exception e) {
        String field;
        String msg;
        if (e instanceof ConstraintViolationException) {
            ConstraintViolation&amp;lt;?&amp;gt; constraintViolation = ((ConstraintViolationException) e).getConstraintViolations().stream().findFirst().get();
            List&amp;lt;Path.Node&amp;gt; pathList = StreamSupport.stream(constraintViolation.getPropertyPath().spliterator(), false)
                    .collect(Collectors.toList());
            field = pathList.get(pathList.size() - 1).getName();
            msg = constraintViolation.getMessage();
        } else {
            // 这个不是JSR标准返回的异常，要自定义提示文本
            field = ((MissingServletRequestParameterException) e).getParameterName();
            msg = &quot;不能为空&quot;;
        }
        return ResponseEntity.ok(Result.fail(Result.CODE_PARAMS_INVALID, &quot;[&quot; + field + &quot;]&quot; + msg));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再访问一下接口，可以看到错误提示已经按自定义的规范显示了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;spring-boot-jsr380/2021-02-26-10-45-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到都不需要写任何提示文本就可以完成校验和提示，上图的&lt;code&gt;不能为空&lt;/code&gt;是框架内置的&lt;code&gt;I18N&lt;/code&gt;国际化支持，每个注解都内置相应的提示模板。&lt;/p&gt;
&lt;h2&gt;常用校验注解&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;注解&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;@NotNull&lt;/td&gt;
&lt;td&gt;验证值不为 null&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AssertTrue&lt;/td&gt;
&lt;td&gt;验证值为 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Size&lt;/td&gt;
&lt;td&gt;验证值的长度介于 min 和 max 之间，可应用于 String、Collection、Map 和数组类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Min&lt;/td&gt;
&lt;td&gt;验证值不小于该值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Max&lt;/td&gt;
&lt;td&gt;验证值不大于该值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Email&lt;/td&gt;
&lt;td&gt;验证字符串是有效的电子邮件地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@NotEmpty&lt;/td&gt;
&lt;td&gt;验证值不为 null 或空，可应用于 String、Collection、Map 和数组类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@NotBlank&lt;/td&gt;
&lt;td&gt;验证字符串不为 null 并且不是空白字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Positive&lt;/td&gt;
&lt;td&gt;验证数字为正数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@PositiveOrZero&lt;/td&gt;
&lt;td&gt;验证数字为正数(包括 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Negative&lt;/td&gt;
&lt;td&gt;验证数字为负数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@NegativeOrZero&lt;/td&gt;
&lt;td&gt;验证数字为负数(包括 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Past&lt;/td&gt;
&lt;td&gt;验证日期值是过去&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@PastOrPresent&lt;/td&gt;
&lt;td&gt;验证日期值是过去(包括现在)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Future&lt;/td&gt;
&lt;td&gt;验证日期值是未来&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@FutureOrPresent&lt;/td&gt;
&lt;td&gt;验证日期值是未来(包括现在)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;附录&lt;/h2&gt;
&lt;p&gt;本文完整代码放在&lt;a href=&quot;https://github.com/monkeyWie/spring-boot-best-practices/tree/master/jsr380&quot;&gt;github&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/javax-validation&quot;&gt;Java Bean Validation Basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jcp.org/en/jsr/detail?id=380&quot;&gt;JSR-380 规范&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>cgo传递回调函数</title><link>https://monkeywie.cn/posts/cgo-pass-callback</link><guid isPermaLink="true">https://monkeywie.cn/posts/cgo-pass-callback</guid><pubDate>Mon, 26 Apr 2021 16:10:47 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;cgo 是个好东西，可以很方便的和 c、c++交互，这篇文章主要是记录下 cgo 声明回调函数入参，然后在 c 中进行实现并传递。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;go 代码&lt;/h2&gt;
&lt;p&gt;在 go 里面每秒调用一次回调函数，回调函数由 c 来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

/*
typedef void (*EventCb)(char* event);
static void bridge_event_cb(EventCb cb,char* event)
{
	cb(event);
}
*/
import &quot;C&quot;
import (
	&quot;time&quot;
)

func main() {}

//export SetListener
func SetListener(cb C.EventCb) {
	for i := 0; i &amp;lt; 100; i++ {
		time.Sleep(time.Second)
		C.bridge_event_cb(cb, C.CString(fmt.Sprintf(&quot;event:%d&quot;, i)))
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译成动态库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go build -buildmode=c-shared -o main.dll main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C 代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &quot;./main.h&quot;

// 实现回调函数
void cb(char* event){
    printf(&quot;%s\n&quot;,event);
}

int main(int argc, char const *argv[])
{
    SetListener(cb);
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc main.c main.dll -o main.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;踩坑过程&lt;/h2&gt;
&lt;p&gt;在 go 里面声明的&lt;code&gt;C.EventCb&lt;/code&gt;入参，不能直接进行调用，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func SetListener(cb C.EventCb) {
	cb(C.CString(&quot;test&quot;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样编译的时候直接会报错，要通过 C 代码桥接来进行回调函数的调用才能通过编译。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>spring-cloud-gateway缓存区不够用的解决办法</title><link>https://monkeywie.cn/posts/spring-cloud-gateway-exceeded-limit-on-max-bytes-to-buffer-issue</link><guid isPermaLink="true">https://monkeywie.cn/posts/spring-cloud-gateway-exceeded-limit-on-max-bytes-to-buffer-issue</guid><pubDate>Wed, 02 Jun 2021 14:18:22 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近碰到一个问题，我们的&lt;code&gt;Spring Cloud Gateway&lt;/code&gt;网关有个接口一直报错，错误堆栈如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
	at org.springframework.core.io.buffer.LimitedDataBufferList.raiseLimitException(LimitedDataBufferList.java:98) ~[spring-core-5.2.12.RELEASE.jar!/:5.2.12.RELEASE]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
	Error has been observed at the following site(s):
	|_ checkpoint ⇢ Body from UNKNOWN  [DefaultClientResponse]
	|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;看起来应该是有个&lt;code&gt;DataBuffer&lt;/code&gt;缓冲区不够用，然后确认了下目标接口的响应报文确实有点大，于是乎开始 google 寻找答案。&lt;/p&gt;
&lt;h2&gt;解决办法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;spring&lt;/code&gt;配置直接调整对应的内存大小&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;spring:
  codec:
    max-in-memory-size: 16MB
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;通过实现&lt;code&gt;WebFluxConfigurer&lt;/code&gt;接口来配置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;@Configuration
public class WebfluxConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(16 * 1024 * 1024);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是使用自定义的&lt;code&gt;WebClient&lt;/code&gt;，那么需要这样配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public WebClient getWebClientBuilder(){
    return WebClient.builder()
                .codecs(configurer -&amp;gt; configurer
                        .defaultCodecs()
                        .maxInMemorySize(16 * 1024 * 1024))
                .build();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;踩坑&lt;/h2&gt;
&lt;p&gt;通过上面两个办法配置之后，问题还是一直存在，后来发现是因为使用了手动构造的&lt;code&gt;List&amp;lt;HttpMessageReader&amp;lt;?&amp;gt;&amp;gt;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public List&amp;lt;HttpMessageReader&amp;lt;?&amp;gt;&amp;gt; messageReaders() {
    return HandlerStrategies.withDefaults().messageReaders();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式通过 Spring 配置的缓存区大小不会生效，后面改成通过&lt;code&gt;ServerCodecConfigurer&lt;/code&gt;中来获取就 OK 了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Bean
public List&amp;lt;HttpMessageReader&amp;lt;?&amp;gt;&amp;gt; messageReaders(ServerCodecConfigurer codecConfigurer) {
    return codecConfigurer.getReaders();
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>VitrualBox安装增强功能失败的解决办法</title><link>https://monkeywie.cn/posts/vitrual-box-installation-on-ubuntu</link><guid isPermaLink="true">https://monkeywie.cn/posts/vitrual-box-installation-on-ubuntu</guid><pubDate>Wed, 21 Jul 2021 12:43:58 GMT</pubDate><content:encoded>&lt;p&gt;使用&lt;code&gt;VitrualBox&lt;/code&gt;安装 ubuntu20 之后，需要启用&lt;code&gt;VitrualBox&lt;/code&gt;增强功能，发现一直装不上，这里记录下解决方案：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;问题&lt;/h2&gt;
&lt;p&gt;安装时错误日志如下：&lt;/p&gt;
&lt;p&gt;Please install the gcc make perl packages from your distribution。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;vitrual-box-installation-on-ubuntu/2021-07-21-12-47-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;解决办法&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y build-essential linux-kernel-headers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新安装，完成后重启虚拟机即可。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>自定义SpringBoot+Swagger中@ApiModel默认名称</title><link>https://monkeywie.cn/posts/custom-spring-swagger-apimodel-default-name</link><guid isPermaLink="true">https://monkeywie.cn/posts/custom-spring-swagger-apimodel-default-name</guid><pubDate>Fri, 30 Jul 2021 16:28:19 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;项目使用的&lt;code&gt;springfox-swagger2@2.9.2&lt;/code&gt;版本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 Spring 中集成 swagger 文档功能，需要通过&lt;code&gt;@ApiModel&lt;/code&gt;注解修饰出入参的类，但是如果有两个不同包下的相同名称的类都使用了&lt;code&gt;@ApiModel&lt;/code&gt;注解时，会导致文档被覆盖，例如：&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;com.example.demo.login.dto.UserDTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.example.demo.login.dto;

@Data
@ApiModel
public class UserDTO{
  @ApiModelProperty(&quot;姓名&quot;)
  private String name;
  @ApiModelProperty(&quot;年龄&quot;)
  private Integer age;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;com.example.demo.vip.dto.UserDTO&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;package com.example.demo.vip.dto;

@Data
@ApiModel
public class UserDTO{
  @ApiModelProperty(&quot;姓名&quot;)
  private String name;
  @ApiModelProperty(&quot;会员级别&quot;)
  private Integer vipLevel;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面两个类生成出来的文档会变成一个&lt;code&gt;swagger model&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-07-30-17-49-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从而导致接口文档显示错误：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-07-30-17-53-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;解决冲突&lt;/h2&gt;
&lt;h3&gt;修改@ApiModel 注解（推荐）&lt;/h3&gt;
&lt;p&gt;通过修改@ApiModel 的 value 属性，来规避同名冲突，修改之后为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package com.example.demo.login.dto;

@Data
@ApiModel(&quot;login$UserDTO&quot;)
public class UserDTO{}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package com.example.demo.vip.dto;

@Data
@ApiModel(&quot;vip$UserDTO&quot;)
public class UserDTO{}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到生成了两个&lt;code&gt;swagger model&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-07-30-18-01-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;修改类名&lt;/h3&gt;
&lt;p&gt;把两个类名做修改，让类名不冲突即可。&lt;/p&gt;
&lt;h2&gt;自定义 swagger 插件&lt;/h2&gt;
&lt;p&gt;然而上面解决冲突的方式还是太麻烦了，定义一个文档的出入参类而已，还要考虑类重名的问题，这种增加心智负担和工作量的问题应该要尽量避免掉的，我在想有没有可能做到每个类上只需要加上&lt;code&gt;@ApiModel&lt;/code&gt;注解就行，剩下的冲突问题全部不用考虑。&lt;/p&gt;
&lt;p&gt;于是乎通过跟踪源码，找到了&lt;code&gt;swagger model&lt;/code&gt;名称生成的地方，详见：&lt;a href=&quot;https://github.com/springfox/springfox/blob/09d4a734b64a216bb5c26c0329f3d15b8276c0e4/springfox-swagger-common/src/main/java/springfox/documentation/swagger/schema/ApiModelTypeNameProvider.java#L38-L43&quot;&gt;github&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-08-08-15-02-04.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到取名的逻辑是，优先取&lt;code&gt;@ApiModel&lt;/code&gt;的&lt;code&gt;value&lt;/code&gt;值，如果没有就会使用&lt;code&gt;defaultTypeName&lt;/code&gt;,跟进去一看，&lt;code&gt;defaultTypeName&lt;/code&gt;是直接取类的&lt;code&gt;简称&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-08-08-15-03-16.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正是因为默认情况下取类的&lt;code&gt;简称&lt;/code&gt;，导致不同包名下的同名类生成出来的&lt;code&gt;swagger model&lt;/code&gt;被覆盖。
原因已经分析出来了，接下来其实就是看看能不能定制化这个&lt;code&gt;super.nameFor(type)&lt;/code&gt;方法了，然而很遗憾这个方法是写死的，没地方下手，但是&lt;code&gt;ApiModelTypeNameProvider&lt;/code&gt;这个类上两个注解&lt;code&gt;@Component&lt;/code&gt;和&lt;code&gt;@Order&lt;/code&gt;已经明示了这个是一个&lt;code&gt;Spring bean&lt;/code&gt;，并且是通过&lt;code&gt;Spring插件机制进行加载的&lt;/code&gt;，所以可以自定义一个插件来完成，在默认时通过完整的类路径和类名来生成唯一的&lt;code&gt;swagger model&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER - 100)
public class FullPathTypeNameProvider extends DefaultTypeNameProvider {

    public static final String SPLIT_CHAR = &quot;$&quot;;

    @Override
    public String nameFor(Class&amp;lt;?&amp;gt; type) {
        ApiModel annotation = AnnotationUtils.findAnnotation(type, ApiModel.class);
        if (annotation == null) {
            return super.nameFor(type);
        }
        if (StringUtils.hasText(annotation.value())) {
            return annotation.value();
        }
        // 如果@ApiModel的value为空，则默认取完整类路径
        int packagePathLength = type.getPackage().getName().length();
        return Stream.of(type.getPackage().getName().split(&quot;\\.&quot;))
                .map(path -&amp;gt; path.substring(0, 1))
                .collect(Collectors.joining(SPLIT_CHAR))
                + SPLIT_CHAR
                + type.getName().substring(packagePathLength + 1);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-spring-swagger-apimodel-default-name/2021-08-08-15-34-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;通过这一个小小的优化，就可以减少许多团队中不必要的沟通成本，让我们能更专注于业务开发。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>通过Apollo SPI机制实现默认环境变量配置支持</title><link>https://monkeywie.cn/posts/custom-apollo-support-defualt-env</link><guid isPermaLink="true">https://monkeywie.cn/posts/custom-apollo-support-defualt-env</guid><pubDate>Fri, 13 Aug 2021 16:34:22 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近公司项目要接入配置中心，后来调研下来决定使用&lt;code&gt;apollo&lt;/code&gt;，但是在使用的时候发现有
个小细节特别难受，&lt;code&gt;apollo&lt;/code&gt;不支持通过项目代码配置默认的&lt;code&gt;environment&lt;/code&gt;，官网文档如下：&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-apollo-support-defualt-env/2021-08-13-15-09-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样会导致代码拉下来不能直接启动服务，还需要通过上面的三种方式之一来指定&lt;code&gt;environment&lt;/code&gt;，非常的麻烦。&lt;/p&gt;
&lt;h2&gt;进行魔改&lt;/h2&gt;
&lt;p&gt;我想着是能不能项目代码的配置文件里配置一个默认的&lt;code&gt;environment&lt;/code&gt;，这样在没有外部指定&lt;code&gt;environment&lt;/code&gt;的情况下就默认使用配置文件里的。&lt;/p&gt;
&lt;p&gt;通过跟踪&lt;a href=&quot;https://github.com/ctripcorp/apollo/blob/891010618214b8e826b3c124f5572988135ade58/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/provider/DefaultServerProvider.java#L149-L177&quot;&gt;源码&lt;/a&gt;可以看到加载&lt;code&gt;environment&lt;/code&gt;是通过&lt;code&gt;DefaultServerProvider类的initialize方法&lt;/code&gt;进行加载的，然后通过&lt;code&gt;DefaultProviderManager类中的register方法&lt;/code&gt;进行注册，时序图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-apollo-support-defualt-env/2021-08-13-15-34-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本来理论上来说魔改&lt;code&gt;DefaultServerProvider&lt;/code&gt;这个类就可以实现目的了，然而这源码里没有为接口&lt;code&gt;ServerProvider&lt;/code&gt;实现&lt;code&gt;SPI机制&lt;/code&gt;，是直接&lt;code&gt;new&lt;/code&gt;出来的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-apollo-support-defualt-env/2021-08-13-15-48-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么只能从&lt;code&gt;DefaultProviderManager&lt;/code&gt;类进行入手了，这个接口是通过&lt;code&gt;SPI机制&lt;/code&gt;去加载的实现类：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;custom-apollo-support-defualt-env/2021-08-13-15-50-06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在唯一的入口点就是&lt;code&gt;register&lt;/code&gt;方法了，通过这个方法可以把注册的&lt;code&gt;DefaultServerProvider&lt;/code&gt;魔改掉，这个时候设计模式就派上用场了，我直接进行一个&lt;code&gt;装饰器模式&lt;/code&gt;的秀：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class HookServerProviderManager extends DefaultProviderManager {

    @Override
    public synchronized void register(Provider provider) {
        // 如果是注册DefaultServerProvider的时候替换成装饰之后的类
        if (provider instanceof DefaultServerProvider) {
            super.register(new DefaultServerProviderWrapper((DefaultServerProvider) provider));
        } else {
            super.register(provider);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;public class DefaultServerProviderWrapper implements ServerProvider {

    private DefaultServerProvider serverProvider;

    private String defaultEnv;

    public DefaultServerProviderWrapper(DefaultServerProvider serverProvider) {
        this.serverProvider = serverProvider;

        // 如果没有读取到环境变量，则加载META-INF/app.properties文件中的default.env变量
        if (serverProvider.getEnvType() == null) {
            Properties prop = new Properties();
            try (InputStream inputStream = Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream(DefaultApplicationProvider.APP_PROPERTIES_CLASSPATH.substring(1))) {
                prop.load(inputStream);
                this.defaultEnv = prop.getProperty(&quot;default.env&quot;);
            } catch (Exception e) {
                log.warn(&quot;load app.properties fail&quot;, e);
            }
        }
    }

    @Override
    public String getEnvType() {
        String originEnv = serverProvider.getEnvType();
        return originEnv != null ? originEnv : defaultEnv;
    }

    @Override
    public boolean isEnvTypeSet() {
        return getEnvType() != null;
    }

    @Override
    public String getDataCenter() {
        return serverProvider.getDataCenter();
    }

    @Override
    public boolean isDataCenterSet() {
        return serverProvider.isDataCenterSet();
    }

    @Override
    public void initialize(InputStream in) throws IOException {
        serverProvider.initialize(in);
    }

    @Override
    public Class&amp;lt;? extends Provider&amp;gt; getType() {
        return serverProvider.getType();
    }

    @Override
    public String getProperty(String name, String defaultValue) {
        return serverProvider.getProperty(name, defaultValue);
    }

    @Override
    public void initialize() {
        serverProvider.initialize();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码写好之后，按 SPI 的规范在&lt;code&gt;META-INF/services&lt;/code&gt;下新建文件&lt;code&gt;com.ctrip.framework.foundation.spi.ProviderManager&lt;/code&gt;，把刚刚的&lt;code&gt;HookServerProviderManager&lt;/code&gt;类路径填入即可。&lt;/p&gt;
&lt;p&gt;这样的话如果没有读到&lt;code&gt;environment&lt;/code&gt;的话就会去读取&lt;code&gt;META-INF/app.propertie&lt;/code&gt;的&lt;code&gt;default.env&lt;/code&gt;配置。&lt;/p&gt;
&lt;h2&gt;吐槽&lt;/h2&gt;
&lt;p&gt;这个应该是一个很常见的需求，不知道为啥阿波罗竟然不支持，也有可能是我的使用方式不对吧，PR 就懒得提了，毕竟不是一定要改源码才能实现。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>wsl2官方gui安装IDEA踩坑记录</title><link>https://monkeywie.cn/posts/wsl2-gui-idea-config</link><guid isPermaLink="true">https://monkeywie.cn/posts/wsl2-gui-idea-config</guid><pubDate>Sun, 26 Sep 2021 10:59:18 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近由于某些需求，需要在&lt;code&gt;linux&lt;/code&gt;环境下做 java 开发，刚好可以试试&lt;code&gt;wsl2 gui&lt;/code&gt;，一通折腾下来总算符合自己的预期了，这里就记录下踩坑历程。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装步骤&lt;/h2&gt;
&lt;h3&gt;开启 WSL2&lt;/h3&gt;
&lt;p&gt;首先 WSL2 gui 需要&lt;code&gt;Windows 11 Build 22000&lt;/code&gt;版本以上才支持，然后关于升级到 Windows11 和开启 WSL2 的步骤就不多叙了，网上很多教程。&lt;/p&gt;
&lt;p&gt;附：&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/wsl/tutorials/gui-apps&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;安装到指定目录&lt;/h3&gt;
&lt;p&gt;默认情况下&lt;code&gt;WSL2系统&lt;/code&gt;会安装在 C 盘，但是我有块开发专用的 SSD 硬盘，所以需要安装到指定的目录里。&lt;/p&gt;
&lt;p&gt;这需要手动下载安装包进行安装，找到微软提供的 wsl 支持的操作系统安装包列表，然后下载对应的安装包即可(&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/wsl/install-manual#downloading-distributions&quot;&gt;https://docs.microsoft.com/en-us/windows/wsl/install-manual#downloading-distributions&lt;/a&gt;)。&lt;/p&gt;
&lt;p&gt;我下载的是&lt;code&gt;Ubuntu 20.04&lt;/code&gt;，下载完之后把后缀名改成&lt;code&gt;.zip&lt;/code&gt;然后解压到对应的目录，双击
&lt;code&gt;ubuntu2004.exe&lt;/code&gt;就安装好了，然后目录下会生成一个&lt;code&gt;ext4.vhdx&lt;/code&gt;文件，这个就是虚拟机挂载的磁盘文件了。&lt;/p&gt;
&lt;h3&gt;中文乱码修复&lt;/h3&gt;
&lt;p&gt;安装好 IDEA 之后，打开发现中文全部变成了方块乱码，为了解决这个问题需要安装中文字体。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装相关的包&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install language-pack-zh-hans
sudo dpkg-reconfigure locales #这一步要选择en_US.UTF-8和zh_CN.UTF-8, 并且zh_CN.UTF-8为默认语言
sudo apt install fontconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;安装 Windows 字体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建&lt;code&gt;/etc/fonts/local.conf&lt;/code&gt;文件，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot;?&amp;gt;
&amp;lt;!DOCTYPE fontconfig SYSTEM &quot;fonts.dtd&quot;&amp;gt;
&amp;lt;fontconfig&amp;gt;
    &amp;lt;dir&amp;gt;/mnt/c/Windows/Fonts&amp;lt;/dir&amp;gt;
&amp;lt;/fontconfig&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;刷新字体缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;fc-cache -f -v
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;重启 wsl 即可&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;输入法问题&lt;/h3&gt;
&lt;p&gt;由于 wsl gui 底层依旧是基于 RDP 的远程桌面实现，官方架构图：
&lt;img src=&quot;wsl2-gui-idea-config/2021-09-26-14-44-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以输入法不能使用 Windows 宿主机上的，需要在虚拟机里安装输入法，目前有一个很大的问题就是，输入法的候选框不会跟随光标，其它的暂时没啥问题，勉强能用。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装 fcitx&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install fcitx dbus-x11 im-config fcitx-sunpinyin
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编辑&lt;code&gt;/etc/locale.gen&lt;/code&gt;文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 找到 # zh_CN.UTF-8 这一行，取消注释
zh_CN.UTF-8
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;编辑&lt;code&gt;~/.profile&lt;/code&gt;文件&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS=@im=fcitx
export DefaultIMModule=fcitx
fcitx-autostart &amp;amp;&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;刷新&lt;code&gt;~/.profile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;source ~/.profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候在&lt;code&gt;gedit&lt;/code&gt;里已经可以切出中文输入法了，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wsl2-gui-idea-config/2021-09-26-15-06-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要注意的时，fcitx 默认输入法切换快捷键是&amp;lt;kdb&amp;gt;ctrl&amp;lt;/kdb&amp;gt;+&amp;lt;kdb&amp;gt;space&amp;lt;/kdb&amp;gt;，会覆盖 IDEA 的提示快捷键，可以通过&lt;code&gt;fcitx-config-gtk3&lt;/code&gt;修改，但是不能和 Windows 宿主机上的全局热键冲突，不然会无效。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wsl2-gui-idea-config/2021-09-26-15-04-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IDEA&lt;/code&gt; 输入法支持&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面配好之后，IDEA 切不出输入法，需要特殊配置一下才行，编辑 IDEA 启动脚本&lt;code&gt;idea.sh&lt;/code&gt;，在上面加入以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export XMODIFIERS=@im=fcitx
export QT_IM_MODULE=fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启&lt;code&gt;IDEA&lt;/code&gt;就可以切出输入法了，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wsl2-gui-idea-config/2021-09-26-15-09-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>手撸一个ingress controller来打通dubbo+k8s网络</title><link>https://monkeywie.cn/posts/dubbo-in-k8s</link><guid isPermaLink="true">https://monkeywie.cn/posts/dubbo-in-k8s</guid><pubDate>Tue, 16 Nov 2021 10:31:41 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;由于公司内部所有服务都是跑在阿里云 k8s 上的，然后 dubbo 提供者默认向注册中心上报的 IP 都是&lt;code&gt;Pod IP&lt;/code&gt;，这意味着在 k8s 集群外的网络环境是调用不了 dubbo 服务的，如果本地开发需要访问 k8s 内的 dubbo 提供者服务的话，需要手动把服务暴露到外网，我们的做法是针对每一个提供者服务暴露一个&lt;code&gt;SLB IP+自定义端口&lt;/code&gt;，并且通过 dubbo 提供的&lt;code&gt;DUBBO_IP_TO_REGISTRY&lt;/code&gt;和&lt;code&gt;DUBBO_PORT_TO_REGISTRY&lt;/code&gt;环境变量来把对应的&lt;code&gt;SLB IP+自定义端口&lt;/code&gt;注册到注册中心里，这样就实现了本地网络和 k8s dubbo 服务的打通，但是这种方式管理起来非常麻烦，每个服务都得自定义一个端口，而且每个服务之间端口还不能冲突，当服务多起来之后非常难以管理。&lt;/p&gt;
&lt;p&gt;于是我就在想能不能像&lt;code&gt;nginx ingress&lt;/code&gt;一样实现一个&lt;code&gt;七层代理+虚拟域名&lt;/code&gt;来复用一个端口，通过目标 dubbo 提供者的&lt;code&gt;application.name&lt;/code&gt;来做对应的转发，这样的话所有的服务只需要注册同一个&lt;code&gt;SLB IP+端口&lt;/code&gt;就可以了，大大的提升便利性，一方调研之后发现可行就开撸了！&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;项目已开源：&lt;a href=&quot;https://github.com/monkeyWie/dubbo-ingress-controller&quot;&gt;https://github.com/monkeyWie/dubbo-ingress-controller&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;技术预研&lt;/h2&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;首先 dubbo RPC 调用默认是走的&lt;code&gt;dubbo协议&lt;/code&gt;，所以我需要先去看看协议里有没有可以利用做转发的报文信息，就是寻找类似于 HTTP 协议里的 Host 请求头，如果有的话就可以根据此信息做&lt;code&gt;反向代理&lt;/code&gt;和&lt;code&gt;虚拟域名&lt;/code&gt;的转发，在此基础之上实现一个类似&lt;code&gt;nginx&lt;/code&gt;的&lt;code&gt;dubbo网关&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二步就是要实现&lt;code&gt;dubbo ingress controller&lt;/code&gt;，通过 k8s ingress 的 watcher 机制动态的更新&lt;code&gt;dubbo 网关&lt;/code&gt;的虚拟域名转发配置，然后所有的提供者服务都由此服务同一转发，并且上报到注册中心的地址也统一为此服务的地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;架构图&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;dubbo-in-k8s/2021-11-16-11-01-58.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;dubbo 协议&lt;/h3&gt;
&lt;p&gt;先上一个官方的协议图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;dubbo-in-k8s/2021-11-16-10-56-51.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到 dubbo 协议的 header 是固定的&lt;code&gt;16个字节&lt;/code&gt;，里面并没有类似于 HTTP Header 的可扩展字段，也没有携带目标提供者的&lt;code&gt;application.name&lt;/code&gt;字段，于是我向官方提了个&lt;a href=&quot;https://github.com/apache/dubbo/issues/9251&quot;&gt;issue&lt;/a&gt;，官方的答复是通过消费者&lt;code&gt;自定义Filter&lt;/code&gt;来将目标提供者的&lt;code&gt;application.name&lt;/code&gt;放到&lt;code&gt;attachments&lt;/code&gt;里，这里不得不吐槽下 dubbo 协议，扩展字段竟然是放在&lt;code&gt;body&lt;/code&gt;里，如果要实现转发需要把请求报文全部解析完才能拿到想要报文，不过问题不大，因为主要是做给开发环境用的，这一步勉强可以实现。&lt;/p&gt;
&lt;h3&gt;k8s ingress&lt;/h3&gt;
&lt;p&gt;k8s ingress 是为 HTTP 而生的，但是里面的字段够用了，来看一段 ingress 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: user-rpc-dubbo
  annotations:
    kubernetes.io/ingress.class: &quot;dubbo&quot;
spec:
  rules:
    - host: user-rpc
      http:
        paths:
          - backend:
              serviceName: user-rpc
              servicePort: 20880
            path: /
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置和 http 一样通过&lt;code&gt;host&lt;/code&gt;来做转发规则，但是&lt;code&gt;host&lt;/code&gt;配置的是目标提供者的&lt;code&gt;application.name&lt;/code&gt;，后端服务是目标提供者对应的&lt;code&gt;service&lt;/code&gt;，这里有一个比较特殊的是使用了一个&lt;code&gt;kubernetes.io/ingress.class&lt;/code&gt;注解，这个注解可以指定此&lt;code&gt;ingress&lt;/code&gt;对哪个&lt;code&gt;ingress controller&lt;/code&gt;生效，后面我们的&lt;code&gt;dubbo ingress controller&lt;/code&gt;就只会解析注解值为&lt;code&gt;dubbo&lt;/code&gt;的 ingress 配置。&lt;/p&gt;
&lt;h2&gt;开发&lt;/h2&gt;
&lt;p&gt;前面的技术预研一切顺利，接着就进入开发阶段了。&lt;/p&gt;
&lt;h3&gt;消费者自定义 Filter&lt;/h3&gt;
&lt;p&gt;前面有提到如果请求里要携带目标提供者的&lt;code&gt;application.name&lt;/code&gt;，需要消费者&lt;code&gt;自定义Filter&lt;/code&gt;，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@Activate(group = CONSUMER)
public class AddTargetFilter implements Filter {

  @Override
  public Result invoke(Invoker&amp;lt;?&amp;gt; invoker, Invocation invocation) throws RpcException {
    String targetApplication = StringUtils.isBlank(invoker.getUrl().getRemoteApplication()) ?
        invoker.getUrl().getGroup() : invoker.getUrl().getRemoteApplication();
    // 目标提供者的application.name放入attachment
    invocation.setAttachment(&quot;target-application&quot;, targetApplication);
    return invoker.invoke(invocation);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里又要吐槽一下，dubbo 消费者首次访问时会发起一个获取 metadata 的请求，这个请求通过&lt;code&gt;invoker.getUrl().getRemoteApplication()&lt;/code&gt;是拿不到值的，通过&lt;code&gt;invoker.getUrl().getGroup()&lt;/code&gt;才能拿到。&lt;/p&gt;
&lt;h3&gt;dubbo 网关&lt;/h3&gt;
&lt;p&gt;这里就要开发一个类似&lt;code&gt;nginx&lt;/code&gt;的&lt;code&gt;dubbo网关&lt;/code&gt;，并实现七层代理和虚拟域名转发，编程语言直接选择了 go，首先 go 做网络开发心智负担低，另外有个 dubbo-go 项目，可以直接利用里面的解码器，然后 go 有原生的 k8s sdk 支持，简直完美！&lt;/p&gt;
&lt;p&gt;思路就是开启一个&lt;code&gt;TCP Server&lt;/code&gt;，然后解析 dubbo 请求的报文，把&lt;code&gt;attachment&lt;/code&gt;里的&lt;code&gt;target-application&lt;/code&gt;属性拿到，再反向代理到真正的 dubbo 提供者服务上，核心代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;routingTable := map[string]string{
  &quot;user-rpc&quot;: &quot;user-rpc:20880&quot;,
  &quot;pay-rpc&quot;:  &quot;pay-rpc:20880&quot;,
}

listener, err := net.Listen(&quot;tcp&quot;, &quot;:20880&quot;)
if err != nil {
  return err
}
for {
  clientConn, err := listener.Accept()
  if err != nil {
    logger.Errorf(&quot;accept error:%v&quot;, err)
    continue
  }
  go func() {
    defer clientConn.Close()
    var proxyConn net.Conn
    defer func() {
      if proxyConn != nil {
        proxyConn.Close()
      }
    }()
    scanner := bufio.NewScanner(clientConn)
    scanner.Split(split)
    // 解析请求报文，拿到一个完整的请求
    for scanner.Scan() {
      data := scanner.Bytes()
      // 通过dubbo-go提供的库把[]byte反序列化成dubbo请求结构体
      buf := bytes.NewBuffer(data)
      pkg := impl.NewDubboPackage(buf)
      pkg.Unmarshal()
      body := pkg.Body.(map[string]interface{})
      attachments := body[&quot;attachments&quot;].(map[string]interface{})
      // 从attachments里拿到目标提供者的application.name
      target := attachments[&quot;target-application&quot;].(string)
      if proxyConn == nil {
        // 反向代理到真正的后端服务上
        host := routingTable[target]
        proxyConn, _ = net.Dial(&quot;tcp&quot;, host)
        go func() {
          // 原始转发
          io.Copy(clientConn, proxyConn)
        }()
      }
      // 把原始报文写到真正后端服务上，然后走原始转发即可
      proxyConn.Write(data)
    }
  }()
}

func split(data []byte, atEOF bool) (advance int, token []byte, err error) {
	if atEOF &amp;amp;&amp;amp; len(data) == 0 {
		return 0, nil, nil
	}

	buf := bytes.NewBuffer(data)
	pkg := impl.NewDubboPackage(buf)
	err = pkg.ReadHeader()
	if err != nil {
		if errors.Is(err, hessian.ErrHeaderNotEnough) || errors.Is(err, hessian.ErrBodyNotEnough) {
			return 0, nil, nil
		}
		return 0, nil, err
	}
	if !pkg.IsRequest() {
		return 0, nil, errors.New(&quot;not request&quot;)
	}
	requestLen := impl.HEADER_LENGTH + pkg.Header.BodyLen
	if len(data) &amp;lt; requestLen {
		return 0, nil, nil
	}
	return requestLen, data[0:requestLen], nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;dubbo ingress controller 实现&lt;/h3&gt;
&lt;p&gt;前面已经实现了一个&lt;code&gt;dubbo网关&lt;/code&gt;，但是里面的虚拟域名转发配置(&lt;code&gt;routingTable&lt;/code&gt;)还是写死在代码里的，现在要做的就是当检测到&lt;code&gt;k8s ingress&lt;/code&gt;有更新时，动态的更新这个配置就可以了。&lt;/p&gt;
&lt;p&gt;首先先简单的说明下&lt;code&gt;ingress controller&lt;/code&gt;的原理，拿我们常用的&lt;code&gt;nginx ingress controller&lt;/code&gt;为例，它也是一样通过监听&lt;code&gt;k8s ingress&lt;/code&gt;资源变动，然后动态的生成&lt;code&gt;nginx.conf&lt;/code&gt;文件，当发现配置发生了改变时，触发&lt;code&gt;nginx -s reload&lt;/code&gt;重新加载配置文件。&lt;/p&gt;
&lt;p&gt;里面用到的核心技术就是&lt;a href=&quot;https://github.com/kubernetes/client-go/tree/master/informers&quot;&gt;informers&lt;/a&gt;，利用它来监听&lt;code&gt;k8s资源&lt;/code&gt;的变动，示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在集群内获取k8s访问配置
cfg, err := rest.InClusterConfig()
if err != nil {
  logger.Fatal(err)
}
// 创建k8s sdk client实例
client, err := kubernetes.NewForConfig(cfg)
if err != nil {
  logger.Fatal(err)
}
// 创建Informer工厂
factory := informers.NewSharedInformerFactory(client, time.Minute)
handler := cache.ResourceEventHandlerFuncs{
  AddFunc: func(obj interface{}) {
    // 新增事件
  },
  UpdateFunc: func(oldObj, newObj interface{}) {
    // 更新事件
  },
  DeleteFunc: func(obj interface{}) {
    // 删除事件
  },
}
// 监听ingress变动
informer := factory.Extensions().V1beta1().Ingresses().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过实现上面的三个事件来动态的更新转发配置，每个事件都会携带对应的&lt;code&gt;Ingress&lt;/code&gt;对象信息过来，然后进行对应的处理即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ingress, ok := obj.(*v1beta12.Ingress)
if ok {
  // 通过注解过滤出dubbo ingress
  ingressClass := ingress.Annotations[&quot;kubernetes.io/ingress.class&quot;]
  if ingressClass == &quot;dubbo&quot; &amp;amp;&amp;amp; len(ingress.Spec.Rules) &amp;gt; 0 {
    rule := ingress.Spec.Rules[0]
    if len(rule.HTTP.Paths) &amp;gt; 0 {
      backend := rule.HTTP.Paths[0].Backend
      host := rule.Host
      service := fmt.Sprintf(&quot;%s:%d&quot;, backend.ServiceName+&quot;.&quot;+ingress.Namespace, backend.ServicePort.IntVal)
      // 获取到ingress配置中host对应的service，通知给dubbo网关进行更新
      notify(host,service)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;docker 镜像提供&lt;/h3&gt;
&lt;p&gt;k8s 之上所有的服务都需要跑在容器里的，这里也不例外，需要把&lt;code&gt;dubbo ingress controller&lt;/code&gt;构建成 docker 镜像，这里通过两阶段构建优化，来减小镜像体积：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.17.3 AS builder
WORKDIR /src
COPY . .
ENV GOPROXY https://goproxy.cn
ENV CGO_ENABLED=0
RUN go build -ldflags &quot;-w -s&quot; -o main cmd/main.go

FROM debian AS runner
ENV TZ=Asia/shanghai
WORKDIR /app
COPY --from=builder /src/main .
RUN chmod +x ./main
ENTRYPOINT [&quot;./main&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;yaml 模板提供&lt;/h3&gt;
&lt;p&gt;由于要在集群内访问 k8s API，需要给 Pod 进行授权，通过&lt;code&gt;K8S rbac&lt;/code&gt;进行授权，并以&lt;code&gt;Deployment&lt;/code&gt;类型服务进行部署，最终模板如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: v1
kind: ServiceAccount
metadata:
  name: dubbo-ingress-controller
  namespace: default

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: dubbo-ingress-controller
rules:
  - apiGroups:
      - extensions
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch

---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: dubbo-ingress-controller
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: dubbo-ingress-controller
subjects:
  - kind: ServiceAccount
    name: dubbo-ingress-controller
    namespace: default

---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: dubbo-ingress-controller
  labels:
    app: dubbo-ingress-controller
spec:
  selector:
    matchLabels:
      app: dubbo-ingress-controller
  template:
    metadata:
      labels:
        app: dubbo-ingress-controller
    spec:
      serviceAccountName: dubbo-ingress-controller
      containers:
        - name: dubbo-ingress-controller
          image: liwei2633/dubbo-ingress-controller:0.0.1
          ports:
            - containerPort: 20880
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后期需要的话可以做成&lt;code&gt;Helm&lt;/code&gt;进行管理。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;至此&lt;code&gt;dubbo ingress controller&lt;/code&gt;实现完成，可以说麻雀虽小但是五脏俱全，里面涉及到了&lt;code&gt;dubbo协议&lt;/code&gt;、&lt;code&gt;TCP协议&lt;/code&gt;、&lt;code&gt;七层代理&lt;/code&gt;、&lt;code&gt;k8s ingress&lt;/code&gt;、&lt;code&gt;docker&lt;/code&gt;等等很多内容，这些很多知识都是在&lt;code&gt;云原生&lt;/code&gt;越来越流行的时代需要掌握的，开发完之后感觉受益匪浅。&lt;/p&gt;
&lt;p&gt;关于完整的使用教程可以通过&lt;a href=&quot;https://github.com/monkeyWie/dubbo-ingress-controller#%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E&quot;&gt;github&lt;/a&gt;查看。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://dubbo.apache.org/zh/docs/concepts/rpc-protocol/#protocol-spec&quot;&gt;dubbo 协议&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/dubbo-go&quot;&gt;dubbo-go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/zh/docs/concepts/services-networking/ingress-controllers/#%E4%BD%BF%E7%94%A8%E5%A4%9A%E4%B8%AA-ingress-%E6%8E%A7%E5%88%B6%E5%99%A8&quot;&gt;使用多个-ingress-控制器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.qikqiak.com/post/custom-k8s-ingress-controller-with-go/&quot;&gt;使用 Golang 自定义 Kubernetes Ingress Controller&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded><author>Levi</author></item><item><title>升级到Java 17没这么简单</title><link>https://monkeywie.cn/posts/java17-compatibility</link><guid isPermaLink="true">https://monkeywie.cn/posts/java17-compatibility</guid><pubDate>Thu, 18 Nov 2021 17:24:50 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近在给公司新架构做技术选型，刚好 Java 17 也正式发布一段日子了，而且是&lt;code&gt;LTS&lt;/code&gt;长期支持版本，就想着直接用起来吧，里面有些特性还是非常好用的，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.java.net/jeps/378&quot;&gt;JEP 378&lt;/a&gt;：文本块支持&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.java.net/jeps/395&quot;&gt;JEP 395&lt;/a&gt;：Record 类型&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://openjdk.java.net/jeps/286&quot;&gt;JEP 286&lt;/a&gt;：变量类型推导&lt;/li&gt;
&lt;li&gt;More...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;遇到的问题&lt;/h2&gt;
&lt;p&gt;其中最主要的原因就是 Java 模块化之后，有些 jdk 内部的类不能被访问了，但是在 Java 16 之前都只是警告，而在 Java 16 之后则会直接报错，目前依赖了&lt;code&gt;cglib&lt;/code&gt;和&lt;code&gt;javassist&lt;/code&gt;的框架可能都会因此导致项目无法启动，抛出如下异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not &quot;opens java.lang&quot; to unnamed module @39aeed2f
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:199)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:193)
	at net.sf.cglib.core.ReflectUtils$1.run(ReflectUtils.java:61)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:554)
	at net.sf.cglib.core.ReflectUtils.&amp;lt;clinit&amp;gt;(ReflectUtils.java:52)
	at net.sf.cglib.core.KeyFactory$Generator.generateClass(KeyFactory.java:243)
	at net.sf.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:332)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Java 16 开始，&lt;a href=&quot;https://openjdk.java.net/jeps/396&quot;&gt;JEP 396&lt;/a&gt;会默认把&lt;code&gt;--illegal-access&lt;/code&gt;参数设置为&lt;code&gt;deny&lt;/code&gt;，即默认禁用访问封装的包以及反射其他模块，这样就会导致上面的异常，在此之前该参数默认值一直都是&lt;code&gt;--illegal-access=permit&lt;/code&gt;，只会产生警告，而不会报错，所以如果是 Java 16 的话需要在执行 Java 程序时把&lt;code&gt;--illegal-access&lt;/code&gt;设置为&lt;code&gt;permit&lt;/code&gt;，这样就可以解决问题，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar --illegal-access=permit app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Java 17 开始就更狠了，&lt;a href=&quot;https://openjdk.java.net/jeps/403&quot;&gt;JEP 403&lt;/a&gt;直接把&lt;code&gt;--illegal-access&lt;/code&gt;参数移除了，如果需要启用访问封装的包，需要在执行 Java 程序时加上&lt;code&gt;--add-opens java.base/java.lang=ALL-UNNAMED&lt;/code&gt;选型，示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -jar --add-opens java.base/java.lang=ALL-UNNAMED app.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是在 IDEA 中运行需要配置对应的 VM 参数，示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;java17-compatibility/2021-11-18-17-53-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然说加完参数之后是可以跑起来，但是我认为这是一个破坏性的改动，因为这样的话，如果有一天 Java 版本变化了，参数又失效了，那么所有的项目都需要更新，这样会导致项目的维护成本大大增加，所以这里不建议使用。&lt;/p&gt;
&lt;h2&gt;开源框架升级进度跟踪&lt;/h2&gt;
&lt;p&gt;那么有没有办法不加启动参数就能正常运行呢，答案是肯定的，只不过需要等开源框架全换算 Java 17 的新 API，目前我跟踪到的两个项目都还没有适配 Java 17。&lt;/p&gt;
&lt;h3&gt;Spring&lt;/h3&gt;
&lt;p&gt;SpringBoot 2.5.0 开始支持 Java 17，没啥问题。&lt;/p&gt;
&lt;h3&gt;apollo 配置中心&lt;/h3&gt;
&lt;p&gt;apollo 目前的 master 分支代码是已经适配好了，但是还没有正式发版，比较奇怪的事是， apollo 之前&lt;a href=&quot;https://github.com/apolloconfig/apollo/pull/3646&quot;&gt;升级&lt;/a&gt;了底层依赖包来适配 Java 17，但是后来又&lt;a href=&quot;https://github.com/apolloconfig/apollo/commit/a10da56e97a585ee960c4967843287bc0bcfc176&quot;&gt;回滚&lt;/a&gt;回来了，不知道是出于什么原因。&lt;/p&gt;
&lt;h3&gt;dubbo&lt;/h3&gt;
&lt;p&gt;dubbo 有个&lt;a href=&quot;https://github.com/apache/dubbo/issues/7593&quot;&gt;issue 7593&lt;/a&gt;四月份就提出来了，但是一直没人跟进。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;一顿操作下来发现不行，最终还是先换成了 Java 15，待时机成熟的时候再升级到 Java 17。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>vuetify使用本地图标和字体文件</title><link>https://monkeywie.cn/posts/vuetify-ues-local-resource</link><guid isPermaLink="true">https://monkeywie.cn/posts/vuetify-ues-local-resource</guid><pubDate>Tue, 22 Feb 2022 14:11:53 GMT</pubDate><content:encoded>&lt;p&gt;为了开发公司的一些效率工具 UI，我选择了 Vuetify，它是一个基于 Vue 的 UI 框架，它提供了一个简单的组件库，可以让我们快速开发出一些简单并且好看的 material design 的 UI。&lt;/p&gt;
&lt;p&gt;但是通过官方脚手架生成的项目，默认是通过引入外网 cdn 的方式导入图标以及字体文件，然而国内的网络访问这些资源比较慢，所以就想把这些资源放到本地，提高访问速度。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;安装步骤&lt;/h2&gt;
&lt;p&gt;首先修改项目根目录下的 &lt;code&gt;public/index.html&lt;/code&gt;文件，删除如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900&quot;/&amp;gt;
&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后安装 npm 包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install typeface-roboto -D
npm install @mdi/font -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再通过修改&lt;code&gt;main.js&lt;/code&gt;进行引用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;typeface-roboto/index.css&quot;;
import &quot;@mdi/font/css/materialdesignicons.css&quot;;
import Vue from &quot;vue&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;npm run serve&lt;/code&gt;一下就可以看到，所有的资源都是通过本地服务来访问了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>swagger文档change log生成工具</title><link>https://monkeywie.cn/posts/draft2</link><guid isPermaLink="true">https://monkeywie.cn/posts/draft2</guid><pubDate>Sun, 10 Apr 2022 16:11:18 GMT</pubDate><content:encoded>&lt;p&gt;// TODO&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>突破神奇的Cloudflare防火墙</title><link>https://monkeywie.cn/posts/break-through-cf-firewall</link><guid isPermaLink="true">https://monkeywie.cn/posts/break-through-cf-firewall</guid><pubDate>Wed, 18 May 2022 16:07:10 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;最近碰到一个神奇的网站，在浏览器可以打开，但是通过 curl 或者 代码访问就直接 403，我估摸着这肯定是做了&lt;code&gt;UA校验&lt;/code&gt;，于是请求的时候把浏览器的 UA 给带上，然后访问发现还是 403，不过这也难不倒我，肯定是还有校验其它的请求头，直接浏览器打开 network，把所有的请求头复制过来并且带上，确保我和浏览器在 http 协议层面的请求完全一样，这样不可能会失败了吧，然而运行完发现还是 403。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;放个地址: https://pixabay.com&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;思考&lt;/h2&gt;
&lt;p&gt;服务端校验客户端没有什么黑魔法，因为都是通过 TCP 协议通讯，不可能存在浏览器发送一个 HTTP 报文和我发送一个同样的 HTTP 报文服务器能识别出来，既然不是校验的 HTTP 层，那只可能是在 TLS 层校验的，于是祭出&lt;code&gt;wireshark&lt;/code&gt;抓包，看看能不能找到 TLS 握手中差异化的东西，众所周知在 TLS 握手时有一个客户端发送给服务端的&lt;code&gt;Client Hello&lt;/code&gt;报文，很有可能就是根据它来辨别浏览器和非浏览器请求的，因为在这个报文中，客户端要告诉服务端支持的加密套件，TLS 版本等等信息，而这些信息根据客户端的实现都会有所差异，先抓个正常浏览器请求的报文看看，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;break-through-cf-firewall/2022-07-21-17-11-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后再通过 curl 访问抓包，如下图；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;break-through-cf-firewall/2022-07-21-17-36-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到两边的报文确实存在很大的差异，逐一对比排查之后发现很有可能是因为 curl 的请求报文里缺少&lt;code&gt;supported_versions&lt;/code&gt;扩展信息导致的 403，浏览器那边在此扩展信息内容如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;break-through-cf-firewall/2022-07-21-17-52-54.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;表示支持&lt;code&gt;TLSv1.2&lt;/code&gt;和&lt;code&gt;TLSv1.3&lt;/code&gt;，而且最终握手之后的协议也是切换到了&lt;code&gt;TLSv1.3&lt;/code&gt;，在上面两个对比图可以看到，浏览器走的是&lt;code&gt;TLSv1.3&lt;/code&gt;，而 curl 走的是&lt;code&gt;TLSv1.2&lt;/code&gt;，可能是一定要使用&lt;code&gt;TLSv1.3&lt;/code&gt;才能访问成功。&lt;/p&gt;
&lt;h2&gt;验证&lt;/h2&gt;
&lt;p&gt;马上 google 了下如何指定 curl 的 TLS 版本，发现只需要加上&lt;code&gt;--tlsv1.3&lt;/code&gt;参数就可以了，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -I --tlsv1.3 &apos;https://pixabay.com/&apos;  \
&amp;gt; -H &apos;accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6&apos; \
&amp;gt; -H &apos;user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36 Edg/103.0.1264.49&apos;
HTTP/2 200
date: Fri, 22 Jul 2022 02:40:35 GMT
content-type: text/html; charset=utf-8
cf-ray: 72e8cffc18c73d5a-HKG
cache-control: s-maxage=86400
content-language: en
vary: Accept-Encoding, Cookie, Accept-Language
cf-cache-status: MISS
content-security-policy: frame-ancestors none
expect-ct: max-age=604800, report-uri=&quot;https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct&quot;
referrer-policy: strict-origin-when-cross-origin
x-frame-options: DENY
set-cookie: __cf_bm=Cy4a751rDND6kHhu.RzEr5DpqnaxRdpUxaMfNfkya0A-1658457635-0-AS1DaewDqNjWHZ/m74A88bNyEG0EFsZAwmsm/ON5QQEuh8B6XOS7PkSnhGgXPLV+LtEvzOKTy/WWHmwY63uGlD0=; path=/; expires=Fri, 22-Jul-22 03:10:35 GMT; domain=.pixabay.com; HttpOnly; Secure; SameSite=None
server: cloudflare
alt-svc: h3=&quot;:443&quot;; ma=86400, h3-29=&quot;:443&quot;; ma=86400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过反复验证，发现除了要指定&lt;code&gt;tlsv1.3&lt;/code&gt;之外，还需要加上&lt;code&gt;accept-language&lt;/code&gt;和&lt;code&gt;user-agent&lt;/code&gt;头，并且一定得是 http2 协议，三个条件缺一不可。&lt;/p&gt;
&lt;h2&gt;nodejs 访问&lt;/h2&gt;
&lt;p&gt;上面说到了一定要走 http2 协议，而现在市面上流行的 http client 基本都是只支持 http1.1，所以只能直接从基础库入手了，官方有一个&lt;code&gt;http2&lt;/code&gt;的库，一番调教之后也是成功请求了，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const http2 = require(&quot;http2&quot;);

function get(host, path) {
  return new Promise((resolve, reject) =&amp;gt; {
    const session = http2.connect(`https://${host}`, {
      minVersion: &quot;TLSv1.3&quot;,
      maxVersion: &quot;TLSv1.3&quot;,
    });

    session.on(&quot;error&quot;, (err) =&amp;gt; {
      reject(err);
    });

    const req = session.request({
      [http2.constants.HTTP2_HEADER_AUTHORITY]: host,
      [http2.constants.HTTP2_HEADER_METHOD]: http2.constants.HTTP2_METHOD_GET,
      [http2.constants.HTTP2_HEADER_PATH]: path,
      &quot;user-agent&quot;:
        &quot;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.1185.50&quot;,
    });

    req.setEncoding(&quot;utf8&quot;);
    let data = &quot;&quot;;
    req.on(&quot;data&quot;, (chunk) =&amp;gt; {
      data += chunk;
    });
    req.on(&quot;end&quot;, () =&amp;gt; {
      session.close();
      if (data) {
        try {
          resolve(data);
        } catch (e) {
          reject(e);
        }
      }
    });
    req.on(&quot;error&quot;, (err) =&amp;gt; {
      reject(err);
    });
    req.end();
  });
}

(async function () {
  const data = await get(&quot;pixabay.com&quot;, &quot;/&quot;);
  console.log(data);
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;深入&lt;/h2&gt;
&lt;p&gt;虽然已经成功请求了，但是本着探索的精神继续深入发现 cloudflare 官方有一篇博客就是专门介绍这个 TLS 拦截技术的，链接如下：
&lt;a href=&quot;https://blog.cloudflare.com/monsters-in-the-middleboxes/&quot;&gt;https://blog.cloudflare.com/monsters-in-the-middleboxes/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其中有一段内容也证明了我的猜想，翻译后如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;break-through-cf-firewall/2022-07-22-11-00-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也就是说 cloudflare 会维护一组浏览器的 TLS 指纹，当收到一个 Client Hello 请求时，会检查这组指纹，如果匹配不上，就会拦截这个请求，这样可以拦截掉大部分不是来自浏览器的请求了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Go语言中interface{}指针赋值</title><link>https://monkeywie.cn/posts/go-assign-interface-pointer-value</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-assign-interface-pointer-value</guid><pubDate>Fri, 30 Sep 2022 17:45:18 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 Go 中，可以通过传递指针来改变函数外部变量的值，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var a int = 1
    fmt.Println(a) // 1
    change(&amp;amp;a,2)
    fmt.Println(a) // 2
}

func change(a *int, b int) {
   // 通过解引用来改变外部变量的值
    *a = b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在某些情况下，我们可能需要传递&lt;code&gt;interface{}&lt;/code&gt;来接收任意的指针变量，这时候就会遇到一个问题，&lt;code&gt;interface{}&lt;/code&gt;类型声明的变量是不能直接赋值指针的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var a int = 1
    fmt.Println(a) // 1
    change(&amp;amp;a,2)
    fmt.Println(a) // 2
}

func change(a interface{},b interface{}) {
    // 这一行会报错: invalid operation: cannot indirect a (variable of type interface{})
    *a = b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;查阅了一些资料，发现可以通过&lt;code&gt;reflect&lt;/code&gt;包来解决这个问题，最终代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var a int = 1
    fmt.Println(a) // 1
    change(&amp;amp;a,2)
    fmt.Println(a) // 2
}

func change(a interface{},b interface{}) {
    // 通过反射来获取指针的值
    val := reflect.ValueOf(a)
    val.Elem().Set(reflect.ValueOf(b))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用新版本范型特性来解决(推荐)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    var a int = 1
    fmt.Println(a) // 1
    // 通过指定范型来获取指针的值
    change[int](&amp;amp;a,2)
    fmt.Println(a) // 2
}

func change[T any](a *T,b T) {
    *a = b
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>Levi</author></item><item><title>wsl终端运行命令提示 &apos;bash\r&apos; No such file or directory</title><link>https://monkeywie.cn/posts/wsl-bash-no-suchffile-or-directory</link><guid isPermaLink="true">https://monkeywie.cn/posts/wsl-bash-no-suchffile-or-directory</guid><pubDate>Mon, 22 May 2023 15:05:39 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;想在 wsl 上安装 flutter 跑一跑我的&lt;code&gt;gopeed&lt;/code&gt;项目，在安装完 flutter 并设置好环境变量后，运行&lt;code&gt;flutter doctor&lt;/code&gt;提示&lt;code&gt;bash\r: No such file or directory&lt;/code&gt;，搜索了一番发现没有找到解决方案，于是自己摸索了一番，记录下来。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;p&gt;通过之前搜索的结果，大部分都是说是换行符的问题，可是我安装的明明是 linux 版本的 flutter，不可能是换行符的问题，怀疑是调用到了 windows 宿主机上的 flutter，于是输入&lt;code&gt;which flutter&lt;/code&gt;一看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which flutter
/mnt/d/flutter/bin/flutter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;果然是调用到了 windows 上的 flutter，明明在&lt;code&gt;~/.bashrc&lt;/code&gt;配置好了 flutter 的&lt;code&gt;PATH&lt;/code&gt;，为什么还是调用到了 windows 上的呢？输入&lt;code&gt;echo $PATH&lt;/code&gt;一看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo $PATH
/mnt/d/flutter/bin:...:/home/levi/flutter/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个 flutter 的路径都在，这就是问题所在了，因为&lt;code&gt;/mnt/d/flutter/bin&lt;/code&gt;在&lt;code&gt;/home/levi/flutter/bin&lt;/code&gt;前面，所以会优先调用 windows 上的 flutter，解决方案就是把&lt;code&gt;/mnt/d/flutter/bin&lt;/code&gt;放到&lt;code&gt;/home/levi/flutter/bin&lt;/code&gt;后面，这样就不会调用到 windows 上的 flutter 了，修改&lt;code&gt;~/.bashrc&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 修改前
export PATH=&quot;$PATH:home/levi/flutter/bin&quot;
# 修改后
export PATH=&quot;/home/levi/flutter/bin:$PATH&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新打开终端，输入&lt;code&gt;flutter doctor&lt;/code&gt;，问题解决。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>在vuepress整合firebase analytics</title><link>https://monkeywie.cn/posts/integrating-firebase-in-vuepress</link><guid isPermaLink="true">https://monkeywie.cn/posts/integrating-firebase-in-vuepress</guid><pubDate>Fri, 30 Jun 2023 20:24:27 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近想给&lt;code&gt;gopeed&lt;/code&gt;的文档网站添加一个统计用户的访问量的功能，所以就想到了 firebase 的 analytics，但是在 vuepress 中整合 firebase 的 analytics 并不是很简单，所以就有了这篇文章。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;问题和解决&lt;/h2&gt;
&lt;p&gt;firebase 官方给出的文档是通过&lt;code&gt;esm&lt;/code&gt;模块化的方式进行引入，但是 vuepress 是不支持引入&lt;code&gt;esm&lt;/code&gt;文件并打包的的，所以需要通过&lt;code&gt;script&lt;/code&gt;标签的方式引入，然而官方给的 js 文件也是&lt;code&gt;esm&lt;/code&gt;的，官方代码示例如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script type=&quot;module&quot;&amp;gt;
  import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js&quot;;
  import { getAnalytics } from &quot;https://www.gstatic.com/firebasejs/9.1.0/firebase-analytics.js&quot;;

  const firebaseConfig = {
    // ...
  };

  const app = initializeApp(firebaseConfig);
  const analytics = getAnalytics(app);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里可以通过 vuepress 中 head 的配置来引入，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  head: [
    [
      &quot;script&quot;,
      {
        type: &quot;module&quot;,
      },
      `
  import { initializeApp } from &quot;https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js&quot;;
  import { getAnalytics } from &quot;https://www.gstatic.com/firebasejs/9.1.0/firebase-analytics.js&quot;;

  const firebaseConfig = {
    // ...
  };

  const app = initializeApp(firebaseConfig);
  const analytics = getAnalytics(app);
      `,
    ],
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我觉得这种 cdn 方式引入对国内用户不太友好，gstatic 在国内的访问速度很慢，所以我选择了把两个 js 文件下载到本地，然后通过&lt;code&gt;script&lt;/code&gt;标签引入，这样就不会有 cdn 的问题了，于是改造后的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module.exports = {
  head: [
    [
      &quot;script&quot;,
      {
        type: &quot;module&quot;,
      },
      `
  import { initializeApp } from &quot;/js/firebase-app.js&quot;;
  import { getAnalytics } from &quot;/js/firebase-analytics.js&quot;;

  const firebaseConfig = {
    // ...
  };

  const app = initializeApp(firebaseConfig);
  const analytics = getAnalytics(app);
      `,
    ],
  ],
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而这样 js 是加载到了，但是执行的时候会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;component analytics has not been registered yet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;google 了一番也没找到什么有用的信息，最后在看&lt;code&gt;firebase-analytics.js&lt;/code&gt;代码的时候发现了代码里有这么一段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  registerVersion as e,
  _registerComponent as t,
  _getProvider as n,
  getApp as a,
} from &quot;https://www.gstatic.com/firebasejs/9.1.0/firebase-app.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我猜可能就是因为这里引入的路径不是本地路径导致的，于是把这段 js 改成了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import {
  registerVersion as e,
  _registerComponent as t,
  _getProvider as n,
  getApp as a,
} from &quot;/js/firebase-app.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以正常使用了。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;不得不说 firebase 的 sdk 还是挺激进的，直接用上了 esm，根本不考虑老旧浏览器的兼容性，好歹给个 UMD 版本吧，这样就不用我自己改代码了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>[转]安卓逆向某生鲜平台app</title><link>https://monkeywie.cn/posts/android-reverse</link><guid isPermaLink="true">https://monkeywie.cn/posts/android-reverse</guid><pubDate>Wed, 16 Aug 2023 10:14:10 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;转载申明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文章转载自互联网，如有侵权，请联系删除
本文仅作为学习交流，禁止用于非法用途&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 背景&lt;/h2&gt;
&lt;p&gt;阿里系当前采用的加密版本是 6.3，6.2 版本的大家几乎都解决了，6.3 的网上资料很少，这里讲讲 6.3 的解密过程&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;阿里系通用这一套加密算法，主要是 x-sign，x-sgext，x_mini_wua，x_umt 这四个加密参数，解决了其中一个 app，其他的比如淘 X，咸 X 等 app 都相差不大了，改改参数，或者替换不同的方法名称就行；&lt;/li&gt;
&lt;li&gt;使用的是 frida-rpc 主动调用的方法（对加密算法解密的话，难度很高，我没做出来）；&lt;/li&gt;
&lt;li&gt;本次做的是阿里系的某生鲜平台 app， &lt;strong&gt;仅作为学习交流，禁止用于商业使用&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;首先我们抓个包先，可以看到，加密版本是 6.3，加密参数还是我们常见的这四大参数
这里请求头具体分析就不说了，直接去逆向&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-05-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2.逆向&lt;/h2&gt;
&lt;p&gt;1.查壳
第一步不用多说，不管什么 app，先查壳，查壳工具 PKID，基本上满足需求，我们运气好，HM 没做什么加壳措施，所以我们直接略过这个步骤；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-06-09.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果有遇到加壳的，可以用一下几种办法&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Frida-Unpack&lt;/strong&gt;
firda-unpack 原理是利用 frida hook libart.so 中的 OpenMemory 方法，拿到内存中 dex 的地址，计算出 dex 文件的大小，从内存中将 dex 导出，我们可以查看项目中的 OpenMemory.js 文件中的代码更清晰直观地了解。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub地址：https://github.com/GuoQiang1993/Frida-Apk-Unpack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;FRIDA-DEXDump&lt;/strong&gt;
葫芦娃所写，脱壳后的 dex 文件保存在 PC 端 main.py 同一目录下，以包名为文件名&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub地址：https://github.com/hluwa/FRIDA-DEXDump
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;frida_dump&lt;/strong&gt;
会搜索 dex 文件并 dump 下来，保存在 data/data/packageName/files 目录下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub地址：https://github.com/lasting-yang/frida_dump
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Frida_Fart[推荐]&lt;/strong&gt;
寒冰写的， Frida 版的 Fart, 目前只能在 andorid8 上使用该 frida 版 fart 是使用 hook 的方式实现的函数粒度的脱壳，仅仅是对类中的所有函数进行了加载，但依然可以解决绝大多数的抽取保护&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GitHub地址：https://github.com/hanbinglengyue/FART
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.反编译&lt;/strong&gt;
既然 app 没有做加壳措施，那我们直接上手 &lt;code&gt;jadx&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-06-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里反编译工具推荐使用 Android Killer，jadx，JEB，当你反编译失败的时候，去尝试另外的工具，会发现结果不同哦。&lt;strong&gt;千万别仅使用一个工具；&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3.查找加密方法&lt;/h2&gt;
&lt;p&gt;1.在 jadx 里面直接全局搜索 x-sign 吧&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-06-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们很容易就找到这个 getUnifiedSign 函数，仔细分析函数发现是一个接口，那这个函数所在的类 mtopsdk.security.InnerSignImpl 就是我们要找的实现类。&lt;/p&gt;
&lt;p&gt;这里教你们一个小方法，在 jadx 里面对这个 getUnifiedSign 函数直接右击，复制 frida 代码，我们函数找的对不对，直接 hook 一下就知道了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-07-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写一段调用 js 的 python 程序&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import frida, sys


def on_message(message, data):
    if message[&apos;type&apos;] == &apos;send&apos;:

        print(&quot;[*] {0}&quot;.format(message[&apos;payload&apos;]))

    else:

        print(message)


jscode = &apos;&apos;&apos;
Java.perform(function(){

/**  把该部分替换为刚刚复制的内容即可**、

}
)
    &apos;&apos;&apos;


process = frida.get_remote_device().attach(&apos;app的包名&apos;)

script = process.create_script(jscode)

script.on(&apos;message&apos;, on_message)

script.load()
sys.stdin.read()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行程序我们查看一下结果&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-08-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;非常 nice，我们 hook 之后查看输出，这个方法传入了哪些参数，又输出了哪些值，一目了然，x-sign 等加密值都在里面，说明我们方法找对了&lt;/p&gt;
&lt;h2&gt;4.调用方法&lt;/h2&gt;
&lt;p&gt;这里我们就直接用 rpc 主动调用的方法获取加密值
方法我们找到了，传入的参数我们也找到了，那用 rpc 调用也不在话下了
直接上代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import frida


def on_message(message, data):
    if message[&apos;type&apos;] == &apos;send&apos;:

        print(&quot;[*] {0}&quot;.format(message[&apos;payload&apos;]))

    else:

        print(message)
def start_hook():
	jscode = &apos;&apos;&apos;
	    rpc.exports = {
	        para: function(a,b,c,d,e,f) {
	            var ret = {};
	            Java.perform(function() {
	                Java.choose(&quot;mtopsdk.security.InnerSignImpl&quot;,{
	                onMatch: function(instance){
	                var a= &quot;&quot;;
	                var b= &quot;&quot;;
	                var c = ;
	                var d = ;
	                var e = ;
	                var f = ;
	                //这些都是传入的参数，具体传参内容根据实际修改
	                var res = instance.getUnifiedSign(a, b, c, d, e, f).toString();
	                
	                //console.log(&apos;getUnifiedSign ret value is &apos; + res);
	                ret[&quot;result&quot;] = res;
	                                        },
	                onComplete: function(){
	                    //console.log(&apos;******js load over*****&apos;)
	                                        }
	                                        
	                                                                 })
	                                    })
	                                    return ret;
	                                                                            }
	            };
	        &apos;&apos;&apos;
	process = frida.get_remote_device().attach(&apos;&apos;)

    script = process.create_script(jscode)

    script.on(&apos;message&apos;, on_message)

    script.load()
    return script


result_hook = start_hook().exports.para() # 可传参进去

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在验证一下 rpc 调用是否可行&lt;/p&gt;
&lt;p&gt;1.首先执行 rpc 调用的代码，打印出其中的部分参数比如时间戳，以及解密后的 x-sign
&lt;img src=&quot;android-reverse/2023-08-16-10-09-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.于此同时我们查看一下前面部分我们提到的 hook 这个 getUnifiedSign 函数，去查看一下结果
&lt;img src=&quot;android-reverse/2023-08-16-10-09-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们对比一下，时间戳，x-sign 的值都是一样的，说明传入的参数正常，并能够输出加密参数&lt;/p&gt;
&lt;h2&gt;5.请求数据&lt;/h2&gt;
&lt;p&gt;上一步实现 frida-rpc 的调用，接下来就可去写请求数据的代码了
这个就没有什么好说的，请求头 headers 放进去，rpc 调用一下，替换加密参数，然后直接 request 请求即可。&lt;/p&gt;
&lt;h2&gt;6.结果&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-10-16.png&quot; alt=&quot;&quot; /&gt;
让我们看一下请求后的结果吧!
app 里面的数据可以正常的拿到了，这个 app 的逆向我们就大功告成了，有什么问题可以联系我。&lt;/p&gt;
&lt;h2&gt;九月四号更新 wua 加密算法&lt;/h2&gt;
&lt;p&gt;很多人问我 wua 怎么获取，还是用咱们这一套 rpc 主动调用，在传入的参数中，&lt;strong&gt;有个 z 参数，需要参入 boolen 值，当传入 false 时，不返回 wua 加密参数&lt;/strong&gt; 如图&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;android-reverse/2023-08-16-10-11-00.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当传入 true 时，返回 wua 加密参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;有新的问题可以继续找我，谢谢！&lt;/p&gt;
&lt;p&gt;————————————————
版权声明：本文为 CSDN 博主「我想吃橘子味的橙子々」的原创文章，遵循 CC 4.0 BY-SA 版权协议，转载请附上原文出处链接及本声明。
原文链接：https://blog.csdn.net/qq_44130722/article/details/126621134&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>[转]某麦网APK抢票接口加密参数分析</title><link>https://monkeywie.cn/posts/android-hack</link><guid isPermaLink="true">https://monkeywie.cn/posts/android-hack</guid><pubDate>Wed, 16 Aug 2023 10:14:10 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;转载申明&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;文章转载自互联网，如有侵权，请联系删除
本文仅作为学习交流，禁止用于非法用途&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;0x00 概述&lt;/h1&gt;
&lt;p&gt;针对某麦网部分演唱会门票仅能在 app 渠道抢票的问题，本文研究了 APK 的抢票接口并编写了抢票工具。本文介绍的顺序为环境搭建、抓包、trace 分析、接口参数获取、rpc 调用实现，以及最终的功能实现。通过阅读本文，你将学到反抓包技术破解、Frida hook、jadx apk 逆向技术，并能对淘系 APP 的运行逻辑有所了解。本文仅用于学习交流，严禁用于非法用途。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;关键词&lt;/strong&gt;： frida, damai.cn, Android 逆向
先放成功截图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/b7c8e2be-cf53-432f-9a33-9960a66f715e&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;0x01 缘起&lt;/h1&gt;
&lt;p&gt;疫情结束的 2023 年 5 月，大家对出去玩都有点疯狂，歌手们也扎堆开演唱会。演唱会虽多，票却一点也不好抢，抢五月天的门票难度不亚于买五一的高铁票。所以想尝试找一些脚本来辅助抢票，之前经常用 selenium 和 request 做一些小爬虫来搞定自动化的工作，所以在 &lt;a href=&quot;https://github.com/MakiNaruto/Automatic_ticket_purchase&quot;&gt;MakiNaruto/Automatic_ticket_purchase&lt;/a&gt; 的基础上改了改，实现抢票功能。但是某麦网实在太&lt;strong&gt;狡猾&lt;/strong&gt;了，改完爬虫才发现几乎所有的热门演唱会只允许在 app 购买，所以就需要利用 APP 实现接口自动化。&lt;/p&gt;
&lt;p&gt;本着能省事则省事的原则，笔者在文章 &lt;a href=&quot;https://github.com/m2kar/m2kar.github.io/issues/20&quot;&gt;[Android] 基于 Airtest 实现某麦网 app 自动抢票程序&lt;/a&gt; 中用自动化测试技术实现了抢票程序，但是速度太慢，几乎不能用。果然捷径往往不好走，因此继续尝试分析某麦网 apk 的 api 接口。&lt;/p&gt;
&lt;h1&gt;0x02 环境&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;windows 10&lt;/li&gt;
&lt;li&gt;cn.damai apk 版本 8.5.4 (2023-04-26)&lt;/li&gt;
&lt;li&gt;bluestacks 5.11.56.1003 p64&lt;/li&gt;
&lt;li&gt;adb 31.0.2&lt;/li&gt;
&lt;li&gt;Root Checker 6.5.3&lt;/li&gt;
&lt;li&gt;wireshark 4.0.5&lt;/li&gt;
&lt;li&gt;frida 16.0.19&lt;/li&gt;
&lt;li&gt;jadx-gui 1.4.7&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;0x03 环境搭建&lt;/h1&gt;
&lt;h2&gt;bluestacks 环境搭建&lt;/h2&gt;
&lt;p&gt;目前 Android 模拟器竞品很多，选择 Bluestacks &lt;strong&gt;5&lt;/strong&gt;是因为它能和 windows 的 hyper-v 完美兼容，root 过程也相对简单。&lt;/p&gt;
&lt;h3&gt;首先需要 root Bluestacks 环境。&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;下载安装 Bluestacks。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;运行 Bluestacks Multi-instance Manager，发现默认安装的版本为 Android Pie 64bit 版本，即 Android 9.0。此时退出 bluestack 所有程序。
&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/674ae190-5f7d-4f23-9c26-b38242ebc496&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭 bluestack 后编辑 bluestacks 配置文件， &lt;code&gt;%programdata%\BlueStacks_nxt\bluestacks.conf&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由于作者安装时 C 盘空间不足，真实的&lt;code&gt;bluestacks.conf&lt;/code&gt;在&lt;code&gt;D:\BlueStacks_nxt\bluestacks.conf&lt;/code&gt;，大家也根据实际情况调整
&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/fa99b25a-6105-48db-9b14-8a6175b141a4&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在配置文件中查找 root 关键词，对应值修改为 1，共两处。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;bst.feature.rooting=&quot;1&quot;
bst.instance.Pie64.enable_root_access=&quot;1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/074afb2c-f29d-4d92-bb4f-91f9827e8e45&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;启动 bluestack 模拟器，安装 &lt;code&gt;Root Checker&lt;/code&gt; APP，点击验证 root，即可发现 root 已成功。
&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/8f26602e-101f-4f02-ae48-e63766810d25&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;上述 root 过程主要参考了 https://appuals.com/root-bluestacks/ ，部分地方做了改正，在此感谢原文作者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;打开 adb 调试&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;bluestack 设置-高级中打开 Adb 调试，并记录下端口&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/d4165eb6-960a-4c19-bad7-5115548b04a5&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开主机命令行，运行 &lt;code&gt;adb connect localhost:6652&lt;/code&gt;，端口号修改为上一步的端口号，即可连接。再运行&lt;code&gt;adb devices&lt;/code&gt;，如有对应设备则连接成功。&lt;/li&gt;
&lt;li&gt;进入 adb shell，执行 su 进入 root 权限，命令行标识由&lt;code&gt;$&lt;/code&gt;变为&lt;code&gt;#&lt;/code&gt;，即表示 adb 进入 root 权限成功。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/e8f2f0c5-7df1-4226-9f16-a5d5d2a8cb2b&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;frida 环境搭建&lt;/h2&gt;
&lt;p&gt;frida 是大名鼎鼎的动态分析的 hook 神器，用它可以直接访问修改二进制的内存、函数和对象，非常方便。它对于 Android 的支持也是很完美。&lt;/p&gt;
&lt;p&gt;frida 的运行采用 C/S 架构，客户端为电脑端的开发环境，服务器端为 Android，均需对应部署搭建。&lt;/p&gt;
&lt;h3&gt;客户端环境搭建(Windows)&lt;/h3&gt;
&lt;p&gt;firda 客户端基于 python3 开发，因此首先需要配置好 python3 的运行环境，然后执行 &lt;code&gt;pip install frida-tools&lt;/code&gt;即可完成安装。运行 &lt;code&gt;frida --version&lt;/code&gt;可验证 frida 版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(py3) PS E:\TEMP\damai&amp;gt; frida --version
16.0.19
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;服务器 环境搭建(Android)&lt;/h3&gt;
&lt;p&gt;环境搭建第二步是在 Android 模拟器中运行 frida-server。这样可以让 Frida 通过 ADB/USB 调试与我们的 Android 模拟器连接。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;下载 frida-server
最新的 frida-server 可以从 https://github.com/frida/frida/releases 下载。请注意下载与设备匹配的架构。如果您的模拟器是 x86_64，请下载相应版本的 frida-server。本文使用的版本为 &lt;a href=&quot;https://github.com/frida/frida/releases/download/16.0.18/frida-server-16.0.18-android-x86_64.xz&quot;&gt;frida-server-16.0.18-android-x86_64.xz&lt;/a&gt;
&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/d9d085a1-f4dd-4873-8e95-a3a3d881d585&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;传入 Android 模拟器。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将下载后的.xz 文件解压，将&lt;code&gt;frida-server&lt;/code&gt;传入 Android 模拟器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb push frida-server /data/local/tmp/
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行 frida-server&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;使用 adb root 以 root 模式重新启动 ADB，并通过 adb shell 重新进入 shell 的访问。进入 shell 后，进入我们放置 frida-server 的目录并为其授予执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /data/local/tmp/
chmod +x frida-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;code&gt;./frida-server &lt;/code&gt;，运行 frida-server，并保持本 shell 窗口开启。&lt;/p&gt;
&lt;p&gt;成功截图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/92c87610-7dd7-4352-9744-7f29a504bf00&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有些情况下，应用程序会检测在是否在模拟器中运行，但对某麦网 app 的分析暂无影响。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;测试是否连接成功&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 window 端运行 frida-ps 命令：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/b150db94-ed85-4f8e-a3a2-c90839f263a1&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看到一堆熟悉的 Android 进程，我们就连接成功啦&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;转发 frida-server 端口 (可选)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;frida-server 跑在 Android 端，frida 需要通过连接 frida-server。上一步使用 adb 的方式连接，frida 认为是 USB 模式，需要&lt;code&gt;-U&lt;/code&gt;命令。frida 也支持依赖端口的远程连接模式，在某些场景下更加灵活。可以通过端口转发的方式实现此功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;27042 是用于与 frida-server 通信的默认端口号,之后的每个端口对应每个注入的进程，检查 27042 端口可检测 Frida 是否存在。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本部分主要参考了 https://learnfrida.info/java/ ， 在此感谢原文作者。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;0x04 抓包&lt;/h1&gt;
&lt;h2&gt;抓包及 https 解密方法&lt;/h2&gt;
&lt;p&gt;本章节将介绍用 tcpdump+frida+wireshark 实现 Android 的全流量抓包，能实现 https 解密。&lt;/p&gt;
&lt;p&gt;惯用的 Android 抓包手段是用 fiddler/burpsuite/mitmproxy 搭建代理服务器，设置 Android 代理服务器并用中间人劫持的方式获取 http 协议流量的内容。如需对 https 流量解密，还需要在安卓上安装 https 根证书。Android9.0 以后的版本对用户自定义根证书有了一些限制，抓包不再那么简单，但这难不倒技术大神们，大家可以可以参考以下几篇文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://segmentfault.com/a/1190000041674464&quot;&gt;从原理到实战，全面总结 Android HTTPS 抓包&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jishuin.proginn.com/p/763bfbd5f92e&quot;&gt;Android 高版本 HTTPS 抓包解决方案&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述的抓包方式只能抓到 http 协议层以上的流量，这次我们来点不一样的，用 tcpdump+frida+wireshark 实现 Android 的全流量抓包，能实现 https 解密。&lt;/p&gt;
&lt;h3&gt;1. 搞定 tcpdump&lt;/h3&gt;
&lt;p&gt;本文基于 termux 安装使用 tcpdump。&lt;/p&gt;
&lt;p&gt;首先安装 termux apk。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/57afcdaa-2417-4b61-8f42-7c83391f9144&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;打开 termux 运行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;挂载存储&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;termux-setup-storage
## 会弹出授权框，点允许
ls ~/storage/
## 如果出现dcim, downloads等目录，即表示成功
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;安装 tcpdump&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;pkg install root-repo
pkg install tcpdump
pkg install tsu
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;运行抓包&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo tcpdump -i any -s 0 -w ~/storage/downloads/capture.pcap
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;tcpdump 成功截图:
&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/b42d5a4d-1384-4d90-9575-006e395d7fad&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之后就可以把 downloads 目录下的抓包文件拷贝到电脑上，用 wireshark 打开做进一步分析。&lt;/p&gt;
&lt;h3&gt;2. 解密 https 流量&lt;/h3&gt;
&lt;p&gt;Wireshark 解密 https 流量的方法和原理介绍有很多，可参考以下文章，本文不再赘述。&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;https://unit42.paloaltonetworks.com/wireshark-tutorial-decrypting-https-traffic/&lt;/li&gt;
&lt;li&gt;https://zhuanlan.zhihu.com/p/36669377&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;wireshark 解密技术的重点在于拿到客户端通信的密钥日志文件(ssl key log)，像下面这种：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/1c7c211f-f8cd-420b-9c80-691c427c1504&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 Android 中实现抓取 ssl key log 需要 hook 系统的 SSL 相关函数，可以用 frida 实现。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先将下面的 hook 代码保存为&lt;code&gt;sslkeyfilelog.js&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// sslkeyfilelog.js
function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
  console.log(&quot;start----&quot;);
  function keyLogger(ssl, line) {
    console.log(new NativePointer(line).readCString());
  }
  const keyLogCallback = new NativeCallback(keyLogger, &quot;void&quot;, [
    &quot;pointer&quot;,
    &quot;pointer&quot;,
  ]);

  Interceptor.attach(SSL_CTX_new, {
    onLeave: function (retval) {
      const ssl = new NativePointer(retval);
      const SSL_CTX_set_keylog_callbackFn = new NativeFunction(
        SSL_CTX_set_keylog_callback,
        &quot;void&quot;,
        [&quot;pointer&quot;, &quot;pointer&quot;]
      );
      SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
    },
  });
}
startTLSKeyLogger(
  Module.findExportByName(&quot;libssl.so&quot;, &quot;SSL_CTX_new&quot;),
  Module.findExportByName(&quot;libssl.so&quot;, &quot;SSL_CTX_set_keylog_callback&quot;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;然后用 frida 加载运行 hook&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;frida -U -l .\sslkeyfilelog.js  -f cn.damai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/e0c46289-213e-4e49-821a-def3fcfc8367&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最后，抓包结束后将得到的 key 保存到 sslkey.txt，格式是下面这样的，不要掺杂别的。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;CLIENT_RANDOM 557e6dc49faec93dddd41d8c55d3a0084c44031f14d66f68e3b7fb53d3f9586d 886de4677511305bfeaee5ffb072652cbfba626af1465d09dc1f29103fd947c997f6f28962189ee809944887413d8a20
CLIENT_RANDOM e66fb5d6735f0b803426fa88c3692e8b9a1f4dca37956187b22de11f1797e875 65a07797c144ecc86026a44bbc85b5c57873218ce5684dc22d4d4ee9b754eb1961a0789e2086601f5b0441c35d76c448

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在运行 Frida Hook 获取 sslkey 的同时，运行 tcpdump 抓包。抓包中依次测试获取详情页、选择价位、提交订单等操作，并对应记录下执行操作的时间，方便后续分析。&lt;/p&gt;
&lt;p&gt;抓包完成后，用 wireshark 打开 tcpdump 抓包获得的 pcap 文件，在 wireshark 首选项-protocols-TLS 中，设置 (Pre)-Master-Secret log filename 为上述 sslkey.txt。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/30197b16-e429-4b9b-bb32-e18be8b1952e&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;即可实现 https 流量的解密。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本部分主要参考了 https://www.52pojie.cn/thread-1405917-1-1.html ，向原作者表示感谢&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;流量分析&lt;/h2&gt;
&lt;p&gt;在上述步骤中拿到了解密后的流量，我们就能对某麦网的流量做进一步分析了。&lt;/p&gt;
&lt;h3&gt;某麦网的 API 流&lt;/h3&gt;
&lt;p&gt;在此铺垫一下，通过前期对某麦网 PC 端和移动端 H5 的分析，某麦网购票的工作流程大概为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获得详情：接口为&lt;code&gt;mtop.alibaba.damai.detail.getdetail&lt;/code&gt;。基于某演出的 id(itemId)获得演出的详细信息，包括详情、场次、票档(SkuId)价位及状态信息，&lt;/li&gt;
&lt;li&gt;构建订单：接口为&lt;code&gt;mtop.trade.order.build.h5&lt;/code&gt;。发送 演出 id+数量+票档 id(&lt;code&gt;itemId_count_skuId&lt;/code&gt;)，得到提交订单所需的表单信息，包括观众、收货地址等。&lt;/li&gt;
&lt;li&gt;提交订单：接口为&lt;code&gt;mtop.trade.order.create.h5&lt;/code&gt;。对上一步构建订单得到的表单参数作出修改后，发送给服务器，得到最后的订单提交结果和支付信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;apk 流量分析&lt;/h3&gt;
&lt;p&gt;首先用过滤器&lt;code&gt;http &amp;amp;&amp;amp; tcp.dstport==443&lt;/code&gt;，得到向服务器发送的 https 包，如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/ce1f9928-cc83-4305-886e-f0c70bb9ec40&quot; alt=&quot;https包&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到大量向服务器请求的数据包，但其中有很多干扰的图片请求，因为修改过滤器把图片过滤一下。过滤器：&lt;code&gt;http &amp;amp;&amp;amp; tcp.dstport==443 and !(http.request.uri contains &quot;.webp&quot; or http.request.uri contains &quot;.jpg&quot; or http.request.uri contains &quot;.png&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;结果清爽了很多。&lt;/p&gt;
&lt;h4&gt;订单构建(order.build)&lt;/h4&gt;
&lt;p&gt;根据之前记录的操作的时间，以及对网页版的分析结果，笔者注意到了下图的这条流量：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/10ff344a-ee60-4eb4-a0da-5447d1cdc34e&quot; alt=&quot;订单创建包&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我们右键选择这条流量包，点击追踪 http 流，可以看到对应的响应包。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/1acd1ae8-d7ed-4df4-bef4-c99ad6647583&quot; alt=&quot;追踪流&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/61724496-f60c-4501-a28c-0e8cf0ecb468&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;响应包里有些中文使用了 UTF-8 编码，可以点击右下角的&lt;code&gt;Show data as&lt;/code&gt;，选择 UTF-8，便可以正常显示。此时可以点击另存为，保存为 txt 文件，方便后续分析。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/e2398d11-2da8-4461-8719-94a652143d0b&quot; alt=&quot;请求包内容&quot; /&gt;&lt;/p&gt;
&lt;p&gt;订单构建的请求包中核心的数据部分为图中青色圈出来的部分，使用 URL 解码后为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;buyNow&quot;:&quot;true&quot;,&quot;buyParam&quot;:&quot;716435462268_2_5005943905715&quot;,&quot;exParams&quot;:&quot;{\&quot;atomSplit\&quot;:\&quot;1\&quot;,\&quot;channel\&quot;:\&quot;damai_app\&quot;,\&quot;coVersion\&quot;:\&quot;2.0\&quot;,\&quot;coupon\&quot;:\&quot;true\&quot;,\&quot;seatInfo\&quot;:\&quot;\&quot;,\&quot;umpChannel\&quot;:\&quot;10001\&quot;,\&quot;websiteLanguage\&quot;:\&quot;zh_CN_#Hans\&quot;}&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;buyParam 为最核心的部分，拼接方式为演出 id+数量+票档 id。其他部分只需照抄。&lt;/p&gt;
&lt;p&gt;请求包中还包含大量的各种加密参数、ID，而破解实现自动购票脚本的关键就在于如何通过代码的方式拿到这些加密参数。&lt;/p&gt;
&lt;p&gt;订单构建的响应包为订单提交表单的各项参数，用于生成“确认订单”的表单。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/628bec87-c60b-450f-93a7-380ec51bbad3&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/32cf7a2f-1a9b-42bf-b5ab-9bbf696729cb&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;订单提交(order.create)&lt;/h4&gt;
&lt;p&gt;按照同样的方式可以找到订单提交包，订单提交包的 API 路径为&lt;code&gt;/gw/mtop.trade.order.create&lt;/code&gt;，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/990b1661-c675-4c17-ba65-664f6aafe0e9&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中青色圈出来的部分为 data 发送的核心数据，对数据用 URL 解码后为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{&quot;feature&quot;:&quot;{\&quot;gzip\&quot;:\&quot;true\&quot;}&quot;,&quot;params&quot;:&quot;H4sIAAAAAAA.................AAWk3NKAAA\n&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来像是把原始数据用 gzip 压缩后又使用了 base64 编码，尝试解码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import base64
import gzip
import json

# 解码后变为python dict
decode_data=base64.b64decode(params_str.replace(&quot;\\n&quot;,&quot;&quot;))
decompressed_data=gzip.decompress(decode_data).decode(&quot;utf-8&quot;)
params=json.loads(decompressed_data)

with open(&quot;reverse\order.create-params.json&quot;,&quot;w&quot;) as f:
    json.dump(params,f,indent=2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解码成功，存到&lt;code&gt;order.create-params.json&lt;/code&gt;,&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/b4fad1b9-9943-4eac-ba78-84eee3620cee&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;解码后发现 order.create 发送的 data 参数和 order.build 请求返回的结果很相似，增加了一些用户对表单操作的记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/3c59f063-3b70-451d-9ce6-b09d533bcdf4&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;order.create 请求的 header 中的各种加密参数和 order.build 一致。&lt;/p&gt;
&lt;p&gt;order.create 请求的返回结果中包含了订单创建是否成功的结果以及支付链接。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/7aef0ac4-3d91-4ed9-a736-85ba62ae559c&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;0x05 trace 分析&lt;/h1&gt;
&lt;p&gt;通过前面对流量的分析，我们已经知道客户端向服务器发送的核心数据和加密参数，核心数据的拼接相对简单，但加密参数怎么获得还比较困难。因此，下面要开始分析加密参数的生成方法。本章节主要采用 frida trace 动态分析和 jadx 静态分析相结合的方式，旨在找到加密参数生成的核心函数和输入输出数据的格式。&lt;/p&gt;
&lt;p&gt;根据文章 ( &lt;a href=&quot;https://blog.csdn.net/qq_44130722/article/details/126621134&quot;&gt;app 安卓逆向 x-sign，x-sgext，x_mini_wua，x_umt 加密参数解析&lt;/a&gt; )，其中数据包的加密参数和本文的某麦网很类似，而且提到了 mtopsdk.security.InnerSignImpl 生成的加密函数，本文也参考了这篇文章的思路进行分析。&lt;/p&gt;
&lt;h2&gt;跟踪 InnerSignImpl&lt;/h2&gt;
&lt;p&gt;运行&lt;code&gt;frida-trace -U -j &quot;*InnerSignImpl*!*&quot; 大麦&lt;/code&gt;，执行选座提交订单的操作，发现确实有结果输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(py3) PS E:\TEMP\damai&amp;gt; frida-trace -U -j &quot;*InnerSignImpl*!*&quot; 大麦
Instrumenting...
InnerSignImpl$1.$init: Loaded handler at &quot;E:\\TEMP\\damai\\__handlers__\\mtopsdk.security.InnerSignImpl_1\\_init.js&quot;
....此处省略...
InnerSignImpl.init: Loaded handler at &quot;E:\\TEMP\\damai\\__handlers__\\mtopsdk.security.InnerSignImpl\\init.js&quot;
Started tracing 27 functions. Press Ctrl+C to stop.
           /* TID 0x144f */
  6725 ms  InnerSignImpl.getUnifiedSign(&quot;&amp;lt;instance: java.util.HashMap&amp;gt;&quot;, &quot;&amp;lt;instance: java.util.HashMap&amp;gt;&quot;, &quot;23781390&quot;, null, true)
  6726 ms     | InnerSignImpl.convertInnerBaseStrMap(&quot;&amp;lt;instance: java.util.Map, $className: java.util.HashMap&amp;gt;&quot;, &quot;23781390&quot;, true)
  6726 ms     | &amp;lt;= &quot;&amp;lt;instance: java.util.Map, $className: java.util.HashMap&amp;gt;&quot;
  6727 ms     | InnerSignImpl.getMiddleTierEnv()
  6727 ms     | &amp;lt;= 0
  6737 ms  &amp;lt;= &quot;&amp;lt;instance: java.util.HashMap&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击发送请求时，调用了 InnerSignImpl.getUnifiedSign 函数。但是输入参数和数据参数均为 HashMap 类型，结果中未显示具体内容。从结果输出中猜测 frida-trace 是通过对需要 hook 的函数在&lt;strong&gt;handlers&lt;/strong&gt;下生成 js 文件，并调用 js 文件进行 hook 操作的，因此笔者修改了“&lt;strong&gt;handlers&lt;/strong&gt;\mtopsdk.security.InnerSignImpl\getUnifiedSign.js”，使其能正确输出 HashMap 类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// __handlers__\mtopsdk.security.InnerSignImpl\getUnifiedSign.js

{onEnter(log, args, state) {
 // 增加了HashMap2Str函数，将HashMap类型转换为字符串
    function HashMap2Str(params_hm) {
      var HashMap=Java.use(&apos;java.util.HashMap&apos;);
      var args_map=Java.cast(params_hm,HashMap);
      return args_map.toString();
  };
     // 当调用函数时，输出函数参数
    log(`InnerSignImpl.getUnifiedSign(${HashMap2Str(args[0])},${HashMap2Str(args[1])},${args[2]},${args[3]})`);
  }, onLeave(log, retval, state) {
      function HashMap2Str(params_hm) {
        var HashMap=Java.use(&apos;java.util.HashMap&apos;);
        var args_map=Java.cast(params_hm,HashMap);
        return args_map.toString();	};
    if (retval !== undefined) {
     // 当函数运行结束时，输出函数结果
      log(`&amp;lt;= ${HashMap2Str(retval)}`);
    } }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行 frida-trace，输出的结果已经可以看到具体内容了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(py3) PS E:\TEMP\damai&amp;gt; frida-trace -U -j &quot;*InnerSignImpl*!*&quot; 大麦
        ......
Started tracing 27 functions. Press Ctrl+C to stop.
           /* TID 0x15ab */
  2653 ms  InnerSignImpl.getUnifiedSign({data={&quot;itemId&quot;:&quot;719193771661&quot;,&quot;performId&quot;:&quot;211232892&quot;,&quot;skuParamListJson&quot;:&quot;[{\&quot;count\&quot;:1,\&quot;price\&quot;:36000,\&quot;priceId\&quot;:\&quot;251592963\&quot;}]&quot;,&quot;dmChannel&quot;:&quot;*@damai_android_*&quot;,&quot;channel_from&quot;:&quot;damai_market&quot;,&quot;appType&quot;:&quot;1&quot;,&quot;osType&quot;:&quot;2&quot;,&quot;calculateTag&quot;:&quot;0_0_0_0&quot;,&quot;source&quot;:&quot;10101&quot;,&quot;version&quot;:&quot;6000168&quot;}, deviceId=null, sid=13abe677c5076a4fa3382afc38a96a04, uid=2215803849550, x-features=27, appKey=23781390, api=mtop.damai.item.calcticketprice, utdid=ZF3KUN8khtQDAIlImefp4RYz, ttid=10005890@damai_android_8.5.4, t=1684828096, v=2.0},{pageId=, pageName=},23781390,null)
  2654 ms     | InnerSignImpl.convertInnerBaseStrMap(&quot;&amp;lt;instance: java.util.Map, $className: java.util.HashMap&amp;gt;&quot;, &quot;23781390&quot;, true)
  2655 ms     | &amp;lt;= &quot;&amp;lt;instance: java.util.Map, $className: java.util.HashMap&amp;gt;&quot;
  2655 ms     | InnerSignImpl.getMiddleTierEnv()
  2655 ms     | &amp;lt;= 0
  2662 ms  &amp;lt;= {x-sgext=JA2qmBOxRVDxFRzca3r9BZibqJqvn7uerZOriayYu4mpnKCeoJiunKGZu5qqyfmaqJqhmvqYr5n8zPyJqImpmbvLrImomqidu5m7m7uYu5u7mLuYu5u7m7ubqYmtiaiJqImoiaiJqImoiaiJu8+7iaCf/cypnruaqJqomruau5j8y7uau4mgiaiJqInf6fDIu5o=, x-umt=+D0B/05LPEvOgwKIQ1x+SeV5wNE6NzOo, x-mini-wua=atASnVJw3vGX1Tw3Y/zDaVZkDUbLxOxtlUmgDOnIjMTBcMPMqQJLpnxoOWEL53Fq/OPcQZiMpDXWNvDz8UQkI5mtkZvIcDN1oxZnuH0M22LHKar4rnO/xm4LtAiniKgYtfgMGK3stXuCmvtE4raIhROimslSk7hCkxaL/DYuLzBLYwXmNyr9UZi1g, x-sign=azG34N002xAAK0H9KwNr3txWFMxzW0H7ROfkLQK+Db7ueJHktR/yP/0TcdPFzoYf36zd9lJYMsHCmYX3EcoFnJPMk2pxu0H7QbtB+0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到返回结果中包含了 &lt;code&gt;x-sgext&lt;/code&gt;,&lt;code&gt;x-umt&lt;/code&gt;,&lt;code&gt;x-mini-wua&lt;/code&gt;,&lt;code&gt;x-sign&lt;/code&gt; 等加密参数。至此，前面的一大堆分析也算有了小的收获。但对比流量分析结果中的发送参数，还是缺失了很多参数。下面我们继续跟踪，找出剩下的参数。&lt;/p&gt;
&lt;h2&gt;跟踪 mtopsdk&lt;/h2&gt;
&lt;p&gt;调研发现淘系的 apk 都包含 mtopsdk，猜想会不会有公开的官方文档描述 mtopsdk 的使用方法，因此我们就找到了 &lt;a href=&quot;https://help.aliyun.com/apsara/agile/v_3_5_0_20210228/emas/development-guide/android.html&quot;&gt;【阿里云 mtopsdk Android 接入文档】&lt;/a&gt; 。其中介绍了请求构建的流程为，笔者重点关注了请求构建和发送的部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 3. 请求构建
// 3.1生成MtopRequest实例
MtopRequest request = new MtopRequest();
// 3.2 生成MtopBuilder实例
MtopBuilder builder = instance.build(MtopRequest request, String ttid);
// 4. 请求发送
// 4.2 异步调用
ApiID apiId = builder.addListener(new MyListener).asyncRequest();

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此我们不妨大胆一些，直接跟踪所有对 mtopsdk 中函数的调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(py3) PS E:\TEMP\damai&amp;gt; frida-trace -U -j &quot;*mtopsdk*!*&quot; 大麦
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/95ff2d41-9fd6-4ed6-aeb9-4bba0547261c&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输出的结果大概有 2000 行，直接看太费劲，我们复制到文本编辑器里做进一步分析。&lt;/p&gt;
&lt;p&gt;我们按照阿里的官方文档介绍的流程，对应可以找到在输出的 trace 中找到一些关键的日志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# MtopRequest初始化
  3249 ms  MtopRequest.$init()
  3249 ms  MtopRequest.setApiName(&quot;mtop.trade.order.build&quot;)
  3249 ms  MtopRequest.setVersion(&quot;4.0&quot;)
  3249 ms  MtopRequest.setNeedSession(true)
  3249 ms  MtopRequest.setNeedEcode(true)
  3249 ms  MtopRequest.setData(&quot;{\&quot;buyNow\&quot;:\&quot;true\&quot;,\&quot;buyParam\&quot;:\&quot;7191937661_1_51826442779\&quot;,\&quot;exParams\&quot;:\&quot;{\\\&quot;atomSplit\\\&quot;:\\\&quot;1\\\&quot;,\\\&quot;channel\\\&quot;:\\\&quot;damai_app\\\&quot;,\\\&quot;coVersion\\\&quot;:\\\&quot;2.0\\\&quot;,\\\&quot;coupon\\\&quot;:\\\&quot;true\\\&quot;,\\\&quot;seatInfo\\\&quot;:\\\&quot;\\\&quot;,\\\&quot;umpChannel\\\&quot;:\\\&quot;10001\\\&quot;,\\\&quot;websiteLanguage\\\&quot;:\\\&quot;zh_CN_#Hans\\\&quot;}\&quot;}&quot;)

# MtopBuilder初始化
  3251 ms  MtopBuilder.$init(&quot;&amp;lt;instance: mtopsdk.mtop.intf.Mtop&amp;gt;&quot;, &quot;&amp;lt;instance: mtopsdk.mtop.domain.MtopRequest&amp;gt;&quot;, null)

# MtopBuilder发送异步请求
3268 ms  MtopBuilder.asyncRequest()

# 参数构建
3301 ms     |    |    | InnerProtocolParamBuilderImpl.buildParams(&quot;&amp;lt;instance: mtopsdk.framework.domain.MtopContext&amp;gt;&quot;)
3391 ms     |    |    | &amp;lt;= &quot;&amp;lt;instance: java.util.Map, $className: java.util.HashMap&amp;gt;&quot;,{wua=CofS_+7HCuvRCdz1EN8ICI6A4ZBCJwgY1hi+Bsivjcijs8GggmUxLQQUVTEQ5mYYtPuV7R2QNG5JEONIJRfmzjxFXMrs9AHdepIuqoJJJAyewWALprRnjIAu75t47Tm/RU9xRi7IEo9w0P2aCquLzf7uhiO8JEDSRK/ZdVhURBbof7reFtzEBoYYeIPgnwz7CL3kRlbyqyJcYKxO7ZmmVq1PtMXF2HGJqRSDjdv9l4mySJljIQzBmpX393L6eO1ZQVG1fpp6RaCRcFF+UgfjJXaeMFziHzfQF7KfUQZIeAJV/4GyVEE2f55RwPluOTuQubXQnq+qu41a0V5oyEOFXMoQRYFZzLOv3CjwkiIXsqJFeIHc=, x-sgext=JA0VLKcO8e9Fqqhj38VJuiwkHCUbIA8jGCwUNh0mDzYdIxQhFCcVJxskDyUedk0lHCUVJU4nGyZIc0g2HDYdJg90GDYcJRwiDyYPJA8kDyQPJA8kDyQPJA8nDyQPJQ8lDyUPJQ8lDyUPJQ82STYPLRlwSiUcNhwlHCUcNhw2HnFNNhw2Dy0PJQ8lD1JvfU42HA==, nq=WIFI, data={&quot;buyNow&quot;:&quot;true&quot;,&quot;buyParam&quot;:&quot;719193771661_1_5182956442779&quot;,&quot;exParams&quot;:&quot;{\&quot;atomSplit\&quot;:\&quot;1\&quot;,\&quot;channel\&quot;:\&quot;damai_app\&quot;,\&quot;coVersion\&quot;:\&quot;2.0\&quot;,\&quot;coupon\&quot;:\&quot;true\&quot;,\&quot;seatInfo\&quot;:\&quot;\&quot;,\&quot;umpChannel\&quot;:\&quot;10001\&quot;,\&quot;websiteLanguage\&quot;:\&quot;zh_CN_#Hans\&quot;}&quot;}, pv=6.3, sign=azG34N002xAAKiYA2sv237H04abW2iYKIxaD3GVPak+JifYV0u6VzpriFiKiP+HuuF26BzWpVTClaOIGdjtibfQ99JomGiYKJhomCi, deviceId=null, sid=13abe677c5076a4fa3382afc38a96a04, uid=2215803849550, x-features=27, x-app-conf-v=0, x-mini-wua=a3gSvx5K5/NRy/W8+fDouCSQ6VSmMK3awHwo5X+IayY7JL5SwHtiL0soynSAvCobk01qRQ2fQcTvZWakhmhA9xlNOKdwvxdA5nZ4Tno2asO5e7EvSMj6yqVYAXZZUBjZPUOBw3vpH8L2GUq9Gi6MTszU57a58+hJE2BCGTVsxhRonDw1Nnxp74Ffm, appKey=23781390, api=mtop.trade.order.build, umt=+D0B/05LPEvOgwKIQ1x+SeV5wNE6NzOo, f-refer=mtop, utdid=ZF3KUN8khtQDAIlImefp4RYz, netType=WIFI, x-app-ver=8.5.4, x-c-traceid=ZF3KUN8khtQDAIlImefp4RYz1684829318230001316498, ttid=10005890@damai_android_8.5.4, t=1684829318, v=4.0, user-agent=MTOPSDK/3.1.1.7 (Android;9;samsung;SM-S908E)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;笔者注意到了 InnerProtocolParamBuilderImpl.buildParams 函数的输出结果完全覆盖了需要的各类加密参数，其输入类型是 MtopContext。从 jadx 逆向的 apk 代码中可以找到 MtopContext 类，即包含 Mtop 生命周期的各个类的一个容器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class MtopContext {
    public ApiID apiId;
    public String baseUrl;
    public MtopBuilder mtopBuilder;
    public Mtop mtopInstance;
    public MtopListener mtopListener;
    public MtopRequest mtopRequest;
    public MtopResponse mtopResponse;
    public Request networkRequest;
    public Response networkResponse;
    public MtopNetworkProp property = new MtopNetworkProp();
    public Map&amp;lt;String, String&amp;gt; protocolParams;
    public Map&amp;lt;String, String&amp;gt; queryParams;
    public ResponseSource responseSource;
    public String seqNo;
    @NonNull
    public MtopStatistics stats;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以现在的问题变为如何能够构建出来 MtopContext，然后调用 buildParams 函数生成各类加密参数。&lt;/p&gt;
&lt;h2&gt;分析业务模块与 mtopsdk 的交互过程&lt;/h2&gt;
&lt;p&gt;在写本文复盘分析过程的时候，笔者发现仅依赖 mtopsdk 的调用过程其实已经可以得到 MtopContext 的全部生成逻辑了。但所谓当局者迷，笔者在当时分析的时候还是一头雾水。因此在此也介绍一下笔者的思考逻辑。&lt;/p&gt;
&lt;p&gt;当时看着 mtopsdk 的调用过程，感觉很复杂。但是猜想从用户点击操作-&amp;gt;业务代码-&amp;gt;mtopsdk 的数据流，以及模块间高内聚低耦合的原则，所以猜想模块间的调用不会很复杂，所以笔者就想分析业务代码与 mtopsdk 的调用逻辑。所以就想跟踪主要业务代码的 trace。所以笔者继续跟踪 trace，运行&lt;code&gt;frida-trace -U -j &quot;*cn.damai*!*&quot; 大麦 &lt;/code&gt;，以分析&lt;code&gt;cn.damai&lt;/code&gt;包的调用过程，在其中发现了 &lt;code&gt;NcovSkuFragment.buyNow()&lt;/code&gt; 函数，看起来是和购买紧密相关的函数。又找到 DMBaseMtopRequest 类。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/2349df97-2e39-441a-b66a-e43e91a64ae5&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是在这里有点卡住了，因为只找到了构建 MtopRequest，并未在 cn.damai 的 trace 日志中并未发现其他对 mtop 的调用。&lt;/p&gt;
&lt;p&gt;然后笔者又尝试搜索和 api(order.build)相关的代码，找到了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/c609e091-af47-4555-b925-bd3940416e12&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然而并没有多大用处。&lt;/p&gt;
&lt;p&gt;然后，作者又读了大量的源代码，终于定位到了 &lt;code&gt;com.taobao.tao.remotebusiness.MtopBussiness&lt;/code&gt;这个关键类。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/ed0df1c1-ce0f-4e4e-89d7-02e9bc75742b&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;笔者本以为 com.taobao 开头的代码不是那么重要，所以最开始把这个类完全忽略了。但通过对源码的阅读，发现这个类是 motpsdk 中 MtopBuilder 类的子类，主要负责管理业务代码和 Mtopsdk 的交互。&lt;/p&gt;
&lt;p&gt;因此我们继续通过 trace 跟踪 MtopBussiness 类。运行&lt;code&gt;frida-trace -U -j &quot;*!*buyNow*&quot; -j &quot;com.taobao.tao.remotebusiness.MtopBusiness!*&quot; -j &quot;*MtopContext!*&quot; -j &quot;*mtopsdk.mtop.intf.MtopBuilder!*&quot; 大麦&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/56d6ee8d-9395-45b9-bf3e-23461e4850d9&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在业务代码和 mtopsdk 的交互就很清晰了，红色的部分是业务代码的函数，绿色的部分是 mtopsdk 的函数。&lt;/p&gt;
&lt;h1&gt;0x06 hook 得到接口参数&lt;/h1&gt;
&lt;p&gt;通过以上对 trace 的分析，已经知道了具体执行的操作，因此我们可以使用 frida 编写 js 代码，直接调用 APK 中的类，实现功能调用。&lt;/p&gt;
&lt;p&gt;先展示一个简单的示例，用于构建一个自定义的 MtopRequest 类:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// new_request.js
Java.perform(function () {
  const MtopRequest = Java.use(&quot;mtopsdk.mtop.domain.MtopRequest&quot;);
  let myMtopRequest = MtopRequest.$new();
  myMtopRequest.setApiName(&quot;mtop.trade.order.build&quot;);
  //item_id + count + ski_id  716435462268_1_5005943905715
  myMtopRequest.setData(
    &apos;{&quot;buyNow&quot;:&quot;true&quot;,&quot;buyParam&quot;:&quot;716435462268_1_5005943905715&quot;,&quot;exParams&quot;:&quot;{\\&quot;atomSplit\\&quot;:\\&quot;1\\&quot;,\\&quot;channel\\&quot;:\\&quot;damai_app\\&quot;,\\&quot;coVersion\\&quot;:\\&quot;2.0\\&quot;,\\&quot;coupon\\&quot;:\\&quot;true\\&quot;,\\&quot;seatInfo\\&quot;:\\&quot;\\&quot;,\\&quot;umpChannel\\&quot;:\\&quot;10001\\&quot;,\\&quot;websiteLanguage\\&quot;:\\&quot;zh_CN_#Hans\\&quot;}&quot;}&apos;
  );
  myMtopRequest.setNeedEcode(true);
  myMtopRequest.setNeedSession(true);
  myMtopRequest.setVersion(&quot;4.0&quot;);
  console.log(`${myMtopRequest}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再使用运行命令 &lt;code&gt;frida -U -l .\reverse\new_request.js 大麦&lt;/code&gt;，以在某麦 Apk 中执行 js hook 代码。运行之后即可输出笔者自己构建的 MtopRequest 实例。（frida 真的很奇妙！）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/13d32747-5e05-4fee-a0af-c4ea9c0cc5d8&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了上面的结果，下面继续完善这个示例，添加 MtopBussiness 的构建过程和输出过程&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//引入Java中的类
const MtopBusiness = Java.use(&quot;com.taobao.tao.remotebusiness.MtopBusiness&quot;);
const MtopBuilder = Java.use(&quot;mtopsdk.mtop.intf.MtopBuilder&quot;);
// let RemoteBusiness = Java.use(&quot;com.taobao.tao.remotebusiness.RemoteBusiness&quot;);
const MethodEnum = Java.use(&quot;mtopsdk.mtop.domain.MethodEnum&quot;);
const MtopListenerProxyFactory = Java.use(
  &quot;com.taobao.tao.remotebusiness.listener.MtopListenerProxyFactory&quot;
);
const System = Java.use(&quot;java.lang.System&quot;);
const ApiID = Java.use(&quot;mtopsdk.mtop.common.ApiID&quot;);
const MtopStatistics = Java.use(&quot;mtopsdk.mtop.util.MtopStatistics&quot;);
const InnerProtocolParamBuilderImpl = Java.use(
  &quot;mtopsdk.mtop.protocol.builder.impl.InnerProtocolParamBuilderImpl&quot;
);

// create MtopBusiness
let myMtopBusiness = MtopBusiness.build(myMtopRequest);
myMtopBusiness.useWua();
myMtopBusiness.reqMethod(MethodEnum.POST.value);
myMtopBusiness.setCustomDomain(&quot;mtop.damai.cn&quot;);
myMtopBusiness.setBizId(24);
myMtopBusiness.setErrorNotifyAfterCache(true);
myMtopBusiness.reqStartTime = System.currentTimeMillis();
myMtopBusiness.isCancelled = false;
myMtopBusiness.isCached = false;
myMtopBusiness.clazz = null;
myMtopBusiness.requestType = 0;
myMtopBusiness.requestContext = null;
myMtopBusiness.mtopCommitStatData(false);
myMtopBusiness.sendStartTime = System.currentTimeMillis();

let createListenerProxy = myMtopBusiness.$super.createListenerProxy(
  myMtopBusiness.$super.listener.value
);
let createMtopContext = myMtopBusiness.createMtopContext(createListenerProxy);
let myMtopStatistics = MtopStatistics.$new(null, null); //创建一个空的统计类
createMtopContext.stats.value = myMtopStatistics;
myMtopBusiness.$super.mtopContext.value = createMtopContext;
createMtopContext.apiId.value = ApiID.$new(null, createMtopContext);

let myMtopContext = createMtopContext;
myMtopContext.mtopRequest.value = myMtopRequest;
let myInnerProtocolParamBuilderImpl = InnerProtocolParamBuilderImpl.$new();
let res = myInnerProtocolParamBuilderImpl.buildParams(myMtopContext);
console.log(
  `myInnerProtocolParamBuilderImpl.buildParams =&amp;gt; ${HashMap2Str(res)}`
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次执行&lt;code&gt;frida -U -l .\reverse\new_request.js 大麦&lt;/code&gt;，输出结果如下图，此时已能根据笔者任意构建的请求 data 输出其他加密参数：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/53a239e1-38e8-471b-9be8-584f8c9dba0f&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于 order.create 的原理类似，此处不再赘述。&lt;/p&gt;
&lt;h2&gt;补充说明&lt;/h2&gt;
&lt;p&gt;通过 frida 调用 Apk 中的 Java 类有时候会出现找不到类的情况，原因可能是 classloader 没有正确加载。可以在 js 代码前的最前面加上下面的代码，指定正确的 classloader，即可解决该问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Java.perform(function () {
  //get real classloader
  //from http://www.lixiaopeng.top/article/63.html
  var application = Java.use(&quot;android.app.Application&quot;);
  var classloader;
  application.attach.overload(&quot;android.content.Context&quot;).implementation =
    function (context) {
      var result = this.attach(context); // run attach as it is
      classloader = context.getClassLoader(); // get real classloader
      Java.classFactory.loader = classloader;
      return result;
    };
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;frida hook 的强大功能&lt;/h2&gt;
&lt;p&gt;通过 frida 操纵 Java 类的功能实在过于强大，安全人员可以执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;打印函数输入输出&lt;/em&gt;。通过 hook 函数，以实现打印函数的输入输出结果。
操作代码可以在 jadx 右键菜单可以很方便的生成。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/c741ef2d-a144-4187-8233-bc2ae81ee4a1&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let LocalInnerSignImpl = Java.use(&quot;mtopsdk.security.LocalInnerSignImpl&quot;);
LocalInnerSignImpl[&quot;$init&quot;].implementation = function (str, str2) {
  console.log(`LocalInnerSignImpl.$init is called: str=${str}, str2=${str2}`);
  this[&quot;$init&quot;](str, str2);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;修改已有的类和函数&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;定义新类和新函数&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;主动生成类的实例或调用函数&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;RPC 调用&lt;/em&gt;。通过 RPC 调用提供 python 编程接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;0x07 通过 rpc 调用&lt;/h1&gt;
&lt;p&gt;前文提到 frida 的一个特性是可以通过 rpc 调用提供 python 编程接口。&lt;/p&gt;
&lt;p&gt;一个简单的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import frida

def on_message(message, data):
    if message[&quot;type&quot;] == &quot;send&quot;:
        print(&quot;[*] {0}&quot;.format(message[&quot;payload&quot;]))
    else:
        print(message)

# hook代码
jscode = &quot;&quot;&quot;
rpc.exports = {
    testrpc: function (a, b) { return a + b; },
};  &quot;&quot;&quot;

def start_hook():
# 开始hook
    process = frida.get_usb_device().attach(&quot;大麦&quot;)
    script = process.create_script(jscode)
    script.on(&quot;message&quot;, on_message)
    script.load()
    return script

script = start_hook()
# 调用hook代码
print(script.exports.testrpc(1, 2))

# &amp;gt;&amp;gt;&amp;gt; 输出
# 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;frida 使用 rpc 的方法也很简单，仅需使用 rpc.exports，将对应的函数暴露出来，就能被 python 调用。&lt;/p&gt;
&lt;p&gt;完整的代码就是将上一章的代码封装为函数，并通过 rpc 对外提供接口，就可以了。为避免侵权，本文不贴出完整利用代码。&lt;/p&gt;
&lt;p&gt;代码封装完成后测了一下，平均一次调用的时间为 0.024 秒，完全可以达到抢票的要求。&lt;/p&gt;
&lt;h1&gt;0x08 提示和技巧&lt;/h1&gt;
&lt;p&gt;参考大家经常问的问题以及评论区大佬的思路,总结一些提示和技巧.&lt;/p&gt;
&lt;h2&gt;编码格式细节&lt;/h2&gt;
&lt;p&gt;很多朋友出现服务端返回&quot;非法签名的情况&quot;,是由于细节的问题.&lt;/p&gt;
&lt;p&gt;order.build 和 order.create 接口的具体编码规则很细节,比如一些空格,引号,是否 urlEncode 等等. python requests 包自己的封装格式可能和和大麦 apk 不兼容,因此最后出来的包实质是差别比较大.&lt;/p&gt;
&lt;p&gt;解决措施:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 wireshark 抓 apk 发包和自己代码发的包,分析区别&lt;/li&gt;
&lt;li&gt;尝试一层一层解 apk 发的包,然后再重新打包,看是否能和之前保持一致&lt;/li&gt;
&lt;li&gt;request 的 post 内容不要用 dict,用文本.&lt;/li&gt;
&lt;li&gt;编码多用字符串拼接.&lt;/li&gt;
&lt;li&gt;header 里的字段名大小写/顺序最好保持一致.&lt;/li&gt;
&lt;li&gt;create 发送的 data 是在 build 的返回值做了一些编辑&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;禁用 spdy&lt;/h2&gt;
&lt;p&gt;感谢 @IWasSleeping
参考: https://github.com/m2kar/m2kar.github.io/issues/21#issuecomment-1634733885&lt;/p&gt;
&lt;p&gt;对于 mtopsdk ssl 的抓包可以通过屏蔽掉 spdy 协议，关闭 spdy ssl 和全局的 spdy 来实现让 APP 通过 http 协议，来方便任何安卓版本实现简单抓包，通过 hook 的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let SwitchConfig = Java.use(&quot;mtopsdk.mtop.global.SwitchConfig&quot;);
SwitchConfig[&quot;isGlobalSpdySslSwitchOpen&quot;].implementation = function () {
    console.log(`SwitchConfig.isGlobalSpdySslSwitchOpen is called`);
    let result = this[&quot;isGlobalSpdySslSwitchOpen&quot;]();
    console.log(`SwitchConfig.isGlobalSpdySslSwitchOpen result=${result}`);
    return false;
};

SwitchConfig[&quot;isGlobalSpdySwitchOpen&quot;].implementation = function () {
    console.log(`SwitchConfig.isGlobalSpdySwitchOpen is called`);
    let result = this[&quot;isGlobalSpdySwitchOpen&quot;]();
    console.log(`SwitchConfig.isGlobalSpdySwitchOpen result=${result}`);
    return false;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过主动调用的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var SwitchConfig = Java.use(&apos;mtopsdk.mtop.global.SwitchConfig&apos;)
var config = SwitchConfig.getInstance();
config.setGlobalSpdySslSwitchOpen(false);
config.setGlobalSpdySwitchOpen(false);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;滑动验证码&lt;/h2&gt;
&lt;p&gt;感谢: @svcvit
参考: https://github.com/m2kar/m2kar.github.io/issues/21#issuecomment-1635989770&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/3512f3fb-8876-4007-ac53-c76b7697d6bc&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p&gt;滑块过了，原理：FAIL_SYS_USER_VALIDATE 的时候，返回头里有个 location，用浏览器打开这个 url，滑动，获取 cookies，装入 request 里，就可以了。效果参考下方。&lt;/p&gt;
&lt;p&gt;代码参考：https://github.com/kuxigua/TaoBaoSpider/blob/02fd1dc437c1b0fd49fc64bfbedd6c070d9e21e5/AntiReptile/imgCodeHandle.py&lt;/p&gt;
&lt;h2&gt;traceid&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;请问 header 中的 x-c-traceid 是怎么构建的，rpc 返回的对象中这个值是空的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;建议参考：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/m2kar/m2kar.github.io/assets/16930652/1cc88b97-27cf-4f05-b204-86187c83fc80&quot; alt=&quot;2ffee419651189d952611ad6d3fbef8&quot; /&gt;&lt;/p&gt;
&lt;p&gt;感谢 @HenryWu01&lt;/p&gt;
&lt;p&gt;也可以参考，但固定较多内容，可能增加被识别的概率：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;utdid = &quot;ZHmSZ78mpAEDALjcMTWN1YHF&quot;
timestamp = int(time.time() * 1000)
padded_number = format(int(number), &quot;04&quot;)
f71332q = &quot;122782&quot;
x_c_traceid = str(utdid) + (str(timestamp)) + (str(padded_number)) + (str(f71332q))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;感谢 @nobewp&lt;/p&gt;
&lt;h1&gt;0x09 踩坑经历花絮&lt;/h1&gt;
&lt;h2&gt;关于 wiresharkhelper&lt;/h2&gt;
&lt;p&gt;txthinking 放出了一个抓包辅助工具&lt;a href=&quot;https://github.com/txthinking/wiresharkhelper&quot;&gt;wiresharkhelper&lt;/a&gt;，看视频介绍很诱人很方便，但是实测是要收费的。本人穷，所以就没用他的方法。然而也是因为这个才开始尝试用 frida 工具得到 https 的密钥，发现了 frida 这个神器。&lt;/p&gt;
&lt;h2&gt;关于 Cookie&lt;/h2&gt;
&lt;p&gt;细心的朋友可能发现发送的请求头里是包含 cookie 的，但是本文没有介绍。其实笔者本来是再继续找 cookie 的，但是发现把&lt;code&gt;InnerProtocolParamBuilderImpl.buildParams&lt;/code&gt;函数的参数填进去之后，就已经能正常获取服务器的返回值了，所以就没继续搞 cookie&lt;/p&gt;
&lt;h2&gt;关于 MtopStatistics&lt;/h2&gt;
&lt;p&gt;MtopStatistics 是 mtopsdk 里比较重要的一个类，用来跟踪用户的操作记录状态，可能有助于判断用户是否是机器人。但笔者尝试自己构建 MtopStatistics 失败，所以直接生成了一个空的 MtopStatistics 类，好在也没对服务器的正常返回造成影响。&lt;/p&gt;
&lt;h2&gt;如何获取票价信息&lt;/h2&gt;
&lt;p&gt;这里笔者是直接用的某麦网 Web 端 PC 版，网页中有一段 json，包含静态的描述信息和动态的场次、余票信息。&lt;/p&gt;
&lt;h2&gt;如何脱离模拟器运行&lt;/h2&gt;
&lt;p&gt;目前是需要模拟器一直运行着的，而且仅能用一个人的账户。这对于个人使用是完全够用的。如何能脱离模拟器，而且增加并发用户数量还需要继续研究。目前时间不允许，暂时不再继续此问题的研究。&lt;/p&gt;
&lt;h2&gt;还是抢不到票&lt;/h2&gt;
&lt;p&gt;虽然流程全都搞定，而且对于非热门场次抢票完全没有问题。但对于热门场次，官方可能还是增加了或明或暗的检测机制。比如有些是淘票票限定渠道，在对特权用户开放抢票一段时间后才会对其他人，但开放状态仅从网页端无法判断，导致脚本会提前开抢，被系统提前拦截。或者有的场次明明第一时间开抢，却还是一直提示请求失败。这个还需要进一步踩坑理解某麦网的机制。&lt;/p&gt;
&lt;h2&gt;BP 链接&lt;/h2&gt;
&lt;p&gt;这篇公众号文章( https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MDEzMTg0OQ==&amp;amp;action=getalbum&amp;amp;album_id=2885498232984993792#wechat_redirect ) 介绍了某麦网的 bp 链接及使用方式，可以跳过票档选择直接进入订单确认页面。后续可以尝试用于自动抢票。&lt;/p&gt;
&lt;p&gt;如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 毛不易 -  5月27日 上海站
1、五层480 x 1张
https://m.damai.cn/app/dmfe/h5-ultron-buy/index.html?buyParam=718707599799_1_5008768308765&amp;amp;buyNow=true&amp;amp;exParams=%257B%2522channel%2522%253A%2522damai_app%2522%252C%2522damai%2522%253A%25221%2522%252C%2522umpChannel%2522%253A%2522100031004%2522%252C%2522subChannel%2522%253A%2522damai%2540damaih5_h5%2522%252C%2522atomSplit%2522%253A1%257D&amp;amp;spm=a2o71.project.sku.dbuy&amp;amp;sqm=dianying.h5.unknown.value

2、五层480 x 2张
https://m.damai.cn/app/dmfe/h5-ultron-buy/index.html?buyParam=718707599799_2_5008768308765&amp;amp;buyNow=true&amp;amp;exParams=%257B%2522channel%2522%253A%2522damai_app%2522%252C%2522damai%2522%253A%25221%2522%252C%2522umpChannel%2522%253A%2522100031004%2522%252C%2522subChannel%2522%253A%2522damai%2540damaih5_h5%2522%252C%2522atomSplit%2522%253A1%257D&amp;amp;spm=a2o71.project.sku.dbuy&amp;amp;sqm=dianying.h5.unknown.value
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;0x10 总结&lt;/h1&gt;
&lt;p&gt;本文完整的记录了笔者对于 Apk 与服务器交互 API 的解析过程，包括环境搭建、抓包、trace 分析、hook、rpc 调用。本文对于淘系 Apk 的分析可以提供较多参考。本文算是笔者第一次深入且成功的用动态+静态分析结合的方式，借助神器 frida+jadx，成功破解 Apk，因此本文的记录也较为细致的记录了作者的思考过程，可以给新手提供参考。&lt;/p&gt;
&lt;p&gt;本文也有一些不足之处，如无法脱离模拟器运行、仅能单用户、抢票成功率仍不高等。对于这些问题，如果未来作者有时间，会再回来填坑。&lt;/p&gt;
&lt;p&gt;本文作者为 m2kar，原文发表在 &lt;a href=&quot;https://github.com/m2kar/m2kar.github.io/issues/21&quot;&gt;&lt;code&gt;https://github.com/m2kar/m2kar.github.io/issues/21&lt;/code&gt;&lt;/a&gt; ，转载请注明出处。&lt;/p&gt;
&lt;p&gt;分享一个 tg 讨论组， https://t.me/+IbWm3n0o1KlkMTg1 ,感谢 @svcvit&lt;/p&gt;
&lt;p&gt;最后，欢迎大家在 issue 评论区或 tg 讨论组多多提出问题相互交流。&lt;/p&gt;
&lt;p&gt;&amp;lt;hr/&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;欢迎&lt;a href=&quot;https://github.com/m2kar/m2kar.github.io/issues/21&quot;&gt;评论&lt;/a&gt;以及发邮件和作者交流心得。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版权声明&lt;/strong&gt;：本文为 m2kar 的原创文章，遵循 CC 4.0 BY-SA 版权协议，转载请附上原文出处链接及本声明。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;作者&lt;/strong&gt;: m2kar&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打赏链接&lt;/strong&gt;: &lt;a href=&quot;http://m2kar-cn.mikecrm.com/wy97haW&quot;&gt;欢迎打赏 m2kar,您的打赏是我创作的重要源泉&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮箱&lt;/strong&gt;: &lt;code&gt;m2kar.cn&amp;lt;at&amp;gt;gmail.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主页&lt;/strong&gt;: &lt;a href=&quot;https://m2kar.cn&quot;&gt;m2kar.cn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Github&lt;/strong&gt;: &lt;a href=&quot;https://github.com/m2kar&quot;&gt;github.com/m2kar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSDN&lt;/strong&gt;: &lt;a href=&quot;https://m2kar.blog.csdn.net&quot;&gt;M2kar 的专栏&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;hr/&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;欢迎在 ISSUE 参与本博客讨论&lt;/strong&gt;: &lt;a href=&quot;https://github.com/m2kar/m2kar.github.io/issues/21&quot;&gt;m2kar/m2kar.github.io#21&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 令人兴奋的新版本发布</title><link>https://monkeywie.cn/posts/gopeed-excited-to-release</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-excited-to-release</guid><pubDate>Tue, 24 Oct 2023 17:27:30 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;经过近三年的迭代，&lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;Gopeed&lt;/a&gt; 终于迎来了我觉得比较满意的一个版本 &lt;code&gt;v1.4.3&lt;/code&gt;，早在 &lt;a href=&quot;https://github.com/proxyee-down-org/proxyee-down&quot;&gt;proxyee-down&lt;/a&gt; 停更的时候，我就立下了一个&lt;code&gt;flag&lt;/code&gt;要用 Golang 来重写一个下载器：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-excited-to-release/2023-10-24-17-34-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当时刚接触 Golang 就被深深吸引了，它优秀的网络编程、协程和交叉编译等特性，不经感叹这简直就是开发下载器的天选编程语言，于是就有了这个项目，刚开始的时候想着就做一个类似&lt;code&gt;aria2&lt;/code&gt;这样的命令行工具，后来&lt;code&gt;flutter&lt;/code&gt;异军突起，我就一直在调研用&lt;code&gt;flutter&lt;/code&gt;来开发一个支持所有平台的下载器的可行性，一开始&lt;code&gt;flutter desktop&lt;/code&gt;还不是很成熟，我甚至有考虑过用&lt;code&gt;electron&lt;/code&gt;做桌面端，&lt;code&gt;flutter&lt;/code&gt;做移动端，后来&lt;code&gt;flutter 2.0&lt;/code&gt;发布之后，&lt;code&gt;flutter desktop&lt;/code&gt;也正式发布了，测了下基本没啥大坑，觉得这是一个很好的机会，就开始把命令行工具改造成&lt;code&gt;GUI&lt;/code&gt;，更方便用户使用，最终不出所望，通过 Golang + flutter 的组合，实现了一个支持所有平台的下载器。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关链接：
官网 -&amp;gt; &lt;a href=&quot;https://gopeed.com&quot;&gt;https://gopeed.com&lt;/a&gt;
Github -&amp;gt; &lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;https://github.com/GopeedLab/gopeed&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;特性&lt;/h2&gt;
&lt;p&gt;先来简单的介绍一下 Gopeed 的特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持所有平台，包括&lt;code&gt;windows&lt;/code&gt;、&lt;code&gt;mac&lt;/code&gt;、&lt;code&gt;linux&lt;/code&gt;、&lt;code&gt;android&lt;/code&gt;、&lt;code&gt;ios&lt;/code&gt;、&lt;code&gt;web&lt;/code&gt;、&lt;code&gt;docker&lt;/code&gt;、&lt;code&gt;命令行工具&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;同时支持 HTTP(S) / BT / 磁力链 多种下载协议，并且后续会支持更多的下载协议。&lt;/li&gt;
&lt;li&gt;支持多协程下载，支持断点续传。&lt;/li&gt;
&lt;li&gt;支持暗黑模式和国际化。&lt;/li&gt;
&lt;li&gt;对外开放&lt;code&gt;HTTP API&lt;/code&gt;，方便定制化开发。&lt;/li&gt;
&lt;li&gt;去中心化的扩展系统，可以通过扩展来实现更多的功能，例如：某某网盘下载、某某网站视频下载等等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;扩展系统&lt;/h3&gt;
&lt;p&gt;为什么说新版本是令我满意的呢？正是因为新版本更新的扩展系统，这意味着用户可以通过扩展来实现更多的功能，例如：某某网盘下载、某某网站视频下载等等，先看两个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;B 站视频下载扩展：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-excited-to-release/2023-10-24-17-40-08.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;百度网盘下载扩展：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-excited-to-release/2023-10-24-17-50-08.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以上两个扩展都是通过&lt;code&gt;JavaScript&lt;/code&gt;来开发的，具体的设计草案可以在&lt;a href=&quot;https://github.com/GopeedLab/gopeed/issues/107&quot;&gt;这里&lt;/a&gt;查看。&lt;/p&gt;
&lt;p&gt;基于&lt;code&gt;git&lt;/code&gt;去中心化和远程仓库的特性，用户只需要输入一个链接地址，即可安装扩展，并且是扩展也是全平台支持的，这样的扩展系统是不是很酷？&lt;/p&gt;
&lt;p&gt;这里有人可能会问了，去中心化的扩展系统要怎么找到扩展呢？这里巧妙利用 &lt;code&gt;Github Topic&lt;/code&gt;功能，只要给你的扩展项目仓库打上&lt;code&gt;gopeed-extension&lt;/code&gt;的主题标签，然后通过&lt;a href=&quot;https://github.com/topics/gopeed-extension&quot;&gt;topics/gopeed-extension&lt;/a&gt;就可以找到所有的扩展了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-excited-to-release/2023-10-25-09-24-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开放生态&lt;/h2&gt;
&lt;p&gt;Gopeed 目前开放了两种方式来进行定制化开发：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过对接 HTTP API&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;JavaScript&lt;/code&gt; 脚本开发扩展进行增强&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Gopeed 的愿景是打造一个丰富的扩展生态，上面的示例只是抛砖引玉，希望有更多感兴趣的开发者参与进来，一起来丰富 Gopeed 的扩展生态。&lt;/p&gt;
&lt;h3&gt;配套设施&lt;/h3&gt;
&lt;p&gt;为了更方便的用户参与到 Gopeed 的生态开发，我还开发了一个配套&lt;code&gt;JavaScript&lt;/code&gt;库，开源在：&lt;a href=&quot;https://github.com/GopeedLab/gopeed-js&quot;&gt;gopeed-js&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;项目使用&lt;code&gt;TypeScript&lt;/code&gt;+&lt;code&gt;Rollup&lt;/code&gt;+&lt;code&gt;pnpm workspace&lt;/code&gt;技术栈，目前已经实现了以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create-gopeed-ext：脚手架工具，可以快速的创建一个扩展项目，详细文档可以在&lt;a href=&quot;https://docs.gopeed.com/zh/dev-extension.html#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B&quot;&gt;这里&lt;/a&gt;查看，使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-gopeed-ext@latest

√ Project name (gopeed-extension-demo) ...
√ Choose a template » Webpack

Success! Created gopeed-extension-demo at D:\code\study\js\gopeed-extension-demo
Inside that directory, you can run several commands:

  git init
    Initialize git repository

  npm install
    Install dependencies

  npm run dev
    Compiles and hot-reloads for development.

  npm run build
    Compiles and minifies for production.

We suggest that you begin by typing:

  cd gopeed-extension-demo

Happy coding!
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;@gopeed/rest: 可以方便的通过 js 调用 Gopeed 的&lt;code&gt;HTTP API&lt;/code&gt;，详细文档可以在&lt;a href=&quot;https://docs.gopeed.com/zh/dev-api.html&quot;&gt;这里&lt;/a&gt;查看，使用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Client } from &quot;@gopeed/rest&quot;;

(async function () {
  // 创建客户端
  const client = new Client();
  // 调用API创建任务
  const res = await client.createTask({
    req: {
      url: &quot;https://example.com/file.zip&quot;,
    },
  });
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;开发文档&lt;/h3&gt;
&lt;p&gt;除了&lt;code&gt;JavaScript&lt;/code&gt;库之外，还贴心的提供了&lt;a href=&quot;https://docs.gopeed.com/site/openapi/index.html&quot;&gt;RESTFul API&lt;/a&gt;文档和&lt;a href=&quot;https://docs.gopeed.com/site/reference/index.html&quot;&gt;SDK Reference&lt;/a&gt;文档，提供给用户查阅。&lt;/p&gt;
&lt;h2&gt;接下来的计划&lt;/h2&gt;
&lt;p&gt;目前呼声很高的是开发&lt;code&gt;浏览器扩展&lt;/code&gt;用于接管浏览器的下载，所以目前优先级会是最高的。&lt;/p&gt;
&lt;p&gt;未来还会支持更多的特性，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;边下边播&lt;/li&gt;
&lt;li&gt;DLNA 投屏&lt;/li&gt;
&lt;li&gt;更丰富的扩展生命周期&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;感谢期间国内外小伙伴们提交的&lt;code&gt;PR&lt;/code&gt;和&lt;code&gt;Issue&lt;/code&gt;，也很开心今年因为 Gopeed 又登顶了 GitHub Trending：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-excited-to-release/2023-10-24-18-46-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;截止到目前为止，Gopeed 已经有 7.3k+ 的 star，希望能继&lt;code&gt;proxyee-down&lt;/code&gt;之后，再拿下一个 10k+ 的 star 的项目。&lt;/p&gt;
&lt;p&gt;最后的最后如果你对 Gopeed 感兴趣，欢迎加入我们的开源社区共同建设！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>给我的开源下载器打造一套扩展系统</title><link>https://monkeywie.cn/posts/how-to-develop-a-cross-platform-extension-system</link><guid isPermaLink="true">https://monkeywie.cn/posts/how-to-develop-a-cross-platform-extension-system</guid><pubDate>Thu, 14 Dec 2023 10:04:44 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;前一段时间给我的开源下载器 &lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;Gopeed&lt;/a&gt; 实现了一套扩展系统，相关设计草案可以在&lt;a href=&quot;https://github.com/GopeedLab/gopeed/issues/107&quot;&gt;这里&lt;/a&gt;查看，基于这套扩展系统可以很方便的通过&lt;code&gt;javascript&lt;/code&gt;来实现一些定制化的功能，目前已经实现的扩展有：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-14-10-40-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/monkeyWie/gopeed-extension-baiduwp&quot;&gt;百度网盘扩展&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/monkeyWie/gopeed-extension-youtube&quot;&gt;油管扩展&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/monkeyWie/gopeed-extension-twitter&quot;&gt;推特扩展&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后重点是这些扩展也是全平台支持的，这意味着你可以在&lt;code&gt;windows&lt;/code&gt;、&lt;code&gt;mac&lt;/code&gt;、&lt;code&gt;linux&lt;/code&gt;、&lt;code&gt;android&lt;/code&gt;、&lt;code&gt;ios&lt;/code&gt;、&lt;code&gt;web&lt;/code&gt;平台上安装和使用这些扩展，是不是很酷？接下来就来介绍下我是如何实现这套扩展系统的。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;扩展系统设计&lt;/h2&gt;
&lt;p&gt;从大体上来说，扩展系统分为四个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;扩展标准&lt;/li&gt;
&lt;li&gt;扩展脚本引擎&lt;/li&gt;
&lt;li&gt;扩展管理器&lt;/li&gt;
&lt;li&gt;扩展开发工具包&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;扩展标准&lt;/h3&gt;
&lt;p&gt;要实现一个扩展系统，首先要定义一个扩展标准，这个标准包括扩展的目录结构、扩展的配置文件、扩展的脚本文件等等，这一部分我参照（抄）了&lt;code&gt;Chrome&lt;/code&gt;的扩展标准，即扩展由一个文件夹组成，文件夹中必须包含一个&lt;code&gt;manifest.json&lt;/code&gt;声明文件，最简单的扩展目录结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── index.js
└── manifest.json
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;去中心化设计&lt;/h4&gt;
&lt;p&gt;市面上大多数软件的扩展系统都是中心化的，比如：&lt;code&gt;Chrome 扩展&lt;/code&gt;、&lt;code&gt;VS Code 扩展&lt;/code&gt;，这样的好处是可以很方便检索和分发扩展，不过我觉得&lt;code&gt;Gopeed&lt;/code&gt;作为一个下载器，中心化的扩展系统一点也没有&lt;code&gt;BitTorrent&lt;/code&gt;协议的去中心化精神，而且我也不想去维护一个中心化的扩展仓库，所以我决定基于&lt;code&gt;git&lt;/code&gt;来实现一个去中心化的扩展系统。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git&lt;/code&gt;作为一个分布式版本控制系统，完美契合了去中心化的需求，每个扩展都是一个&lt;code&gt;git&lt;/code&gt;仓库，扩展的安装和更新都是通过&lt;code&gt;git clone&lt;/code&gt;来实现，这样只需要把扩展托管到&lt;code&gt;github&lt;/code&gt;、&lt;code&gt;gitlab&lt;/code&gt;、&lt;code&gt;gitee&lt;/code&gt;等平台上，就可以实现扩展的分发和更新了。&lt;/p&gt;
&lt;h4&gt;巧妙利用 Topic 分发扩展&lt;/h4&gt;
&lt;p&gt;去中心化之后没有一个类似扩展商店的平台来分发扩展，那么用户要怎么找到扩展呢？这里巧妙利用了&lt;code&gt;Github Topic&lt;/code&gt;功能，只要给扩展项目仓库打上&lt;code&gt;gopeed-extension&lt;/code&gt;的主题标签，然后通过&lt;a href=&quot;https://github.com/topics/gopeed-extension&quot;&gt;topics/gopeed-extension&lt;/a&gt;就可以让用户找到你的扩展了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-16-11-37-17.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有个问题就是没法通过关键字来搜索扩展，庆幸的是&lt;code&gt;Github&lt;/code&gt;有着强大的搜索功能，可以通过&lt;code&gt;topic&lt;/code&gt;和关键字进行搜索，例如：搜索&lt;code&gt;youtube&lt;/code&gt;关键字，输入&lt;code&gt;topic:gopeed-extension youtube&lt;/code&gt;，就可以找到&lt;code&gt;youtube&lt;/code&gt;相关的扩展了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-16-12-04-36.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;扩展配置文件&lt;/h4&gt;
&lt;p&gt;扩展配置文件是一个&lt;code&gt;json&lt;/code&gt;文件，用来声明扩展的一些基本信息，例如：扩展的名称、版本、描述、图标、脚本文件等等，这里我贴一个上面&lt;code&gt;Youtube&lt;/code&gt;扩展的配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;youtube&quot;,
  &quot;author&quot;: &quot;monkeyWie&quot;,
  &quot;title&quot;: &quot;Youtube&quot;,
  &quot;description&quot;: &quot;Youtube video download&quot;,
  &quot;icon&quot;: &quot;icon.png&quot;,
  &quot;version&quot;: &quot;1.0.4&quot;,
  &quot;homepage&quot;: &quot;https://github.com/monkeyWie/gopeed-extension-youtube&quot;,
  &quot;repository&quot;: {
    &quot;url&quot;: &quot;https://github.com/monkeyWie/gopeed-extension-youtube&quot;
  },
  &quot;scripts&quot;: [
    {
      &quot;event&quot;: &quot;onResolve&quot;,
      &quot;match&quot;: {
        &quot;urls&quot;: [
          &quot;*://youtube.com/watch/*&quot;,
          &quot;*://m.youtube.com/watch/*&quot;,
          &quot;*://www.youtube.com/watch/*&quot;
        ]
      },
      &quot;entry&quot;: &quot;dist/index.js&quot;
    }
  ],
  &quot;settings&quot;: [
    {
      &quot;name&quot;: &quot;quality&quot;,
      &quot;title&quot;: &quot;Quality&quot;,
      &quot;description&quot;: &quot;Video quality&quot;,
      &quot;type&quot;: &quot;string&quot;,
      &quot;value&quot;: &quot;highest&quot;,
      &quot;options&quot;: [
        {
          &quot;label&quot;: &quot;Highest&quot;,
          &quot;value&quot;: &quot;highest&quot;
        },
        {
          &quot;label&quot;: &quot;Lowest&quot;,
          &quot;value&quot;: &quot;lowest&quot;
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置里有几个关键字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：扩展的名称。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt;：扩展的作者。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;repository&lt;/code&gt;：扩展的仓库地址，用来检测扩展是否有更新。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;scripts&lt;/code&gt;：扩展的脚本文件，用来配置脚本的入口和匹配规则。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;settings&lt;/code&gt;：扩展的设置声明，在下载器中生成对应的界面提供给用户进行设置。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;扩展重名问题&lt;/h4&gt;
&lt;p&gt;由于扩展是去中心化的，就有可能存在扩展重名的问题，所以我在扩展的配置文件里增加了一个&lt;code&gt;author&lt;/code&gt;字段，用来区分扩展的作者，通过&lt;code&gt;name&lt;/code&gt;和&lt;code&gt;author&lt;/code&gt;来作为扩展的一个唯一标识，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;youtube&quot;,
  &quot;author&quot;: &quot;monkeyWie&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个扩展的唯一标识是&lt;code&gt;monkeyWie@youtube&lt;/code&gt;，这样就可以降低扩展重名的概率了，当然这并不能完全避免扩展重名的问题，不过先这样吧，以后如果扩展数量真的多到一定程度了，再考虑其他的解决方案。&lt;/p&gt;
&lt;h4&gt;monorepo 支持&lt;/h4&gt;
&lt;p&gt;考虑到有可能在一个仓库里开发多个扩展，所以我也对这种情况进行了支持，通过&lt;code&gt;repository&lt;/code&gt;里的&lt;code&gt;path&lt;/code&gt;字段来支持&lt;code&gt;monorepo&lt;/code&gt;，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;repository&quot;: {
    &quot;url&quot;: &quot;https://github.com/GopeedLab/gopeed-extension-samples&quot;,
    &quot;path&quot;: &quot;github-release-sample&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在安装扩展的时候只需要用&lt;code&gt;#&lt;/code&gt;来拼接&lt;code&gt;url&lt;/code&gt;和&lt;code&gt;path&lt;/code&gt;即可，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/GopeedLab/gopeed-extension-samples#github-release-sample
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;扩展脚本执行入口&lt;/h4&gt;
&lt;p&gt;现在有了脚本，但是还需要配置一个脚本的运行入口，也就是脚本在什么时候才会被执行，目前我定了三个入口：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onResolve&lt;/code&gt;：当解析一个下载链接时触发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onStart&lt;/code&gt;：当开始下载时触发。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onError&lt;/code&gt;：当下载失败时触发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;执行入口有了，但是你肯定不希望脚本在所有的下载任务中都执行，所以还需要配置一个匹配规则，目前支持两种匹配规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;urls&lt;/code&gt;：按下载链接匹配，规则和 chrome 扩展的&lt;a href=&quot;https://developer.chrome.com/docs/extensions/mv3/match_patterns/&quot;&gt;匹配规则&lt;/a&gt;一致。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;labels&lt;/code&gt;：按下载任务标签匹配。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;拿上面&lt;code&gt;Youtube&lt;/code&gt;扩展来举例，这个扩展的脚本入口配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;scripts&quot;: [
    {
      &quot;event&quot;: &quot;onResolve&quot;,
      &quot;match&quot;: {
        &quot;urls&quot;: [
          &quot;*://youtube.com/watch/*&quot;,
          &quot;*://m.youtube.com/watch/*&quot;,
          &quot;*://www.youtube.com/watch/*&quot;
        ]
      },
      &quot;entry&quot;: &quot;dist/index.js&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示当解析一个&lt;code&gt;Youtube&lt;/code&gt;的视频链接时，就会执行&lt;code&gt;dist/index.js&lt;/code&gt;脚本，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-14-14-25-25.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;扩展设置&lt;/h4&gt;
&lt;p&gt;有时候扩展需要用户提供一些基本的设置，比如&lt;code&gt;Cookie&lt;/code&gt;、&lt;code&gt;默认清晰度&lt;/code&gt;等等，所以我设计了一套标准化的扩展声明，用来声明扩展的设置，这样下载器就可以根据声明来生成对应的界面，让用户进行设置，然后在扩展脚本里可以获取到用户设置的值，这样就可以实现一些定制化的功能了。&lt;/p&gt;
&lt;p&gt;还是拿上面的&lt;code&gt;Youtube&lt;/code&gt;扩展来举例，这个扩展的设置声明如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;settings&quot;: [
    {
      &quot;name&quot;: &quot;quality&quot;,
      &quot;title&quot;: &quot;Quality&quot;,
      &quot;description&quot;: &quot;Video quality&quot;,
      &quot;type&quot;: &quot;string&quot;,
      &quot;value&quot;: &quot;highest&quot;,
      &quot;options&quot;: [
        {
          &quot;label&quot;: &quot;Highest&quot;,
          &quot;value&quot;: &quot;highest&quot;
        },
        {
          &quot;label&quot;: &quot;Lowest&quot;,
          &quot;value&quot;: &quot;lowest&quot;
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示这个扩展有一个&lt;code&gt;quality&lt;/code&gt;的设置，类型是&lt;code&gt;string&lt;/code&gt;，默认值是&lt;code&gt;highest&lt;/code&gt;，用户可以在&lt;code&gt;highest&lt;/code&gt;和&lt;code&gt;lowest&lt;/code&gt;中选择一个，然后在扩展脚本里获取用户设置的值，这样就可以实现根据用户设置的清晰度来下载视频了，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-15-17-20-18.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;扩展脚本引擎&lt;/h3&gt;
&lt;p&gt;要支持灵活的扩展开发需求，肯定是需要一个图灵完备的脚本编程语言，常用的有&lt;code&gt;lua&lt;/code&gt;、&lt;code&gt;python&lt;/code&gt;、&lt;code&gt;javascript&lt;/code&gt;等，这里我选择了&lt;code&gt;javascript&lt;/code&gt;，因为&lt;code&gt;javascript&lt;/code&gt;是一门非常流行的脚本语言，而且有着非常丰富的生态，当然还有个很重要的原因就是有一个非常优秀的&lt;code&gt;javascript&lt;/code&gt;解释器库 &lt;a href=&quot;https://github.com/dop251/goja&quot;&gt;goja&lt;/a&gt;，性能非常好，而且支持完整的&lt;code&gt;ES5.1&lt;/code&gt;标准和大部分&lt;code&gt;ES6+&lt;/code&gt;标准，得益于它是纯&lt;code&gt;golang&lt;/code&gt;实现的，所以可以很方便的进行跨平台编译。&lt;/p&gt;
&lt;h4&gt;注入全局对象&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;goja&lt;/code&gt;只是一个纯粹的&lt;code&gt;javascript&lt;/code&gt;解释器，它不像在&lt;code&gt;浏览器&lt;/code&gt;或者&lt;code&gt;Node.js&lt;/code&gt;环境一样内置了特殊的&lt;code&gt;API&lt;/code&gt;，比如&lt;code&gt;XMLHttpRequest&lt;/code&gt;、&lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;setTimeout&lt;/code&gt;等等，所以我需要在&lt;code&gt;golang&lt;/code&gt;中实现这些&lt;code&gt;API&lt;/code&gt;，然后注入到全局对象中，这样才能让扩展脚本能像在浏览器环境一样使用这些&lt;code&gt;API&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当然我不可能完全模拟一个&lt;code&gt;浏览器&lt;/code&gt;环境，目前只实现了一些常用的&lt;code&gt;API&lt;/code&gt;，例如：&lt;code&gt;XMLHttpRequest&lt;/code&gt;、&lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;setInterval&lt;/code&gt;、&lt;code&gt;crypto&lt;/code&gt;、&lt;code&gt;Buffer&lt;/code&gt;、&lt;code&gt;console&lt;/code&gt;等等，这样就得到了一个阉割版的&lt;code&gt;浏览器&lt;/code&gt;环境的&lt;code&gt;javascript&lt;/code&gt;解释器。&lt;/p&gt;
&lt;p&gt;这里有个很有意思的地方，其中&lt;code&gt;XMLHttpRequest&lt;/code&gt;是用&lt;code&gt;golang&lt;/code&gt;实现的，然后&lt;code&gt;fetch&lt;/code&gt;是通过&lt;code&gt;whatwg-fetch&lt;/code&gt;这个 npm 包做的&lt;code&gt;polyfill&lt;/code&gt;，不得不感叹&lt;code&gt;js&lt;/code&gt;还是好玩，各种奇技淫巧，而且偷偷告诉你，&lt;code&gt;react-native&lt;/code&gt;也是用&lt;code&gt;whatwg-fetch&lt;/code&gt;来实现的&lt;code&gt;fetch&lt;/code&gt;，别问我怎么知道的，因为我就是借鉴（抄）的它。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于这部分具体实现，大家如果有兴趣可以看下&lt;a href=&quot;https://github.com/GopeedLab/gopeed/tree/main/pkg/download/engine&quot;&gt;源码&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;脚本引擎和 Golang 的交互&lt;/h4&gt;
&lt;p&gt;脚本引擎准备好了，回到之前脚本入口配置，每当满足匹配规则时，就会执行对应的脚本，这里就需要脚本引擎和 Golang 的交互了，我需要把&lt;code&gt;Golang&lt;/code&gt;中的对象传递给脚本引擎，然后脚本引擎访问和修改这些对象，这样就可以实现脚本和 Golang 的交互了。&lt;/p&gt;
&lt;p&gt;比如在&lt;code&gt;onResovle&lt;/code&gt;入口中，我需要把&lt;code&gt;Golang&lt;/code&gt;中的&lt;code&gt;Request&lt;/code&gt;对象传递给脚本引擎，先来看看扩展的脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gopeed.events.onResolve((ctx) =&amp;gt; {
  ctx.res = {
    name: &apos;example&apos;,
    files: [
      {
        name: &apos;index.html&apos;,
        req: {
          url: &apos;https://example.com&apos;,
        },
      },
    ],
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;脚本引擎执行&lt;code&gt;gopeed.events.onResolve()&lt;/code&gt;来注册一个回调函数，然后在&lt;code&gt;Golang&lt;/code&gt;中获取到回调函数，并在执行的时候把上下文&lt;code&gt;ctx&lt;/code&gt;作为参数传递进去，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Request struct {
  URL     string `json:&quot;url&quot;`
}

type FileInfo struct {
	Name string `json:&quot;name&quot;`
	Path string `json:&quot;path&quot;`
	Size int64  `json:&quot;size&quot;`

	Req *Request `json:&quot;req&quot;`
}

type Resource struct {
  Name string `json:&quot;name&quot;`
	Size int64  `json:&quot;size&quot;`
	Files []*FileInfo `json:&quot;files&quot;`
}

type OnResolveContext struct {
	Req *Request  `json:&quot;req&quot;`
	Res *Resource `json:&quot;res&quot;`
}

// 当触发解析任务时，执行扩展脚本，以下为伪代码
func (d *Download) Resolve(req *Request) *Response{
  // 1. 按照规则匹配生效的扩展
  ext := d.matchExt(req)
  if ext != nil {
    // 2. 注入 gopeed 全局对象
    gopeed := &amp;amp;Gopeed{
      events: map[string]goja.Callable
    }
    d.engine.Set(&quot;gopeed&quot;, gopeed)
    // 3. 执行扩展脚本拿到回调函数
    d.engine.RunScript(ext.script)
    onResolve := gopeed.events[&quot;onResolve&quot;]
    // 4. 执行回调函数，传入上下文参数
    ctx := &amp;amp;OnResolveContext{
      Req: &amp;amp;Request{
        URL: &quot;https://example.com&quot;,
      },
    }
    d.engine.CallFunction(onResolve, ctx)
    // 5. 获取上下文结果进行处理
    if ctx.Res != nil {
      // 如果扩展脚本返回了解析结果，就直接返回
      return ctx.Res
    }
  }

  // ... 正常解析逻辑
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上就是一个扩展脚本的执行流程，这里只是举例了&lt;code&gt;onResolve&lt;/code&gt;入口，其他入口的执行流程也是类似的。&lt;/p&gt;
&lt;h3&gt;扩展管理器&lt;/h3&gt;
&lt;p&gt;扩展的基本功能已经有了，接下来就需要一个扩展管理器来管理扩展的安装、更新、卸载等等。&lt;/p&gt;
&lt;h4&gt;安装扩展&lt;/h4&gt;
&lt;p&gt;前面说过，扩展是通过&lt;code&gt;git clone&lt;/code&gt;来进行安装的，这里我通过&lt;a href=&quot;https://github.com/go-git/go-git&quot;&gt;go-git&lt;/a&gt;这个库来进行&lt;code&gt;git&lt;/code&gt;相关操作，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过&lt;code&gt;git clone&lt;/code&gt;把扩展仓库克隆到本地临时目录。&lt;/li&gt;
&lt;li&gt;读取&lt;code&gt;manifest.json&lt;/code&gt;配置文件，解析扩展的基本信息。&lt;/li&gt;
&lt;li&gt;如果是一个有效的扩展项目，就把扩展移动到下载器扩展文件夹下，并按照扩展的唯一标识重命名扩展文件夹。&lt;/li&gt;
&lt;li&gt;把扩展信息写入到本地数据库中。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;更新扩展&lt;/h4&gt;
&lt;p&gt;更新扩展和安装扩展步骤类似，区别就是第 3 步做&lt;code&gt;diff&lt;/code&gt;操作，找出需要新增、修改、删除的文件，然后进行相应的操作。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里可能会有人疑问为啥不直接用 git pull 来做更新，因为考虑到要支持直接通过本地文件夹安装扩展，而这种方式安装的扩展不一定是个 git 仓库，所以就自己实现了一套更新逻辑，也就是说我只依赖 git clone 来作为一种扩展安装方式，这样的话抛开 git 扩展系统也是可以正常工作的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;卸载扩展&lt;/h4&gt;
&lt;p&gt;这个就比较简单了，只需要把扩展文件夹删除，然后从数据库中删除扩展信息即可。&lt;/p&gt;
&lt;h3&gt;扩展开发工具包&lt;/h3&gt;
&lt;p&gt;前面说过扩展引擎是一个阉割版的&lt;code&gt;浏览器&lt;/code&gt;环境，并且只支持部分&lt;code&gt;es6+&lt;/code&gt;语法，如果是纯手写&lt;code&gt;javascript&lt;/code&gt;来开发扩展，那么开发体验肯定是非常糟糕的，就像在&lt;code&gt;IE 8&lt;/code&gt;上用着&lt;code&gt;es5&lt;/code&gt;语法开发一样，所以为了能提升扩展开发体验，我开发了一个配套的&lt;a href=&quot;https://github.com/GopeedLab/gopeed-js&quot;&gt;javascript 库&lt;/a&gt;，这个库是由&lt;code&gt;pnpm monorepo&lt;/code&gt;管理的，里面包含多个&lt;code&gt;npm&lt;/code&gt;包，接下来我会一一介绍。&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://github.com/GopeedLab/gopeed-js/tree/main/packages/create-gopeed-ext&quot;&gt;脚手架&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;首先是一个脚手架，用来初始化一个扩展项目，只需要执行以下命令即可快速初始化一个扩展项目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx create-gopeed-ext@latest

√ Project name (gopeed-extension-demo) ...
√ Choose a template » Webpack

Success! Created gopeed-extension-demo at D:\code\study\js\gopeed-extension-demo
Inside that directory, you can run several commands:

  git init
    Initialize git repository

  npm install
    Install dependencies

  npm run dev
    Compiles and hot-reloads for development.

  npm run build
    Compiles and minifies for production.

We suggest that you begin by typing:

  cd gopeed-extension-demo

Happy coding!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;里面提供了两种模板：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Webpack：&lt;/p&gt;
&lt;p&gt;内置了&lt;code&gt;webpack&lt;/code&gt;+&lt;code&gt;babel&lt;/code&gt;+&lt;code&gt;eslint&lt;/code&gt;+&lt;code&gt;prettier&lt;/code&gt;的模版，对于有前端开发经验的同学来说应该很熟悉了，有了&lt;code&gt;webpack&lt;/code&gt;+&lt;code&gt;babel&lt;/code&gt;，可以随意使用最新的&lt;code&gt;es&lt;/code&gt;语法开发扩展，而且还可以使用&lt;code&gt;npm&lt;/code&gt;包，webpack 配置好了&lt;a href=&quot;https://github.com/GopeedLab/gopeed-polyfill-webpack-plugin&quot;&gt;GopeedPolyfillPlugin&lt;/a&gt;，它会自动垫片上&lt;code&gt;node&lt;/code&gt;环境下才有的&lt;code&gt;API&lt;/code&gt;，比如&lt;code&gt;http&lt;/code&gt;、&lt;code&gt;path&lt;/code&gt;等等，这样就可以在扩展脚本里使用这些&lt;code&gt;API&lt;/code&gt;了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pure：
纯&lt;code&gt;javascript&lt;/code&gt;的模版，没有任何依赖，适合那些不想用装&lt;code&gt;node&lt;/code&gt;环境的同学，当然只适合开发一些简单的扩展，如果要用到&lt;code&gt;npm&lt;/code&gt;包，还是推荐用&lt;code&gt;Webpack&lt;/code&gt;模版。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;a href=&quot;https://github.com/GopeedLab/gopeed-js/tree/main/packages/gopeed&quot;&gt;扩展类型声明&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;没有类型提示的开发体验是非常糟糕的，我用&lt;code&gt;typescript&lt;/code&gt;写了一个配套的类型声明库，这样在&lt;code&gt;vscode&lt;/code&gt;里就可以有完整的类型提示了，由于我注入的是全局对象，在&lt;code&gt;typescript&lt;/code&gt;中需要这样声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;declare global {
  const gopeed: Gopeed
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在安装完依赖之后，无需显示引用就可以获得类型提示，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;how-to-develop-a-cross-platform-extension-system/2023-12-16-16-14-52.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;&lt;a href=&quot;https://github.com/GopeedLab/gopeed-polyfill-webpack-plugin&quot;&gt;Polyfill 插件&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;此仓库&lt;code&gt;fork&lt;/code&gt;自&lt;a href=&quot;https://github.com/Richienb/node-polyfill-webpack-plugin&quot;&gt;node-polyfill-webpack-plugin&lt;/a&gt;，用于垫片&lt;code&gt;node&lt;/code&gt;环境的 API，比如&lt;code&gt;http&lt;/code&gt;、&lt;code&gt;path&lt;/code&gt;等等，在&lt;code&gt;webpack&lt;/code&gt;生态中这已经是一套非常成熟的垫片方案了，但是由于&lt;code&gt;Gopeed&lt;/code&gt;的扩展环境并不是真正的&lt;code&gt;浏览器环境&lt;/code&gt;，所以在使用这些垫片方案时碰到了一些问题，比如说有个&lt;code&gt;vm&lt;/code&gt;模块，它是&lt;code&gt;node&lt;/code&gt;环境下用来环境隔离的，基于浏览器环境的垫片实现是通过&lt;code&gt;iframe&lt;/code&gt;来实现的，但是&lt;code&gt;Gopeed&lt;/code&gt;扩展环境是没有&lt;code&gt;iframe&lt;/code&gt;的，导致此垫片方案都不适用，我就&lt;code&gt;fork&lt;/code&gt;了一份来为&lt;code&gt;Gopeed&lt;/code&gt;定制了一套&lt;code&gt;vm&lt;/code&gt;垫片方案，当然我还修改了部分垫片实现，使其更适合&lt;code&gt;Gopeed&lt;/code&gt;的扩展环境。&lt;/p&gt;
&lt;p&gt;总的来说就是我要让&lt;code&gt;Gopeed&lt;/code&gt;扩展环境尽可能对齐&lt;code&gt;浏览器&lt;/code&gt;环境，然后其它的都通过垫片来实现，如果有需要特殊处理的垫片，就做定制化开发处理，有了这套垫片方案，就可以愉快的使用大部分&lt;code&gt;npm&lt;/code&gt;包了，目前我开发的几个扩展都是基于&lt;code&gt;npm&lt;/code&gt;包来实现的，效果还是不错的。&lt;/p&gt;
&lt;p&gt;后续如果有更多的垫片需求，我会继续完善这个库。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;我花费了大量的时间和精力来实现这套扩展系统，希望&lt;code&gt;Gopeed&lt;/code&gt;能像油猴那样有一个完善的扩展生态，目前我开发的几个扩展只是抛砖引玉，毕竟我一个人的力量还是有限的，希望能有更多感兴趣的同学参与进来开发扩展，让&lt;code&gt;Gopeed&lt;/code&gt;的功能更加强大，最后的最后，希望大家能给 &lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;Gopeed&lt;/a&gt; 点个&lt;code&gt;star&lt;/code&gt;支持一下，十分感谢！&lt;/p&gt;
&lt;h3&gt;相关链接&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;Gopeed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gopeed.com/zh/dev-extension.html&quot;&gt;Gopeed 扩展开发指南&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><author>Levi</author></item><item><title>修复Next主题LeanCloud计数的问题</title><link>https://monkeywie.cn/posts/fix-next-leancloud-counter</link><guid isPermaLink="true">https://monkeywie.cn/posts/fix-next-leancloud-counter</guid><pubDate>Wed, 27 Dec 2023 10:35:39 GMT</pubDate><content:encoded>&lt;p&gt;不知道从什么时候开始，新写的文章阅读数量一直是&lt;code&gt;1&lt;/code&gt;，一直没有去管，今天把问题解决了，记录下希望能帮到有同样问题的人。&lt;/p&gt;
&lt;h2&gt;问题原因&lt;/h2&gt;
&lt;p&gt;Next 中内置的&lt;code&gt;leancloud_visitors&lt;/code&gt;插件是用&lt;code&gt;leancloud&lt;/code&gt;的&lt;code&gt;存储&lt;/code&gt;功能来实现的，类似一个&lt;code&gt;mongodb&lt;/code&gt;，不知道什么时候开始默认把权限变成了&lt;code&gt;只读&lt;/code&gt;，所以每次更新统计数的时候都会报&lt;code&gt;403&lt;/code&gt;权限问题，通过网络请求可以看到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fix-next-leancloud-counter/2023-12-27-10-45-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;解决方案&lt;/h2&gt;
&lt;h3&gt;修改默认权限为无限制&lt;/h3&gt;
&lt;p&gt;登录&lt;code&gt;leancloud&lt;/code&gt;控制台，找到&lt;code&gt;数据存储 -&amp;gt; 结构化数据&lt;/code&gt;，然后找到&lt;code&gt;Counter&lt;/code&gt;表，点击&lt;code&gt;权限&lt;/code&gt;，修改默认 ACL 权限为&lt;code&gt;无限制&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fix-next-leancloud-counter/2023-12-27-10-48-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;批量更新已存在的数据&lt;/h3&gt;
&lt;p&gt;把之前表里只读的数据批量更新为&lt;code&gt;无限制&lt;/code&gt;，选择所有数据，点击&lt;code&gt;批量操作 -&amp;gt; 批量操作所有数据&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fix-next-leancloud-counter/2023-12-27-11-18-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择更新&lt;code&gt;ACL&lt;/code&gt;为&lt;code&gt;{&quot;*&quot;:{&quot;write&quot;:true,&quot;read&quot;:true}}&lt;/code&gt;，然后批量执行即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fix-next-leancloud-counter/2023-12-27-11-19-20.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>github action 支持 macOS arm 架构了</title><link>https://monkeywie.cn/posts/github-action-macos-arm-support</link><guid isPermaLink="true">https://monkeywie.cn/posts/github-action-macos-arm-support</guid><pubDate>Sun, 18 Feb 2024 18:56:54 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前苦于&lt;code&gt;github action&lt;/code&gt;不支持&lt;code&gt;macOS arm&lt;/code&gt;架构，导致我为了打&lt;code&gt;gopped macos&lt;/code&gt;包煞费苦心，我在 linux 上用&lt;code&gt;xgo&lt;/code&gt;交叉编译出来&lt;code&gt;macos arm64&lt;/code&gt;的动态库，然后在&lt;code&gt;macOS x64&lt;/code&gt;上再打包成&lt;code&gt;macos amd64&lt;/code&gt;的动态库，最后把两个架构的动态库合并成一个&lt;code&gt;universal binary&lt;/code&gt;包，来同时支持&lt;code&gt;Intel&lt;/code&gt;和&lt;code&gt;Apple Silicon&lt;/code&gt;架构，如今&lt;code&gt;github action&lt;/code&gt;终于支持&lt;code&gt;macOS arm&lt;/code&gt;架构了，这下可以省去很多麻烦了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;开始改造&lt;/h2&gt;
&lt;p&gt;gopeed 目前的构建依赖图是这样的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-action-macos-arm-support/2024-05-08-19-11-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到需要先编译出&lt;code&gt;macos arm64&lt;/code&gt;的动态库，之前这里是用&lt;code&gt;xgo&lt;/code&gt;交叉编译的，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build-macos-arm64-lib:
  if: ${{ github.event.inputs.platform == &apos;all&apos; || github.event.inputs.platform == &apos;macos&apos; }}
  runs-on: ubuntu-latest
  needs: [get-release]
  steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-go@v4
      with:
        go-version: ${{ env.GO_VERSION }}
    - name: Build
      env:
        VERSION: ${{ needs.get-release.outputs.version }}
      run: |
        go install src.techknowlogick.com/xgo@latest
        xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags=&quot;nosqlite&quot; -ldflags=&quot;-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION&quot; -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
        mv libgopeed-*.dylib libgopeed.dylib
    - uses: actions/upload-artifact@v3
      with:
        name: macos-arm64-lib
        path: libgopeed.dylib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在有了&lt;code&gt;macOS arm&lt;/code&gt;架构的支持，我们可以直接用&lt;code&gt;macos-latest-arm&lt;/code&gt;来构建&lt;code&gt;macos arm64&lt;/code&gt;的动态库，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;build-macos-arm64-lib:
  if: ${{ github.event.inputs.platform == &apos;all&apos; || github.event.inputs.platform == &apos;macos&apos; }}
  runs-on: macos-latest
  needs: [get-release]
  steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-go@v4
      with:
        go-version: ${{ env.GO_VERSION }}
    - name: Build
      env:
        VERSION: ${{ needs.get-release.outputs.version }}
      run: |
        go build -tags nosqlite -ldflags=&quot;-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION&quot; -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
    - uses: actions/upload-artifact@v3
      with:
        name: macos-arm64-lib
        path: libgopeed.dylib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一下简单了很多，而且节省了大量的构建时间，因为&lt;code&gt;xgo&lt;/code&gt;这货要拉个很大的镜像，然后再编译，速度真的很慢，目前基本上是从 3 分钟缩短到了 30 秒，非常不错。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>go升级到1.21版本gomobile编译报错：Undefined symbols</title><link>https://monkeywie.cn/posts/gomobile-1-21-undefined-symbols</link><guid isPermaLink="true">https://monkeywie.cn/posts/gomobile-1-21-undefined-symbols</guid><pubDate>Wed, 08 May 2024 18:28:55 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;之前把&lt;code&gt;gopeed&lt;/code&gt;项目升级到&lt;code&gt;go1.21&lt;/code&gt;版本后，发现&lt;code&gt;gomobile&lt;/code&gt;编译完后，在 flutter ios 端编译会报错，如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ld: Undefined symbols:
  _res_9_nclose, referenced from:
      _runtime.text in Libgopeed[arm64][2](go.o)
  _res_9_ninit, referenced from:
      _runtime.text in Libgopeed[arm64][2](go.o)
  _res_9_nsearch, referenced from:
      _runtime.text in Libgopeed[arm64][2](go.o)
clang: error: linker command failed with exit code 1 (use -v to see invocation)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我在&lt;code&gt;go&lt;/code&gt;的&lt;code&gt;github&lt;/code&gt;里找到了这个&lt;a href=&quot;https://github.com/golang/go/issues/58416&quot;&gt;issue&lt;/a&gt;，但是看到这个&lt;code&gt;@bcmills&lt;/code&gt;哥们的回复感觉太麻烦了就用了后面&lt;code&gt;@dreacot&lt;/code&gt;老哥的解决方案，编译的时候加上&lt;code&gt;-tags netgo&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gomobile bind -target=ios -tags netgo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以正常编译了，因为我自己没有&lt;code&gt;ios&lt;/code&gt;设备，就没有测试过，按理说编译通过了应该就没问题了，然而最近 github 上好多用户都在反馈 ios 端无法正常使用，附上一个&lt;a href=&quot;https://github.com/GopeedLab/gopeed/issues/490&quot;&gt;issue&lt;/a&gt;，看起来都是&lt;code&gt;DNS&lt;/code&gt;解析的问题，起初以为是用户的网络问题，但是随着越来越多的用户反馈，这肯定就不是单纯的用个例问题了，于是又重新开始研究&lt;code&gt;@bcmills&lt;/code&gt;哥们的解决方案。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;添加 libresolv.tbd 库&lt;/h2&gt;
&lt;p&gt;他提到的解决方案是通过添加&lt;code&gt;libresolv.tbd&lt;/code&gt;或者&lt;code&gt;libresolv.9.tbd&lt;/code&gt;库来解决，但是很尴尬的是我没有&lt;code&gt;mac&lt;/code&gt;设备，所以没办法在 xcode 里添加这个库，于是问了下无所不知的&lt;code&gt;ChatGPT&lt;/code&gt;，看看能不能通过命令行添加这个库：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gomobile-1-21-undefined-symbols/2024-05-08-18-44-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看起来好像可行，在&lt;code&gt;github action&lt;/code&gt;中跑了下，然后顺便把生成出来的&lt;code&gt;project.pbxproj&lt;/code&gt;文件打印出来，如果没问题的话就复制出来提交，脚本如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gem install xcodeproj
cat &amp;lt;&amp;lt;EOF &amp;gt; temp.rb
require &apos;xcodeproj&apos;
project_path = &apos;ui/flutter/ios/Runner.xcodeproj&apos;
project = Xcodeproj::Project.open(project_path)
target = project.targets.first

# 添加系统库
lib_name = &apos;libresolv.tbd&apos;
framework = &apos;usr/lib/&apos; + lib_name
target.frameworks_build_phase.add_file_reference(project.frameworks_group.new_file(framework))

project.save
EOF
ruby temp.rb

echo &quot;==========edit project.pbxproj============&quot;
cat ui/flutter/ios/Runner.xcodeproj/project.pbxproj
echo &quot;==========edit project.pbxproj============&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着就是等待&lt;code&gt;github action&lt;/code&gt;的结果了，结果是编译成功的，然后把&lt;code&gt;IPA&lt;/code&gt;包发给用户测试，一切正常，问题解决，最后把更新之后的&lt;code&gt;project.pbxproj&lt;/code&gt;文件提交到&lt;code&gt;github&lt;/code&gt;上，至此一个没有&lt;code&gt;mac&lt;/code&gt;设备的我就这样把这个问题解决了，哈哈，不得不说我可真是个天才(狗头保命)。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>js模拟键盘输入</title><link>https://monkeywie.cn/posts/js-simulate-keyboard-typing</link><guid isPermaLink="true">https://monkeywie.cn/posts/js-simulate-keyboard-typing</guid><pubDate>Wed, 29 May 2024 14:04:00 GMT</pubDate><content:encoded>&lt;p&gt;最近发现了一个&lt;code&gt;沉浸式翻译&lt;/code&gt;的神奇功能，就是在输入框输入中文的时候敲三下空格就可以自动翻译成英文，这功能真的挺方便的，效果如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;js-simulate-keyboard-typing/2024-5-30-9-46-06.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这直接引起了我的好奇心，要知道模拟文本输入是一个很麻烦的事情，记得之前有一次我想用&lt;code&gt;油猴&lt;/code&gt;来做一个&lt;code&gt;discord&lt;/code&gt;自动发消息水经验的脚本，但是卡在了模拟输入这一步，最后就放弃了。&lt;/p&gt;
&lt;p&gt;于是就去&lt;code&gt;discord&lt;/code&gt;试了下&lt;code&gt;沉浸式翻译&lt;/code&gt;的这个功能，发现是可以正常运作的，这我就不得不好好研究下了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;无用的尝试&lt;/h2&gt;
&lt;p&gt;众所周知&lt;code&gt;html&lt;/code&gt;里只有两种输入方式，一种是&lt;code&gt;input | textarea&lt;/code&gt;，还有另一种是&lt;code&gt;contenteditable=ture&lt;/code&gt;的&lt;code&gt;div&lt;/code&gt;，通过&lt;code&gt;F12&lt;/code&gt;可以看到&lt;code&gt;discord&lt;/code&gt;的输入框是一个&lt;code&gt;contenteditable div&lt;/code&gt;，直接在控制台运行代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var input = document.querySelector(&quot;div.markup_a7e664.editor__66464&quot;);
input.textContent = &quot;测试&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果如下，可以看到输入框里的内容已经被修改了，但是消息并不能发出去也不能被删除：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;js-simulate-keyboard-typing/2024-05-30-15-32.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;说明事情并没有那么简单，很多情况下输入框都会有一些&lt;code&gt;js&lt;/code&gt;的事件监听，比如&lt;code&gt;input&lt;/code&gt;、&lt;code&gt;keydown&lt;/code&gt;、&lt;code&gt;keyup&lt;/code&gt;、&lt;code&gt;keypress&lt;/code&gt;等，这些事件监听会影响到输入框的行为，所以直接修改&lt;code&gt;textContent&lt;/code&gt;这条路行不通。&lt;/p&gt;
&lt;p&gt;同样的在现代化的前端框架中，输入框都是通过&lt;code&gt;input&lt;/code&gt;事件做双向绑定的，这样的输入框也是无法通过修改&lt;code&gt;input.value&lt;/code&gt;来达到模拟输入目的。&lt;/p&gt;
&lt;p&gt;也就是说要达到模拟键盘输入的目的，不止需要修改输入框的内容，还需要触发一系列相关的事件才行，想想就觉得头大，不过既然&lt;code&gt;沉浸式翻译&lt;/code&gt;能做到，那说明一定有办法，接下来就研究下它咋做到的。&lt;/p&gt;
&lt;h2&gt;源码之下无秘密&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;沉浸式翻译&lt;/code&gt;自从被收购之后就没有开源了，但是谁叫它是&lt;code&gt;js&lt;/code&gt;写的呢，直接把扩展里的&lt;code&gt;content_script.js&lt;/code&gt;拿出来分析，虽然是混淆过的但是问题不大，具体代码阅读过程这里就不展开了，总之最后定位到了几个关键函数，这里反混淆加工一下贴出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 方式一：下发粘贴文本事件进行输入
 */
function type1(el, text) {
  let r = new DataTransfer();
  r.setData(&quot;text/plain&quot;, text);
  el.dispatchEvent(
    new ClipboardEvent(&quot;paste&quot;, {
      clipboardData: r,
      bubbles: true,
      cancelable: true,
    })
  );
  r.clearData();
}

/**
 * 方式二：调用insertText命令进行输入
 */
function type2(el, text) {
  el.select();
  document.execCommand(&quot;insertText&quot;, false, text);
}

/**
 * 方式三：改变输入框的value值并下发input事件进行输入
 */
function type3(el, text) {
  el.value = e.text;
  el.dispatchEvent(
    new Event(&quot;input&quot;, {
      bubbles: true,
    })
  );
}

/**
 * 方式四：下发textInput事件进行输入
 */
function type4(el, text) {
  let n = document.createEvent(&quot;TextEvent&quot;);
  if (n.initTextEvent) {
    n.initTextEvent(&quot;textInput&quot;, true, true, window, text);
    el.dispatchEvent(n);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一共有这四种方式，然后逐一进行尝试，这样来提高兼容性，我测试了下&lt;code&gt;discord&lt;/code&gt;的输入框是可以通过&lt;code&gt;type1&lt;/code&gt;方式进行模拟输入的，效果如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;js-simulate-keyboard-typing/2024-05-30-15-53.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是这样只能&lt;code&gt;append&lt;/code&gt;文本，如果要把文本完全替换掉还需要先选中所有文本再调用，相关代码也贴一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function selectAll(el) {
  el.focus();
  let t = window.getSelection();
  if (!t) return;
  let n = document.createRange();
  n.selectNodeContents(el);
  t.removeAllRanges();
  t.addRange(n);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;js-simulate-keyboard-typing/2024-05-30-16-32.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用现成的库&lt;/h2&gt;
&lt;p&gt;上面的代码虽然可以实现模拟键盘输入，但是实现的还是比较粗糙，可能会有一些兼容性问题，所以我&lt;code&gt;google&lt;/code&gt;了一下，发现了一个现成的库&lt;a href=&quot;https://github.com/testing-library/user-event&quot;&gt;user-event&lt;/a&gt;，这个库的介绍如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user-event tries to simulate the real events that would happen in the browser as the user interacts with it. For example userEvent.click(checkbox) would change the state of the checkbox.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说它在模拟输入的时候会把所有的事件都模拟出来，就像真实的用户输入一样，这样可以最大程度上提高兼容性，使用起来也非常简单，直接&lt;code&gt;npm install @testing-library/user-event&lt;/code&gt;安装即可，然后在代码中引入即可快速实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import userEvent from &quot;@testing-library/user-event&quot;;

const user = userEvent.setup();
await user.keyboard(&quot;测试&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过有一点要吐槽的是这个库不支持&lt;code&gt;umd&lt;/code&gt;引入，如果是油猴之类的脚本的话就不太方便了，得上&lt;code&gt;webpack&lt;/code&gt;才行。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;模拟键盘输入是一个比较麻烦的事情，不过通过一些技巧还是可以实现的，不过要注意兼容性问题，最好使用现成的库来实现，这样可以提高开发效率，减少不必要的麻烦。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>golang 方法作为字段转JSON</title><link>https://monkeywie.cn/posts/golang-struct-json-marshal-with-method</link><guid isPermaLink="true">https://monkeywie.cn/posts/golang-struct-json-marshal-with-method</guid><pubDate>Wed, 14 Aug 2024 18:43:20 GMT</pubDate><content:encoded>&lt;p&gt;假设有一个结构体来表示用户信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
    Name string `json:&quot;name&quot;`
    Sex  string `json:&quot;sex&quot;`
    Age  int `json:&quot;age&quot;`
    Vip  bool `json:&quot;vip&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后有个需求是展示给用户的名称要根据用户的&lt;code&gt;性别&lt;/code&gt;和&lt;code&gt;VIP&lt;/code&gt;来生成，比如说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果用户是 VIP，那么展示 &lt;code&gt;尊贵的 + 名字 + 先生/女士&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果用户不是 VIP，那么展示 &lt;code&gt;名字 + 先生/女士&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个时候一般会有个做法，在结构体中加一个字段：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
    Name string `json:&quot;name&quot;`
    Sex  string `json:&quot;sex&quot;`
    Age  int `json:&quot;age&quot;`
    Vip  bool `json:&quot;vip&quot;`
    DisplayName string `json:&quot;display_name&quot;`
}

// 实现一个方法来设置 DisplayName 字段
func (u *User) SetDisplayName() {

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在每次更新&lt;code&gt;Name&lt;/code&gt;、&lt;code&gt;Sex&lt;/code&gt;、&lt;code&gt;Vip&lt;/code&gt;的时候调用&lt;code&gt;SetDisplayName&lt;/code&gt;方法来更新&lt;code&gt;DisplayName&lt;/code&gt;字段，或者在序列化的时候调用&lt;code&gt;SetDisplayName&lt;/code&gt;方法来更新&lt;code&gt;DisplayName&lt;/code&gt;字段，这样做是没问题的，但是非常的不利于维护，要到处硬编码调用&lt;code&gt;SetDisplayName&lt;/code&gt;方法，非常容易遗漏，那么有没有什么优雅的方式来解决这个问题呢？答案是有的，下面来介绍一下。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;方法作为字段转JSON&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;golang&lt;/code&gt;在做JSON序列化的时候，会调用结构体的&lt;code&gt;MarshalJSON&lt;/code&gt;方法，可以利用这个特性来解决这个问题，也就是自定义序列化实现，在每次序列化的时候动态计算&lt;code&gt;DisplayName&lt;/code&gt;字段。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 实现一个方法来返回 DisplayName 字段
func (u *User) DisplayName() string {

}

// 实现 MarshalJSON 方法
func (u *User) MarshalJSON() ([]byte, error) {
    // 这里要用一个新的结构体来存储原始的 User 结构体，不然会造成递归调用 DisplayName 方法
    type rawUser User
    return json.Marshal(struct {
        rawUser
        DisplayName string `json:&quot;display_name&quot;`
    }{
        rawUser:     rawUser(*u),
        DisplayName: u.DisplayName(),
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以在序列化的时候动态计算&lt;code&gt;DisplayName&lt;/code&gt;字段了。&lt;/p&gt;
&lt;p&gt;当然上面的示例只是抛砖引玉，可以根据实际业务来使用，比如说直接交给前端来处理&lt;code&gt;DisplayName&lt;/code&gt;字段逻辑，本文主要是记录一下&lt;code&gt;golang&lt;/code&gt;中方法作为字段转JSON的方法。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>typescript使用类型推导来声明第三方库类型</title><link>https://monkeywie.cn/posts/typescript-import-third-package-type</link><guid isPermaLink="true">https://monkeywie.cn/posts/typescript-import-third-package-type</guid><pubDate>Fri, 20 Sep 2024 18:00:07 GMT</pubDate><content:encoded>&lt;p&gt;假设有一个第三方库，我们引用了它并且调用了它的方法，然后我们需要拿到这个方法的返回类型作为我们一个公共函数的参数类型，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Innertube } from &quot;youtubei.js&quot;;

const innertube = await Innertube.create();
const video = await innertube.getBasicInfo(&quot;video_id&quot;);
handleVideo(video);

// 注意这里的VideoInfo是getBasicInfo的返回类型，这里并没有拿到
function handleVideo(video: VideoInfo) {
  // do something
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候可能就会有人说，很简单啊直接&lt;code&gt;Ctrl + 点击&lt;/code&gt;进去方法看一下返回类型不就行了吗？，好的那我们点进&lt;code&gt;getBasicInfo&lt;/code&gt;看下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;typescript-import-third-package-type/2024-09-20-18-11-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;p&gt;可以看到这个方法的返回类型是&lt;code&gt;Promise&amp;lt;VideoInfo&amp;gt;&lt;/code&gt;，然后我们再点进&lt;code&gt;VideoInfo&lt;/code&gt;看下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;typescript-import-third-package-type/2024-09-20-18-12-18.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后，然后就没了，根本就找不到要怎么&lt;code&gt;import&lt;/code&gt;这个类型，当然如果仔细看源码的话还是能找到的，比如这里我最终找到了&lt;code&gt;VideoInfo&lt;/code&gt;的定义：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;typescript-import-third-package-type/2024-09-20-18-22-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它竟然是在套娃在某个导出模块里，这种情况只能说运气还是比较好，起码作者把类型导出了，还有些库可能连类型都不导出，这种情况下就需要用到类型推导了。&lt;/p&gt;
&lt;h2&gt;类型推导&lt;/h2&gt;
&lt;h3&gt;typeof&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;typeof&lt;/code&gt;是 TS 中的一个关键字，它可以获取一个变量的类型，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const a = 1;
type A = typeof a; // number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合上面的例子，我们可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Innertube } from &quot;youtubei.js&quot;;

const innertube = await Innertube.create();
const video = await innertube.getBasicInfo(&quot;video_id&quot;);
handleVideo(video);

type VideoInfo = typeof video;
// 注意这里的VideoInfo是getBasicInfo的返回类型，这里并没有拿到
function handleVideo(video: VideoInfo) {
  // do something
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而这并不满足我的需求，因为&lt;code&gt;typeof&lt;/code&gt;只能获取变量的类型，而我需要在没有变量的情况下获取方法的返回类型。&lt;/p&gt;
&lt;h3&gt;ReturnType&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ReturnType&lt;/code&gt;是 TS 中的一个工具类型，它可以获取一个函数的返回类型，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fn() {
  return 1;
}

type FnReturnType = ReturnType&amp;lt;typeof fn&amp;gt;; // number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是不是感觉很接近了，但是这里的&lt;code&gt;fn&lt;/code&gt;是本地定义的一个函数，而&lt;code&gt;getBasicInfo&lt;/code&gt;是存在于&lt;code&gt;Innertube&lt;/code&gt;类型中的一个方法，大概长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Innertube = {
  getBasicInfo: (videoId: string) =&amp;gt; Promise&amp;lt;VideoInfo&amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那要怎么取到&lt;code&gt;getBasicInfo&lt;/code&gt;的方法签名类型呢？这里就需要用到索引类型查询（Indexed Access Types）特性了，就是通过&lt;code&gt;[]&lt;/code&gt;来获取类型的属性，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type GetBasicInfo = Innertube[&quot;getBasicInfo&quot;];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在取到了&lt;code&gt;getBasicInfo&lt;/code&gt;的方法签名类型，然后我们就可以使用&lt;code&gt;ReturnType&lt;/code&gt;来获取它的返回类型了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type VideoInfo = ReturnType&amp;lt;GetBasicInfo&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到此为止，我们取到了想要的类型为，但是实际上它现在还是&lt;code&gt;Promise&amp;lt;VideoInfo&amp;gt;&lt;/code&gt;，还要把&lt;code&gt;Promise&lt;/code&gt;给去掉，这里就需要用到&lt;code&gt;Awaited&lt;/code&gt;工具类型了，它可以获取一个&lt;code&gt;Promise&lt;/code&gt;的返回类型，修改如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type VideoInfo = Awaited&amp;lt;ReturnType&amp;lt;GetBasicInfo&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { Innertube } from &quot;youtubei.js&quot;;

type VideoInfo = Awaited&amp;lt;ReturnType&amp;lt;Innertube[&quot;getBasicInfo&quot;]&amp;gt;&amp;gt;;
function handleVideo(video: VideoInfo) {
  // do something
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;举一反三&lt;/h3&gt;
&lt;p&gt;比如要获取第三方库中某个函数的参数类型，可以使用&lt;code&gt;Parameters&lt;/code&gt;工具类型，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function fn(a: number, b: string) {
  return a + b;
}

type FnParameters = Parameters&amp;lt;typeof fn&amp;gt;; // [number, string]
type ParamA = FnParameters[0]; // number
type ParamB = FnParameters[1]; // string
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;通过&lt;code&gt;ReturnType&lt;/code&gt;、&lt;code&gt;Parameters&lt;/code&gt;、&lt;code&gt;Awaited&lt;/code&gt;等工具类型，我们可以很方便的获取第三方库中的类型，这样就不用再去翻乱七八糟的模块导出源码了，而且这种方式还能保证类型的准确性，因为它是直接推导出来的。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>使用 fluttericon.com 来管理图标</title><link>https://monkeywie.cn/posts/fluttericon-guide</link><guid isPermaLink="true">https://monkeywie.cn/posts/fluttericon-guide</guid><pubDate>Sat, 04 Jan 2025 10:21:00 GMT</pubDate><content:encoded>&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;在开发 flutter 项目时，经常会用到一些图标，这些图标通常是从一些图标库中下载下来的，然后需要把图标&lt;code&gt;svg&lt;/code&gt;格式转换成&lt;code&gt;flutter&lt;/code&gt;项目中的&lt;code&gt;icon font&lt;/code&gt;，这个过程是比较繁琐的，而且如果图标库中的图标有更新，那么还需要手动去下载新的图标，然后再转换，在有了&lt;code&gt;fluttericon.com&lt;/code&gt;这个网站之后，这个过程就变得非常简单了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;fluttericon.com 是什么&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fluttericon.com&lt;/code&gt; 是一个在线的图标管理网站，它提供了大量的免费图标库，可以直接在线检索图标:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fluttericon-guide/2025-01-04-10-29-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在挑选好所需的图标之后，点击&lt;code&gt;DOWNLOAD&lt;/code&gt;按钮，就可以下载一个&lt;code&gt;zip&lt;/code&gt;文件，里面包含了字体文件和&lt;code&gt;flutter&lt;/code&gt;项目中的&lt;code&gt;icons.dart&lt;/code&gt;文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fluttericon-guide/2025-01-04-10-32-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;使用&lt;/h3&gt;
&lt;p&gt;下载下来的&lt;code&gt;zip&lt;/code&gt;文件解压之后，里面包含了两个文件夹，一个是&lt;code&gt;fonts&lt;/code&gt;文件夹，里面包含了字体文件，&lt;code&gt;my_flutter_app_icons.dart&lt;/code&gt;文件，将这两个文件夹拷贝到&lt;code&gt;flutter&lt;/code&gt;项目中，然后在&lt;code&gt;pubspec.yaml&lt;/code&gt;文件中引入字体文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fonts:
  - family: MyFlutterApp
    fonts:
      - asset: fonts/my_flutter_app.ttf
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里的&lt;code&gt;my_flutter_app&lt;/code&gt;是根据 fluttericon.com 网站中自定义的项目名称来的，这里是默认的名称，可以随意修改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后在项目中使用这个图标：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;package:flutter/material.dart&apos;;
import &apos;my_flutter_app_icons.dart&apos;;

class MyWidget extends StatelessWidget {
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(MyFlutterApp.my_icon),
      onPressed: () {
        print(&apos;icon clicked&apos;);
      },
    );
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;备份&lt;/h3&gt;
&lt;p&gt;由于&lt;code&gt;fluttericon.com&lt;/code&gt;是使用浏览器&lt;code&gt;cookie&lt;/code&gt;来保存用户的图标库，如果清除了&lt;code&gt;cookie&lt;/code&gt;，那么之前的图标库就会丢失，所以在使用&lt;code&gt;fluttericon.com&lt;/code&gt;的时候，最好将下载的图标库(.zip)备份一下，这样以后就可以直接导入自己的图标库，而不用重新挑选一遍。&lt;/p&gt;
&lt;h2&gt;导入自定义 SVG&lt;/h2&gt;
&lt;p&gt;除了自带的图标库之外，&lt;code&gt;fluttericon.com&lt;/code&gt;还支持导入自定义的&lt;code&gt;svg&lt;/code&gt;文件，但是需要注意的是，导入的必须是&lt;code&gt;compound path svg&lt;/code&gt;，而且也不支持类似&lt;code&gt;fill&lt;/code&gt;这样的属性，所以在导入之前需要先处理一下&lt;code&gt;svg&lt;/code&gt;文件。&lt;/p&gt;
&lt;h3&gt;碰到的问题&lt;/h3&gt;
&lt;p&gt;我这里就碰到了一个场景，想在一个&lt;code&gt;SVG&lt;/code&gt;文件加上文本，然后导入到&lt;code&gt;fluttericon.com&lt;/code&gt;中，但是如果没有做处理的话会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;If image looks not as expected please convert to compound path manually.

Skipped tags and attributes: polygon,fill
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为如果直接编辑的&lt;code&gt;SVG&lt;/code&gt;添加文本导出来的实际上是变成了多个&lt;code&gt;path&lt;/code&gt;，比如这个：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fluttericon-guide/2025-01-04-10-34-28.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我在原始的&lt;code&gt;SVG&lt;/code&gt;中间添加了一个字，但是导出来的&lt;code&gt;SVG&lt;/code&gt;变成了这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --&amp;gt;
&amp;lt;svg version=&quot;1.1&quot; baseProfile=&quot;tiny&quot;
	 xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; x=&quot;0px&quot; y=&quot;0px&quot; viewBox=&quot;0 0 384 512&quot;
	 overflow=&quot;visible&quot; xml:space=&quot;preserve&quot;&amp;gt;
&amp;lt;g&amp;gt;
	&amp;lt;g&amp;gt;
		&amp;lt;path fill=&quot;none&quot; d=&quot;M185.8,397.9V296.4c-8.1,4-21.2,7.6-37.4,9.8v3l11.2,1c12.3,1,13.3,2.4,13.3,11v76.7
			c0,13.4-1.6,15.6-25.9,17.4v2.8H212v-2.8C187.7,413.6,185.8,411.4,185.8,397.9z&quot;/&amp;gt;
		&amp;lt;path d=&quot;M256,160c-17.7,0-32-14.3-32-32V0H64C28.7,0,0,28.7,0,64v384c0,35.3,28.7,64,64,64h256c35.3,0,64-28.7,64-64V160H256z
			 M212,418.2h-64.9v-2.8c24.3-1.8,25.9-4,25.9-17.4v-76.7c0-8.6-1-10-13.3-11l-11.2-1v-3c16.2-2.2,29.3-5.8,37.4-9.8v101.5
			c0,13.4,1.8,15.6,26.2,17.4V418.2z&quot;/&amp;gt;
	&amp;lt;/g&amp;gt;
	&amp;lt;polygon points=&quot;256,0 256,128 384,128 	&quot;/&amp;gt;
	&amp;lt;path fill=&quot;none&quot; d=&quot;M185.8,397.9V296.4c-8.1,4-21.2,7.6-37.4,9.8v3l11.2,1c12.3,1,13.3,2.4,13.3,11v76.7
		c0,13.4-1.6,15.6-25.9,17.4v2.8H212v-2.8C187.7,413.6,185.8,411.4,185.8,397.9z&quot;/&amp;gt;
&amp;lt;/g&amp;gt;
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;通过 Adobe Illustrator 处理&lt;/h3&gt;
&lt;p&gt;我本人对&lt;code&gt;SVG&lt;/code&gt;不是很熟悉，网上找了很多资料也没有找到相关的解决方案，最后靠着 AI 和无尽的尝试，总算是盲人摸象的处理出来了，这里记录一下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先打开&lt;code&gt;SVG&lt;/code&gt;文件，然后把文本添加好，这一步很简单。&lt;/li&gt;
&lt;li&gt;选中文本，然后&lt;code&gt;属性&lt;/code&gt;-&amp;gt;&lt;code&gt;创建轮廓&lt;/code&gt;：
&lt;img src=&quot;fluttericon-guide/2025-01-04-10-54-40.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;还是选中文本，然后&lt;code&gt;路径查找器&lt;/code&gt;-&amp;gt;&lt;code&gt;轮廓&lt;/code&gt;：
&lt;img src=&quot;fluttericon-guide/2025-01-04-10-57-41.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;li&gt;最后全选所有节点，然后&lt;code&gt;对象&lt;/code&gt;-&amp;gt;&lt;code&gt;复合路径&lt;/code&gt;-&amp;gt;&lt;code&gt;建立&lt;/code&gt;：
&lt;img src=&quot;fluttericon-guide/2025-01-04-10-59-23.png&quot; alt=&quot;&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个时候再导出&lt;code&gt;SVG&lt;/code&gt;文件，可以看到就只保留一条&lt;code&gt;path&lt;/code&gt;了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --&amp;gt;
&amp;lt;svg version=&quot;1.1&quot; id=&quot;图层_1&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; x=&quot;0px&quot; y=&quot;0px&quot;
	 viewBox=&quot;0 0 384 512&quot; style=&quot;enable-background:new 0 0 384 512;&quot; xml:space=&quot;preserve&quot;&amp;gt;
&amp;lt;path d=&quot;M0,64C0,28.7,28.7,0,64,0h160v128c0,17.7,14.3,32,32,32h128v288c0,35.3-28.7,64-64,64H64c-35.3,0-64-28.7-64-64V64z
	 M384,128H256V0L384,128z M157.2,301.3v1.5h34.6v-1.5c-13-1-13.9-2.1-13.9-9.3v-54.2c-4.3,2.1-11.3,4.1-19.9,5.2v1.6l6,0.5
	c6.6,0.5,7.1,1.3,7.1,5.9v41C171,299.2,170.2,300.4,157.2,301.3z&quot;/&amp;gt;
&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来就可以成功导入到&lt;code&gt;fluttericon.com&lt;/code&gt;中了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;折腾了这么久，终于搞定了 fluttericon.com 的图标导入问题！说实话，一开始处理 SVG 文件时还真是让人头大，特别是遇到那些路径格式不兼容的情况。不过通过这次实践，我总结出了一套还算顺手的处理流程，希望能帮其他开发者少走点弯路。&lt;/p&gt;
&lt;p&gt;关键点就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SVG 文件预处理很重要，特别是路径格式的转换&lt;/li&gt;
&lt;li&gt;导入前最好先检查下 SVG 的结构是否规范&lt;/li&gt;
&lt;li&gt;如果导入失败，99% 都是 SVG 格式的问题&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之，熟悉了这套流程后，处理自定义图标就变得轻松多了。搞定！🚀&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>我的 2025 独立开发技术栈分享</title><link>https://monkeywie.cn/posts/share-my-2025-solo-stack</link><guid isPermaLink="true">https://monkeywie.cn/posts/share-my-2025-solo-stack</guid><pubDate>Wed, 20 Aug 2025 13:55:57 GMT</pubDate><content:encoded>&lt;h1&gt;我的 2025 独立开发穷鬼套餐&lt;/h1&gt;
&lt;p&gt;除了域名分文不掏！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;share-my-2025-solo-stack/2025-09-02-14-07-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;🛠️ 技术栈&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端框架&lt;/strong&gt;: TypeScript + TanStack Router&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端框架&lt;/strong&gt;: Hono.js&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库&lt;/strong&gt;: Drizzle ORM + SQLite&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认证&lt;/strong&gt;: Better Auth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UI 组件&lt;/strong&gt;: shadcn/ui&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;🏗️ 基础设施&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;服务&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;th&gt;成本&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代码托管&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据库&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQLite WASM(浏览器) + Cloudflare D1(服务端)&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;认证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Better Auth&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;邮件服务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resend + Cloudflare 邮件转发&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;文件存储&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare R2&lt;/td&gt;
&lt;td&gt;免费额度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;部署&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;td&gt;免费&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 工具&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;白嫖&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;如果需要开发 local-first 的应用，强烈推荐这一套技术栈，浏览器 SQLite 和 Cloudflare D1 可以共用一套 drizzle entity 和 sql 方言，这样可以非常优雅的做到 CRDT，甚至可以实现浏览器和后端复用同一套业务代码&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以上代码已开源，项目地址：
&lt;a href=&quot;https://github.com/monkeyWie/typix&quot;&gt;github.com/monkeyWie/typix&lt;/a&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>独立开发者的 Logo 设计</title><link>https://monkeywie.cn/posts/ai-logo-design-in-3-steps</link><guid isPermaLink="true">https://monkeywie.cn/posts/ai-logo-design-in-3-steps</guid><pubDate>Tue, 02 Sep 2025 13:52:44 GMT</pubDate><content:encoded>&lt;h1&gt;独立开发者的 Logo 制作记录&lt;/h1&gt;
&lt;p&gt;最近做了个一站式 AI 生图工具&lt;code&gt;Typix&lt;/code&gt;，需要设计一个 Logo，作为一个完全没有设计基础的程序员，我摸索出了一套简单实用的方法，记录下来分享给同样需要的朋友。&lt;/p&gt;
&lt;p&gt;这个方法主要用到了 AI 生成 + 网页工具处理。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;步骤一：用 AI 生成基础图案&lt;/h2&gt;
&lt;p&gt;既然是做文本生成图像的工具，我希望 Logo 能体现这个概念，同时要简洁现代。于是写了这样一个提示词：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以 Type To Pixel 输入文本生成图像为主题，设计一个带有 AI 元素、有质感、现代化、科技感、极简风格的 APP logo。黑色背景打底，主体用白色呈现，形状为带圆角的方形。不要出现文字，主体是一只可爱小动物的极简轮廓，最终是透明背景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我用的是 GPT-4o 的图像生成功能，生成了几张图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ai-logo-design-in-3-steps/2025-09-02-14-11-08.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这张还不错，简洁清晰。&lt;/p&gt;
&lt;p&gt;我也试了下 Google 的 Imagen 4，同样的提示词得到了这个结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ai-logo-design-in-3-steps/2025-09-02-14-11-15.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;两个都挺好的，最后选了第一个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;小贴士&lt;/strong&gt;：多抽卡几次总能找到满意的，提示词也可以根据自己需求进行微调。&lt;/p&gt;
&lt;h2&gt;步骤二：转成矢量格式&lt;/h2&gt;
&lt;p&gt;AI 生成的是 PNG 位图，放大会糊。Logo 需要在不同尺寸下都清晰，所以要转成矢量格式。&lt;/p&gt;
&lt;p&gt;我用的工具：&lt;a href=&quot;https://svgtrace.com/png-to-svg&quot;&gt;https://svgtrace.com/png-to-svg&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不用登录，免费转换，直接把 PNG 拖上去就能转成 SVG，效果还不错。这样 Logo 就能任意缩放而不失真了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ai-logo-design-in-3-steps/2025-09-02-14-23-05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;步骤三：生成 ICO 文件&lt;/h2&gt;
&lt;p&gt;网站和应用通常需要 ICO 格式的图标，我用了这个工具：&lt;a href=&quot;https://www.icoconverter.com&quot;&gt;https://www.icoconverter.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;把 SVG 上传，选择需要的尺寸，就能生成包含多种分辨率的 ICO 文件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;ai-logo-design-in-3-steps/2025-09-02-14-23-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;整个流程就这三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AI 生成图案（GPT-4o 或 Google Imagen 4）&lt;/li&gt;
&lt;li&gt;转成矢量格式（svgtrace.com）&lt;/li&gt;
&lt;li&gt;生成多格式文件（icoconverter.com）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;成本基本为零，效果对独立开发者来说完全够用了。虽然比不上专业设计师的作品，但至少看起来不会太业余。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;如果你也在做独立开发项目，欢迎交流经验。这些小工具小技巧积累起来还是挺有用的。&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>博客搬家到 Astro</title><link>https://monkeywie.cn/posts/migrate-my-blog-to-astro</link><guid isPermaLink="true">https://monkeywie.cn/posts/migrate-my-blog-to-astro</guid><pubDate>Thu, 13 Nov 2025 08:03:00 GMT</pubDate><content:encoded>&lt;p&gt;最近把博客从 Hexo 搬家到了 Astro，原因有主要是 Hexo 官方已经很久没有更新了，社区活跃度也在下降,还有就是 Next 主题的 UI 看起来有点过时了，然后刚好看到有人分享&lt;code&gt;Astro&lt;/code&gt;的博客模板，感觉挺不错的，就决定搬家了。当然也可以直接到&lt;a href=&quot;https://astro.build/themes/1/?search=&amp;amp;categories%5B%5D=blog&amp;amp;price%5B%5D=free&quot;&gt;官网&lt;/a&gt;选择适合的免费博客模板。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;搬家记&lt;/h2&gt;
&lt;h3&gt;寻找合适的模板&lt;/h3&gt;
&lt;p&gt;我用的模版是&lt;a href=&quot;https://github.com/stelcodes/multiterm-astro&quot;&gt;multiterm-astro&lt;/a&gt;，这个模版最吸引我的是它首页展示的 &lt;strong&gt;GitHub Contributions 数据面板&lt;/strong&gt;。可以直观地展示 GitHub 的代码提交活动热力图，对于一个开源爱好者的博客来说，这简直完美！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;migrate-my-blog-to-astro/2025-11-13-10-30-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;GitHub Contributions 数据本地抓取&lt;/h3&gt;
&lt;p&gt;当我把模板下载下来准备使用时，发现 GitHub Contributions 面板无法正常显示，原因是其依赖的第三方 API 服务已经不可用，然后我就顺藤摸瓜去 GitHub 上搜索相关的开源项目。很快就找到了 &lt;a href=&quot;https://github.com/grubersjoe/github-contributions-api&quot;&gt;github-contributions-api&lt;/a&gt; 这个开源仓库，然后把它的核心代码直接搬到了我&lt;a href=&quot;https://github.com/monkeyWie/monkeywie.github.io/blob/master/src/libs/github-contributions-api.ts&quot;&gt;博客项目&lt;/a&gt;里，对应的把&lt;code&gt;GitHubActivityCalendar.astro&lt;/code&gt;组件也更新了，这样就不用依赖任何第三方 API，在本地构建时就能抓取数据了。&lt;/p&gt;
&lt;h3&gt;博客汉化&lt;/h3&gt;
&lt;p&gt;由于原模板是英文界面，为了更符合中文博客的使用习惯，我对模板进行了全面汉化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;导航栏汉化&lt;/strong&gt;：将 &quot;Home&quot;、&quot;Posts&quot;、&quot;Tags&quot; 等改为 &quot;首页&quot;、&quot;文章&quot;、&quot;标签&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页面标题汉化&lt;/strong&gt;：如 &quot;All Posts&quot; → &quot;所有文章&quot;、&quot;Read more&quot; → &quot;阅读更多&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日期格式调整&lt;/strong&gt;：将英文日期格式改为中文友好的 &quot;YYYY年MM月DD日&quot; 格式&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;其他细节&lt;/strong&gt;：阅读时间、字数统计等提示文字的汉化&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;评论系统迁移：从 Utterances 到 Giscus&lt;/h3&gt;
&lt;p&gt;之前的 Hexo 博客使用的是 Utterances 作为评论系统，但这次迁移顺便也升级到了更强大的 Giscus。&lt;/p&gt;
&lt;h4&gt;为什么选择 Giscus？&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;✅ 基于 GitHub Discussions，比 Issues 更适合作为评论系统&lt;/li&gt;
&lt;li&gt;✅ 支持回复、表情回应等功能&lt;/li&gt;
&lt;li&gt;✅ 可以自定义主题样式&lt;/li&gt;
&lt;li&gt;✅ 支持多语言&lt;/li&gt;
&lt;li&gt;✅ 完全开源免费&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;迁移步骤&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;启用 GitHub Discussions&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 GitHub 仓库设置中启用 Discussions 功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进入仓库 Settings&lt;/li&gt;
&lt;li&gt;勾选 Features 下的 &quot;Discussions&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装 Giscus App&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;访问 &lt;a href=&quot;https://giscus.app/&quot;&gt;Giscus 官网&lt;/a&gt;，按照提示安装 Giscus App 到你的 GitHub 仓库。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;获取配置参数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Giscus 官网填写仓库信息后，会生成配置参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data-repo&lt;/code&gt;: 你的仓库名称&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-repo-id&lt;/code&gt;: 仓库 ID&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-category&lt;/code&gt;: 讨论分类&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-category-id&lt;/code&gt;: 分类 ID&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;配置站点&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 &lt;code&gt;site.config.ts&lt;/code&gt; 中添加 Giscus 配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default {
  // ... 其他配置
  giscus: {
    repo: &apos;username/repo&apos;,
    repoId: &apos;R_xxxxx&apos;,
    category: &apos;Announcements&apos;,
    categoryId: &apos;DIC_xxxxx&apos;,
    reactionsEnabled: true,
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;从 Utterances 迁移评论&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你之前使用的是 Utterances，可以通过 GitHub 的 Issue to Discussion 转换功能来迁移历史评论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进入对应的 Issue&lt;/li&gt;
&lt;li&gt;点击右侧的 &quot;Convert to discussion&quot;&lt;/li&gt;
&lt;li&gt;选择对应的 Category&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Giscus 评论框位置问题解决&lt;/h4&gt;
&lt;p&gt;我发现 Giscus 评论框加载位置在评论区的上方，感觉怪怪的，然后我找到相关的组件代码&lt;code&gt;GiscusLoader.astro&lt;/code&gt;把位置改成了&lt;code&gt;bottom&lt;/code&gt;，但是还是一样的，看了下官方文档使用姿势是没有问题的，后来仔细一看这个模板的作者也发现了这个问题，直接注释标出来了，如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;migrate-my-blog-to-astro/2025-11-14-11-07-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后经过我的反复测试发现是由于Giscus自定义主题的问题，如果使用外部的css文件就会出现这个问题，但是呢不用自定义主题又和博客的主题风格不搭配，不过还好Giscus官方提供了内置的&lt;code&gt;github dart&lt;/code&gt;和&lt;code&gt;github light&lt;/code&gt;主题和我现在博客配置的一致，所幸就直接用了它的内置主题，于是调整了一下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const theme = document.documentElement.getAttribute(&apos;data-theme&apos;).includes(&apos;dark&apos;)
  ? &apos;dark&apos;
  : &apos;light&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的话就是以后如果博客主题想换了，又得重新调整Giscus的主题了，不过目前在Giscus自定义主题功能修复之前，只能这样了。&lt;/p&gt;
&lt;h3&gt;效果展示&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;migrate-my-blog-to-astro/2025-11-13-10-42-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;migrate-my-blog-to-astro/2025-11-13-10-42-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;migrate-my-blog-to-astro/2025-11-13-10-42-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Google Antigravity 登录提示地区不可用的解决办法</title><link>https://monkeywie.cn/posts/google-antigravity-account-not-eligible</link><guid isPermaLink="true">https://monkeywie.cn/posts/google-antigravity-account-not-eligible</guid><pubDate>Thu, 20 Nov 2025 07:48:55 GMT</pubDate><content:encoded>&lt;p&gt;就在昨天，Google 发布了他们家的AI IDE &lt;code&gt;Antigravity&lt;/code&gt;，目前可以免费使用，支持&lt;code&gt;Gemini3&lt;/code&gt;和&lt;code&gt;Claude4.5&lt;/code&gt;，但是在测试使用的过程中一直因为地区问题无法登录，折腾半天总算解决了，来分享下我的经验&lt;/p&gt;
&lt;h1&gt;📍第一步：先确认你的Google账户地区&lt;/h1&gt;
&lt;p&gt;去&lt;a href=&quot;https://policies.google.com/terms&quot;&gt;账户设置&lt;/a&gt;里看看，地区得是美国或者新加坡才行。我一开始是香港，结果一直用不了&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;google-antigravity-account-not-eligible/2025-11-20-09-37-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;🔄第二步：改地区！&lt;/h1&gt;
&lt;p&gt;如果地区不对，别慌～进入&lt;a href=&quot;https://policies.google.com/country-association-form&quot;&gt;地区修改页面&lt;/a&gt;，申请变更地区，原因参考我这个模版&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;google-antigravity-account-not-eligible/2025-11-20-09-37-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;🌐第三步：网络IP要和账户地区一致&lt;/h1&gt;
&lt;p&gt;保持网络IP和账户地区匹配，不然还是会失败哦，比如我的IP是新加坡然后申请的变更为新加坡&lt;/p&gt;
&lt;h1&gt;🚀第四步：一定一定要开TUN模式&lt;/h1&gt;
&lt;p&gt;这个真的是关键中的关键，不开的话前面都白搭&lt;/p&gt;
&lt;p&gt;搞定之后就能愉快使用啦～&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;google-antigravity-account-not-eligible/2025-11-20-09-37-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>终于等到你！Gopeed 官方公众号上线 &amp; v1.8.3 更新速览 🚀</title><link>https://monkeywie.cn/posts/gopeed-v183-release</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-v183-release</guid><pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;👋 哈喽，家人们！&lt;/h2&gt;
&lt;p&gt;久等了！真的久等了！&lt;/p&gt;
&lt;p&gt;其实从 Gopeed 发布到现在，一直有都有用户在问：“有没有国内的交流群？”、“GitHub 访问太慢了怎么办？”、“有没有官方公众号？”... 但是我这个人有亿点点佛系，怕被催更，所以就一直拖到现在才行动。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;听劝！我们来了！&lt;/strong&gt; 🙋‍♂️&lt;/p&gt;
&lt;p&gt;为了让大家能更方便地找到组织，我们正式开通了 &lt;strong&gt;「Gopeed 官方公众号」&lt;/strong&gt;，当然这是通过我个人账号申请的公众号，后续也会分享一些其它干货内容。&lt;/p&gt;
&lt;p&gt;以后这里就是咱们的 &lt;strong&gt;“国内大本营”&lt;/strong&gt; 了！
所有版本更新的解读、独家使用技巧、硬核技术分享，都会第一时间在这里 &lt;strong&gt;首发&lt;/strong&gt;。
别犹豫，&lt;strong&gt;关注不迷路&lt;/strong&gt;，上车就对了！🚗💨&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;v1.8.3 版本更新&lt;/h2&gt;
&lt;p&gt;说回正题，这次带来的 v1.8.3 版本，更新了许多实用功能和修复了不少 Bug，下面来快速浏览一下更新内容。&lt;/p&gt;
&lt;h3&gt;新特性&lt;/h3&gt;
&lt;h4&gt;支持自定义 GitHub 镜像配置&lt;/h4&gt;
&lt;p&gt;众所周知, Gopeed 是一个极度依赖 GitHub 的软件，里面的&lt;code&gt;BT tracker&lt;/code&gt;、&lt;code&gt;扩展&lt;/code&gt;、&lt;code&gt;更新&lt;/code&gt;等功能都需要访问 GitHub，之前&lt;code&gt;Gopeed&lt;/code&gt;中其实已经内置了一些常用的 GitHub 镜像源，但是这些镜像源并不一定都稳定可用，而且最可恶的是有些镜像源访问会被防火墙拦截警告，导致用户以为中病毒了，另外内置的话就只能等下个版本更新才能更换，所以这次决定直接开放自定义 GitHub 镜像源配置，让用户支持自定义配置，不过目前仍然内置了两个镜像源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jsdelivr&lt;/li&gt;
&lt;li&gt;fastgit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个镜像源相对来说比较稳定，大家可以根据自己的网络环境选择使用，可以在 &lt;code&gt;设置 -&amp;gt; 高级 -&amp;gt; GitHub 镜像源&lt;/code&gt; 中进行配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-04-58.png&quot; alt=&quot;GitHub 镜像源&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-06-22.png&quot; alt=&quot;新增 GitHub 镜像源&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;jsdelivr&lt;/code&gt;这种镜像只支持源码文件，只能用来更新&lt;code&gt;BT tracker&lt;/code&gt;，现在市面上比较流行的基本都是&lt;code&gt;ghProxy&lt;/code&gt;系列的镜像，这种镜像支持所有 GitHub 资源，包括&lt;code&gt;Release&lt;/code&gt;资源，可以用来更新，至于&lt;code&gt;Github&lt;/code&gt;镜像站点怎么找，以后单独写一篇文章来讲。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;支持 rpm 包安装&lt;/h4&gt;
&lt;p&gt;此功能由社区小伙伴 &lt;code&gt;@BerryMC&lt;/code&gt; 贡献，非常感谢！&lt;/p&gt;
&lt;p&gt;现在 Gopeed 支持通过 rpm 包进行安装了，适用于基于 rpm 的 Linux 发行版，例如：&lt;code&gt;Fedora&lt;/code&gt;、&lt;code&gt;CentOS&lt;/code&gt;、&lt;code&gt;openSUSE&lt;/code&gt; 等，有需要的用户可以前往&lt;code&gt;Github Release&lt;/code&gt;页面下载使用。&lt;/p&gt;
&lt;h4&gt;添加下载目录分类和占位符支持&lt;/h4&gt;
&lt;p&gt;这个下载目录相关的功能社区呼声一直很高，我本来想着等到 v2.0 新UI大版本再加上的，但是新版本一直鸽着没出，实在不好意思了，就先加上这个功能。&lt;/p&gt;
&lt;p&gt;目前内置了几个常用的分类，然后支持自定义设置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-17-21.png&quot; alt=&quot;目录分类&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后在创建任务的时候可以快速选择分类：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-18-34.png&quot; alt=&quot;目录分类选择&quot; /&gt;&lt;/p&gt;
&lt;p&gt;目前还没有实现根据下载的文件类型自动选择分类目录，后面会考虑加上这个功能。&lt;/p&gt;
&lt;p&gt;然后就是&lt;code&gt;占位符&lt;/code&gt;了，也就是用户提到的&lt;code&gt;FDM&lt;/code&gt;里那个叫宏的东西，目前仅支持一些日期相关的占位符，后续有需求的话可以继续扩展：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-21-39.png&quot; alt=&quot;配置下载目录占位符&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样下载目录就会动态的根据占位符来生成了，比如：&lt;code&gt;/Downloads/%date%&lt;/code&gt; 会被解析成 &lt;code&gt;/Downloads/2025-11-30&lt;/code&gt;，另外分类目录也是支持占位符的，这样就可以实现更灵活的目录管理了。&lt;/p&gt;
&lt;h4&gt;支持 Webhook 推送&lt;/h4&gt;
&lt;p&gt;作为市面上&lt;code&gt;扩展性&lt;/code&gt;和&lt;code&gt;可玩性&lt;/code&gt;最强的下载器，怎么能少的了&lt;code&gt;Webhook&lt;/code&gt;功能呢，有了这个功能就可以实现更多的自动化操作了，比如：下载完成后通知&lt;code&gt;Telegram&lt;/code&gt;、&lt;code&gt;Discord&lt;/code&gt;、&lt;code&gt;企业微信&lt;/code&gt;等，配合&lt;code&gt;RESTFul API&lt;/code&gt;，应该可以实现更多有趣的玩法，就等社区的大佬们来挖掘了。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;设置 -&amp;gt; 高级 -&amp;gt; Webhook&lt;/code&gt; 中可以进行配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-27-19.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.8.3-release/2025-11-30-21-27-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在添加 Webhook 的时候可以选择测试来验证配置是否正确，验证的时候会发送一个测试的 JSON 数据过去，然后在收到&lt;code&gt;HTTP 200&lt;/code&gt;响应后表示测试成功，方便用户调试。&lt;/p&gt;
&lt;h4&gt;支持 Waterfox 浏览器&lt;/h4&gt;
&lt;p&gt;只能说又在用户这里学到了，这次是&lt;code&gt;Waterfox&lt;/code&gt;，一个基于&lt;code&gt;Firefox&lt;/code&gt;的浏览器，主打隐私保护和性能优化，这次的更新中添加了对&lt;code&gt;Waterfox&lt;/code&gt;浏览器的支持，以便在安装浏览器扩展之后开箱即用，拦截下载。&lt;/p&gt;
&lt;h3&gt;Bug 修复&lt;/h3&gt;
&lt;h4&gt;HTTP 文件名解析不正确问题&lt;/h4&gt;
&lt;p&gt;这个问题真的是修了又修，只能说&lt;code&gt;HTTP&lt;/code&gt;协议真是屎山堆积，各种标准不统一，这次又新增了两个标准的解析，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Content-Disposition&lt;/code&gt; 头中出现多个&lt;code&gt;filename&lt;/code&gt;字段的处理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Content-Disposition&lt;/code&gt; 头中&lt;code&gt;filename&lt;/code&gt;字段由GBK编码传输的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外我宣布，修完这一次之后，肯定不会再出现中文文件名解析错误的问题了，如果还有公众号抽个键盘助助兴！&lt;/p&gt;
&lt;h4&gt;代理设置无法保存用户名和密码问题&lt;/h4&gt;
&lt;p&gt;这是一个低级 Bug，竟然连着好几个版本都没发现，这次光速修复了，感谢社区小伙伴的反馈！&lt;/p&gt;
&lt;h4&gt;下载同名且没有后缀的情况下不自动重命名问题&lt;/h4&gt;
&lt;p&gt;该 Bug 由社区小伙伴 &lt;code&gt;@Little-King2022&lt;/code&gt; 反馈和自行 PR 修复，非常优秀！&lt;/p&gt;
&lt;p&gt;当然由于&lt;code&gt;Gopeed&lt;/code&gt;设计问题，目前会存在待下载的重名文件任务在并发下载时，仍然可能会出现重名覆盖的问题，这个问题会在下个版本中彻底修复。&lt;/p&gt;
&lt;h2&gt;🤝 最后的碎碎念&lt;/h2&gt;
&lt;p&gt;后续我们会在这里分享更多好玩、硬核的内容，感谢大家一路以来的支持，Gopeed 因为有你们才更好！&lt;/p&gt;
&lt;p&gt;我们下个版本见！👋&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>FLUX2生图太费显卡？我开源了免费方案，帮你省下一张4090！</title><link>https://monkeywie.cn/posts/my-new-open-source-project-free-flux2</link><guid isPermaLink="true">https://monkeywie.cn/posts/my-new-open-source-project-free-flux2</guid><pubDate>Fri, 05 Dec 2025 19:12:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;你是不是还在为没有显卡或者没有稳定的 AI 生图渠道而烦恼？&lt;/p&gt;
&lt;p&gt;最近 Cloudflare Workers AI 推出了 &lt;strong&gt;FLUX.2-dev&lt;/strong&gt; 模型，这是一个&lt;code&gt;32B&lt;/code&gt;参数的生图模型，同时支持&lt;code&gt;文生图&lt;/code&gt;和&lt;code&gt;图像编辑&lt;/code&gt;功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-15-53-53.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在这之前赛博菩萨也有提供其它模型，例如&lt;code&gt;lucid&lt;/code&gt;、&lt;code&gt;flux1&lt;/code&gt;等，但这些模型要么参数量小，要么只能文生图，效果在这个时间点已经不够看了，而最新上架的 FLUX.2-dev 则是目前 Workers AI 里最强的模型，生成效果媲美商业化模型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;项目简介&lt;/h2&gt;
&lt;p&gt;那么如何优雅地&lt;s&gt;使用&lt;/s&gt;白嫖这个 FLUX.2-dev 模型呢？这就请出主角，我的开源新作 &lt;strong&gt;Typix&lt;/strong&gt; ！&lt;/p&gt;
&lt;p&gt;这是一款专注于 AI 媒体内容生成的开源工具，让你能够轻松调用 Cloudflare Workers AI 和其它主流AI提供商的生图模型，无需复杂配置，支持&lt;code&gt;一键部署&lt;/code&gt;，开箱即用！更重要的是，它完全免费，让你零成本享受 AI 生图的乐趣。&lt;/p&gt;
&lt;h3&gt;核心特性&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地优先&lt;/strong&gt; - 数据存储在浏览器本地，无需担心隐私泄露&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自托管部署&lt;/strong&gt; - 完全掌控你的数据和隐私&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;免费生图&lt;/strong&gt; - 免费使用 Cloudflare Workers AI 生图，无需付费 API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一键部署&lt;/strong&gt; - 支持 Cloudflare Workers 和 Docker 快速部署&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模型支持&lt;/strong&gt; - 集成 Google、OpenAI、Flux、Fal、Cloudflare 等多种 AI 服务&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云同步&lt;/strong&gt; - 可选的云同步功能，在多设备间无缝切换&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;数据安全保障&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浏览器本地存储&lt;/strong&gt; - 基于 WASM SQLite 技术，数据完全存储在你的浏览器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;零数据上传&lt;/strong&gt; - 创作内容、设置信息等敏感数据从不离开你的设备&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无服务器依赖&lt;/strong&gt; - 客户端模式无需外部服务器，保障数据主权&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;直接体验&lt;/strong&gt;：https://typix.art&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;开源地址&lt;/strong&gt;：https://github.com/monkeyWie/typix&lt;/p&gt;
&lt;h2&gt;示例展示&lt;/h2&gt;
&lt;p&gt;以下是一些我测试的示例，大家可以直观感受一下 FLUX.2-dev 的强大能力。&lt;/p&gt;
&lt;h3&gt;文生图示例&lt;/h3&gt;
&lt;h4&gt;示例 1：赛博朋克风格城市&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;一个充满霓虹灯的赛博朋克未来城市夜景，高楼林立，飞行汽车穿梭其间，地面湿润反射着五彩灯光，细节丰富，8K高清，电影级质感
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-11-53-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;示例 2：中国风水墨画&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;中国传统水墨画风格，一位古装仙女站在云端，长发飘逸，身穿白色长裙，周围环绕着仙鹤和祥云，意境唯美，留白艺术，大师级作品
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-11-55-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;示例 3：可爱的 3D 角色&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;3D渲染风格，一只超级可爱的橘猫，圆滚滚的身材，大大的眼睛，毛茸茸的质感，坐在彩色糖果堆上，明亮柔和的灯光，皮克斯动画风格
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-11-55-48.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;图像编辑示例&lt;/h3&gt;
&lt;p&gt;就拿上面那个橘猫来做个图像编辑的示例吧，我个人觉得一致性和提示词理解能力都非常不错。&lt;/p&gt;
&lt;h4&gt;示例 1：给橘猫加个帽子&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;在这只橘猫的头上加一顶红色的圣诞帽，帽子上有白色的毛球和毛边，整体风格可爱温馨
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-11-56-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;示例 2：把橘猫换成一只小狗&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;把这只橘猫换成一只可爱的金毛猎犬，保持同样的姿势和背景
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-11-58-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;示例 3: 多图融合&lt;/h4&gt;
&lt;p&gt;上传一个LOGO图片，然后让模型把它融合到上面的狗狗图片中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;把图片2作为LOGO融合到图片1中，放在右上角，保持整体风格一致
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-14-14-57.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;快速部署教程&lt;/h2&gt;
&lt;h3&gt;Cloudflare Workers 一键部署&lt;/h3&gt;
&lt;p&gt;这是最简单的部署方式，部署好之后直接享受 Cloudflare AI 的图像生成服务！&lt;/p&gt;
&lt;h4&gt;前置准备&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Github 账户（必须）&lt;/li&gt;
&lt;li&gt;Cloudflare 账户（可以用 Github 账号登录）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后点击下面的链接，一键部署 Typix 到你的 Cloudflare Workers：&lt;/p&gt;
&lt;p&gt;https://deploy.workers.cloudflare.com/?url=https://github.com/monkeyWie/typix&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：国内访问比较慢，建议自备🪜&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;步骤截图&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;点击链接后，先选择 Github 账户授权&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-16-57-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;授权完之后直接拉到最下面，点击部署&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-29-56.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;等待部署完成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-35-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;访问地址&lt;/h4&gt;
&lt;p&gt;部署成功后，你将获得一个 &lt;code&gt;typix.xxx.workers.dev&lt;/code&gt; 域名，直接访问即可使用 Typix：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-42-25.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：worker.dev 域名在国内无法直接访问，需要🪜，也可以自己买一个域名绑定到 Workers 上，这样就能直连了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Docker 部署&lt;/h3&gt;
&lt;p&gt;说到一键部署，怎么能少了 Docker 呢？下面是通过 Docker 部署 Typix 的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run --name typix -d -p 9999:9999 liwei2633/typix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Docker 部署方式不是运行在 Cloudflare Workers 环境里，所以无法直接使用 Workers AI 服务，需要自己配置 API Key，具体配置方法如下：&lt;/p&gt;
&lt;h4&gt;获取 Workers AI 配置&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;获取 Account ID，在 Workers &amp;amp; Pages 页面可以看到：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-54-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;访问 Cloudflare Dashboard，进入 API Tokens 页面创建：https://dash.cloudflare.com/profile/api-tokens&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-51-05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;选择 Workers AI 模板&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-51-47.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建 Token 并保存&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-52-27.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;配置 Typix&lt;/h4&gt;
&lt;p&gt;上面拿到的两个参数，&lt;code&gt;Account ID&lt;/code&gt; 和 &lt;code&gt;API Token&lt;/code&gt;，进入 Typix 提供商设置页面进行配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;my-new-open-source-project-free-flux2/2025-12-08-17-57-43.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就可以愉快地使用 Workers AI 进行图像生成了！&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;Typix 是我个人业余时间开发的开源项目，目的是为创作者提供一个免费、易用且安全的 AI 图像生成工具。如果你觉得这个项目对你有帮助，欢迎给个⭐️支持，也可以参与贡献代码或提出建议！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Claude Code 被吹上天，但我还是选 GitHub Copilot</title><link>https://monkeywie.cn/posts/cc-is-good-but-i-choose-copilot</link><guid isPermaLink="true">https://monkeywie.cn/posts/cc-is-good-but-i-choose-copilot</guid><pubDate>Tue, 09 Dec 2025 19:56:50 GMT</pubDate><content:encoded>&lt;p&gt;说起 VibeCoding，谈论最多的应该非 Claude Code 莫属。之前我也跟风用过一段时间，但现在还是换回了 Copilot。&lt;/p&gt;
&lt;p&gt;不可否认 Claude Code 的工程能力确实牛逼，但综合考虑下来，Copilot 对我来说还是更合适一些。主要原因有以下几点：&lt;/p&gt;
&lt;h2&gt;1. Claude Code 用不起&lt;/h2&gt;
&lt;p&gt;按官方 max 套餐算，Claude Code 一个月要 100 刀，而且还要搭配干净的住宅 IP 使用，否则很容易被 ban 账号，用中转站的话又极不稳定，而且安全性也是个问题——理论上你的所有 prompt 和代码都会被中转站看到。&lt;/p&gt;
&lt;p&gt;我也尝试接过第三方 Claude Sonnet 4.5 API，随便一个需求几刀就没了。后来换成 GLM 4.6，便宜是便宜多了，但效果没法比，尤其是面对复杂任务时，差距明显。&lt;/p&gt;
&lt;h2&gt;2. 纯终端形态不如 IDE 集成方便&lt;/h2&gt;
&lt;p&gt;现阶段 VibeCoding 如果不自己去 review，你会发现到后面完全是一堆屎山。当然，如果不考虑维护性是无所谓的，毕竟只要功能实现了就没问题。&lt;/p&gt;
&lt;p&gt;而纯终端的 Claude Code 在这方面体验不太好。在 IDE 里，Copilot 的改动建议可以直接高亮显示，做 Review 和 Accept 都非常直观。更重要的是，IDE 能把 LSP（语言服务协议）的上下文喂给模型——比如编译错误、lint 警告这些信息会直接传给大模型，生成的代码报错的几率就更少了。&lt;/p&gt;
&lt;h2&gt;3. Copilot 性价比极高&lt;/h2&gt;
&lt;p&gt;Copilot 一个月只要 10 刀，Claude、GPT、Gemini 这些模型都能用，而且还有免费模型可以用。&lt;/p&gt;
&lt;p&gt;对我来说，10 刀套餐基本够用了。一般简单的功能我就直接用免费模型搞定，复杂的再切换付费模型。每次有新模型出来，Copilot 也基本上很快就能跟上。另外开源项目维护者如果符合条件还能免费申请，我就是通过这种方式白嫖的。&lt;/p&gt;
&lt;h2&gt;4. GitHub 深度集成&lt;/h2&gt;
&lt;p&gt;Copilot 跟 GitHub 集成得非常好，在 Issue 里直接 @copilot 就能自动开 PR 并且在云端执行 agent 任务处理，非常方便。&lt;/p&gt;
&lt;p&gt;最良心的是，这种自动化操作只消耗一次 Premium 额度。我现在开源项目里很多小改动，比如修个 Bug、更新依赖、补充文档，都直接让它搞定。既省钱又省力。&lt;/p&gt;
&lt;h2&gt;5. 只有 VS Code 能完整享受扩展商店&lt;/h2&gt;
&lt;p&gt;这一点很多人可能忽略了。Copilot 是微软自家的产品，和 VS Code 的集成度是最高的。&lt;/p&gt;
&lt;p&gt;现在市面上有很多基于 VSCodium 二次开发的 AI IDE，它们都有一个硬伤：&lt;strong&gt;无法完整使用 VS Code 官方扩展商店&lt;/strong&gt;。比如 Remote SSH、C++ 扩展这些微软官方的插件都没有开放出去。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;不是说 Claude Code 不好，它肯定还是最能打的那个。只不过在同样能完成任务的前提下，性价比有点低了而已，况且 Copilot 也在不断进步，未来说不定就更好了，相信巨硬！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>不敢信！Augment Code 直接送我一年订阅服务</title><link>https://monkeywie.cn/posts/i-got-augment-code-oss-free-plan</link><guid isPermaLink="true">https://monkeywie.cn/posts/i-got-augment-code-oss-free-plan</guid><pubDate>Fri, 12 Dec 2025 19:36:16 GMT</pubDate><content:encoded>&lt;p&gt;事情是这样的：最近我收到一封来自 Augment Code 官方的邮件，说他们在做一个面向开源项目的推广活动——给参与者免费赞助一年订阅服务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;i-got-augment-code-oss-free-plan/2025-12-15-11-23-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我抱着“试试看，反正不亏”的心态回了邮件表达兴趣，结果真的收到了确认邮件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;i-got-augment-code-oss-free-plan/2025-12-15-17-44-45.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;登录到 Augment Code 一看，好家伙，账号里直接多了一大把 &lt;code&gt;Credits&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;i-got-augment-code-oss-free-plan/2025-12-15-17-45-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Augment Code 简介&lt;/h2&gt;
&lt;p&gt;Augment Code 和 GitHub Copilot 差不多，都是做 AI 编程助手：它不做自己的 IDE，而是以插件的方式接入现有编辑器，主打在大仓库里用更强的上下文理解来辅助开发（Chat、补全、Agent、CLI 等）。背景方面，他们在 2024 年 4 月对外宣布完成 2.27 亿美元的 B 轮融资（投后估值 9.77 亿美元）。至于这次面向开源项目的合作，我更倾向理解为：他们想把产品放到真实仓库里跑一跑。&lt;/p&gt;
&lt;p&gt;Augment Code支持的模型也挺多的，包括 OpenAI 的 GPT-5.1、Anthropic 的 Claude Opus 4.5 等顶级模型，&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;i-got-augment-code-oss-free-plan/2025-12-15-18-35-14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;免费赞助也不是白拿&lt;/h2&gt;
&lt;p&gt;当然，赞助不是“无条件白给”。我从邮件沟通里理解到，这个开源赞助计划的重点，是希望参与者把它用在真实的协作流程里，尤其是它的 GitHub PR Review（Augment Code Review）能力。&lt;/p&gt;
&lt;p&gt;他们对 Code Review 的定位也很明确：不是在 PR 里刷一堆“改个命名/加个注释”的噪音，而是更关注 correctness、架构/跨文件影响、潜在风险、缺测试等更可能影响合并决策的问题，并且可以直接在 GitHub 的 PR 里以内联评论的形式给到反馈。&lt;/p&gt;
&lt;p&gt;换句话说：你拿到免费额度，最好别只拿来“写写 demo”，而是要把它用在 review 上，真的让它参与到你项目的质量把关里。&lt;/p&gt;
&lt;h2&gt;AI 左右互搏&lt;/h2&gt;
&lt;p&gt;我平常写代码基本上离不开 &lt;code&gt;GitHub Copilot&lt;/code&gt;，既然现在 Augment 给了我一年的订阅，那正好来一波“AI 左右互搏”：让 Copilot 负责写，让 Augment 负责审。&lt;/p&gt;
&lt;p&gt;我拿自己的开源项目 &lt;code&gt;Typix&lt;/code&gt; 先热热身，刚好手头有个小 bug 要修。按 Augment 的文档把 PR Review 相关的配置走一遍之后，我把 PR 创建好，Augment 就自动开始 Review：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;i-got-augment-code-oss-free-plan/2025-12-15-18-22-13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过这次可能是因为 bug 本身比较简单，它并没有 review 出什么问题。&lt;/p&gt;
&lt;p&gt;另外我还发现一个点：它跟 GitHub 的工作流结合得还不够紧。在 Review 进行的这段时间里，PR 其实仍然可以直接合并；我这边也没找到能把它变成强制的 &lt;code&gt;check&lt;/code&gt;（required status check）来“卡住合并、必须先 review”的方式。这个能力如果后面能补上，日常协作会更顺。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;后面我也会试一试 Augment Agent 模式编码的能力，另外再感慨一下开源这么多年，攒了不少 star，说实话也没因此赚到什么钱，但偶尔在这种时刻会突然觉得：还是挺有用的。&lt;/p&gt;
&lt;p&gt;一方面是认识了一些认真做事的人；另一方面是项目真的被人用着，甚至还能换来一些国外大公司“实打实”的支持。对我来说，它更像是一种正反馈：你持续把东西做出来、维护下去，总会在某个节点收到回响。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>大刀阔斧，彻底重构 Gopeed HTTP 下载实现</title><link>https://monkeywie.cn/posts/gopeed-http-download-rewrite</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-http-download-rewrite</guid><pubDate>Tue, 13 Jan 2026 15:08:27 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 GitHub 上经常收到用户反馈类似问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下载老是失败，要手动重试好几次&lt;/li&gt;
&lt;li&gt;下载卡在 99% 不动了&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实也算是项目初期埋下的技术债了，这次痛定思痛，决定对下载引擎进行一次&lt;strong&gt;大刀阔斧的重构&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;此次重构（&lt;a href=&quot;https://github.com/GopeedLab/gopeed/pull/1229&quot;&gt;PR #1229&lt;/a&gt;）改动了 30 个文件、3000 多行代码，&lt;strong&gt;希望从根本上解决这些痛点&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;Gopeed 在设计之初就规划了多协议支持能力，为此我将下载流程抽象为两个独立阶段：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解析阶段&lt;/strong&gt;：获取文件元信息（大小、名称、是否支持断点续传等）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下载阶段&lt;/strong&gt;：基于文件信息执行实际的数据传输&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种设计在理论上看似合理，但在实际应用中逐渐暴露出一些问题：&lt;/p&gt;
&lt;h3&gt;问题一：解析阶段造成资源浪费&lt;/h3&gt;
&lt;p&gt;经常使用浏览器下载的用户应该会发现：当你还在选择保存路径时，下载实际上已经在后台进行了。而 Gopeed 的实现却是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建立连接 → 获取文件信息 → &lt;strong&gt;立即关闭连接&lt;/strong&gt; → 用户确认 → &lt;strong&gt;重新建立连接&lt;/strong&gt; → 开始下载&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着解析阶段建立的连接完全被浪费，没有用于实际数据传输。对于小文件，这种设计尤其低效——等用户确认时，文件本可以早已下载完成。&lt;/p&gt;
&lt;h3&gt;问题二：固定分片导致“卡在 99%”&lt;/h3&gt;
&lt;p&gt;当前实现采用固定分片算法：将文件平均分成 N 份，每个连接负责一份。这种设计存在致命缺陷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果某个分片对应的服务器节点响应慢，其他连接即使完成也只能空闲等待&lt;/li&gt;
&lt;li&gt;形成“一方有难，八方围观”的局面&lt;/li&gt;
&lt;li&gt;用户体验表现为：前 99% 飞快，最后 1% 可能卡很久&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问题三：缺乏智能重试机制&lt;/h3&gt;
&lt;p&gt;现有实现对网络错误的处理过于简单粗暴：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络抖动、服务器临时繁忙等&lt;strong&gt;可恢复错误&lt;/strong&gt;直接导致下载失败&lt;/li&gt;
&lt;li&gt;用户只能手动点击“继续下载”，体验糟糕&lt;/li&gt;
&lt;li&gt;没有区分可重试和不可重试的错误类型&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;问题四：第三方库深度魔改带来的维护困境&lt;/h3&gt;
&lt;p&gt;为了适配“解析-下载”两阶段模型，对 &lt;code&gt;anacrolix/torrent&lt;/code&gt; 库进行了大量侵入式修改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法及时跟进上游更新，错失性能优化和 Bug 修复&lt;/li&gt;
&lt;li&gt;维护成本随时间推移呈指数级增长&lt;/li&gt;
&lt;li&gt;技术债务积累严重&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;重构方案：三大核心改进&lt;/h2&gt;
&lt;p&gt;针对上述问题，此次重构引入了三个关键技术改进：&lt;/p&gt;
&lt;h3&gt;改进一：连接复用机制&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心思路&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;既然已经建立了连接，就不要浪费了，让它直接用于下载。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;旧版本流程&lt;/strong&gt;：点击下载 → 建立连接 → 获取文件信息 → &lt;strong&gt;关闭连接&lt;/strong&gt; → 用户确认 → &lt;strong&gt;重新建立连接&lt;/strong&gt; → 开始下载&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新版本流程&lt;/strong&gt;：点击下载 → 建立连接 → 获取文件信息 → &lt;strong&gt;保持连接&lt;/strong&gt; → 用户确认 → &lt;strong&gt;直接开始下载&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;技术细节&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;第一个连接在获取文件元信息后不再关闭，而是立即开始预下载（写入临时文件）。这样做带来两个好处：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;零延迟启动&lt;/strong&gt;：用户点击确认后，进度条立即开始移动&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源高效利用&lt;/strong&gt;：用户确认前就已完成大部分下载&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h3&gt;改进二：更智能调度算法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心思路&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;下载连接之间应该互相协作，而不是各自为战。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;问题根源分析&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;旧版本采用&lt;strong&gt;固定分片算法&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;文件大小：1GB，连接数：16
每个连接负责：1GB ÷ 16 = 64MB

连接1: [0MB - 64MB]
连接2: [64MB - 128MB]
...
连接16: [960MB - 1024MB]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有一个连接下载速度很慢，其他 15 个连接即使完成也无法帮忙。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;新版本解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;引入&lt;strong&gt;动态分片 + 工作窃取&lt;/strong&gt;机制：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;动态分片&lt;/strong&gt;：不再预先固定每个连接的下载范围&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作窃取&lt;/strong&gt;：空闲连接主动“窃取”未完成连接的剩余任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;算法示意&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始状态：
连接1-15: 已完成
连接16: 还剩 10MB [990MB - 1000MB]

触发工作窃取：
连接1: 窃取 [990MB - 991MB]
连接2: 窃取 [991MB - 992MB]
...
连接15: 窃取 [1004MB - 1005MB]
连接16: 继续下载 [1005MB - 1010MB]

结果：16 个连接同时工作，快速完成最后 10MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这个优化极大的提升了最后阶段的下载速度，避免“卡在 99%”太久的问题。&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;改进三：智能重试 + 渐进式连接扩展&lt;/h3&gt;
&lt;h4&gt;3.1 智能重试机制&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;错误分类处理&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;根据错误类型采取不同的策略：对 &lt;code&gt;429 Too Many Requests&lt;/code&gt;、&lt;code&gt;503 Service Unavailable&lt;/code&gt; 这类临时性错误，采用指数退避并自动重试；而对 &lt;code&gt;404 Not Found&lt;/code&gt;、&lt;code&gt;403 Forbidden&lt;/code&gt; 等确定性错误，则直接失败并给出清晰提示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;指数退避算法&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在每次重试之间引入指数退避等待时间，避免短时间内频繁请求服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第1次重试：等待 1 秒
第2次重试：等待 2 秒
第3次重试：等待 4 秒
第4次重试：等待 8 秒
第5次重试：等待 16 秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;快速失败重试&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;“指数退避”只能管住重试的节奏，但如果有一条连接比较慢，比如卡在&lt;code&gt;TCP连接&lt;/code&gt;或者&lt;code&gt;TLS握手&lt;/code&gt;阶段，也会把整体下载速度拖下来。&lt;/p&gt;
&lt;p&gt;所以新版本做了一个更“聪明”的处理：&lt;strong&gt;只要下载已经跑起来&lt;/strong&gt;（已经有连接成功开始传数据），后续连接就会进入快速失败重试阶段——不再死等固定的 15 秒超时；而是根据实际网络状况，动态调整超时时间，以便更快地放弃无效连接，重新发起新下载请求。&lt;/p&gt;
&lt;h4&gt;3.2 渐进式连接扩展&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;问题&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;一次性同时开启大量连接容易触发服务器限流，而且对于小文件来说，很多连接可能根本用不上，浪费资源。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;借鉴 TCP 慢启动思想，逐步增加连接数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 个连接
2 个连接  (x2)
4 个连接  (x2)
8 个连接  (x2)
16 个连接 (达到上限)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;好处&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对服务器更友好，降低被限流/封禁风险&lt;/li&gt;
&lt;li&gt;下载快的时候只需更少的连接即可完成&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;技术债务的清理&lt;/h2&gt;
&lt;p&gt;除了上述三大核心改进，此次重构还顺带解决了&lt;strong&gt;问题四&lt;/strong&gt;：&lt;/p&gt;
&lt;h3&gt;Torrent 库：不再需要魔改&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;旧版本的困境&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;为了适配“解析-下载”两阶段模型，对 &lt;code&gt;anacrolix/torrent&lt;/code&gt; 进行了大量魔改：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改了 200+ 行核心代码&lt;/li&gt;
&lt;li&gt;每次同步上游库，都可能要重新适配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重构之后&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在解析阶段就直接指定下载目录，这样就可以避免魔改，直接使用其原生 torrent 的“指定下载目录”能力。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这次重构过程中消耗了大量 &lt;code&gt;Claude Opus 4.5 tokens&lt;/code&gt;来辅助设计和编码，最终改动了 &lt;strong&gt;30 个文件、3000+ 行代码&lt;/strong&gt;，也得益于 AI 编程的帮助，才得以顺利完成。&lt;/p&gt;
&lt;h3&gt;单元测试体系的保障&lt;/h3&gt;
&lt;p&gt;能够如此大胆地重构，很大程度上也是因为 Gopeed 项目从一开始就建立的&lt;strong&gt;完善的单元测试&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;项目中积累了大量关于下载功能的单元测试，覆盖了协议边界、网络异常、断点续传、并发场景等各种边界场景。每次修改代码后，只需跑一遍相关测试，就能快速验证改动是否引入了新的 Bug。&lt;/p&gt;
&lt;h3&gt;何时可以体验？&lt;/h3&gt;
&lt;p&gt;这些改进将在&lt;strong&gt;下个版本&lt;/strong&gt;中正式上线。由于此次改动存在部分 API 的破坏性变更，会安排在 &lt;code&gt;v1.9.0&lt;/code&gt; 版本发布，敬请期待！&lt;/p&gt;
&lt;h3&gt;关于 Gopeed&lt;/h3&gt;
&lt;p&gt;Gopeed（Go Speed）是一个用 Go 语言开发的高性能下载器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;跨平台&lt;/strong&gt;：一套代码，支持 6 大平台&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多协议&lt;/strong&gt;：HTTP(S)、BitTorrent、Magnet&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高性能&lt;/strong&gt;：Go 语言原生并发，充分利用多核&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;易扩展&lt;/strong&gt;：插件系统，支持自定义协议和功能&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;感谢一直以来的支持！有任何问题或建议，欢迎留下评论或在 GitHub 提 Issue。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 扩展系统 JS 引擎修复小记</title><link>https://monkeywie.cn/posts/gopeed-js-engine-fix</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-js-engine-fix</guid><pubDate>Sun, 18 Jan 2026 19:37:26 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;如果看过我之前写的&lt;a href=&quot;/posts/how-to-develop-a-cross-platform-extension-system&quot;&gt;给我的开源下载器打造一套扩展系统&lt;/a&gt;，应该对 Gopeed 的扩展体系有点印象：为了让扩展在 &lt;code&gt;Windows/macOS/Linux/Android/iOS/Web&lt;/code&gt; 上都能跑，我选择了纯 Go 实现的 JavaScript 引擎 &lt;a href=&quot;https://github.com/dop251/goja&quot;&gt;goja&lt;/a&gt; 作为运行时，并在上层 &lt;code&gt;inject&lt;/code&gt;/&lt;code&gt;polyfill&lt;/code&gt; 了一部分常用浏览器 API，尽量让扩展开发体验贴近“写前端”。&lt;/p&gt;
&lt;p&gt;最近 &lt;a href=&quot;https://github.com/monkeyWie/gopeed-extension-youtube&quot;&gt;油管扩展&lt;/a&gt; 有用户反馈：解析失败，没法正常下载。第一反应当然是“油管又更新反爬了”，于是把上游库 &lt;a href=&quot;https://github.com/LuanRT/YouTube.js&quot;&gt;YouTube.js&lt;/a&gt; 升级到最新版——结果：依然不行。&lt;/p&gt;
&lt;p&gt;再往下追，才发现这次锅不在油管，也不在 YouTube.js，而是在 goja：某些 JavaScript 行为在规范里是允许的，但 goja 的实现踩了坑，直接把扩展脚本干崩了。&lt;/p&gt;
&lt;p&gt;&amp;lt;!--more--&amp;gt;&lt;/p&gt;
&lt;h2&gt;问题排查&lt;/h2&gt;
&lt;p&gt;新版本的 &lt;code&gt;YouTube.js&lt;/code&gt; 在需要执行 &lt;code&gt;signature&lt;/code&gt; 解密逻辑时，会要求调用方提供一个“JS 解释器”去执行一段动态脚本，核心逻辑大概是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Platform.shim.eval = async (data, env) =&amp;gt; {
  const properties = []

  if (env.n) {
    properties.push(`n: exportedVars.nFunction(&quot;${env.n}&quot;)`)
  }

  if (env.sig) {
    properties.push(`sig: exportedVars.sigFunction(&quot;${env.sig}&quot;)`)
  }

  const code = `${data.output}\nreturn { ${properties.join(&apos;, &apos;)} }`

  return new Function(code)()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;都是 JS 运行时了还要多此一举提供一个 &lt;code&gt;js解释器&lt;/code&gt; 来执行脚本，这就非常的幽默。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这段代码本质就是&lt;code&gt;动态运行js脚本&lt;/code&gt;。但在 goja 里跑的时候会直接 &lt;code&gt;panic&lt;/code&gt;，报一个看起来很奇怪的 &lt;code&gt;Stack overflow&lt;/code&gt; 异常。&lt;/p&gt;
&lt;p&gt;同样的代码我丢到浏览器里执行完全 OK，那基本就可以确认：问题出在 goja。&lt;/p&gt;
&lt;p&gt;去 GitHub issues 搜了一圈，很快找到了高度相关的讨论：&lt;a href=&quot;https://github.com/dop251/goja/issues/275&quot;&gt;Issue #275&lt;/a&gt;。核心原因是：当对象/数组存在 &lt;code&gt;circular reference（循环引用）&lt;/code&gt;，某些隐式类型转换（例如触发 &lt;code&gt;toPrimitive -&amp;gt; toString&lt;/code&gt;）会导致 goja 递归爆栈。&lt;/p&gt;
&lt;p&gt;更麻烦的是：作者在 issue 里提到这种行为在 &lt;code&gt;ECMAScript&lt;/code&gt; 规范里是允许的，因此当时并没有打算修。&lt;/p&gt;
&lt;p&gt;这个 issue 我在 2023 年就看过，还在下面问过“有没有修复计划”。没想到 2026 年了还是老样子——那就只能自己动手了。&lt;/p&gt;
&lt;p&gt;我对 &lt;code&gt;PL（Programming Languages）&lt;/code&gt; 这块属于门外汉，但现在有 AI 了我觉得应该可以试着解决一下。于是我让 AI 根据报错现象整理出了一个最小可复现用例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;github.com/dop251/goja&quot;
)

func main() {
	vm := goja.New()

	code := `
		var T = [1, 2, 3];
		T[42] = T;       // Create circular reference
		var x = T % 2;   // Modulo operation triggers toPrimitive -&amp;gt; toString
	`

	_, err := vm.RunString(code)
	if err != nil {
		fmt.Printf(&quot;ERROR: %v\n&quot;, err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子可以稳定复现同样的爆栈问题，接下来就好办了：让 AI 阅读 goja 源码，把递归链路找出来，然后补上对循环引用场景的处理。&lt;/p&gt;
&lt;p&gt;最终我给 goja 提了一个修复：&lt;a href=&quot;https://github.com/dop251/goja/pull/695&quot;&gt;PR #695&lt;/a&gt;，目前已经合并到主分支。&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;不得不感叹 AI 的进化速度：2023 年我还只能在 issue 底下“蹲作者”，现在已经可以借助 AI 把自己不擅长的领域搞定了，等 Gopeed 下个版本发布后，油管扩展也就能正常工作了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 1.9.0 发布，内核全面进化，解锁未来新特性</title><link>https://monkeywie.cn/posts/gopeed-v190-release</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-v190-release</guid><pubDate>Sat, 24 Jan 2026 09:50:53 GMT</pubDate><content:encoded>&lt;p&gt;Gopeed v1.9.0 终于发布了！这个版本包含了很多新功能和优化，下面来快速介绍一下主要更新内容。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;v1.9.0 主要更新&lt;/h2&gt;
&lt;h3&gt;新特性&lt;/h3&gt;
&lt;h4&gt;HTTP 下载全面重构 (PR #1229)&lt;/h4&gt;
&lt;p&gt;关于这次重构，我之前在 &lt;a href=&quot;/posts/gopeed-http-download-rewrite&quot;&gt;大刀阔斧，彻底重构 Gopeed HTTP 下载实现&lt;/a&gt; 里写过详细的介绍。主要是解决&lt;code&gt;HTTP&lt;/code&gt;下载的稳定性和速度问题，顺便还掉了点技术债。&lt;/p&gt;
&lt;p&gt;这次要解决的都是用户经常反馈的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下载老是失败，要手动重试好几次&lt;/li&gt;
&lt;li&gt;下载卡在 99% 不动了&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;支持自动解压 (PR #1212, #1217, #1222, #1227)&lt;/h4&gt;
&lt;p&gt;现在下载完成后可以自动解压了。为了避免多个任务同时解压把磁盘和 CPU 干爆，做了解压队列。还支持分卷压缩包，UI 上也能看到解压状态。功能默认关闭，需要的话可以在设置里打开：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-26-17-50-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;任务完成后会自动开始解压，状态可以在 UI 上看到：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-26-17-52-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@ilteoood&lt;/code&gt;，一口气提交了 4 个相关的 PR。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;下载进度百分比及预计剩余时间显示 (PR #1238)&lt;/h4&gt;
&lt;p&gt;现在任务列表里能看到下载百分比和预计剩余时间了。移动端 UI 空间有限，所以目前只在桌面端显示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-26-18-05-26.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本来打算放到新 UI 里做的，但既然用户主动贡献了 PR，那当然要合进来。顺便放两张新 UI 的谍照：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;桌面端:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-26-18-15-52.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;移动端：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-26-18-07-42.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@Locon213&lt;/code&gt;。吐槽一下，新 UI 鸽了好久，平常上班太忙，周末又都拿时间去钓鱼了😂。今年下载器基础建设完善后，新 UI 必须安排上！&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;注册为安卓系统级下载器 (PR #1251)&lt;/h4&gt;
&lt;p&gt;这个需求之前有不少用户提过，但我对安卓原生开发不太熟。早期用 Google 搜了一圈没找到方案，用&lt;code&gt;Claude Sonnet 4.5&lt;/code&gt;试了也没解决，后来换&lt;code&gt;Claude Opus 4.5&lt;/code&gt;总算搞定了，不愧是最强编程大模型！&lt;/p&gt;
&lt;p&gt;现在在安卓端用浏览器下载文件时，如果浏览器支持第三方下载器（比如 Firefox），就能选 Gopeed 了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-10-24-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;自动清理任务选项 (PR #1243)&lt;/h4&gt;
&lt;p&gt;设置里可以配置任务自动清理了。开启后会自动清理文件被删除的任务，避免任务列表堆积无效任务：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-11-54-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@Locon213&lt;/code&gt;，这是这位小伙伴这个版本贡献的第二个 PR 了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;MacOS 支持以菜单栏应用运行 (PR #1216)&lt;/h4&gt;
&lt;p&gt;MacOS 上现在可以选择以菜单栏应用运行 Gopeed 了。开启后不会在 Dock 里显示图标，只出现在菜单栏：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-13-55-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个 PR 还加了个快捷键，MacOS 上按 &amp;lt;kbd&amp;gt;conmmand&amp;lt;/kbd&amp;gt; + &amp;lt;kbd&amp;gt;w&amp;lt;/kbd&amp;gt; 可以关闭主窗口，更符合 MacOS 用户的使用习惯。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@awe123343&lt;/code&gt;，这个 PR review 了很多轮才合进来，也很感谢小伙伴的耐心配合。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;支持 Windows ARM 架构 (PR #1206)&lt;/h4&gt;
&lt;p&gt;这个版本正式支持 Windows ARM 架构了。这个 PR 挺出乎我意料的，因为 flutter 官方并没有正式支持 Windows ARM 的构建，PR 里直接指定了一个 flutter 的特殊版本来编译。这里也留了个坑，就是这个 flutter 版本不是最新的，导致我后续升级到 flutter 3.38 时遇到了依赖问题。不过最后还是解决了，感觉我是第一个吃上 flutter Windows ARM 螃蟹的人😂，连 flutter 的明星项目&lt;code&gt;LocalSend&lt;/code&gt;都还没支持 Windows ARM 架构。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@Minessential&lt;/code&gt;，后续可以单独写篇文章分享下 flutter Windows ARM 的构建问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;自动下载 .torrent 配置化 (PR #1219)&lt;/h4&gt;
&lt;p&gt;之前的版本会在下载&lt;code&gt;.torrent&lt;/code&gt;文件后自动开启对应的 torrent 任务。收到社区用户反馈，不需要每次都自动开启，所以改成了可选项。可以在创建任务时选择是否自动从&lt;code&gt;.torrent&lt;/code&gt;文件创建 torrent 任务：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-14-54-06.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也可以在设置里配置默认行为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-14-54-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;应用启动时自动恢复未完成任务的功能 (PR #1203)&lt;/h4&gt;
&lt;p&gt;开启后每次启动应用都会自动恢复上次未完成的任务。可以在设置里配置，默认关闭：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-15-03-32.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;Web 版本地化 Google 字体资源 (PR #1241)&lt;/h4&gt;
&lt;p&gt;这也是社区用户反馈的，Web 版本打开会显示方块乱码：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.0-release/2026-01-27-14-56-44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原因是 Web 版本依赖 Google Fonts 加载字体资源，在国内访问 Google Fonts 非常慢甚至打不开，导致字体加载失败显示方块。Flutter 官方没有提供解决方案，现在的做法是构建完后把&lt;code&gt;dart.main.js&lt;/code&gt;里的 Google Fonts 链接预下载到本地，再替换为本地路径。这样就能避免访问 Google Fonts 了。当然字体文件体积较大，导致 Web 版本体积从之前的 20MB 变成了 40MB 左右，但也没更好的办法了。&lt;/p&gt;
&lt;h3&gt;BUG 修复&lt;/h3&gt;
&lt;h4&gt;升级 goja 库以规避特殊场景下的堆栈溢出问题 (PR #1242)&lt;/h4&gt;
&lt;p&gt;这个问题之前写过一篇文章详细介绍了：&lt;a href=&quot;/posts/gopeed-js-engine-fix&quot;&gt;Gopeed 扩展系统 JS 引擎修复小记&lt;/a&gt;，主要是修复&lt;code&gt;油管扩展&lt;/code&gt;视频无法下载的问题。&lt;/p&gt;
&lt;h3&gt;基础设施&lt;/h3&gt;
&lt;p&gt;前面介绍了这次大版本更新的主要内容。接下来说说这个版本里不起眼但其实很重要的基础设施改进，为后续的用户体验提升打下基础。&lt;/p&gt;
&lt;h4&gt;增强 Deep Link 用以支持安装扩展 (PR #1234)&lt;/h4&gt;
&lt;p&gt;这次增强是为了能直接从浏览器一键安装扩展，比如在浏览器中打开以下链接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gopeed:///extension?params=eyJ1cmwiOiJodHRwczovL2dpdGh1Yi5jb20vbW9ua2V5V2llL2dvcGVlZC1leHRlbnNpb24tYmlsaWJpbGkiLCJkZXZNb2RlIjpmYWxzZX0=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就能自动唤醒并安装&lt;code&gt;bilibli扩展&lt;/code&gt;，不需要手动输入扩展&lt;code&gt;git&lt;/code&gt;远程地址了。之后这个功能会配合官网的&lt;code&gt;扩展市场&lt;/code&gt;使用，方便用户检索和安装扩展。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后续也会把相关的 gopeed scheme 协议文档整理出来，方便社区开发者使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;支持通过宿主 RPC 调用 Gopeed API (PR #1247)&lt;/h4&gt;
&lt;p&gt;这也是个重要的基础设施改进，直接打通了&lt;code&gt;浏览器扩展&lt;/code&gt;和&lt;code&gt;Gopeed&lt;/code&gt;之间的通信能力。以后浏览器扩展可以直接调用&lt;code&gt;Gopeed&lt;/code&gt;的能力了，这是我某天突然想到的点子，配合&lt;code&gt;Gopeed&lt;/code&gt;的扩展系统，可以实现很强大的功能。&lt;/p&gt;
&lt;p&gt;比如安装了&lt;code&gt;油管扩展&lt;/code&gt;后，用户在浏览器访问油管视频页面时，就能直接通过&lt;code&gt;RPC&lt;/code&gt;调用 Gopeed 的解析能力，在浏览器上直接抓取视频进行下载，不需要再复制粘贴链接到 Gopeed 了。也能直接打通&lt;code&gt;Cookie&lt;/code&gt;的传递，不需要像之前一样去浏览器开发者工具里手动复制&lt;code&gt;Cookie&lt;/code&gt;了，小白用户也能轻松使用。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;这个版本收到了很多社区贡献者的 PR，也是好起来了，后续我会继续完善 Gopeed 的基础设施，比如&lt;code&gt;扩展市场&lt;/code&gt;、&lt;code&gt;浏览器资源抓取&lt;/code&gt;等，然后就是争取早日把新 UI 搞定，免得的一直被吐槽界面丑😂。&lt;/p&gt;
&lt;p&gt;感谢大家的支持和反馈，我们下个版本再见！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>GitHub Copilot Skills 悄悄上线，前端设计再也没有 AI 味了</title><link>https://monkeywie.cn/posts/github-copliot-skills-experience</link><guid isPermaLink="true">https://monkeywie.cn/posts/github-copliot-skills-experience</guid><pubDate>Wed, 28 Jan 2026 20:11:06 GMT</pubDate><content:encoded>&lt;p&gt;最近&lt;code&gt;Skills&lt;/code&gt;火得一塌糊涂，VS Code 1.108 版本也紧跟步伐加入了对该功能的支持。目前处于预览阶段，刚好作为一个 Copilot 白嫖怪，第一时间上手体验后，感觉相当不错，聊一聊实测感受。&lt;/p&gt;
&lt;h2&gt;Skills 是什么？&lt;/h2&gt;
&lt;p&gt;Skills 和 MCP 一样，也是来自 &lt;code&gt;Anthropic&lt;/code&gt; 推出用于增强大模型能力的方案。&lt;/p&gt;
&lt;p&gt;简单来说，Skills 就是&lt;strong&gt;把你项目里的一些规则、脚本、模板等内容，固化成一个&quot;技能包&quot;&lt;/strong&gt;，让 Agent 在处理项目相关的任务时能自动加载这些内容，从而更好地符合你的项目需求，其实初步看起来有点像早期的那种&lt;code&gt;提示词模版&lt;/code&gt;，但其实在设计理念上有很大区别。&lt;/p&gt;
&lt;p&gt;比起常规的&lt;code&gt;提示词模版&lt;/code&gt;，Skills 有几个关键差异：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;按需加载&lt;/strong&gt;：只需加载元数据，在匹配到相关意图时才会加载具体内容，而不是每次都把所有提示词都塞进去，避免上下文过载&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能带工具&lt;/strong&gt;：不只是文字规则，还能附带脚本、模板这些实际工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一标准&lt;/strong&gt;：统一定义了Skills的目录结构规范，方便不同工具识别和使用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目前基本上主流 AI 工具都添加了对 Skills 的支持，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;Codex&lt;/li&gt;
&lt;li&gt;Cursor&lt;/li&gt;
&lt;li&gt;GitHub Copilot&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Github Copilot Skills 初体验&lt;/h2&gt;
&lt;h3&gt;开启 Skills&lt;/h3&gt;
&lt;p&gt;目前 GitHub Copilot 对 Skills 的支持还在预览阶段，需要手动开启，打开VS Code 设置页，然后搜索 &lt;code&gt;chat.useAgentSkills&lt;/code&gt;，把它启用就行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-copliot-skills-experience/2026-01-29-16-15-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;启用后 VS Code 会自动从这些路径来识别 Skills：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;项目级：&lt;code&gt;.github/skills/&amp;lt;skill-name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;兼容 claude：&lt;code&gt;~/.claude/skills/...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;个人级：&lt;code&gt;~/.copilot/skills/...&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实战试水&lt;/h3&gt;
&lt;p&gt;先写个官网落地页看看效果，根据网上的推荐，安装&lt;code&gt;frontend-design&lt;/code&gt;试试水，看看能不能摆脱看吐了的紫色渐变，下面是同样一份 &lt;code&gt;提示词&lt;/code&gt; + &lt;code&gt;Claude Sonnet 4.5&lt;/code&gt;，来生成 Gopeed 官网落地页，分别在开启和关闭 Skills 的情况下的对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提示词：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;帮我用 next.js + tailwindcss 写一个 Gopeed 下载器的官网落地页，要求包含以下内容：
1. 首页横幅，突出产品的核心卖点
2. 功能介绍，展示下载器的主要功能
3. 用户评价，展示用户对下载器的好评
4. 下载按钮，方便用户下载产品
5. 联系我们，提供联系方式和社交媒体链接
请使用简洁现代的设计风格，确保页面响应式设计，适配不同设备。
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;没装 Skills 的实测：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;github-copliot-skills-experience/2026-01-29-17-15-38.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这里挺出乎意料的，竟然没有使用AI味道很重的紫色渐变，但是排版和设计还是一眼看出来是 AI 生成的，整体感觉比较普通。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;装了 Skills 后的实测：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先通过命令行安装&lt;code&gt;frontend-design&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx skills add anthropics/claude-code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再次创建一个新的项目执行，可以看到在copliot的输出里，已经有调用 Skill 的痕迹了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-copliot-skills-experience/2026-01-29-17-23-07.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这是最终的结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;github-copliot-skills-experience/2026-01-29-17-34-38.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;效果高下立判，在使用 Skills 后，整体设计感提升了一个档次，基本看不出 AI 生成的痕迹了，确实令人惊艳。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;结尾&lt;/h2&gt;
&lt;p&gt;上面演示的只是 Skills 的冰山一角，它的潜力远不止于此。感兴趣的可以去逛逛 skillsmp，那里收集了从编程、设计到写作，甚至视频剪辑的各类技能包，基本覆盖了常见的开发与创作需求。如果说 2025 年属于 MCP 的爆发，那 2026 年大概率就是 Skills 的元年了。生态已经起势，让我们静待更多有趣玩法的诞生吧。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Flutter Windows ARM 构建踩坑实录</title><link>https://monkeywie.cn/posts/flutter-build-windows-arm</link><guid isPermaLink="true">https://monkeywie.cn/posts/flutter-build-windows-arm</guid><pubDate>Mon, 02 Feb 2026 19:23:00 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;上个版本&lt;code&gt;Gopeed&lt;/code&gt;加入了 Windows ARM64 支持，来自社区的一个 PR，让它成了少数支持 Windows ARM64 的 Flutter 应用。不过这事儿没那么简单，Flutter 官方对 ARM64 的支持还不够完善，后续构建的时候各种问题接踵而至。这篇文章记录一下踩过的坑和找到的解法，算是给后来人铺个路。&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- more --&amp;gt;&lt;/p&gt;
&lt;h2&gt;Windows ARM 这两年才起来&lt;/h2&gt;
&lt;p&gt;得先说说背景。苹果 M 芯片干得漂亮之后，高通也开始在 Windows 笔记本上推 Snapdragon X 系列处理器，虽然市场份额还小，但能支持的话总归是个加分项。&lt;/p&gt;
&lt;h2&gt;官方的支持现状&lt;/h2&gt;
&lt;p&gt;翻了下 Flutter 官方的 &lt;a href=&quot;https://github.com/flutter/flutter/issues/62597&quot;&gt;issue #62597&lt;/a&gt;，计划看着挺完整：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dart SDK&lt;/strong&gt; — 在 Windows ARM64 上编译运行，支持从 x64 交叉编译到 ARM64&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Engine&lt;/strong&gt; — 编译成 ARM64 版本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;构建工具链&lt;/strong&gt; — &lt;code&gt;flutter&lt;/code&gt;命令和 build system 要识别 ARM64 平台&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发环境&lt;/strong&gt; — 既要在 ARM64 机器上开发，也要支持 x64 交叉编译&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt; — 专门的 CI 机器定期构建 ARM64 产物&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;测试&lt;/strong&gt; — devicelab 上有 ARM64 设备持续测试&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但这都 2026 年了，stable 分支还是没有合并进来。从 2020 年到现在，6 年了。&lt;/p&gt;
&lt;h2&gt;实际遇到的坑&lt;/h2&gt;
&lt;h3&gt;版本混乱&lt;/h3&gt;
&lt;p&gt;官方说&lt;code&gt;master&lt;/code&gt;分支支持，但实际上&lt;strong&gt;不是所有 master 上的 commit 都行&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;直接用最新的 master 构建，很可能碰到这个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;The current channel&apos;s Dart SDK does not support Windows Arm64.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;框架代码可能声称支持了，但对应的 Dart SDK 二进制还没编译出来。只能一个个 commit 试，跑&lt;code&gt;flutter doctor&lt;/code&gt;，不行就换下一个。&lt;/p&gt;
&lt;p&gt;Gopeed 升级到 flutter 3.38 之后，PR 里用的 commit 还是兼容 3.24 版本的，导致构建失败。我试了最新的 master commit，还是不行。最后还是 PR 作者给的 commit ID 才解决问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;flutter-build-windows-arm/2026-02-08-18-19-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个版本其实是&lt;code&gt;flutter 3.41.0-0.0.pre&lt;/code&gt;，虽然是预发布版本，但至少能用。&lt;/p&gt;
&lt;h3&gt;mingw 工具链的问题&lt;/h3&gt;
&lt;p&gt;按照 PR 的构建代码，需要配置&lt;code&gt;llvm-mingw-ucrt-aarch64&lt;/code&gt;这个交叉编译工具链，用来生成便携版需要的&lt;code&gt;.dll&lt;/code&gt;文件。&lt;/p&gt;
&lt;p&gt;结果又冒出个新问题。有用户反馈在 arm64 的 Windows 上用不了 gopeed，我在 Mac 上装了个 Windows ARM64 虚拟机测试，确实跑不起来。更神奇的是，用&lt;code&gt;flutter 3.41.0-0.0.pre&lt;/code&gt;构建的安装包能正常运行，但便携版不行。&lt;/p&gt;
&lt;p&gt;反复对比才发现，便携版需要把&lt;code&gt;llvm-mingw-ucrt-aarch64&lt;/code&gt;工具链里的&lt;code&gt;libunwind.dll&lt;/code&gt;拷贝到根目录才能跑。而用&lt;code&gt;Inno Setup&lt;/code&gt;打出来的安装包没有&lt;code&gt;libunwind.dll&lt;/code&gt;却也能正常运行。这个我到现在都没完全搞明白。&lt;/p&gt;
&lt;h2&gt;找版本的小技巧&lt;/h2&gt;
&lt;p&gt;在找支持 arm64 的 commit 时，发现了个有用的方法。Flutter 的 dart sdk 会构建在它们的 CDN 上，每次切换版本时自动下载对应的 sdk。可以通过这个 URL 查看什么时候构建过 windows-arm64 的 dart sdk：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://storage.googleapis.com/flutter_infra_release
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打开是个巨大的 XML 索引文件，搜索&lt;code&gt;dart-sdk-windows-arm64.zip&lt;/code&gt;关键词：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;flutter-build-windows-arm/2026-02-08-18-31-22.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据构建时间，就能大概推断出哪些版本的 flutter 支持 windows-arm64。比如这个是&lt;code&gt;2025-01-14&lt;/code&gt;构建的，就在 master 分支上找这个时间点附近的 commit。可以用 GitHub 的 commit 筛选功能：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;flutter-build-windows-arm/2026-02-08-18-33-50.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这样定位起来快多了。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;Windows ARM64 支持确实是个加分项，但 Flutter 官方的进度实在太慢。现在能跑起来已经不错了，虽然过程中遇到的问题有点莫名其妙。如果你也在做类似的事情，希望这些经验能帮你少走点弯路。&lt;/p&gt;
&lt;p&gt;话说回来，Flutter 的跨平台能力确实强，但要做到真正的全平台支持，还有不少路要走。尤其是这种相对小众的平台，基本上只能靠社区来推动了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 1.9.1 发布，两大实用新特性上线</title><link>https://monkeywie.cn/posts/gopeed-v191-release</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-v191-release</guid><pubDate>Tue, 10 Feb 2026 13:38:26 GMT</pubDate><content:encoded>&lt;p&gt;Gopeed v1.9.1 发布了！这是一个小版本更新，但是也有非常不错的新特性，下面来快速介绍一下主要更新内容。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;v1.9.1 主要更新&lt;/h2&gt;
&lt;h3&gt;新特性&lt;/h3&gt;
&lt;h4&gt;支持任务完成后执行脚本 (PR #1265)&lt;/h4&gt;
&lt;p&gt;现在可以在任务完成后自动执行脚本了，这个功能很实用，可以在下载完成后做一些自动化操作，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;下载完成后自动通知&lt;/li&gt;
&lt;li&gt;自动移动文件到特定目录&lt;/li&gt;
&lt;li&gt;执行自定义的后处理脚本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-10-16-49-33.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以在设置里配置脚本路径和参数，下载完成后会自动执行，脚本内置了以下几个环境变量用于访问任务信息：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;KEY&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GOPEED_EVENT&lt;/td&gt;
&lt;td&gt;事件类型，目前始终为 DOWNLOAD_DONE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOPEED_TASK_ID&lt;/td&gt;
&lt;td&gt;任务ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOPEED_TASK_NAME&lt;/td&gt;
&lt;td&gt;任务名称&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOPEED_TASK_STATUS&lt;/td&gt;
&lt;td&gt;任务状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOPEED_TASK_PATH&lt;/td&gt;
&lt;td&gt;下载完成的文件或文件夹的完整路径&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;比如下面一个例子用于移动下载完成的文件到指定目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
mv &quot;$GOPEED_TASK_PATH&quot; ~/Downloads/backups/
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注：除了操作系统本身支持的脚本类型（比如 Windows 的 BAT、PowerShell，Linux 和 macOS 的 Shell 脚本），还支持&lt;code&gt;python&lt;/code&gt;和&lt;code&gt;node&lt;/code&gt;脚本，只要系统里安装了对应的运行环境就行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;实现任务更新功能 (PR #1267)&lt;/h4&gt;
&lt;p&gt;这也是社区一直在提的需求，如果用过 IDM 的用户应该比较熟悉，有时候任务下载一半可能链接地址就失效了，需要用更新地址的方式继续下载，不同于 IDM 的是，Gopeed 同时支持两种方式来更新：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在任务上右键选择“更新地址 -&amp;gt; 手动更新”，然后输入新的下载链接即可，同时也支持更新&lt;code&gt;HTTP&lt;/code&gt;请求头。
&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-11-10-07-12.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-11-10-07-30.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;和 IDM 类似的监听机制，在任务上右键选择“更新地址 -&amp;gt; 监听更新”，标记一个任务为待更新状态，然后在创建任务的时候，如果发现有待更新的任务，就会提示是否用新的链接更新旧任务。
&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-11-10-09-27.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-11-10-09-41.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;gopeed-v1.9.1-release/2026-02-11-10-10-21.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Bug 修复&lt;/h3&gt;
&lt;p&gt;这个版本修复了不少用户反馈的问题：&lt;/p&gt;
&lt;h4&gt;修复 Windows 上帧率降低的问题 (PR #1279)&lt;/h4&gt;
&lt;p&gt;有用户反馈 Windows 上界面卡顿，帧率很低。排查后发现是 Flutter 的渲染问题，这个版本已经修复了，据说 flutter 新版本已经修复了这个问题，不过还没有发 stable 版本，所以先按照社区的&lt;code&gt;workaround&lt;/code&gt;先临时修复了。&lt;/p&gt;
&lt;h4&gt;添加 Windows ARM64 便携版缺失的库 (PR #1273)&lt;/h4&gt;
&lt;p&gt;上个版本新增了 Windows ARM64 支持，但便携版打包时漏了一些必要的库文件导致无法正常运行，这个版本把缺失的库补上了，Windows ARM64 的小伙伴可以正常使用了。&lt;/p&gt;
&lt;h4&gt;ZIP 解压文件名乱码问题 (PR #1260)&lt;/h4&gt;
&lt;p&gt;有用户反馈下载的 ZIP 文件自动解压后，中文文件名会乱码。原因是 ZIP 文件里用的是 GBK 编码，而解压时没有正确处理编码转换。现在已经修复了，支持 GBK 编码的文件名了。&lt;/p&gt;
&lt;h4&gt;改善 HTTP 重定向链接下载重试策略 (PR #1263)&lt;/h4&gt;
&lt;p&gt;有些下载链接会重定向到临时 URL，这种 URL 有时效性，过期后重试会失败。现在优化了重试策略，遇到重定向链接过期时，会重新请求原始 URL 获取新的重定向链接。&lt;/p&gt;
&lt;h4&gt;修复 macOS 深度链接唤醒窗口问题 (PR #1262)&lt;/h4&gt;
&lt;p&gt;在 macOS 上通过深度链接唤醒 Gopeed 时，没有做好隐藏窗口的处理，这会导致如果浏览器扩展接管那边关闭了下载确认，Gopeed 冷启动的情况下也还是会弹出窗口。&lt;/p&gt;
&lt;h3&gt;CI/CD 优化&lt;/h3&gt;
&lt;h4&gt;Docker 镜像优化 (PR #1274)&lt;/h4&gt;
&lt;p&gt;优化了 Docker 镜像的构建流程，减小了镜像体积，提升了构建速度。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;感谢来自社区的贡献者 &lt;code&gt;@1lkei&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;升级 Windows ARM Flutter 版本 (PR #1269)&lt;/h4&gt;
&lt;p&gt;Windows ARM 的 Flutter 版本升级了，之前用的是特殊版本，现在升级到了更新的版本，解决了一些依赖问题。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;这个版本主要是修复 bug 和优化体验，感谢社区小伙伴们的反馈和贡献。下一个版本会继续完善基础功能，同时推进新 UI 的开发。&lt;/p&gt;
&lt;p&gt;如果你在使用过程中遇到问题或者有好的想法，欢迎在公众号留言或者去 &lt;a href=&quot;https://github.com/GopeedLab/gopeed&quot;&gt;GitHub&lt;/a&gt; 提 issue 或者 PR。&lt;/p&gt;
&lt;p&gt;我们下个版本再见！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 官网焕新升级，扩展商店正式上线</title><link>https://monkeywie.cn/posts/gopeed-website-relaunch</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-website-relaunch</guid><pubDate>Tue, 03 Mar 2026 18:56:51 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;这段时间主要都在推进 &lt;a href=&quot;https://gopeed.com&quot;&gt;Gopeed 官网&lt;/a&gt; 的重构，最近新版官网发布了。除了界面重做，这次也把&lt;code&gt;扩展商店&lt;/code&gt;上线了，下面来详细介绍一下这次改版的主要内容，先放个预览图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-13-47-38.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;官网内容补充&lt;/h2&gt;
&lt;p&gt;老官网基本只有一个展示页，特性介绍和下载页都没有，整体就是 &lt;code&gt;能用就行&lt;/code&gt;。这次主要补了这几个板块：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;特性介绍&lt;/strong&gt;：把 Gopeed 的核心能力系统化展示出来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-13-54-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩展展示&lt;/strong&gt;：展示热门扩展，并提供扩展商店和开发文档入口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-13-48-01.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下载中心&lt;/strong&gt;：独立下载组件，支持按平台和版本选择。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-13-54-39.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;旧版下载按钮会根据 &lt;code&gt;浏览器 UA&lt;/code&gt; 自动识别并直接下载对应版本。但只要用户想下其他平台，就得自己去 GitHub Releases 里翻，对不熟悉 GitHub 的用户不太友好。&lt;/p&gt;
&lt;p&gt;新版下载页加了平台选择组件：默认按 &lt;code&gt;浏览器 UA&lt;/code&gt; 选中推荐版本，也可以手动切换平台和版本。考虑到国内访问 GitHub 的稳定性问题，下载链接也接入了镜像，下载时会自动测速并优先选择最快节点。&lt;/p&gt;
&lt;h2&gt;开放扩展商店&lt;/h2&gt;
&lt;h3&gt;背景&lt;/h3&gt;
&lt;p&gt;Gopeed 的扩展系统其实做了挺久，但几个痛点一直存在：用户不知道去哪找扩展，安装要手动填 GitHub 链接，开发者也缺稳定曝光入口。所以这次直接把扩展商店做出来，集中展示、集中安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-09-28-47.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;自动爬取&lt;/h3&gt;
&lt;p&gt;扩展商店的数据完全自动化：定期爬取 GitHub 上带 &lt;code&gt;gopeed-extension&lt;/code&gt; 标签的项目，再把扩展信息同步到 Cloudflare D1 数据库。&lt;/p&gt;
&lt;p&gt;这样扩展仓库只要打上标签，就能被商店爬虫自动发现，不需要额外提审，整体还是去中心化分发。&lt;/p&gt;
&lt;h3&gt;浏览和安装&lt;/h3&gt;
&lt;p&gt;商店页面支持按名称和热度搜索。安装流程得益于 &lt;code&gt;1.9.0&lt;/code&gt; 引入的 &lt;strong&gt;Gopeed Scheme&lt;/strong&gt;：点 &lt;code&gt;安装&lt;/code&gt; 就会直接唤起本地 Gopeed 完成安装，扩展安装链路终于闭环了。&lt;/p&gt;
&lt;h3&gt;App 内扩展页&lt;/h3&gt;
&lt;p&gt;同样地，下个版本里 Gopeed 应用内的扩展页也会直接接商店接口。这样用户不用再去 GitHub 到处翻，在应用里就能直接浏览和安装。&lt;/p&gt;
&lt;h2&gt;官网和文档站点统一&lt;/h2&gt;
&lt;h3&gt;之前的问题&lt;/h3&gt;
&lt;p&gt;改版之前，Gopeed 的官网和文档站是两个独立的站点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;官网：&lt;a href=&quot;https://gopeed.com&quot;&gt;gopeed.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;文档站：&lt;a href=&quot;https://docs.gopeed.com&quot;&gt;docs.gopeed.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种拆分的问题其实一直很明显：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SEO 不友好：官网和文档分离，权重被分散。&lt;/li&gt;
&lt;li&gt;维护成本高：两套代码库都要维护，开发和运维都更重。&lt;/li&gt;
&lt;li&gt;体验割裂：用户要在不同域名之间来回跳，视觉和交互也不统一。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;改进方案&lt;/h3&gt;
&lt;p&gt;这次把官网和文档统一到同一套基于 &lt;strong&gt;Next.js&lt;/strong&gt; 的框架：&lt;strong&gt;&lt;a href=&quot;https://fumadocs.vercel.app/&quot;&gt;fumadocs&lt;/a&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;选 fumadocs 的原因很务实：UI 足够好、开箱即用，MDX、搜索、API 文档这些能力都省得自己再造轮子。再配合 Next.js 的 ISR，SEO 也能稳住。&lt;/p&gt;
&lt;p&gt;合并之后，官网和文档都放在 &lt;code&gt;gopeed.com&lt;/code&gt; 下。&lt;code&gt;docs.gopeed.com&lt;/code&gt; 正式退休，文档入口统一为 &lt;code&gt;gopeed.com/docs&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在访问 &lt;code&gt;docs.gopeed.com&lt;/code&gt; 会永久重定向到 &lt;code&gt;gopeed.com/docs&lt;/code&gt;。先把历史收录链接兜住，等搜索引擎索引更新后，再把旧站彻底下线。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;OpenAPI 文档迁移到 Scalar&lt;/h2&gt;
&lt;p&gt;Cloudflare Workers 有 3MB 的代码体积限制。Fumadocs 虽然自带 OpenAPI 组件，但打包后会让项目直接超限。要继续用它，就得回到 Vercel，那前面的迁移就白做了。&lt;/p&gt;
&lt;p&gt;所以改用了 &lt;strong&gt;&lt;a href=&quot;https://scalar.com/&quot;&gt;Scalar&lt;/a&gt;&lt;/strong&gt;。它体积更小，UI 也更清爽，还支持在文档里直接调试 API。实际用下来没有明显功能缺口，是个省心的替代方案。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-website-relaunch/2026-03-03-15-51-03.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;迁移到 Cloudflare Workers&lt;/h2&gt;
&lt;p&gt;之前官网和文档站都部署在 Vercel。Vercel 的免费额度对小项目没问题，但 Gopeed 官网流量不算小，时不时会用完免费额度，访问会受影响。升级付费方案当然能解决，但对开源项目来说，长期成本还是得算。&lt;/p&gt;
&lt;p&gt;所以这次迁移到了 &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;，并且把扩展商店的数据放到 &lt;strong&gt;D1 数据库&lt;/strong&gt;，主打一个白嫖。&lt;/p&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;这次改版前后折腾了差不多两个月，核心目标基本都落地了：站点结构统一、下载体验完整、扩展生态也有了稳定入口，阶段性的重构先告一段落，接下来就是按这个方向继续把细节打磨好。&lt;/p&gt;
&lt;p&gt;刚好今天是元宵节，也祝各位元宵节快乐。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>Gopeed 1.9.2 发布，扩展商店重磅上线</title><link>https://monkeywie.cn/posts/gopeed-v192-release</link><guid isPermaLink="true">https://monkeywie.cn/posts/gopeed-v192-release</guid><pubDate>Mon, 09 Mar 2026 19:22:36 GMT</pubDate><content:encoded>&lt;p&gt;Gopeed v1.9.2 发布了！&lt;code&gt;扩展商店&lt;/code&gt;重磅上线，下面来快速介绍一下主要更新内容。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;v1.9.2 主要更新&lt;/h2&gt;
&lt;h3&gt;新特性&lt;/h3&gt;
&lt;h4&gt;扩展商店正式开放 (PR #1301)&lt;/h4&gt;
&lt;p&gt;这个功能算是前几个版本埋下的伏笔终于落地了。&lt;/p&gt;
&lt;p&gt;在之前的版本里，已经陆续补上了安装扩展需要的基础能力，比如 Deep Link 唤醒安装、宿主 RPC 通信之类的基础设施。这次终于把这些能力串起来，做成了可直接使用的&lt;code&gt;扩展商店&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有了扩展商店之后，安装扩展就不需要再自己去找仓库地址、复制 Git URL、手动导入了，整个体验会顺畅很多，降低扩展安装的门槛，同时也能让更多用户发现和使用到社区开发的优秀扩展了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.2-release/2026-03-09-15-51-51.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而且现在可以直接在应用内打开扩展详情页，省得再往浏览器里跳转。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.2-release/2026-03-09-15-52-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另外还可以直接通过&lt;a href=&quot;https://gopeed.com/store&quot;&gt;官网扩展商店&lt;/a&gt;来安装扩展，点击安装后会自动唤起本地 Gopeed 来完成安装，整个流程非常顺畅：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.2-release/2026-03-09-16-32-55.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;桌面端支持原生系统通知 (PR #1293)&lt;/h4&gt;
&lt;p&gt;这个功能是来自社区用户&lt;code&gt;@Locon213&lt;/code&gt;的贡献，用于支持在任务下载完成或者发生错误时，发送系统通知，默认是开启的，可以在设置页面进行开关配置：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;gopeed-v1.9.2-release/2026-03-09-16-34-05.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我在 macOS 上测试了一下，貌似还没有生效，后续会继续跟进这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Flutter 升级到 3.41.2 (PR #1290)&lt;/h4&gt;
&lt;p&gt;之前 Flutter 在 Windows 上出现了帧率降低的问题，这个版本已经修复了，所以顺势升级了上去。而且 Windows ARM 版本刚好也是 Flutter 3.41，这次就一起对齐了。&lt;/p&gt;
&lt;h3&gt;开发向优化&lt;/h3&gt;
&lt;h4&gt;浏览器调试扩展 ID 改为从环境变量读取 (PR #1281)&lt;/h4&gt;
&lt;p&gt;这个改动主要是为了优化&lt;code&gt;Gopeed 浏览器扩展&lt;/code&gt;的本地开发体验，同时也顺带解决了&lt;code&gt;离线安装扩展&lt;/code&gt;场景下无法正常接管下载的问题，比如这个&lt;a href=&quot;https://github.com/GopeedLab/browser-extension/issues/103&quot;&gt;离线安装扩展不能触发捕获下载&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;原因在于，浏览器扩展和 Gopeed 之间要通过&lt;code&gt;Native Messaging&lt;/code&gt;进行通信，而这套机制需要和指定的浏览器扩展 ID 绑定。只有 ID 匹配的扩展，才有权限和 Gopeed 建立连接并接管下载。&lt;/p&gt;
&lt;p&gt;之前代码里写死的是我本地开发时使用的扩展 ID，这样虽然我自己调试没问题，但其他开发者如果想本地联调，或者用户通过离线方式安装了扩展，都会因为扩展 ID 不一致而无法正常通信，最终表现出来就是扩展不能接管下载。&lt;/p&gt;
&lt;p&gt;现在改成了从环境变量中读取扩展 ID 之后，就可以根据实际安装的扩展动态配置，不需要再去改源码，本地开发调试会方便很多，离线安装扩展的接管问题也一并解决了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：如果是 macOS 系统，由于 Gopeed 是 GUI 应用，环境变量要通过 &lt;code&gt;launchctl setenv GOPEED_DEBUG_EXTENSION_IDS xxx&lt;/code&gt; 来设置。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;后记&lt;/h2&gt;
&lt;p&gt;最近几个版本基本上已经把目前计划的核心功能都补齐了，接下来会把重心放在新 UI 开发和完善文档上。中途可能还会有一些小功能和 Bug 修复的迭代，但下一次大版本更新，可能就要等到新 UI 发布了。&lt;/p&gt;
&lt;p&gt;总之，后续会继续努力把 Gopeed 打造成一个更好用、也更好看的下载器！&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>亲测有效：国内订阅 ChatGPT Plus，不用海外信用卡，安卓和 iPhone 皆可</title><link>https://monkeywie.cn/posts/chatgpt-plus-subscription-via-app-sotre-google-play</link><guid isPermaLink="true">https://monkeywie.cn/posts/chatgpt-plus-subscription-via-app-sotre-google-play</guid><pubDate>Wed, 11 Mar 2026 19:36:14 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;最近 &lt;code&gt;Codex&lt;/code&gt; 开放了免费使用，体验下来确实很不错。不过免费版额度比较低，用不了多久就会触发限制，这时候就得想办法升级。问题是，在国内订阅 &lt;code&gt;ChatGPT Plus&lt;/code&gt; 并不算容易，支付和网络环境都不太友好。所以这篇文章主要分享一下我个人的订阅经验：&lt;strong&gt;无需海外信用卡或虚拟卡，走国内可操作的正规渠道，实测有效&lt;/strong&gt;，供大家参考。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-16-46-59.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;开通教程&lt;/h2&gt;
&lt;p&gt;说白了，开通方式就两种：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过 &lt;strong&gt;OpenAI 官方渠道&lt;/strong&gt;订阅，门槛稍高，但&lt;strong&gt;最稳定&lt;/strong&gt;，这也是本文主要介绍的方式。&lt;/li&gt;
&lt;li&gt;通过&lt;strong&gt;中转站&lt;/strong&gt;购买，缺点很多：&lt;strong&gt;不稳定&lt;/strong&gt;，随时可能卷款跑路；&lt;strong&gt;模型注水严重&lt;/strong&gt;，可能拿开源模型充数；&lt;strong&gt;没有隐私保障&lt;/strong&gt;，所有对话都对中转站透明。所以&lt;strong&gt;非常不推荐&lt;/strong&gt;这种方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;如何订阅 ChatGPT Plus&lt;/h3&gt;
&lt;p&gt;先说结论：直接通过 &lt;code&gt;OpenAI&lt;/code&gt; 官网订阅，国内信用卡百分百会被拒。这时候就需要曲线救国，通过 &lt;code&gt;App Store&lt;/code&gt; 或 &lt;code&gt;Google Play&lt;/code&gt; 的应用内购来绕过这个限制。&lt;/p&gt;
&lt;p&gt;这两个渠道我都试过，而且都成功了。也就是说，只要你手上有 &lt;code&gt;iOS&lt;/code&gt; 或 &lt;code&gt;Android&lt;/code&gt; 设备，&lt;strong&gt;不需要专门开国外信用卡&lt;/strong&gt;，也能通过正规渠道订阅 &lt;code&gt;ChatGPT Plus&lt;/code&gt;。下面分别说一下具体流程。&lt;/p&gt;
&lt;h2&gt;通过 App Store 订阅&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;iOS&lt;/code&gt; 设备实测下来是最简单的，只要有个 🪜 基本就能搞定。&lt;/p&gt;
&lt;h3&gt;前提准备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;干净的 🪜 IP（香港不行，&lt;code&gt;OpenAI&lt;/code&gt; 目前&lt;strong&gt;不对香港提供服务&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;美区 Apple ID&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果没有美区 Apple ID 的话，需要自行注册一个，注册教程网上一大把，这里就不赘述了。&lt;/p&gt;
&lt;h3&gt;订阅流程&lt;/h3&gt;
&lt;h4&gt;安装 ChatGPT&lt;/h4&gt;
&lt;p&gt;通过美区 Apple ID 登录 &lt;code&gt;App Store&lt;/code&gt;，搜索 &lt;code&gt;ChatGPT&lt;/code&gt; 进行安装。如果搜不到，说明你的 Apple ID 不是美区的，想办法弄个美区 Apple ID 再继续。&lt;/p&gt;
&lt;h4&gt;购买美区礼品卡&lt;/h4&gt;
&lt;p&gt;接下来需要购买美区 Apple 礼品卡。&lt;code&gt;ChatGPT Plus&lt;/code&gt; 的价格是 &lt;strong&gt;&lt;code&gt;20 美元/月&lt;/code&gt;&lt;/strong&gt;，所以买 &lt;strong&gt;&lt;code&gt;20 美元&lt;/code&gt;&lt;/strong&gt; 面额的礼品卡就够了。&lt;/p&gt;
&lt;p&gt;购买渠道直接走支付宝里的 &lt;code&gt;PockytShop&lt;/code&gt; 小程序，发货快，稳定性也还不错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-04-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;进去注册登录之后，选择 &lt;code&gt;App Store &amp;amp; iTunes&lt;/code&gt;，输入 &lt;strong&gt;&lt;code&gt;20 美元&lt;/code&gt;&lt;/strong&gt; 面额，最后付款就行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-09-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;付款成功后，等一会儿就会拿到一个礼品卡兑换码。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：如果 &lt;code&gt;iOS&lt;/code&gt; 上的支付宝搜不到 &lt;code&gt;PockytShop&lt;/code&gt; 小程序，可以复制下面这个链接进入(推给同事的时候遇到过这种情况)：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# https://www.wmslz.com/s/QR28LIj06s4#💪复置💪此消息，打开支f`u.保嗖索，体验PockytShop小程序  j:/6 HU1010 $538
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;兑换礼品卡&lt;/h4&gt;
&lt;p&gt;拿到兑换码之后，打开 &lt;code&gt;App Store&lt;/code&gt;，点击头像进入账户设置，选择 &lt;code&gt;兑换礼品卡或代码&lt;/code&gt;，输入兑换码进行兑换：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-13-46.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;兑换完成后，账户里就会有 &lt;strong&gt;&lt;code&gt;20 美元&lt;/code&gt;&lt;/strong&gt; 余额，可以直接用来订阅 &lt;code&gt;ChatGPT Plus&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;订阅 ChatGPT Plus&lt;/h4&gt;
&lt;p&gt;打开 &lt;code&gt;ChatGPT&lt;/code&gt; 应用，注册或登录账号，点击 &lt;code&gt;升级到 Plus&lt;/code&gt;，选择 &lt;code&gt;订阅&lt;/code&gt;，确认后就完成了。后面每个月会自动续费，只需要定期在 &lt;code&gt;PockytShop&lt;/code&gt; 补对应金额的礼品卡即可。&lt;/p&gt;
&lt;h2&gt;通过 Google Play 订阅&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Google Play&lt;/code&gt; 的订阅门槛会稍高一点。除了 🪜 之外，还需要一张国内能申请到的&lt;strong&gt;双币信用卡&lt;/strong&gt;，比如 &lt;code&gt;Visa&lt;/code&gt; 或 &lt;code&gt;Mastercard&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;不过 &lt;code&gt;Google Play&lt;/code&gt; 的优势也很明显，就是可以切到一些更便宜的国家地区来订阅，价格通常会比美区更低。比如我切的是&lt;strong&gt;日本区&lt;/strong&gt;，订阅价格是 &lt;strong&gt;&lt;code&gt;2860 日元/月&lt;/code&gt;&lt;/strong&gt;，折合大概 &lt;strong&gt;&lt;code&gt;18 美元/月&lt;/code&gt;&lt;/strong&gt;，比美区每个月便宜大约 &lt;strong&gt;&lt;code&gt;2 美元&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;至于网上常说的什么 &lt;code&gt;土区&lt;/code&gt;、&lt;code&gt;尼区&lt;/code&gt;，我也都试过。现在可能是被薅得太多了，风控比较严，一支付就失败，提示：&lt;code&gt;无法完成您的购买交易。请检查您是否在 Play 帐号中选择了正确的国家/地区&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;前提准备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;干净的 🪜 IP（香港不行，&lt;code&gt;OpenAI&lt;/code&gt; 目前&lt;strong&gt;不对香港提供服务&lt;/strong&gt;）&lt;/li&gt;
&lt;li&gt;安装了 Google Play 的 Android 设备&lt;/li&gt;
&lt;li&gt;双币信用卡&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;安装 Google Play&lt;/h4&gt;
&lt;p&gt;由于我是小米手机，这里只写一下小米手机的安装方式。小米系统自带 &lt;code&gt;Google 服务&lt;/code&gt;，安装起来比较简单，步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;开启谷歌服务：&lt;code&gt;设置 -&amp;gt; 更多设置 -&amp;gt; 帐号与同步 -&amp;gt; 谷歌基础服务&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;安装 Google Play：在应用商店搜索&lt;code&gt;Google Play&lt;/code&gt;进行安装即可&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其它品牌的手机安装 Google Play 的步骤可能会有些不同，请自行搜索相关教程。&lt;/p&gt;
&lt;h4&gt;查看 Google Play 国家&lt;/h4&gt;
&lt;p&gt;打开 &lt;code&gt;Google Play&lt;/code&gt; 商店，点击头像，选择 &lt;code&gt;设置 -&amp;gt; 常规 -&amp;gt; 账户和设备偏好设置&lt;/code&gt;，在这里就可以看到 &lt;code&gt;国家和个人资料&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-36-35.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注：实测 &lt;code&gt;Google Play&lt;/code&gt; 的国家和 🪜 IP 没有强关联，不一定非要是同一个国家。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;默认会根据注册时的 IP 地址自动分配一个国家。如果你要切到日本区，通常在 App 里是搞不定的，需要走网页版。&lt;/p&gt;
&lt;h4&gt;切换 Google Play 国家&lt;/h4&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://payments.google.com/gp/w/home/settings&quot;&gt;Google Payments 设置页&lt;/a&gt;，拉到最下面，点击 &lt;code&gt;关闭支付资料&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-41-23.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后重新创建一个新的支付资料，国家选择 &lt;code&gt;日本&lt;/code&gt;，再填写相关信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-42-37.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当然也不一定非要日本区，可能还有别的区更便宜。有兴趣的话可以自己多试几个国家。我试了一圈之后，感觉日本区已经算比较划算了，所以就没继续折腾。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;后面的资料可以通过 &lt;a href=&quot;https://www.meiguodizhi.com/jp-address&quot;&gt;日本地址生成器&lt;/a&gt; 来生成，能通过验证就行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-45-28.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加完成后，退出 &lt;code&gt;Google Play&lt;/code&gt; 再重新打开确认一下，会发现已经切到日本区了。&lt;/p&gt;
&lt;h4&gt;添加信用卡&lt;/h4&gt;
&lt;p&gt;点击头像，选择 &lt;code&gt;付款和订阅 -&amp;gt; 付款方式 -&amp;gt; 添加信用卡或借记卡&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-17-50-34.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;添加完成之后，就可以为后续订阅做好准备了。&lt;/p&gt;
&lt;h4&gt;安装 ChatGPT&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;Google Play&lt;/code&gt; 搜索 &lt;code&gt;ChatGPT&lt;/code&gt; 进行安装。注意，如果搜索不到，可能是因为你之前所在的国家没有上架 &lt;code&gt;ChatGPT&lt;/code&gt;。切到日本区之后，通常还要清一下缓存，方法是：&lt;code&gt;设置 -&amp;gt; 常规 -&amp;gt; 账户和设备偏好设置 -&amp;gt; 清空设备上的搜索记录&lt;/code&gt;。清理完成后退出 &lt;code&gt;Google Play&lt;/code&gt; 再重新打开，一般就能搜到了。&lt;/p&gt;
&lt;h4&gt;订阅 ChatGPT Plus&lt;/h4&gt;
&lt;p&gt;打开 &lt;code&gt;ChatGPT&lt;/code&gt; 应用，注册或登录账号，点击 &lt;code&gt;升级到 Plus&lt;/code&gt;，选择 &lt;code&gt;订阅&lt;/code&gt;，确认之后就完成了。后续每个月会自动从信用卡扣款续费。&lt;/p&gt;
&lt;h2&gt;对比 Claude Pro 套餐&lt;/h2&gt;
&lt;p&gt;上面的订阅方法不只适用于 &lt;code&gt;ChatGPT Plus&lt;/code&gt;，其实也同样适用于 &lt;code&gt;Claude订阅&lt;/code&gt;。其实我也顺手订了一个 &lt;strong&gt;&lt;code&gt;20 美元/月&lt;/code&gt;&lt;/strong&gt; 的 &lt;code&gt;Claude Pro&lt;/code&gt;，但实际体验下来，和 &lt;code&gt;ChatGPT Plus&lt;/code&gt; 真的没法比。&lt;/p&gt;
&lt;p&gt;比如用 &lt;code&gt;Opus 4.6&lt;/code&gt; 写代码，不到 &lt;code&gt;20&lt;/code&gt; 分钟就到5小时 &lt;code&gt;limit&lt;/code&gt; 了，基本没法高强度使用。当然富哥们预算充足的话，可以考虑 &lt;strong&gt;&lt;code&gt;200 美元/月&lt;/code&gt;&lt;/strong&gt; 的 &lt;code&gt;Claude Max&lt;/code&gt;。但如果同样都是 &lt;strong&gt;&lt;code&gt;20 美元/月&lt;/code&gt;&lt;/strong&gt;，那 &lt;code&gt;ChatGPT Plus&lt;/code&gt; 真的可以说是&lt;strong&gt;量大管饱&lt;/strong&gt;。像我这种高强度使用 &lt;code&gt;GPT-5.4 High&lt;/code&gt; 一整天，通常也用不到 &lt;code&gt;Weekly Limit&lt;/code&gt; 的 &lt;code&gt;10%&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;chatgpt-plus-subscription-via-app-sotre-google-play/2026-03-11-18-03-10.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不得不说，&lt;code&gt;ChatGPT Plus&lt;/code&gt; 的性价比确实很高。跑复杂任务时，它并不比 &lt;code&gt;Claude&lt;/code&gt; 差，甚至有些场景&lt;code&gt;Opus 4.6&lt;/code&gt;搞不定的，&lt;code&gt;ChatGPT Plus&lt;/code&gt;还能搞定，所以无脑冲就完事了。&lt;/p&gt;
</content:encoded><author>Levi</author></item><item><title>文艺复兴！我用Go语言实现了一个ed2k下载器</title><link>https://monkeywie.cn/posts/go-ed2k-download</link><guid isPermaLink="true">https://monkeywie.cn/posts/go-ed2k-download</guid><pubDate>Sun, 15 Mar 2026 12:05:54 GMT</pubDate><content:encoded>&lt;h2&gt;ed2k 早就过时了，但它还没有“死”&lt;/h2&gt;
&lt;p&gt;如果你经历过电驴时代，ed2k 这个名字不会陌生，它曾经是文件共享网络里非常重要的一环，但今天它确实过时了。生态萎缩、客户端老旧、资料零散，普通用户的行为习惯也彻底迁移到了更现代的协议和服务上。&lt;/p&gt;
&lt;p&gt;但它并没有彻底消失。仍然有不少冷门资源只在这套网络里流通，特别是一些 &lt;code&gt;Windows&lt;/code&gt; 系统镜像。&lt;/p&gt;
&lt;p&gt;也正因为这样，我一直觉得：ed2k 虽然老，但还没到彻底失去价值。问题只在于，它太旧了，旧到几乎已经没人愿意再认真把它实现一遍。&lt;/p&gt;
&lt;h2&gt;早就埋下的一颗种子&lt;/h2&gt;
&lt;p&gt;其实在很早之前，就有用户在 Gopeed 的 issue 里提过 ed2k 的需求：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;go-ed2k-download/2026-03-15-12-28-40.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;而且包括各大开源下载器里也有不少用户在问这个需求，比如 aria2、Motrix：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;go-ed2k-download/2026-03-15-12-31-15.png&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;go-ed2k-download/2026-03-15-12-31-49.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那个时候我就研究过在 Gopeed 上添加 &lt;code&gt;ed2k&lt;/code&gt; 支持的可行性，结果发现这个协议的资料非常零散，连一份权威的官方协议文档都没有。能找到的资料大多散落在 wiki、论坛帖子、老项目源码和零碎博客里，而且不同实现之间还会出现行为差异，基本很难入手。它不像 &lt;code&gt;BitTorrent&lt;/code&gt; 那样，官方有一套标准的 &lt;code&gt;BEP&lt;/code&gt; 协议规范文档，几乎没法直接开展开发，除非自己去看源码、抓包分析协议细节。这工作量非常大，我怕我秃了都不一定能把它做出来，所以就一直拖着没做。&lt;/p&gt;
&lt;h2&gt;AI 的出现带来转机&lt;/h2&gt;
&lt;p&gt;但近一年 &lt;code&gt;AI 编程&lt;/code&gt; 的兴起，让我看到了新的可能性。其实需求很简单，就是让 &lt;code&gt;AI&lt;/code&gt; 参考开源的 &lt;code&gt;ed2k&lt;/code&gt; 项目，并用 &lt;code&gt;Go&lt;/code&gt; 语言 1:1 复刻一个 &lt;code&gt;ed2k&lt;/code&gt; 下载器。不过由于之前 &lt;code&gt;Code Agent&lt;/code&gt; 和大模型的能力还不够，我尝试了几次都没有成功，就先搁置了。最近我刚好开通了 &lt;code&gt;ChatGPT Plus&lt;/code&gt;，想着用最新的 &lt;code&gt;GPT-5.4&lt;/code&gt; 和 &lt;code&gt;codex&lt;/code&gt; 再试一次，结果这次居然出乎意料地成功了，最终复刻出了一个全功能的 &lt;code&gt;ed2k&lt;/code&gt; 下载实现。&lt;/p&gt;
&lt;p&gt;主要参考了以下两个开源项目：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/a-pavlov/jed2k&quot;&gt;jed2k&lt;/a&gt;： 一个java实现的ed2k下载器，功能比较全，代码也比较清晰，适合用来参考协议细节和实现逻辑。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/amule-project/amule&quot;&gt;aMule&lt;/a&gt;：市面上仅存的还在维护的ed2k下载器，由于它是C++实现的，代码比较复杂，不太适合直接参考，但可以用来验证协议细节和实现逻辑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;给AI的&lt;code&gt;prompt&lt;/code&gt;就是先让它去复刻&lt;code&gt;jed2k&lt;/code&gt;的功能实现，然后如果有遗漏和不完整的地方，再让它去参考&lt;code&gt;aMule&lt;/code&gt;的实现来完善细节，最终成功复刻出了一个功能完整的ed2k下载器。&lt;/p&gt;
&lt;h2&gt;开源&lt;/h2&gt;
&lt;p&gt;目前已经开源在 &lt;a href=&quot;https://github.com/monkeyWie/goed2k&quot;&gt;goed2k&lt;/a&gt;，它既可以作为库被其他项目调用，也提供了一个内置的 &lt;code&gt;终端下载器&lt;/code&gt;，可以直接安装体验。&lt;/p&gt;
&lt;h3&gt;特性支持&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;goed2k&lt;/code&gt; 目前已经覆盖了一套可用的 ED2K 客户端基础能力，主要包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] ED2K 文件下载&lt;/li&gt;
&lt;li&gt;[x] 多任务并发下载&lt;/li&gt;
&lt;li&gt;[x] 多个 ED2K server 并发找源&lt;/li&gt;
&lt;li&gt;[x] &lt;code&gt;server.met&lt;/code&gt; 加载&lt;/li&gt;
&lt;li&gt;[x] KAD bootstrap 和 source 查找&lt;/li&gt;
&lt;li&gt;[x] 资源搜索&lt;/li&gt;
&lt;li&gt;[x] 暂停、继续、删除任务&lt;/li&gt;
&lt;li&gt;[x] 状态持久化与恢复&lt;/li&gt;
&lt;li&gt;[x] 上传支持&lt;/li&gt;
&lt;li&gt;[x] 任务、peer、server、piece 状态快照&lt;/li&gt;
&lt;li&gt;[x] 任务进度订阅&lt;/li&gt;
&lt;li&gt;[x] 可交互的终端下载管理器&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;终端下载器&lt;/h3&gt;
&lt;p&gt;如果想体验的话，可以通过命令行安装使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go install github.com/monkeyWie/goed2k/cmd/goed2k@latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，就可以直接运行 &lt;code&gt;goed2k&lt;/code&gt; 进入交互终端使用，支持 &lt;code&gt;搜索&lt;/code&gt; 和 &lt;code&gt;下载&lt;/code&gt;，效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;go-ed2k-download/2026-3-15-13-21-47.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面的演示里，先通过 &lt;code&gt;/search&lt;/code&gt; 命令进入搜索面板搜索资源，然后选择了 2 个资源进行下载。速度也还不错，接近 &lt;code&gt;1MB/s&lt;/code&gt;。在任务管理面板中，左边可以看到任务列表，右边可以看到下载详情，包括下载速度、文件信息、连接数等。&lt;/p&gt;
&lt;h3&gt;作为库被调用&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;goed2k&lt;/code&gt;库的使用也非常简单，下面是一个简单的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;log&quot;

	&quot;github.com/monkeyWie/goed2k&quot;
)

func main() {
	settings := goed2k.NewSettings()
	settings.ReconnectToServer = true

	client := goed2k.NewClient(settings)
	if err := client.Start(); err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	if err := client.ConnectServers(&quot;176.123.5.89:4725&quot;); err != nil {
		log.Fatal(err)
	}

	if _, _, err := client.AddLink(
		&quot;ed2k://|file|example-file.mp3|12345678|0123456789ABCDEF0123456789ABCDEF|/&quot;,
		&quot;./downloads&quot;,
	); err != nil {
		log.Fatal(err)
	}

	if err := client.Wait(); err != nil &amp;amp;&amp;amp; err != goed2k.ErrClientStopped {
		log.Fatal(err)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的示例里，先创建了一个 &lt;code&gt;goed2k&lt;/code&gt; 客户端实例，然后连接到一个 &lt;code&gt;ed2k&lt;/code&gt; 服务器，接着添加一个下载链接，最后等待下载完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;更多示例可以在 &lt;a href=&quot;https://github.com/monkeyWie/goed2k/tree/main/examples&quot;&gt;examples&lt;/a&gt; 目录中找到，这里就不一一列举了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;虽然 &lt;code&gt;ed2k&lt;/code&gt; 协议已经过时了，但它在某些特定资源的下载场景里仍然有价值，尤其是一些冷门资源。借助 &lt;code&gt;AI&lt;/code&gt; 的帮助，我成功复刻了一个 &lt;code&gt;ed2k&lt;/code&gt; 下载器，并且已经将这个项目开源，希望能让更多人受益。&lt;/p&gt;
&lt;p&gt;后续我会把它集成到 Gopeed 里，让用户更方便地使用 &lt;code&gt;ed2k&lt;/code&gt; 下载功能，敬请期待！&lt;/p&gt;
</content:encoded><author>Levi</author></item></channel></rss>