<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://noting.me/feed" rel="self" type="application/rss+xml"/>
<title>北林向南</title>
<link>https://noting.me</link>
<description>鴥彼晨风，郁彼北林</description>
<language>zh-CN</language>
<copyright>© 授简索句 </copyright>
<pubDate>Sat, 09 May 2026 11:55:00 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://avatars.githubusercontent.com/u/5591515?v=4</url>
    <title>北林向南</title>
    <link>https://noting.me</link>
</image>
<item>
    <title>在 OpenWrt 上实现 PPPoE Half Bridge —— 让下游网关直接获取公网 IP</title>
    <link>https://noting.me/posts/geek/pppoe-half-bridge</link>
    <pubDate>Sun, 22 Feb 2026 05:45:39 GMT</pubDate>
    <description>起因

家里用了全套 Unifi，网关是 UDM SE。但 UDM SE 没有 PPPoE Hard</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/geek/pppoe-half-bridge'>https://noting.me/posts/geek/pppoe-half-bridge</a></blockquote>
          <h2>起因</h2>
<p>家里用了全套 Unifi，网关是 UDM SE。但 UDM SE <strong>没有 PPPoE Hardware Offload</strong>，导致用它拨号时单线程速率只有约 500Mbps（多线程可以跑满千兆）。如果改用光猫拨号，又会产生 <strong>Double NAT</strong> 问题，而且 UDM 也无法及时感知 IP 变化来触发 DDNS 更新。</p>
<p>那有什么办法可以 <strong>既用一台专门的设备来处理 PPPoE 拨号（享受硬件卸载的性能），又能让 UDM SE 直接拿到公网 IP 地址</strong> 呢？答案就是本文的主题 —— <strong>PPPoE Half Bridge</strong>。</p>
<h2>什么是 PPPoE Half Bridge？</h2>
<p>PPPoE Half Bridge 并不是什么新鲜概念。部分国外运营商的光猫原生支持 <strong>PPPoE Half Bridge</strong>（也叫 <strong>PPPoE IP Extension</strong>、<strong>PPPoE IP Passthrough</strong>）。其核心思路是：</p>
<blockquote>
<p>由一台前置设备（光猫或路由器）负责 PPPoE 拨号建立会话，但<strong>不使用获取到的公网 IP</strong>，而是通过 DHCP 将这个公网 IP &quot;透传（Passthrough）&quot;给下游网关。让下游网关以为自己直接获得了公网 IP。</p>
</blockquote>
<p>对比几种常见方案：</p>
<table>
<thead>
<tr>
<th>方案</th>
<th>拨号设备</th>
<th>公网 IP 归属</th>
<th>NAT 层数</th>
<th>适用场景</th>
</tr>
</thead>
<tbody><tr>
<td>光猫拨号</td>
<td>光猫</td>
<td>光猫</td>
<td>Double NAT</td>
<td>简单省事但不灵活</td>
</tr>
<tr>
<td>路由器拨号/光猫桥接</td>
<td>路由器</td>
<td>路由器</td>
<td>单层 NAT</td>
<td>最常见方案</td>
</tr>
<tr>
<td><strong>PPPoE Half Bridge</strong></td>
<td><strong>前置设备</strong></td>
<td><strong>下游网关</strong></td>
<td><strong>单层 NAT</strong></td>
<td><strong>本文方案</strong></td>
</tr>
</tbody></table>
<p>简单来说，PPPoE Half Bridge 让前置设备只做&quot;拨号代理&quot;，真正的公网 IP 由下游网关持有和使用。这样做的好处是显而易见的：</p>
<ul>
<li>前端设备（如刷了 OpenWrt 的路由器）负责 PPPoE 拨号，可以利用其硬件卸载能力跑满带宽；</li>
<li>下游网关设备（如 UDM SE）直接获取公网 IP，避免了 Double NAT；</li>
<li>下游网关可以正常处理 NAT、防火墙、DDNS 等一切功能，就像直接拨号一样。</li>
</ul>
<p>本文分享一套基于 OpenWrt 的 PPPoE Half Bridge 实现方案，笔者已经稳定运行大半年，实测可靠。该方案也可以轻松迁移到 Debian 系的系统上。</p>
<h2>为什么可以这么做？</h2>
<p>PPPoE 拨号建立的是一个<strong>点对点连接（Point-to-Point）</strong>。拨号成功后，PPPoE 接口上的 IP 地址其实只是一个标识，并不像以太网接口那样是通信的必要条件。数据包的转发依赖的是 <strong>PPPoE Session</strong> 和路由表，而不是接口上绑定的 IP 地址。</p>
<p>因此我们可以：</p>
<ol>
<li>保持 PPPoE 会话不断开</li>
<li>删除 PPPoE 接口上的 IP 地址（<code>ip addr flush</code>）</li>
<li>手动建立路由规则，将下游网关的流量导向 PPPoE 接口</li>
<li>通过 DHCP 将公网 IP 分配给下游网关</li>
</ol>
<p>下游网关拿到公网 IP 后，所有出站流量经过前置设备的 PPPoE 接口转发到运营商，回程流量经 PPPoE 接口回到前置设备再转发给下游网关。<strong>整个过程不需要 NAT，前置设备只做二层/三层转发</strong>。</p>
<h2>硬件准备</h2>
<p>你需要一台 <strong>支持 PPPoE 硬件卸载（Hardware Offload）的 OpenWrt 设备</strong>，它负责拨号和转发。</p>
<p>我使用的硬件方案：</p>
<ul>
<li><strong>香蕉派 BPI-R4</strong>：刷最新 OpenWrt 固件即可开启 PPPoE 硬件加速（<code>flow_offloading_hw</code>），跑满千兆 CPU 纹丝不动</li>
<li><strong>猫棒</strong>：插在 BPI-R4 的 SFP 口上，替代光猫</li>
<li><strong>DAC 线</strong>：连接 BPI-R4 的另一个 SFP 口到 UDM SE</li>
</ul>
<blockquote>
<p>[!TIP]
现在市面上也有原生支持 OpenWrt 的 10G XGPON 光猫，如果你的运营商支持 GPON 会更方便。不过上海电信是 EPON，没法用。</p>
</blockquote>
<h2>网络拓扑</h2>
<p></p>
<p><strong>接口说明：</strong></p>
<table>
<thead>
<tr>
<th>接口名</th>
<th>物理端口</th>
<th>用途</th>
</tr>
</thead>
<tbody><tr>
<td><code>wan</code> (br-wan)</td>
<td>eth2</td>
<td>连接光猫/猫棒，PPPoE 在此之上建立</td>
</tr>
<tr>
<td><code>ppp</code></td>
<td>pppoe-ppp</td>
<td>PPPoE 拨号接口</td>
</tr>
<tr>
<td><code>lann</code></td>
<td>eth1</td>
<td>连接下游网关（UDM SE）的 WAN 口</td>
</tr>
<tr>
<td><code>lan</code> (br-lan)</td>
<td>lan1/lan2/lan3</td>
<td>管理口</td>
</tr>
</tbody></table>
<h2>整体思路</h2>
<p>在理解具体配置之前，先了解一下整体思路和数据流：</p>
<pre><code class="language-">[光猫/猫棒] &lt;--PPPoE--&gt; [OpenWrt (BPI-R4)] &lt;--DHCP--&gt; [UDM SE]
     eth2                                        eth1</code></pre><ol>
<li>OpenWrt 通过 WAN 口完成 PPPoE 拨号，获取到公网 IP（如 <code>a.b.c.d</code>）；</li>
<li><strong>剥离</strong> PPPoE 接口上的 IP 地址，但保持 PPPoE 会话不中断；</li>
<li>将公网网段的<strong>网关 IP</strong> 配置到连接下游网关的 LAN 口（eth1）上，让 OpenWrt「冒充」运营商网关；</li>
<li>通过 DHCP 把公网 IP 分配给下游网关；</li>
<li>配置策略路由，让下游网关的流量通过 PPPoE 隧道转发到公网；</li>
<li>关闭 OpenWrt 上的 NAT（masquerade）—— PPPoE 接口 IP 被清除后 masquerade 无法工作，且 NAT 应由下游网关处理。</li>
</ol>
<p>完成以上步骤后，下游网关就像直接拨号一样，拿到公网 IP、通过 PPPoE 隧道上网，而 OpenWrt 则变成了一个透明的&quot;桥接拨号器&quot;。</p>
<h2>前置配置要点</h2>
<p>在进入核心操作之前，OpenWrt 的静态配置中有几个关键点需要注意：</p>
<p><strong>network：</strong></p>
<ul>
<li>PPPoE 接口 (<code>ppp</code>) 的 <code>defaultroute</code> 必须设为 <code>&#39;0&#39;</code> —— 默认路由通过策略路由手动管理，PPPoE 自动添加默认路由会产生冲突</li>
<li><code>lann</code> 接口 (<code>eth1</code>) 先配一个占位 IP（如 <code>192.168.2.1</code>），拨号后动态修改</li>
</ul>
<p><strong>dhcp：</strong></p>
<ul>
<li>使用 <strong>odhcpd</strong>（完整版，非 <code>odhcpd-ipv6only</code>）替代 dnsmasq 作为 DHCPv4 服务器。odhcpd 小巧灵活，动态修改 DHCP 范围后重启秒级完成，非常适合这个场景</li>
<li><code>lann</code> 接口的 DHCP 配置为 <code>start=2, limit=1</code>，即池中<strong>只有一个可分配地址</strong>，后续动态调整 <code>start</code> 使这个唯一地址恰好是公网 IP</li>
<li><code>leasetime</code> 设为 <code>60s</code>，极短租约确保 IP 变化时快速更新</li>
</ul>
<p><strong>firewall：</strong></p>
<ul>
<li>为 PPPoE 接口单独创建一个 zone（<code>wann</code>），<strong>不开 <code>masq</code></strong> —— 因为 PPPoE 接口的 IP 会被清除，masquerade 找不到源地址会导致无法上网</li>
<li><code>lann</code> 和 <code>wann</code> 之间双向 forwarding</li>
<li>开启 <code>flow_offloading_hw</code> 硬件流量卸载</li>
</ul>
<h2>核心操作步骤</h2>
<p>以下是 PPPoE 拨号成功后，需要执行的一系列操作。假设拨号获取到的公网 IP 为 <code>58.32.1.100</code>，运营商分配的子网为 <code>/22</code>。</p>
<h3>Step 1：获取公网 IP 和计算网关地址</h3>
<p>PPPoE 拨号成功后，首先通过 <code>ifstatus</code> 获取分配到的公网 IP：</p>
<pre><code class="language-bash">wan_ip=$(ifstatus ppp | jsonfilter -e '@["ipv4-address"][0].address')
# → 58.32.1.100</code></pre><p>然后根据子网掩码计算出网关地址。上海电信精品网分配的是 /22 子网（255.255.252.0），通过位运算将第三个八位组对齐到子网边界：</p>
<pre><code class="language-bash">IFS=. read i1 i2 i3 i4 &lt;&lt;&lt; "$wan_ip"
gateway_ip="$i1.$i2.$(( i3 & 252 )).1"
# 58.32.1.100 → 1 & 252 = 0 → 网关 58.32.0.1</code></pre><blockquote>
<p>[!TIP]
如果你的运营商分配的子网掩码不同，需要修改这里的计算逻辑。比如 /24 子网直接用 <code>$i1.$i2.$i3.1</code> 即可。</p>
</blockquote>
<h3>Step 2：清除 PPPoE 接口 IP，建立策略路由</h3>
<p>这是整个方案中<strong>最关键的一步</strong>。</p>
<pre><code class="language-bash"># 清除 PPPoE 接口上的 IP 地址，但 PPPoE 会话保持不断
ip addr flush dev pppoe-ppp</code></pre><p>删除了 PPPoE 接口上的公网 IP，但 <strong>PPPoE 会话本身不受影响</strong>。</p>
<p><strong>为什么要删掉？</strong> 因为接下来我们要把相同网段的 IP 分配给下游网关。如果 OpenWrt 的 PPPoE 接口上还保留着公网 IP，就会造成&quot;两个设备在同一个子网内拥有相同网段的 IP&quot;的冲突，导致路由混乱。删除 PPPoE 接口的 IP 后，OpenWrt 上不再有这个网段的直连路由，流量走向就完全由我们手动添加的路由规则控制了。</p>
<p><strong>为什么可以删掉？</strong> PPPoE 是一个点对点隧道协议，会话的维持依赖的是底层的 PPPoE session，而不是 IP 层的地址。只要 PPPoE session 没断，隧道就在，数据包就能通过这个隧道转发。IP 地址只是便于上层路由使用，不影响隧道本身。</p>
<pre><code class="language-bash"># 建立策略路由：来自 eth1（下游网关）的流量走 PPPoE 隧道
ip route add default dev pppoe-ppp table 100
ip rule add iif eth1 table 100</code></pre><p>由于 PPPoE 接口的 IP 已被删除，系统默认路由也随之失效。我们需要手动建立路由规则，告诉系统&quot;从下游网关来的流量走 PPPoE 隧道出去&quot;。</p>
<p>这里使用策略路由（Policy Routing）：</p>
<ul>
<li>创建一张独立的路由表（table 100），在其中添加默认路由指向 <code>pppoe-ppp</code> 设备；</li>
<li>添加路由规则：所有从 <code>eth1</code>（下游网关接口）进入的流量，查 table 100 进行路由。</li>
</ul>
<p>这样，下游网关发出的流量就会通过 PPPoE 隧道转发到公网，而 OpenWrt 自身的管理流量不受影响。</p>
<blockquote>
<p>[!IMPORTANT]
这里所有的 IP 变更都是通过 <code>ip</code> / <code>ifconfig</code> 命令直接操作的，<strong>不修改 <code>/etc/config/network</code></strong>。OpenWrt 重启后会自动恢复到默认配置，下次 PPPoE 拨号成功后重新执行这些操作即可。这是刻意的设计，保证系统的可恢复性。</p>
</blockquote>
<h3>Step 3：将 eth1 配置为下游网关的网关</h3>
<pre><code class="language-bash">ifconfig eth1 down
ifconfig eth1 "$gateway_ip" netmask "255.255.252.0" up
# eth1 的 IP 变为 58.32.0.1/22</code></pre><p>将计算出的网关 IP（如 <code>58.32.0.1</code>）配置到 eth1 上，使 OpenWrt 的 eth1「冒充」运营商的网关。这样下游网关通过 DHCP 拿到公网 IP 后，默认网关自然就指向这个地址，路由就能正常工作了。</p>
<blockquote>
<p>[!NOTE]
实测中发现直接修改 IP 有时不生效，需要先将接口 <code>down</code> 再重新配置 <code>up</code> 才能稳定成功。</p>
<p>如果你没有需要访问同子网邻居 IP 的需求，保持和运营商分配的子网掩码一致即可。如果有这个需求，你需要尽量缩短该子网大小，理论上你甚至可以用 /31 掩码——只要确保网关 IP 和你的公网 IP 在同一个子网里就行。</p>
</blockquote>
<h3>Step 4：动态调整 DHCP 范围，精准分配公网 IP</h3>
<p>前置配置中，我们已经将 <code>lann</code> 接口的 odhcpd DHCP 配置为 <code>start=2, limit=1</code>，池子里只能分配一个 IP。现在要做的是：<strong>根据当前获取到的公网 IP，动态修改 <code>start</code> 值，使得 DHCP 分配出去的那个唯一的 IP 恰好就是公网 IP。</strong></p>
<p>odhcpd 的 <code>start</code> 参数表示相对于网段起始地址的偏移量。配置完成后，重启 odhcpd 服务使其生效。</p>
<pre><code class="language-bash"># 计算公网 IP 在 /22 子网中的偏移量
# 58.32.1.100 → 子网起始 58.32.0.0 → 偏移量 = 1*256 + 100 = 356
offset=$(( i3 * 256 + i4 - (i3 & 252) * 256 ))

# 修改 odhcpd 的 DHCP 起始地址并重启
uci -q set dhcp.lann.start="$offset"
uci -q commit dhcp
/etc/init.d/odhcpd restart</code></pre><p>举个具体的例子：</p>
<ul>
<li>PPPoE 获取到 IP <code>58.32.1.100</code>，子网 <code>/22</code></li>
<li><code>eth1</code> 的网段为 <code>58.32.0.0/22</code></li>
<li>DHCP <code>start = 356</code>，<code>limit = 1</code></li>
<li>下游网关通过 DHCP 获取到 <code>58.32.0.0 + 356 = 58.32.1.100</code></li>
</ul>
<h3>Step 5：确保 PPPoE zone 不做 NAT</h3>
<p>检查 PPPoE 所在防火墙 zone 的 <code>masq</code>（NAT）配置，确保为 <code>0</code>：</p>
<pre><code class="language-bash">wan_idx=$(uci show firewall | grep -E "firewall.@zone\[[0-9]+\].name='wann'" \
          | cut -d'[' -f2 | cut -d']' -f1)
uci set firewall.@zone[$wan_idx].masq='0'
uci commit firewall
/etc/init.d/firewall reload</code></pre><p>这一步<strong>至关重要</strong>。在 Step 2 中我们已经将 PPPoE 接口的 IP 地址 <code>flush</code> 掉了，而 <code>masquerade</code> 的工作原理是用出口接口的 IP 地址作为 SNAT 源地址。此时 PPPoE 接口上已经没有 IP，<code>MASQUERADE</code> 规则找不到可用的源地址，<strong>会导致所有经过 NAT 的数据包被直接丢弃</strong>，表现为完全无法上网。因此必须将 PPPoE 所在防火墙区域的 <code>masq</code> 设为 <code>0</code>，让流量以原始源 IP（即下游网关的公网 IP）直接通过 PPPoE 隧道转发，NAT 则完全交给下游网关处理。</p>
<h3>Step 6：通知下游网关立即刷新 DHCP</h3>
<p>完成以上所有配置后，需要让下游网关立即感知到新的 IP 地址。但当 OpenWrt 重新拨号（IP 发生变化）时，下游网关并不会主动刷新 DHCP，它要等到当前 lease 到期才会重新请求。但这里碰到了一个现实问题 —— 如何让下游网关<strong>立即</strong>重新请求 DHCP？</p>
<p>笔者尝试了以下方案：</p>
<ul>
<li><strong>DHCP FORCERENEW（RFC 3203）</strong>：服务端主动通知客户端刷新 DHCP 租约。但尝试了之后，发现 UDM SE 并不支持。</li>
<li><strong>Link Down/Up</strong>：在 OpenWrt 上将 eth1 物理断开再连上，触发下游网关重新请求 DHCP。但笔者用的是 DAC 线连接两台设备的 SFP 口，link 状态变化后 UDM 并没有重新发起 DHCP 请求。</li>
</ul>
<p>最终想到一个简单快捷的方案：<strong>在 UDM SE 上部署一个简单的监听服务</strong>，当收到指定消息时，强制触发 DHCP 重新获取。</p>
<p>原理很简单：在 UDM 上用 <code>nc</code> 监听一个端口（如 99），收到 <code>dhcp_renew</code> 消息后，向 <code>udhcpc</code>（UDM 的 DHCP 客户端）发送 <code>SIGUSR2</code>（释放当前 lease）和 <code>SIGUSR1</code>（重新请求 lease）信号。</p>
<pre><code class="language-bash">while true; do
    message=$(nc -l -p 99)
    message=$(echo "$message" | tr -d "\n\r\t ")
    if [ "$message" = "dhcp_renew" ]; then
        if killall -SIGUSR2 udhcpc && killall -SIGUSR1 udhcpc; then
            logger -t "dhcp_listener" "Successfully renewed DHCP"
        fi
    fi
done</code></pre><p>在 OpenWrt 拨号成功并完成所有配置后，发送一条消息即可：</p>
<pre><code class="language-bash">echo "dhcp_renew" | nc 10.10.10.1 99</code></pre><p>IP 变化时下游网关几乎可以<strong>秒级更新</strong>。</p>
<h2>自动化执行</h2>
<p>以上所有步骤可以封装到一个 Shell 脚本中，并配置为 <strong>PPPoE 拨号成功后自动执行</strong>。在 OpenWrt 中，可以将脚本放到 <code>/etc/ppp/ip-up.d/</code> 目录下，PPPoE 每次拨号成功都会自动触发。</p>
<p>这样，无论是首次拨号还是断线重拨，Half Bridge 配置都会自动完成，实现全自动无人值守。</p>
<p>创建 <code>/etc/hotplug.d/iface/99-half-bridge</code>：</p>
<pre><code class="language-bash">#!/bin/sh
[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "ppp" ] && {
    /root/start-half-bridge.sh
}</code></pre><p>每个函数都做了幂等性处理 —— 如果当前状态已经是目标状态，就跳过操作，避免重复执行带来的问题。</p>
<h2>整体数据流</h2>
<p>配置完成后的数据流路径：</p>
<p><strong>出站（下游设备 → 互联网）：</strong></p>
<pre><code class="language-">下游设备 → UDM SE (NAT, 源IP=公网IP) → eth1 → 策略路由 table 100 → pppoe-ppp → 运营商</code></pre><p><strong>入站（互联网 → 下游设备）：</strong></p>
<pre><code class="language-">运营商 → pppoe-ppp → 路由表匹配目的IP → eth1 → UDM SE (NAT) → 下游设备</code></pre><p>前置设备全程<strong>不做 NAT</strong>，只做三层路由转发 + PPPoE 封装解封装，配合硬件流量卸载，性能开销几乎为零。</p>
<h2>写在最后</h2>
<p>这套 PPPoE Half Bridge 方案本人已经<strong>稳定运行大半年</strong>，没有出过任何问题。它本质上是把&quot;光猫 PPPoE IP Passthrough&quot;的功能在 OpenWrt 上手动实现了一遍。</p>
<p>不过说实话，这个方案确实非常 <strong>Geek</strong>，涉及到 PPPoE、DHCP、策略路由、防火墙等多个模块的联动，很难抽象成一个通用的一键脚本。每个人的网络环境、运营商分配的子网、使用的硬件都不一样，需要根据实际情况调整。</p>
<blockquote>
<p>[!NOTE]
其实我已经更换为 <strong>UCG Fiber</strong> —— Unifi 终于想通了，内置了 PPPoE 硬件加速。所以本篇算是填坑之作，给那些正在折腾 PPPoE Hardware Offload 的人指一条明路。</p>
<p>不过如果你用的是 OpenWrt XGPON 光猫这样的设备，这套方案依然很有价值 —— 你可以用它获得一个&quot;伪 IPoE&quot;宽带体验，让下游网关完全不感知 PPPoE 的存在。</p>
</blockquote>
<p>有兴趣的朋友欢迎一起讨论折腾 🛠️</p>
<hr>
<h2>附录：配置与脚本参考</h2>
<h3>网络配置（/etc/config/network）</h3>
<pre><code class="language-conf">config interface 'loopback'
    option device 'lo'
    option proto 'static'
    option ipaddr '127.0.0.1'
    option netmask '255.0.0.0'

config globals 'globals'
    option ula_prefix 'fd32:a520:fa07::/48'
    option packet_steering '1'

config device
    option name 'br-lan'
    option type 'bridge'
    list ports 'lan1'
    list ports 'lan2'
    list ports 'lan3'

config interface 'lan'
    option device 'br-lan'
    option proto 'static'
    option ipaddr '10.10.10.10'
    option netmask '255.255.255.0'
    option gateway '10.10.10.1'

config interface 'lann'
    option device 'eth1'
    option proto 'static'
    option defaultroute '0'
    option ipaddr '192.168.2.1'
    option netmask '255.255.255.0'

config device
    option name 'br-wan'
    option type 'bridge'
    list ports 'wan'
    list ports 'eth2'

config device
    option name 'wan'
    option macaddr 'fe:fe:b1:95:1f:1f'

config device
    option name 'eth2'
    option macaddr 'FE:FE:B1:95:1F:1D'

config interface 'wan'
    option device 'br-wan'
    option proto 'static'
    option ipaddr '192.168.0.2'
    option netmask '255.255.255.0'

config interface 'wan6'
    option device 'br-wan'
    option proto 'dhcpv6'

config interface 'ppp'
    option proto 'pppoe'
    option device 'br-wan'
    option ipv6 '0'
    option keepalive '0 1'
    option username 'ad12345'
    option password '12345'
    option defaultroute '0'</code></pre><h3>防火墙配置（/etc/config/firewall）关键部分</h3>
<pre><code class="language-conf">config defaults
    option input 'REJECT'
    option output 'ACCEPT'
    option forward 'REJECT'
    option synflood_protect '1'
    option flow_offloading '1'
    option flow_offloading_hw '1'

config zone
    option name 'lann'
    option input 'ACCEPT'
    option output 'ACCEPT'
    option forward 'ACCEPT'
    list network 'lann'

config zone
    option name 'wann'
    list network 'ppp'
    option input 'REJECT'
    option output 'ACCEPT'
    option forward 'REJECT'
    option mtu_fix '1'

config forwarding
    option src 'lann'
    option dest 'wann'

config forwarding
    option src 'wann'
    option dest 'lann'</code></pre><h3>DHCP 配置（/etc/config/dhcp）关键部分</h3>
<pre><code class="language-conf">config odhcpd 'odhcpd'
    option maindhcp '1'
    option leasefile '/tmp/hosts/odhcpd'
    option leasetrigger '/usr/sbin/odhcpd-update'
    option loglevel '4'

config dhcp 'lann'
    option interface 'lann'
    option dhcpv4 'server'
    option leasetime '60s'
    option start '2'
    option limit '1'
    option dns '223.5.5.5'</code></pre><h3>PPPoE Half Bridge 启动脚本（/etc/ppp/ip-up.d/start-half-bridge.sh）</h3>
<pre><code class="language-bash">#!/bin/ash

# 日志函数
log() {
    echo "$1"
    logger -t pppoe-half-bridge "$1"
}

# 获取 WAN IP 并验证
get_wan_ip() {
    local wan_ip=$(ifstatus ppp | jsonfilter -e '@["ipv4-address"][0].address')
    if [ -z "$wan_ip" ]; then
        log "获取 WAN IP 失败"
        exit 1
    fi
    echo "$wan_ip"
}

# 计算网关地址（22位子网掩码）
get_gateway_address() {
    local ip="$1"
    IFS=. read i1 i2 i3 i4 &lt;&lt; EOF
$ip
EOF
    echo "$i1.$i2.$(( i3 & 252 )).1"
}

# 计算 DHCP 起始地址偏移量（22位子网掩码）
get_ip_offset() {
    local ip="$1"
    IFS=. read i1 i2 i3 i4 &lt;&lt; EOF
$ip
EOF
    echo "$(( i3 * 256 + i4 - (i3 & 252) * 256 ))"
}

# 配置 DHCP 服务
configure_dhcp_service() {
    local start="$1"
    local current_start=$(uci -q get dhcp.lann.start)
    
    if [ "$current_start" = "$start" ]; then
        log "DHCP 无需配置"
        return
    fi
    
    uci -q set dhcp.lann.start="$start"
    uci -q commit dhcp
    /etc/init.d/odhcpd restart
    log "DHCP 配置完成"
}

# 配置防火墙
configure_firewall() {
    wan_idx=$(uci show firewall | grep -E "firewall.@zone\[[0-9]+\].name='wann'" | cut -d'[' -f2 | cut -d']' -f1)
    
    if [ -z "$wan_idx" ]; then
        log "未找到 wan zone"
        return
    fi

    masq_val=$(uci -q get firewall.@zone[$wan_idx].masq)
    if [ "$masq_val" = "0" ]; then
        log "防火墙无需配置"
        return
    fi
    uci set firewall.@zone[$wan_idx].masq='0'
    uci commit firewall
    /etc/init.d/firewall reload
    log "防火墙配置完成"
}

# 配置 eth1 IP 地址
configure_eth1_ip() {
    local gateway_ip="$1"
    local current_ip=$(ip addr show dev eth1 | grep -w "inet" | awk '{print $2}')
    
    if [ "$current_ip" = "$gateway_ip/22" ]; then
        log "eth1 IP 无需配置"
        return
    else
        ifconfig eth1 down 
        ifconfig eth1 "$gateway_ip" netmask "255.255.252.0" up
        log "eth1 IP 配置完成"
    fi
}

# 主函数
main() {
    local wan_ip=$(get_wan_ip)
    local gateway_ip=$(get_gateway_address "$wan_ip")
    log "WAN IP: $wan_ip, 网关: $gateway_ip"

    # Step 1 & 2: 清除 PPPoE 接口 IP，建立策略路由
    ip addr flush dev pppoe-ppp
    ip route add default dev pppoe-ppp table 100
    ip rule add iif eth1 table 100
    log "PPPoE 接口配置完成"

    # Step 3: 将 eth1 配置为下游网关的网关
    configure_eth1_ip "$gateway_ip"

    # Step 4: 动态调整 DHCP 范围
    configure_dhcp_service "$(get_ip_offset "$wan_ip")"

    # Step 5: 确保 PPPoE zone 不做 NAT
    configure_firewall

    # Step 6: 通知下游网关刷新 DHCP
    echo "dhcp_renew" | nc 10.10.10.1 99
    log "UDM DHCP 刷新完成"
}

# 执行主函数
main</code></pre><h3>UDM SE 上的 DHCP 监听服务（dhcp_listener.service）</h3>
<p>部署到 <code>/etc/systemd/system/dhcp_listener.service</code>，启用后可接收 OpenWrt 的 DHCP 刷新通知：</p>
<pre><code class="language-ini">[Unit]
Description=DHCP Listener Service

[Service]
User=root
ExecStart=/bin/bash -c 'while true; do \
    message=$(nc -l -p 99); \
    message=$(echo "$message" | tr -d "\n\r\t "); \
    if [ "$message" = "dhcp_renew" ]; then \
        if killall -SIGUSR2 udhcpc && killall -SIGUSR1 udhcpc; then \
            logger -t "dhcp_listener" "Successfully renewed DHCP"; \
        fi; \
    fi; \
done'
Restart=always

[Install]
WantedBy=multi-user.target</code></pre>
          <p style='text-align: right'>
          <a href='https://noting.me/posts/geek/pppoe-half-bridge#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285384</guid>
  <category>post</category>
<category>Art of Geek</category>
 </item>
  <item>
    <title>我的家庭 DNS 配置方案</title>
    <link>https://noting.me/posts/geek/my-home-dns-configuration</link>
    <pubDate>Sun, 02 Feb 2025 14:20:41 GMT</pubDate>
    <description>家庭组网概述

先简单介绍下家庭组网情况，目前主要是使用一台铭凡 MS-01 作为 All in B</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/geek/my-home-dns-configuration'>https://noting.me/posts/geek/my-home-dns-configuration</a></blockquote>
          <h2>家庭组网概述</h2>
<p>先简单介绍下家庭组网情况，目前主要是使用一台铭凡 MS-01 作为 <del>All in Boom</del> 的硬件载体。MS-01 自带两个 10G SFP 口，大大增加了可玩性。为了摆脱厚重的运营商光猫，我购置了 10G EPON 猫棒（剑桥 XE-99S）供 MS-01 直接拨号上网。MS-01 的另一个 SFP 口通过 DAC 线缆下联兮克的 2.5G 交换机，再外挂多个小米、红米的 BE7000、AX6000 等无线路由设备，构成完整的家庭网络。</p>
<p>为充分利用 MS-01 的性能，我安装了 PVE（Proxmox Virtual Environment）作为其虚拟化平台，内部主要运行以下几个虚拟实例：</p>
<ol>
<li><strong>RouterOS</strong>：软路由，通过猫棒直接拨号上网。上海电信的配置相对简单，只需在猫棒中配置正确的 LOID、在 RouterOS 中配置正确的宽带账号密码即可正常拨号上网，没有特别的技术难点。</li>
<li><strong>Main Server</strong>：Debian 服务器，暴露多个端口与 VPS 连接，同时作为外网访问内网的入口点，通过该实例内的服务可以从外部连入内网。</li>
<li><strong>DNS Server</strong>：Debian 服务器，运行 AdGuardHome 与 SmartDNS，用于提供家庭内无污染的 DNS 服务。</li>
</ol>
<h3>从 ImmortalWrt 到 DAE</h3>
<p>作为一个坚定的 Surge 用户，长期以来我并不需要过多折腾家庭 DNS 方案，因为 Surge 已经能很好地处理 DNS 解析和流量管理。但在日常使用过程中，确实遇到了部分家庭设备（Linux、Windows）需要正常访问外网的情况，因此我曾添加了一个 ImmortalWrt 实例来解决这个问题。</p>
<p>然而，ImmortalWrt 的各种插件相对混乱，大多提供整套解决方案，出现问题时不易排查，有时会遇到无法正常访问互联网的情况。于是我开始寻找更简单可控的替代方案，最终找到了性能更好（却并不十分知名）的 <a href="https://github.com/daeuniverse/dae">DAE</a>。</p>
<p>DAE 在内核层面对流量进行分流，能有效提高直连流量的性能。由于 DAE 使用的不是我熟悉的 Fake IP 模式，出于 DNS 配置的洁癖，我开始尝试搭建无污染的家庭 DNS 服务。</p>
<h2>DNS 需求分析</h2>
<p>在开始构建 DNS 方案前，我的需求非常明确：</p>
<ol>
<li>基本的 DNS 广告过滤功能</li>
<li>DNS 服务器能够快速返回结果，减少等待时间</li>
<li>DNS 服务器能够返回精确的结果：解析的 IP 应当是最优的，延迟最低且无污染</li>
</ol>
<p>经过一段时间的调研和实践，我最终选择了 AdGuardHome 作为前置广告过滤器，SmartDNS 作为主 DNS 请求服务的组合方案。</p>
<h2>方案选择理由</h2>
<h3>广告过滤：为什么选择 AdGuardHome</h3>
<p>虽然几乎所有 DNS 服务器都具备广告域名过滤能力，但 AdGuardHome 作为一个成熟稳定的广告过滤工具，具有以下优势：</p>
<ul>
<li>提供可视化界面，可以查看所有 DNS 请求记录</li>
<li>规则更新简单直观</li>
<li>配置灵活，可定制性强</li>
</ul>
<p></p>
<h3>DNS 服务器：SmartDNS vs MosDNS</h3>
<p>目前网上流行的 DNS 服务器主要有两个选择：SmartDNS 和 MosDNS。</p>
<table>
<thead>
<tr>
<th><strong>功能</strong></th>
<th><strong>SmartDNS</strong></th>
<th><strong>MosDNS</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>简介</strong></td>
<td>高性能 DNS 解析加速工具，提供多服务器查询测速最优 IP</td>
<td>模块化 DNS 解析工具，适用于复杂 DNS 需求</td>
</tr>
<tr>
<td><strong>主要特点</strong></td>
<td>支持多服务器并发查询，测速返回最优的结果</td>
<td>规则引擎灵活，支持 DNS 路由降级等高级功能</td>
</tr>
</tbody></table>
<p>SmartDNS 的一个巧妙设计是：当对多个 DNS 服务器并发查询新域名时，它会先返回 TTL=3 秒的最快结果，确保客户端能够第一时间收到回答。同时，它会对所有 DNS 服务器返回的结果进行测速（通过 TCP 或 ICMP），缓存其最优结果。当 3 秒后客户端 DNS 记录过期，再次发起查询时，SmartDNS 就会直接返回缓存里的最优结果。</p>
<p>在短时间试用两者后，考虑到个人需求，我最终选择了 SmartDNS。在我的使用场景下，可能并不需要 DNS 降级等相对复杂的逻辑，而 DNS 测速功能则更为重要。</p>
<h2>关于 DNS 泄漏</h2>
<p>我个人认为，不必过度关注 DNS 泄漏问题，更应该关注 DNS 解析结果的准确性与稳定性。虽然提及 DNS 泄漏这个话题可能会引发无休止的争论，但我认为：如果第三方真的要分析你的流量，只需嗅探 TLS 数据包就能做到，没必要为了保证 DNS 100% 无泄漏而牺牲 DNS 解析的速度和体验。对我而言，只需确保敏感域名不在国内递归 DNS 服务器进行解析即可。</p>
<h2>DNS 请求策略</h2>
<p>结合 SmartDNS 的特性和我的实际需求，我的 DNS 请求策略如下：</p>
<ol>
<li><strong>国内域名</strong>：只使用国内递归 DNS 服务器解析，进行测速并使用最优结果</li>
<li><strong>国外敏感域名</strong>：只使用国外递归 DNS 服务器解析，不进行测速</li>
<li><strong>国外普通域名</strong>：只使用国外递归 DNS 服务器解析，进行测速并使用最优结果</li>
<li><strong>未分类域名</strong>：同时使用国内外递归 DNS 服务器解析，进行测速并使用最优结果</li>
</ol>
<h3>策略解析</h3>
<ul>
<li><strong>国内域名</strong>：对于国内复杂的网络环境，国内 DNS 服务器做了针对性优化，更适合国情。配合 SmartDNS 的测速能力，可获取最优解析结果。</li>
<li><strong>国外敏感域名</strong>：只使用国外递归 DNS 服务器解析，注意这里不能使用无加密的 DNS 协议。常见的 DOH（DNS over HTTPS）服务基本已被干扰，这里可能需要自建或使用 Cloudflare 提供的 ZeroTrust DNS 服务。由于国外敏感域名解析出的 IP 大多已被墙，且大部分科学上网工具会优先在 VPS 上再次解析请求的域名，因此无需进行测速。</li>
<li><strong>国外普通域名</strong>：能确定是国外域名的，只使用国外递归 DNS 服务器解析，防止国内结果造成干扰。</li>
<li><strong>未分类域名</strong>：由于难以准确分辨其属于国内还是国外（特别是使用 CDN 的网站），因此同时使用国内外 DNS 服务器解析，经测速后使用最优结果。因为我家使用的是电信精品网，对国外网站也有相对稳定的访问速度（尤其是香港地区）。对于 CDN 相关资源，使用 Cloudflare DNS（Anycast 技术会选择香港节点）时，也可能返回一些香港的 IP，这些 IP 可以与国内 DNS 服务器返回的 IP 一起参与&quot;竞赛&quot;，从中选出最优结果。</li>
</ul>
<h2>具体配置详情</h2>
<h3>AdGuardHome 配置</h3>
<p>AdGuardHome 的配置相对简单：</p>
<ol>
<li>将上游 DNS 地址改为 <code>127.0.0.1:153</code>（指向 SmartDNS）</li>
<li>关闭 AdGuardHome 内的所有缓存策略，将缓存管理完全交由 SmartDNS 处理</li>
<li>按需添加广告过滤规则，我个人使用了 <a href="https://ruleset.skk.moe/Internal/reject-adguardhome.txt">SKK 大佬维护的规则</a></li>
</ol>
<h3>SmartDNS 配置</h3>
<p>我将 SmartDNS 内的 DNS 服务器分成三组：</p>
<ol>
<li><strong>direct 分组</strong>：主要处理国内域名，直接使用 UDP DNS</li>
</ol>
<pre><code class="language-"># Aliyun
server 223.5.5.5 -group direct
# DNSPod
server 119.29.29.29 -group direct</code></pre><ol>
<li><strong>proxy 分组</strong>：处理国外敏感域名，使用加密的 DOH</li>
</ol>
<pre><code class="language-"># Cloudflare ZeroTrust
server-https https://xxxx.cloudflare-gateway.com/dns-query -group proxy</code></pre><ol>
<li><strong>global-lite 分组</strong>：主要处理 NTP 相关域名，这类域名请求量巨大，为避免消耗上述 Cloudflare worker 的额度限制，直接使用 Cloudflare 的 UDP DNS 服务</li>
</ol>
<pre><code class="language-">server 1.1.1.1 -group global-lite
server 1.0.0.1 -group global-lite</code></pre><h3>域名规则配置</h3>
<p>我使用以下域名列表来区分不同类型的域名：</p>
<table>
<thead>
<tr>
<th>列表</th>
<th>说明</th>
<th>链接</th>
</tr>
</thead>
<tbody><tr>
<td>direct-list</td>
<td>直连列表</td>
<td><a href="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt">https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt</a></td>
</tr>
<tr>
<td>cn-apple-list</td>
<td>中国大陆的 apple 域名列表，日常使用苹果设备比较多，单独处理下</td>
<td><a href="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/apple-cn.txt">https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/apple-cn.txt</a></td>
</tr>
<tr>
<td>cn-cdn-list</td>
<td>国内 CDN 域名列表</td>
<td><a href="https://raw.githubusercontent.com/pmkol/easymosdns/main/rules/cdn_domain_list.txt">https://raw.githubusercontent.com/pmkol/easymosdns/main/rules/cdn_domain_list.txt</a></td>
</tr>
<tr>
<td>proxy-list</td>
<td>国外域名列表</td>
<td><a href="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/proxy-list.txt">https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/proxy-list.txt</a></td>
</tr>
<tr>
<td>gfw-list</td>
<td>国外敏感域名列表</td>
<td><a href="https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt">https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt</a></td>
</tr>
</tbody></table>
<p><strong>注意</strong>：下载的域名列表文件需要进行处理，以符合 SmartDNS 的规则格式：</p>
<ol>
<li>将每行开头的 <code>full:</code> 替换为 <code>-.</code></li>
<li>删除每行开头的 <code>domain:</code></li>
<li>遇到开头是 <code>regexp:</code> 的行，直接忽略</li>
</ol>
<p>为确保 SmartDNS 的规则保持最新，可以编写定时任务定期更新这些列表文件，并重启 SmartDNS 服务使更新生效。</p>
<h3>完整配置</h3>
<h4>SmartDNS 配置文件</h4>
<pre><code class="language-yaml">log-level warn

bind :153

cache-file /etc/smartdns/smartdns.cache
cache-persist yes
cache-checkpoint-time 21600
prefetch-domain yes

force-qtype-SOA 65
force-AAAA-SOA yes
dualstack-ip-selection no
tcp-idle-time 300

server 223.5.5.5 -bootstrap-dns
# --- Direct Server 列表 --- #
# Aliyun
server 223.5.5.5 -group direct
# DNSPod
server 119.29.29.29 -group direct

# --- Proxy Server 列表 --- #
server-https https://xxxx.cloudflare-gateway.com/dns-query -group proxy

# --- global-lite Server 列表 --- #
server 1.1.1.1 -group global-lite
server 1.0.0.1 -group global-lite

# --- 域名配置列表 --- #
domain-set -t list -name direct-list -file /etc/smartdns/rules/direct-list.txt
domain-set -t list -name cn-apple-list -file /etc/smartdns/rules/cn-apple-list.txt
domain-set -t list -name cn-cdn-list -file /etc/smartdns/rules/cn-cdn-list.txt
domain-set -t list -name proxy-list -file /etc/smartdns/rules/proxy-list.txt
domain-set -t list -name gfw-list -file /etc/smartdns/rules/gfw-list.txt

# 本地域名只请求 direct，测速并使用最优结果
domain-rules /domain-set:cn-apple-list/ -nameserver direct
domain-rules /domain-set:cn-cdn-list/ -nameserver direct
domain-rules /domain-set:direct-list/ -nameserver direct

# 国外域名只请求 proxy，测速并使用最优结果
domain-rules /domain-set:proxy-list/ -nameserver proxy

# 国外敏感域名不要测速，只请求 proxy；注意顺序，放最后覆盖前面的配置
domain-rules /domain-set:gfw-list/ -nameserver proxy -speed-check-mode none

# 一些调用非常频繁的请求，直接用 global-lite
domain-rules /in-addr.arpa/ -nameserver direct
domain-rules /time.apple.com/ -nameserver global-lite
domain-rules /time.asia.apple.com/ -nameserver global-lite
domain-rules /pool.ntp.org/ -nameserver global-lite
domain-rules /time-ios.apple.com/ -nameserver global-lite

# 兜底：剩下的域名请求 proxy 与 direct，测速并使用最优结果</code></pre><h4>域名规则更新脚本</h4>
<p>简单写了一个脚本用于每天拉取最新的规则，处理后如果发现有更新，就重启 <code>SmartDNS</code>。简单配置在 <code>crontab</code> 中，每天更新一次。</p>
<pre><code class="language-python">#!/usr/bin/env python3

import os
import sys
import shutil
import requests
from datetime import datetime
from pathlib import Path

def log(msg):
    print(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {msg}")

def process_url(url, filename):
    script_dir = Path(__file__).parent.absolute()
    rules_dir = script_dir / "rules"
    rules_dir.mkdir(exist_ok=True)
    
    log(f"Downloading URL: {url}")
    
    try:
        resp = requests.get(url)
        resp.raise_for_status()
    except requests.exceptions.RequestException as e:
        log(f"Failed to download URL: {url} (Error: {str(e)})")
        return False
        
    log("Processing file content...")
    
    processed_lines = []
    for line in resp.text.splitlines():
        if line.startswith("full:"):
            processed_lines.append(line.replace("full:", "-."))
        elif line.startswith("domain:"):
            processed_lines.append(line.replace("domain:", ""))
        elif line.startswith("regexp:"):
            continue
        else:
            processed_lines.append(line)
            
    if not processed_lines:
        log("New file is empty. No update needed.")
        return False
        
    target_file = rules_dir / filename
    new_content = "\n".join(processed_lines) + "\n"
    
    if target_file.exists():
        with open(target_file) as f:
            old_content = f.read()
        if old_content == new_content:
            log("No differences found. No update needed.")
            return False
            
    log(f"{'Updating' if target_file.exists() else 'Creating'} file: {target_file}")
    with open(target_file, "w") as f:
        f.write(new_content)
    return True

def main():
    urls = [
        ("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt", "direct-list.txt"),
        ("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/apple-cn.txt", "cn-apple-list.txt"),
        ("https://raw.githubusercontent.com/pmkol/easymosdns/main/rules/cdn_domain_list.txt", "cn-cdn-list.txt"),
        ("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/proxy-list.txt", "proxy-list.txt"),
        ("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/gfw.txt", "gfw-list.txt")
    ]
    
    updated = False
    for url, filename in urls:
        if process_url(url, filename):
            updated = True
            
    print("-" * 18)
    if updated:
        log("Updates detected. Restart smartdns")
        os.system("service smartdns restart")
    else:
        log("No updates detected.")

if __name__ == "__main__":
    main()</code></pre><h2>总结</h2>
<p>这套 DNS 配置方案经过实际使用，能够很好地满足我对家庭网络的需求：干净无广告、响应迅速、解析精准。通过组合使用 AdGuardHome 和 SmartDNS，既保证了广告过滤的效果，又能够智能选择最佳的 DNS 解析结果。</p>
<p>当然，每个人的网络环境和需求不同，可以根据自己的实际情况进行调整和优化。欢迎和我一起讨论~</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/geek/my-home-dns-configuration#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285383</guid>
  <category>post</category>
<category>Art of Geek</category>
 </item>
  <item>
    <title>Intel® NUC10 软路由历险记</title>
    <link>https://noting.me/posts/geek/nuc-all-in-one</link>
    <pubDate>Mon, 10 Aug 2020 09:44:03 GMT</pubDate>
    <description>最近入了软路由的坑，入坑过程如下：

家里办了上海电信企业宽带，叠加精品网。上行 100Mbps，下</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/geek/nuc-all-in-one'>https://noting.me/posts/geek/nuc-all-in-one</a></blockquote>
          <p>最近入了软路由的坑，入坑过程如下：</p>
<ol>
<li>家里办了上海电信企业宽带，叠加精品网。上行 100Mbps，下行 500Mbps。好马配好鞍，准备把之前凑数的 K3 换了。</li>
<li>就这样进入了软路由的世界，一开始的打算是买个 3850U CPU 附近价位的，于是最终挑了一只 J4150 工控机。光速入手，让卖家帮忙刷了 esxi 并做了 WAN 口直通。领回家装了 ROS 和 Debian：ROS 不用说用作主路由（毕竟稳定）；Debian 当个服务器，主要干**的事情。</li>
<li>就这样相安无事满意了一星期，内心又开始躁动了。躁动的原因是看到 J4105 跑满了 500M 后，CPU 占用率也快满了。强迫症如我当然不能忍，于是寻求更高端（gui）的解决方案。</li>
<li>我要求挺高，首先路由要小，其次性能要好，最好是工控机。从淘宝里面众多工控机里全量进行了筛选，7500U、8250U 等等的进入我的视野。但是，这些奇奇怪怪牌子的工控机，首先品质无法保障，我还是希望追求稳定；其次就是都不怎么好看（可能是主要原因）……</li>
<li>然后我找了找品牌厂商，终于淘到了索泰 MI643 和本文主角 NUC10。MI643 双网口（大声告诉我，可以做什么），不过据说是螃蟹的，心有芥蒂，其次还很贵，当时天猫活动准系统大概 2850 左右。NUC10 寒霜峡谷售价大概在 2450（咸鱼），单网口令人无奈，其他没什么缺陷。又挣扎了一天，逛了逛各大软路由论坛，实际上 NUC10+千兆网卡也算是一个可行的选择。</li>
<li>本着 NUC10 人气高，出问题也好解决的方针，最终还是入了寒霜峡谷 NUC10i5FNK。当天拼多多入手了 2.5G 网卡，京东入手了西部数据蓝盘 250G，拆了俩 iMac 上的内存条，正式走进了 NUC 的世界。</li>
</ol>
<h3>先小小做个总结：</h3>
<p>经过一星期的使用下来，NUC10i5FNK 做软路由，除了一开始稍微折腾一下之外，之后还是非常稳定的。</p>
<p>优点：</p>
<ol>
<li>外观好看，小巧，整体体积 115*112*38 真的是无人能敌。</li>
<li>没了，好看就够了。说笑了~性能强大，2450 的价位相对于工控机来说也不至于贵的离谱，毕竟三年保修。</li>
</ol>
<p>缺点：</p>
<ol>
<li>当然是只有一个网口，如果要当软路由只能配网关交换机，或者加一张 USB 网卡。我用的是 USB 网卡方案，相对来说网卡会比较烫，不过速度上没什么问题。</li>
</ol>
<h2>正式开始历险</h2>
<p>NUC10 拿到手，真的是好小。拆机非常方便，把新鲜到手的硬盘和内存插上，开机，插入之前写好 esxi 6.7 安装文件的 U 盘开始安装，于是碰到了第一个坑。</p>
<h3>坑一：ESXI 6.7 原生不支持 NUC10 网卡</h3>
<p>安装到一半，ESXI 提示找不到网卡所以安装终止。</p>
<p>这坑非常好解决，Google 搜索“NUC10 ESXI” <a href="https://andrewroderos.com/vmware-esxi-home-lab-intel-nuc-frost-canyon/">第一个结果</a>就是，傻瓜化的教程。总结就是 ESXI 默认的 NE1000 驱动不支持 NUC10 的网卡，不过开发者已经适配好了，但是没有合入 ESXI 的主干，需要自己重新集成进去。</p>
<p>集成驱动<a href="https://download3.vmware.com/software/vmw-tools/Intel-NUC-ne1000_0.8.4-3vmw.670.0.0.8169922-offline_bundle-16654787.zip">地址如下</a> ，因为我是 Mac，所以开了个 Windows 虚拟机集成了一下。注意，Mac 的 Powershell 不支持。</p>
<p>重新写入了 U盘，ESXI 至此安装成功。</p>
<h3>坑二：ESXI 不支持 2.5Gbps USB 网卡</h3>
<p>实际上，在购买之前我已经看了 ESXI 网卡的支持情况，需要安装额外的驱动来支持。主要有两个选择：<a href="https://flings.vmware.com/usb-network-native-driver-for-esxi">第一个</a>是 VMWare Flings，看起来有着半官方背景；<a href="https://www.devtty.uk/homelab/USB-2.5G-Ethernet-driver-support-for-ESXi-6.5-and-6.7/">第二个</a>是用户 Gomes 自己编译的 RTL8152 系列的驱动，但是怎么使用似乎没怎么说清楚（至少作为一个 ESXI 新手我走了很多弯路）。</p>
<p>VMWare Flings 驱动支持千兆的 USB 网卡（例如ASIX88179、RTL8152），比如某联的就完美支持，但是它不支持 2.5Gpbs 的网卡 RTL8156；Gomes 编译的驱动理论上支持 RTL815x 系列。由于我买的某联是 RTL8156，所以只能选择后者。</p>
<p>中途踩了不少坑，直接说驱动 RTL8156 网卡一条正确的途径吧：</p>
<ol>
<li>安装 vib，驱动在上面链接是有。可以选择在集成 ESXI 的时候直接将 vib 集成进去，也可以在安装后 ESXI 后，将 vib 文件通过 scp 传入到 ESXI 上，在命令行安装，重启。</li>
</ol>
<pre><code class="language-shell">esxcli software vib install -v xx.vib # xx.vib 为文件名</code></pre><ol start="2">
<li>最最最关键的一步，关闭 vmkusb。ESXI 新版本用 vmkusb 替代了usb、usbnet、usb-storage 等废弃模块，但是安装的 r8152 驱动依然是基于 usbnet 的。</li>
</ol>
<pre><code class="language-shell">esxcli system module set -m=vmkusb -e=FALSE</code></pre><ol start="3">
<li>重启后，检查 r8152 和 usbnet 是否加载成功。按照作者的说法，在 vmkusb 关闭了之后，usbnet 等会自动进行加载。但是我这边却不会自动加载，只能手动去 <code>vmkload_mod</code>。试过了 <code>esxcfg-module</code> 等命令，但是还是无法成功。如果有同学知道原因，麻烦告知下~</li>
</ol>
<pre><code class="language-shell">vmkload_mod -l | egrep "r8152|usbnet" # 检查 r8152 和 usbnet 的加载情况
# 如果没自动加载，执行以下两句试试
vmkload_mod usbnet
vmkload_mod r8152</code></pre><ol start="4">
<li>理论上这时候就可以在 ESXI 网页上看到网卡啦，或者你也可以通过命令行查看：</li>
</ol>
<pre><code class="language-shell">esxcli network nic list</code></pre><ol start="5">
<li>设置自启动，因为 usb 网卡官方没有支持，所以在重启之后，vSwitch 的关联会掉，所以需要在重启之后进行关联。主要参考了 VMWare Flings 的脚本，需要修改 <code>/etc/rc.local.d/local.sh</code> 文件，加入以下内容</li>
</ol>
<pre><code class="language-shell"># 加载模块（如果你自动加载了，那就不需要这两句了）
vmkload_mod usbnet
vmkload_mod r8152

# 我的 USB 网卡名是 vmnic32，这个可以通过上面第四点拿到，各位根据需要替换成自己的网卡名称。
vusb0_status=$(esxcli network nic get -n vmnic32 | grep 'Link Status' | awk '{print $NF}')
count=0
while [[$count -lt 20 && "${vusb0_status}" != "Up"]]
do
    sleep 1
    count=$(( $count + 1 ))
    vusb0_status=$(esxcli network nic get -n vmnic32 | grep 'Link Status' | awk '{print $NF}')
done
# 等待网卡上线之后，绑定 vSwitch
if ["${vusb0_status}" = "Up"]; then
    esxcfg-vswitch -L vmnic32 vSwitch1
    #esxcfg-vswitch -M vmnic32 -p "Management Network" vSwitch1
    esxcfg-vswitch -M vmnic32 -p "VM Network1" vSwitch1
fi</code></pre><p>至此，ESXI 可以正常识别网卡，将NUC 自带网卡设置直通也是没问题的，不过感觉 NUC 这性能应该也不用。于是乎，ESXI 历险告一段落……</p>
<h2>第二次历险：</h2>
<p>因为 ESXI 天然不支持 USB 网卡，所以是不是可以考虑下天然支持 ESXI 的 Proxmox VE 呢？内心又开始蠢蠢欲动，在一个夜黑风高的夜晚，我用一张写入了 PVE 最新版的 U 盘，成功把自己弄得没网了。</p>
<p>事先我也是查了资料的，PVE USB 网卡资料会少一些，因为毕竟是天然支持，所以我想当然的认为我的 RTL8156 也支持了。事实上，我在装完 PVE 的时候，确实看到了两张网卡（一张内置的，一张 USB 网卡），内心非常兴奋。我尝试了把 USB 网卡当做 WAN 给 ROS 进行拨号，却始终拨号不成功。不死心把 NUC 内置网卡当做 WAN，USB 网卡当做 LAN，拨号成功了，但是上不了网，并且 USB 网卡协商速率只有 10Mbps…</p>
<p>因为我是 PVE 的初初初学者，ESXI 好歹也折腾了一下 J4105 软路由，PVE 就完全没经验了。最终还是在 syslog 里发现了端倪，USB 网卡在不断的 connect、disconnect，于是猜测是驱动问题了。</p>
<h3>坑三：PVE 原生不支持 RTL8156 网卡</h3>
<p>从 Github 上找到了<a href="https://github.com/wget/realtek-r8152-linux">驱动</a>，需要自己编译下，编译其实很简单，安装头文件、依赖后编译即可：</p>
<pre><code class="language-shell"># 安装 pve headers
wget http://download.proxmox.com/debian/pve/dists/buster/pvetest/binary-amd64/pve-headers-5.4.34-1-pve_5.4.34-2_amd64.deb
dpkg -i pve-headers-5.4.34-1-pve_5.4.34-2_amd64.deb
# 下载驱动
wget https://github.com/wget/realtek-r8152-linux/archive/v2.13.20200712.tar.gz
tar zxvf v2.13.20200712.tar.gz
cd realtek-r8152-linux-2.13.20200712/
# 安装编译工具
apt install build-essential libelf-dev -y
# 编译后，可以拿到 r8152.ko module 文件
make
# 顺便找了下原 r8152 的 module，直接替换了，省心。（记得把原文件备份下）
cp r8152.ko /usr/lib/modules/5.4.34-1-pve/kernel/drivers/net/usb/r8152.ko</code></pre><p>重启，发现 PVE 能够正常识别 RTL8156 网卡了，ROS 下也可以把 USB 网卡作为 WAN 拨号使用了。至此，PVE 方案也比较完美解决了。啰嗦一点，PVE 下 ROS 网卡的模型最好选 <code>VirtIO</code> ，选了 Intel E1000 发现跑不满。</p>
<h2>最后</h2>
<ol>
<li>如果没有 2.5Gbps 的需求，还是乖乖买千兆网卡吧，比如某联的 RTL8152 就挺好。我试了下 Dell DA300，也能被 PVE 完美识别。</li>
<li>NUC10 做一台 All in One 的设备来说，性能是完全足够的。同样的隧道跑满 500M，NUC10 下 CPU 占用仅为 35%，非常够用。单网口是 NUC10 的缺陷，但是用 USB 网卡完全可以弥补。目前 PVE 连续运行 6 天了，还没发现什么问题。</li>
<li>如果有什么 ESXI、PVE 网卡驱动上的问题（包括索要 PVE 网卡编译的 ko module），欢迎留言或者邮件。</li>
</ol>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/geek/nuc-all-in-one#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285381</guid>
  <category>post</category>
<category>Art of Geek</category>
 </item>
  <item>
    <title>Copy Pods Resource 导致app打包失败的暂时解决方案</title>
    <link>https://noting.me/posts/code/cocoapods-resource</link>
    <pubDate>Tue, 12 Jun 2018 22:25:00 GMT</pubDate>
    <description>起因

最近在推进app组件模块化，但是从某一天开始，打包机上的Jenkins再也无法成功archi</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/code/cocoapods-resource'>https://noting.me/posts/code/cocoapods-resource</a></blockquote>
          <h3>起因</h3>
<p>最近在推进app组件模块化，但是从某一天开始，打包机上的Jenkins再也无法成功archive。Jenkins返回的错误码是65，虽说是错误但是并没有错误日志，就这么神奇地<code>Build Failed</code>了。</p>
<pre><code class="language-">Verify final result code for completed build operation
Build operation failed without specifying any errors. Individual build tasks may have failed for unknown reasons.
One possible cause is if there are too many (possibly zombie) processes; in this case, rebooting may fix the problem.
Some individual build task failures (up to 12) may be listed below.</code></pre><p>既然是跟组件包相关，我们首先尝试了回退代码。但是距离上一次打包成功时间相差甚远，因此有大量的merge request被merge进主分支。在进行了一系列的尝试之后，依然打包失败，于是放弃。</p>
<p>后来我们发现在自己的电脑上是可以成功archive，但是在打包机上不行（Mac mini和Mac Pro都不行）。打包机会注入一个企业证书进行打包发布，而本地则是用普通的公司证书。所以我们猜测可能是企业证书引起的原因。对比了半天发现工程目录下有一个<code>.mobileprovision</code>文件，删掉后竟然可以成功archive！但是现实又泼了一盆冷水，第二次打包依然失败。如此看来，这个错误的发生是概率性的，且概率十分大。</p>
<p>无奈，继续看日志。在删掉项目中<code>Build Phases</code>中其他无关的项目之后，我们发现删掉之中的<code>[CP] Copy Pods Resources</code>工程就能够成功打包，加上就失败。了解了这个部分的结构后，我们猜测可能是这个阶段中<code>Input Files</code>和<code>Output Files</code>文件数过多导致的。为了验证我们的猜测，我们删掉了拆分出来的组件中的资源目录，果然打包成功。</p>
<h3>分析</h3>
<p>因为我们在组件化的过程中，为了简化拆分工作，我们只是设置了podspec中的<code>resource</code>作为资源索引目录，而没有采用cocoapods推荐的<code>resource_bundles</code>作为资源文件的载体（对原代码改动太大，容易出错）。这样cocoapods在<code>pod install</code>的时候，所有资源会直接拷贝入主项目。而cocoapods的实现方式就是在<code>Build Phases</code>中加入<code>[CP] Copy Pods Resources</code>这个阶段，之中加入了在Pod中所有的资源文件，导致这个阶段的文件数量十分庞大。这么庞大的数量可能触发了Xcode的某些bug，导致打包失败。因此，一个暂行的解决方案就是在打包时，把资源文件从Pods中移到主项目中。</p>
<h3>方案</h3>
<p>代码是Ruby写的，用了<code>Xcodeproj</code>这个项目，如下。</p>
<pre><code class="language-ruby">#!/usr/bin/ruby

require 'xcodeproj'
require 'fileutils'

def add_file_resources(direc, current_group, main_target)
	Dir.glob(direc) do |item|
		next if item == '.' or item == ".." or item == '.DS_Store'
		isAsset = item.include? ".xcassets"
		if File.directory?(item) and !isAsset
			new_folder = File.basename(item)
			created_group = current_group.new_group(new_folder)
			add_file_resources("#{item}/*", created_group, main_target)
		else 
            # i = current_group.new_file(item)
			i = current_group.new_reference(item)
			puts "[Project] add: #{item}"
			# main_target.add_file_references([i])
			main_target.add_resources([i])
		end
	end
end

def move_assets_folder(path, depth)  
	Dir.entries(path).each do |sub| 
		next if sub == '.' or sub == '..'  
		if File.directory?("#{path}/#{sub}")  
			next if depth &gt; 3
			if "#{sub}" == "Assets"
				current_project = path.split('/').last
				FileUtils.mv "#{path}/#{sub}", "./podsAsset/#{current_project}"
				puts "[Folder] #{path}/#{sub} =&gt; podsAsset/#{current_project}"
			else
				move_assets_folder("#{path}/#{sub}", depth + 1)  
			end
		end  
	end  
end

if(File.exist?("podsAsset"))
	puts "[Folder] 'podsAsset' already exist, remove it!"
	FileUtils.rm_rf("podsAsset")
end
FileUtils.mkdir_p("podsAsset")

move_assets_folder("modules", 1)

project = Xcodeproj::Project.open("./xxx.xcodeproj")
target = project.targets.first

group = project.new_group('podsAsset')
group.set_source_tree('SOURCE_ROOT')
add_file_resources("podsAsset/*", group, target)
project.save

puts "[Done]"</code></pre><p>我们一些和工程关系较密切的组件直接放在了根目录的<code>modules</code>文件夹中，因此只要把这个目录下的组件的资源文件拷入主项目应该就没有问题。具体思路就是在当前目录的<code>modules</code>文件中遍历寻找<code>Assets</code>文件夹（组件化规定），如果找到则移动到根目录下的<code>podsAsset</code>文件夹下。为了精准找到的正确的<code>Assets</code>文件夹，我设置了一个最大搜索深度3。在遍历出所有目录后，再调用<code>Xcodeproj</code>中的相关方法将目录下的所有文件加入到工程中。</p>
<p>在工程打包前先执行这个ruby脚本，设置证书后，打包成功。</p>
<h3>后记</h3>
<p>至此，问题基本解决。但是这只能算是暂时性的解决方案。我们还需要再讨论下组件化中资源文件的组织方式，如果可以的话新组件还是应该放在一个独立的bundle中。</p>
<p>除此之外，还有一个未解的问题。为什么部分机器上无法archive，而在我们开发机器中没出现这个问题。如果各位对这个问题有更加深入的了解或者有更加优雅的解决方案，欢迎告知！</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/code/cocoapods-resource#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285379</guid>
  <category>post</category>
<category>Art of Code</category>
 </item>
  <item>
    <title>VPS选购及线路推荐</title>
    <link>https://noting.me/posts/geek/vps-recommendation</link>
    <pubDate>Sat, 12 May 2018 19:49:00 GMT</pubDate>
    <description>我从16年开始接触VPS，曾购买过各式各样的商家及产品，体验了各式各样的线路。这段时间终于有空写一篇</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/geek/vps-recommendation'>https://noting.me/posts/geek/vps-recommendation</a></blockquote>
          <p>我从16年开始接触VPS，曾购买过各式各样的商家及产品，体验了各式各样的线路。这段时间终于有空写一篇博文，分享自己折腾这么久的一些心得，也算是为避免后人踩坑做一些贡献。</p>
<p>这篇博客主要是从主观上介绍一些我认为还不错的VPS商家/线路，链接不带任何aff，放心点击。因为是主观想法，难免有些偏颇或者错误，欢迎讨论。</p>
<h2>必备知识</h2>
<p>首先需要明确一点，速度快/稳定/便宜 任何一家VPS只能满足其中两者。就算某一家VPS提供商同时满足了上述三者，也会被同行挤压到跑路（社会社会）。所以性价比都只是相对而言的，加钱永远是没错的。</p>
<p>任何线路的质量都和本地环境关系密切，譬如你的运营商不同，测试时间不同，你得到的测试结果都会有所偏差。所以如果别人都能跑500Mbps而你只能跑5Mbps，你可能需要从自己本地环境去排查原因。</p>
<p>防火防盗防affman。如果你看到一家IDC有铺天盖地的评测站为其宣传，不妨去看看这家IDC给的佣金比例是多少。例如这几年很火的搬瓦工，基本就是被affman炒起来的。不过不得不说，搬瓦工的超售技术十分强大，总体还挺稳定的。有一些无良affman（不点名了）就是看哪家佣金比例高就强推哪家，呵呵。</p>
<h2>总览</h2>
<p>目前比较热门的VPS所在地主要有香港、台湾、韩国、日本、北美（LAX、SJO）、俄罗斯、新加坡、欧洲等等。接下来我会对这些地点做一些主观上的评价，并且会推荐一些商家/线路。</p>
<h4>香港</h4>
<p>香港应该是针对国人的VPS最热门的地点（没有之一），得益于其优越的位置和开放的环境。而香港的大口子VPS在这两年也日益多了起来，从之前的新立讯SV、LeaseWeb，到这两年越来越多的PCCW、HKT、WTT、HKBN，以及不得不说的CN2。</p>
<p><strong>新立讯SV</strong> : 三网直连。之前在美国的时候买过一个，因为不在国内所以没有测试。据说到国内的口子挺小的，被打立刻绕美国。主要出售商家是<a href="https://my.rfchost.com/cart.php?gid=7">RFCHost</a>，最低价格$7.99，日常没货。（在这里推荐一下RFCHost，背景强大，技术强大）</p>
<p><strong>LeaseWeb</strong> : 大口子，三网直连。没用过，风评一般。<a href="http://oneprovider.com">OneProvider</a>一直在出售，价格也很实惠（最便宜5 刀），不过IP基本上都是被墙的。<a href="https://my.rfchost.com/cart.php?gid=7">RFCHost</a>的HK2好像也是LW线路，月付$20。</p>
<p><strong>PCCW</strong> : 在这里指的是PCCW-Global，其实是与HKT是一家公司。PCCW-Global主要是IDC带宽，而HKT主要是商宽和家宽为主。如果没有说明，以下所有PCCW均指代PCCW-Global。PCCW算是国内到香港比较稳定的线路之一，也是为数不多的普通用户能够负担得起的大口子线路，性价比较高。据我所知，做香港PCCW的比较早的IDC应该是<a href="http://gigsgigscloud.com">GigsGigsCloud</a>；后来<a href="http://bwh1.net">搬瓦工</a>香港也接入了PCCW，但是性价比不高（$9.99 100G流量）；后来<a href="http://dgchost.net">DGCHost</a>也接入了自己的PCCW线路。我个人比较是推荐<a href="http://gigsgigscloud.com">GigsGigsCloud</a>的，算是处于价格、稳定性一个比较均衡的点。而<a href="http://bwh1.net">搬瓦工</a>则较贵，流量相对于<a href="http://gigsgigscloud.com">GigsGigsCloud</a>给得少很多。<a href="http://dgchost.net">DGCHost</a>则是一个让人又爱又恨的IDE，oneman经营，价格较低所以经常被人DDos，老板比较随性脾气较差。总体PCCW线路是不错的，主要考虑的就是商家的超售、以及会不会被DDos等情况了。</p>
<p><strong>HKT、WTT、HKBN</strong> : 这三者价格较高，基本上是月付几十到一百刀，流量充足（可能是无限的），非常适合机场选购。并且其IP大部分都是动态的，可以防墙。这三者都是香港的运营商，个人对其排名是HKT&gt;WTT=HKBN。不管是其综合实力还是线路稳定情况，HKT毋庸置疑排在第一，WTT和HKBN则落后一些。据说HKBN的国际带宽不是很够，WTT容易QOS，所以土豪们有实力还是上HKT吧。出售的商家有<a href="http://pumpcloud.net">Pumpcloud</a>、<a href="http://on9host.com">on9host</a>等等，最近有很多新商家冒出来，我不是土豪没有关注很多。如果你想要低价体验这些线路，就去购买最近极其热门的NAT机器吧。出售NAT机器的商家主要HostHongKong、轻云、50VZ、HKHoster、Uovz、NatHosts等等。其中HostHongKong走的是HGC线路，到大陆会被QOS，混淆大法可破。</p>
<p><strong>HK CN2</strong> : CN2全称为中国电信下一代承载网，缩写为CNCN，进一步缩写为CN2，是中国电信中优先级较高的线路。所以如果你本地运营商是电信，可以考虑选择CN2。需要明确一点，CN2确实价格很贵，具体的报价可以自己Google。香港CN2大部分应该都是沙田小水管为主，价格高性价比低（几十元 1M口子 几十G流量），适合做站。所以，如果你要选择CN2线路，请认准阿里云国际站！30M口、1T流量流量包、$9除了阿里云国际站哪里还找得到？！也亏是阿里这样的巨头才可以支撑起这样的亏损。（注意是国际站，关于国际站如何注册验证请自行Google。）</p>
<h4>台湾</h4>
<p>台湾的线路主要有Hinet、亚太、GCE、台固等等。Hinet晚高峰日常爆炸；亚太线路综合不错；如果之前没用过GCE可以绑定信用卡拿300刀额度玩一年；台固等不太了解。</p>
<p>在售的，我只推荐一家：<a href="http://vps.dct-cloud.com">云高</a>，亚太线路，原生IP，价格也能接受。不过最近好像停售了，据说6月份会以全新的面貌再次上市。</p>
<h4>韩国</h4>
<p>韩国主要是SK和KT两种线路，价格高，并且都不是非常稳定（晚高峰会炸）。</p>
<p>主要有<a href="http://kdatacenter.com">KDataCenter</a>、<a href="https://www.netdedi.com">Netdedi</a>、<a href="http://moguhost.com">蘑菇主机</a>、<a href="http://ucloudbiz.olleh.com">Ucloudbiz</a>等等，推荐<a href="http://kdatacenter.com">KDataCenter</a>、<a href="http://ucloudbiz.olleh.com">Ucloudbiz</a>，超售不严重。我本人对韩国不是非常感兴趣，毕竟到美国延迟和国内差不多（访问github等等）；而国内到韩国本身就不稳定。</p>
<h4>日本</h4>
<p>日本应该是我最喜欢的地区了，一条好的线路到国内东部只要30-40ms，而到美西只需要110ms。所以不管是访问日本当地资源还是美国资源都是最优的选择。但是日本和中国之间一直没有特别稳定的线路存在。</p>
<p>日本VPS主要是由日本当地的IDE提供，因为日本法律对主机管理比较严格，所以很多日本主机商都需要购买者进行身份验证。</p>
<p>日本主要分为五种线路：</p>
<p><strong>NTT线路</strong> : 应该是最差的线路了吧，日常爆炸（参见<a href="http://vultr.com">Vultr</a>），入门门槛低，没有推荐。</p>
<p><strong>KDDI</strong> 线路: 自从<a href="http://linode.com">Linode</a> JP1区不再对外销售之后，逐渐稳定了下来，算是当前去日本比较稳定的线路之一。但是基本上没有IDC还出售着KDDI线路了，所以不用考虑了。</p>
<p><strong>IIJ线路</strong> : 从去年开始，晚高峰电信会爆炸，联通一直都还挺稳定的。推荐的商家主要有<a href="https://www.tsukaeru.net">tsukaeru</a>、<a href="https://vps.sakura.ad.jp">樱花Sakura</a>、<a href="https://www.idcf.jp/cloud/">IDCF</a>。很遗憾，这三者都需要进行身份验证，其中IDCF的身份验证基本通不过的。前两者应该是双向IIJ，IDCF在某些地区回程会走BBTEC，所以即使是晚高峰延迟爆炸，速度依然是不错的。如果想要购买以上VPS，建议找一找代购。</p>
<p><strong>BBTEC线路</strong> : 又称软银Softbank线路，应该是目前中日间最稳定的线路了（CN2除外）。目前常见的就只有阿里云日本到国内会双向走BBTEC，其次是Internap机房在某些时候会走BBTEC。阿里云日本价格较高，购买方式主要是<a href="https://jp.alibabacloud.com">阿里云日本站</a>（最实惠）、<a href="https://www.alibabacloud.com">阿里云国际站</a>（无流量包）、<a href="http://cat.net">Cat.net</a>（方便购买）。其中阿里云日本站需要日本身份，较难认证。而出售Internap国内商家较多，比如StarryDNS、HostKVM、海星云、景文互联等等，带宽什么的好像普遍不是非常充足。之所以说Internap机房在某些时候会走BBTEC，是因为它是动态切换的，很有可能今天走了BBTEC明天走了KDDI。</p>
<p><strong>CN2线路</strong> : 真的没怎么听说有商家在出售日本CN2线路的，除了首都在线之外。但是首都在线的日本CN2据说是半程CN2，并且非常容易爆炸，所以不用考虑了。</p>
<h4>北美</h4>
<p>北美之前一直是 <strong>163</strong> 线路，后来有了优先级较高的 <strong>GT CN2</strong> 线路，后来有了优先级更高的 <strong>GIA CN2</strong> 线路。我一直想找那种速度起得很快的北美线路，但是可能受限于本地环境，始终找不到。</p>
<p>163线路我们就不谈了吧，除了晚高峰会炸之外其实时间好像也不太稳定。</p>
<p>走GT CN2线路的最突出的就是C3（Zenlayer）机房了。表现其实挺一般的，但是已经能用了。这里着重推荐下<a href="https://my.rfchost.com/">RFCHost</a>的LAX1节点，Zenlayer机房，但是回程会走GIA CN2。</p>
<p>GIA CN2应该是我们能够买到的最好的北美线路了。今年开始越来越多的商家加入了这场线路战之中。最开始做GIA CN2的是Lizcat（目前已关），后来是DGCHost，后来是安畅（RFCHost LAX2节点就是安畅机房的），后来是GigsGigsCloud，后来是Hostdare（托管在云镭机房）。除此之外，其他很多商家的GIA CN2都是托管在DGCHost中的，所以都是半斤八两。其中，安畅是最稳定的（相对于DGCHost和GigsGigsCloud来说），当后两者爆炸的时候安畅依然稳如泰山。Hostdare没买过所以不做评价。</p>
<h4>俄罗斯</h4>
<p>俄罗斯开始热门起来主要是因为大家发现了海参崴、伯力这两个地点距离北方很近，并且到日本的延迟较低（最低20ms），是一个较为理想的中转点。</p>
<p>我在南方所以没买过，推荐的商家有<a href="http://rfchost.com">RFCHost</a>和<a href="https://www.zeptovm.com/">ZeptoVM</a>。</p>
<h4>新加坡</h4>
<p>我一直觉得新加坡挺多余的，它到国内线路质量一般，到美国延迟比国内还高。嗯，不推荐。</p>
<h4>欧洲</h4>
<p>没需求，不了解，不评价。</p>
<h2>一些资源</h2>
<p>主机圈的人大多活跃在Telegram群组中，这里我推荐几个质量较高的群组/频道，群内的人都比较热心。但是建议在群组内问问题时注意问问题的方式与态度。</p>
<p><a href="https://t.me/pingcat">Affyun.com 线路讨论</a> 耳机君创立的群组，十分活跃。Affyun主要有两个网站，<a href="http://servercat.me">ServerCat</a>里是一些高质量值得记录的评测；<a href="http://ping.cat">Ping.cat</a>是线路监控网站，上面有大部分IDC的线路监测，非常适合在购买前查阅。</p>
<p><a href="https://t.me/affyunpush">Affyun - 每日推送新offers</a> 耳机君会把当前比较热门的一些商家的offer发到这个频道中，更新频繁。每个offer都会附上测试IP并且有一段相对客观的评价。</p>
<p><a href="https://t.me/express91yun">91yun优惠快讯</a> 91yun附属的频道，会推荐一些商家及offer，今年更新不怎么频繁。其实91yun还有个群组，适合聊天吹水，脸熟前建议不要问问题，会被飞机票。</p>
<p><a href="https://t.me/liyuans">Leonn的博客</a> <a href="https://liyuans.com">Leonn博客</a>开的频道，会分享包括VPS在内的各种资讯，建议关注。</p>
<h2>后记</h2>
<p>匆匆忙忙写完这篇，感觉很多地方都没有讲得很清楚，毕竟相关资源太多了。如果有什么问题，欢迎在下面留言。</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/geek/vps-recommendation#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285380</guid>
  <category>post</category>
<category>Art of Geek</category>
 </item>
  <item>
    <title>从GPA.ZJU谈iOS开发</title>
    <link>https://noting.me/posts/code/gpa-zju</link>
    <pubDate>Tue, 22 Mar 2016 14:55:00 GMT</pubDate>
    <description>可能很少有人会记得GPA.ZJU了吧，这个应用于2013年10月24日正式在AppStore上架，经</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/code/gpa-zju'>https://noting.me/posts/code/gpa-zju</a></blockquote>
          <p>可能很少有人会记得GPA.ZJU了吧，这个应用于2013年10月24日正式在AppStore上架，经历了两年漫长的岁月存活至今。在此期间，迭代过两个大版本更新(最新的3.0版本正在上架审核当中)，也经历过几次重大的崩溃事件。最长的一次崩溃延续至今，因为自己的个人原因一直没能更新，我感到非常抱歉。</p>
<p>GPA.ZJU起初最主要的功能目标是实现新成绩的推送通知。在每次考试周结束之后，总有一些人会每时每刻都在刷成绩(这无关于学霸与学渣或者学酥)，这是一个非常痛苦的事情。作为一个坚信“能用代码解决的事情决不手工做”的开发者，我选择编写一个程序来缓解这一痛苦。因为当时正巧换了iPhone，于是我毅然决然踏上了iOS开发这一条不归路。</p>
<p>两个月之后，一个最初的，非常简陋的版本产生了。原本打算就自娱自乐，不过后来秉着助人为乐的精神自掏了开发者年费上架了AppStore。期间收获了很多鼓励，也有一些质疑。两年的开发积淀让我从一个歌楼上的少年变成了客舟中的壮年。虽坎坷，但也让我有了一些经验与感悟，我希望就这个机会分享给大家。本文内容较为基础，也没有较好的组织，想说什么就说什么。</p>
<p>在开始阅读本文之前，希望你们能够下载一下iOS版本的<a href="https://itunes.apple.com/us/app/gpa.zju-zhe-jiang-da-xue-zhuan/id717203939?ls=1&mt=8">GPA.ZJU</a>，好知道我在说什么……</p>
<p>在接下来的文字中，我将从三个问题入手——</p>
<p>一、如何写一个GPA.ZJU<br>二、如何入门iOS开发<br>三、给你们讲个故事</p>
<h2>一、如何写一个GPA.ZJU</h2>
<p>GPA.ZJU分成两个部分，iOS端与服务器端。iOS端承载数据展示、用户交互；服务器端负责数据交换存储、与教务网通信。</p>
<h4>1. 基本思路</h4>
<p>首先，教务网一定不会提供接口供你调用，你所能做的只能是自己构造HTTP Post请求去请求教务网的数据，然后对教务网数据做解析即可。</p>
<p>这是一个非常简单的过程，你只要在登陆教务网的时候打开审查元素中的网络选项，点击登录你就可以看到如下图类似的Post请求。</p>
<p></p>
<p>在代码中构造类似的请求，发送成功后就可以成功拿到cookie以及服务器响应的数据(HTML代码)。浙大教务网的验证码有个比较严重的Bug，当你在构造的请求中删去这个字段，就不再需要验证码就可以直接登录，这大大降低了开发难度。</p>
<p>登录成功后，采用同样的方式就可以获取到成绩、考试信息。采用正则或者网上现有的Parse开源库对数据进行解析即可生成所需要的数据。</p>
<p>这里需要注意的一点是 _VIEWSTATE 字段，教务网用这一参数跟踪页面切换时表单中各个值的变化。你需要在POST请求中把当前页面中 _VIEWSTATE 值解析出，然后放到新的表单数据(Form Data)中发送请求。关于 _VIEWSTATE 的官方阐述在<a href="https://msdn.microsoft.com/en-us/library/ms972976.aspx">这里</a>可以找到。</p>
<p>对于Mac用户，有一个很好用的软件 Paw 推荐给你们。Paw 可以非常方便地构造请求，非常易于接口调试。</p>
<h4>2. 框架结构设计</h4>
<p>在三个大版本更新中，1.0、2.0我采用了图一的结构，3.0采用了图二的结构。</p>
<p></p>
<p></p>
<p>很明显，对于(经常有一些莫名其妙变动而且不规范的)教务网来说，图二的结构更加适合。如果发生了错误，开发者只要在服务器上修改代码重新部署即可，不再需要修改App代码。毕竟，App修改、提交审核、审核通过是一个非常漫长的过程，如果顺利的话大概需要一周多；申请被驳回的情况下时间会成倍增长。当然，还有一些其他手段能够在紧急情况下，利用Objective-C运行时的特性修改iOS app的代码以达到Hot Patch的目的，这一点在之后会有简单的介绍。</p>
<h4>3. 技术选型</h4>
<p>在3.0之后，我的目标是把GPA.ZJU打造成一个永久可用的产品(毕竟我已经是大四狗了)。就算有一天我不再花大量时间维护代码了，一旦有严重的Bug，我依然能够在很短的时间内修复。除此之外，因为这是一个非盈利的项目，服务器的选择也是非常重要。</p>
<p>在这里我要安利一个平台——LeanCloud。“LeanCloud是加速应用开发的一站式解决方案，专注于为应用开发者提供一流的工具、平台和服务。”它主要提供数据存储 、实时消息和推送、统计分析的服务。在功能上，LeanCloud和国外的Parse非常类似，不过近段时间推出的云引擎功能(支持Python、Node.js)可以满足大部分你对于服务器的基础需求，详见<a href="https://leancloud.cn">官网</a>。</p>
<p>也就是说，你可以免费或者以非常低廉的价格获取一个在沙盒中的服务器，如果你愿意的话，你不需要写任何服务器的代码就可以与服务器进行交互，或者甚至直接修改数据库内容(不建议)。鉴于LeanCloud这么多优秀的特性(我就不说是因为免费了)，我选择了LeanCloud作为GPA.ZJU的后端服务器，用node.js配合express框架写了接口。</p>
<p>之所以上文中不建议直接修改数据库的原因是：如果这样做的话，App与LeanCloud之间的耦合度太高。如果今后LeanCloud倒闭了(Parse不就要关闭了嘛)，你的App想要迁出LeanCloud，那么付出的成本将是巨大的。</p>
<p>App端只要封装一下接口调用即可，万一需要切换服务器了，只需要修改一下服务器地址即可。</p>
<h4>4. iOS端方案</h4>
<p>4.0 最新3.0版本的App设计理念是报纸风格，以黑、白两个类型的颜色填充所有内容。这个理念是我公司(Catch摄影)的设计总监Messizon提出的，里面的设计则主要是我完成的。毕竟我是一个程序员嘛，如果你有觉得不好看或者不习惯的地方，请多多容忍，或者到App中进行吐槽。</p>
<p><strong>4.1</strong> 整个App遵循MVC架构。你不了解MVC？MVC的框架把应用分成Model、View、Controller三个部分。Model主要负责给ViewController提供数据，给ViewController存储数据提供接口；View主要负责响应与业务无关的事件(动画、点击反馈)，界面元素的表达；Controller主要管理ViewContainer的生命周期、负责生成所有View实例并放入ViewContainer、监听来自View与业务相关的事件，与Model合作完成相对应的业务。</p>
<p><strong>4.2</strong> 用CocoaPods管理第三方库。CocoaPods是一个非常流行的依赖管理工具，让你非常简单地集成、更新第三方库，详见<a href="https://cocoapods.org">官网</a>。</p>
<p><strong>4.3</strong> 整个项目目录结构如下：</p>
<p></p>
<p>Macro放一些define类似的文件；Category放类的Category；Vender放一些CocoaPods中没有的第三方库；Helper放一些动画或者其他东文件；UI中放Storyboard；Layout放一些布局文件，比如UICollectionView要用到的layout；Support放图片和其他文件。</p>
<p><strong>4.4</strong> 采用Storyboard结合代码的方式写UI。“采用Storyboard、Xib、代码这三种方式中的哪种写View”是一个永远在争论，永远得不到答案的问题。不得不说，Storyboard作为Apple推荐的方式，确实给我减轻了不少工作量。但是Storyboard有一个弊端，多人协作的时候，如果同时修改Storyboard容易产生冲突。我的建议是将应用的UI按照功能模块分成几个不同的Storyboard，每个Storyboard各司其职。如果几个Storyboard中有一些共用的View，可以考虑将其以xib的形式放在View目录中。除此之外，我个人建议轻量级的Storyboard，也就是在Storyboard中，ViewController之间的复杂切换在代码中实现，而不是在Storyboard中拖segue，这样能够让Storyboard变得更加清楚(不会出现漫天的线)，同时也让页面切换时的判断更加简单。一些更加简单的View，我就直接在代码里实现了。</p>
<p><strong>4.5</strong> 整个App以一个NavigationController贯穿始终，我修改了push、pop transition的动画，让他成为现在的效果。首页右上角的小方块放在了NavigationController的view上方，小方块的实现是利用了iOS 7新推出的UIKitDynamics，介绍可以看此<a href="http://beyondvincent.com/2014/09/02/2014-09-02-uikit-dynamics-tutorial-tossing-views/">博客</a>。</p>
<p><strong>4.6</strong> 热部署方案</p>
<p>GPA.ZJU采用了<a href="http://jspatch.com">JSPatch</a>。“JSPatch 是一个开源项目，只需要在项目里引入极小的引擎文件，就可以使用 JavaScript 调用任何Objective-C 的原生接口，替换任意 Objective-C 原生方法。”也就是说，万一线上的App出现了什么Bug，我可以注入一段代码来替换原生方法达到修复bug的目的。3.0版本是昨天上线的，因为上线测试没有充分，导致出现了通识数据少了一个类目的情况，我通过了JSPatch成功修复了这个Bug。</p>
<h4>5. 服务器端方案</h4>
<p>我用了Node.js的Express框架写了GPA.ZJU的后端，并部署在了LeanCloud的云引擎中。对于一些小型的应用来说，LeanCloud确实给了我很多便捷(此处应该给我发广告费)。从此以后，服务器维护变得非常简单。</p>
<p>服务器实现了以下几个接口：</p>
<p>/auth: 登录认证</p>
<p>/exam: 获取考试信息</p>
<p>/grade: 获取成绩信息</p>
<p>/mix: 获取考试信息与成绩信息混合之后的信息(主要是给App用)</p>
<p>/push: 查看、修改推送状态</p>
<p>获取登录、考试成绩信息原理在最开始的介绍中已经提及，所以不再赘述。服务器在获取完信息时，对数据进行解析，生成一个JSON格式的字符串返回给App。App根据需要将其转化为模块进行调用即可。</p>
<h2>二、如何入门iOS开发</h2>
<p>我个人推荐的步骤是：</p>
<ol>
<li>阅读Objective-C(或Swift)的文档，了解基本语法。</li>
<li>了解相关的Cocoa框架的知识，推荐一本书《精通iOS开发(第7版)》，一套视频《斯坦福大学公开课：iOS 7应用开发》(<a href="http://open.163.com/special/opencourse/ios7.html">网易公开课</a>)。</li>
<li>选择一个自己想要做的项目，从项目入手着手开始开发。Google会告诉你所有问题的答案。我的第一个项目是GPA.ZJU，当初花了一个暑假的时间做出了它，经常出现bug(写得跟shi一样)，不过这种从0到1的过程还是让我感觉非常有成就感的。</li>
<li>阅读一个完整的项目的实现。Github上有很多完整的开源项目，你可以按照Star数从上到下挑一个自己喜欢的。</li>
</ol>
<p>我觉得有一些我觉得重要的Tips：</p>
<ol>
<li>别重复造轮子。或者说，造轮子之前你必须先浏览一些相同功能的开源库的代码，了解他们的实现过程。否则，有可能你写出来的东西，根本没有使用价值。</li>
<li>读一些iOS相关的博客，他们对一个问题的认识是非常深入的，可以学到很多。</li>
<li>关注一些开发者的微博账号，他们每天会转好玩的、有用的、实在的东西。</li>
<li>当你有了一定的水平之后，不要接太多的项目或者外包。这会占用你大量的时间，以至于让你没有心思研究一些新技术，阻碍你水平的进步。要记住，你是一个程序员，而不是一个熟练工。</li>
</ol>
<p>一些资源：</p>
<p>Github合集：<a href="https://github.com/Aufree/trip-to-iOS">https://github.com/Aufree/trip-to-iOS</a></p>
<p>一些微博：@iOS程序犭袁@hangcom2010 @我就叫Sunny怎么了@移动开发小冉 @叶孤城_ @唐巧_boy @onevcat @周楷雯Kevin@雷纯锋2011@汤圣罡 @KITTEN-YANG @程序媛念茜</p>
<p>一些博客：<a href="http://blog.devtang.com">唐巧</a>、<a href="https://onevcat.com">OneV&#39;s Den</a>、<a href="http://casatwy.com">casatwy</a>、<a href="http://beyondvincent.com/">beyondvincent</a></p>
<p>对了，GPA.ZJU的iOS代码已经开源在了我的<a href="http://github.com/dongxinb">Github</a>中，我删去了LeanCloud的一些信息。如果有什么问题，请给我发邮件或者提个issue。</p>
<h2>三、给你们讲个故事</h2>
<p>曾经有一个少年，在很小的时候就非常喜欢电脑。<br>他干过很多事情。比如说初中电脑课上，为了防止别人跟他抢网速，他把其他人的电脑弄蓝屏。当然，利用的漏洞也是非常古老、简单的。<br>他开着挖掘机挖过网站的漏洞，但是由于技术不精没拿到开门的钥匙。<br>他还给喜欢的女孩子写过一个VB的程序，加了一个密码，发给了那个女孩子。<br>女孩子非常感动，最后，没有选择他。</p>
<p>就这样折腾着，2012年的夏天，他进入了大学，很迷茫。<br>由于高中搞的是物理竞赛，在算法这部分他拼不过搞OI的同学。在高中的时候被称为大神的他并没有掌握一两种研究比较深入的技术，他很苦恼。<br>日常的状态是全寝室开黑打LOL，水水代码，水水作业，考前通宵。<br>曾体验过没复习去考微积分，也曾体验过一周时间自学完整本线代书。<br>嗯，结果当然是惨跪。不至于挂科，但是成绩很难看。<br>就这么糟糕地，他如愿以偿进入了CS相关专业，开始了码农之旅。<br>码农之旅还算顺利，也许是因为有一点点聪明。</p>
<p>大一暑假那年，他因为一个需求开始学起了iOS开发。他觉得，能让自己写的东西在手机上跑是一件非常幸福的事情。<br>事实证明，真的很幸福。<br>如果说人生有诸多后悔，那么学习iOS开发始终不属于这个集合。<br>在那之后，他做了很多项目，接过一些外包，参加过一些比赛，获得过一些荣誉与奖励。<br>他加入了一家创业公司，跟着公司一起成长，目前公司已经到了A轮。<br>创始人很靠谱，在交流的过程中，他的贪玩个性有一些改善，他也明白什么叫责任。</p>
<p>小的时候，他像所有人一样，总会问自己“将来是要去清华还是北大？”<br>不过对于一件事情来说，他始终没有改变——“出国”。</p>
<p>朋友问：<br>“你毕业之后准备干啥？”<br>“出国。”<br>“考了托福吗？”<br>“……”<br>“GRE呢？”<br>“……”<br>“绩点咋样？”<br>“就你话多！”<br>这样的对话一直延续到15年9月份……<br>那时候，他终于领悟到英语的难度。</p>
<p>如果说大学四年最让他崩溃的时段是哪个？当之无愧是准备英语的这三个月。<br>每天早上起来背英语、晚上睡觉背英语，一天十几个小时都跟英语相关。<br>不知道谁还送给了他&quot;出不了国&quot;的压力。<br>于是，他很痛苦，比看一个1000行的没有注释的函数还要痛苦，比找不到bug还要痛苦。<br>他考了好多好多次Toefl，好多好多次GRE，每一次考前失眠到凌晨。<br>也算是上帝仁慈，他在12月份，GRE终于突破了320，Toefl也终于突破了100，然而口语18……<br>他很开心，开始申请，准备文书，填写表格，递交成绩。</p>
<p>他有一个一般般的绩点，有一个大于100的托福成绩，有一个大于320的GRE成绩，有一段水水的小科研，有过一些比赛的奖励，有一段创业经历，有一技之长。<br>他以为他要走向人生巅峰了……<br>然而，现实还是很残酷的，他到现在(2016年3月5日)都还没收到Admission。<br>嗯，他还处于失学状态。<br>他开始反思自己的申请，反思自己的成绩，反思自己的文书。<br>为什么，GPA不能再高一些呢！<br>为什么，之前不多练习下口语呢！<br>为什么，文书里这么自信的想要转方向呢！<br>为什么，之前不做一些科研呢！</p>
<p>嗯，他已经想好了，如果失学了，就给自己放个假，出去看看这个世界。<br>也许有一天，这个世界的好些角落都有他的脚印吧……</p>
<p>主要是为了告诉大家，如果想要申到好学校，早日准备英语(口语至少22)，成绩一定要好，有一些科研，有一些特长。(按先后顺序排序)<br>当你做到了，Offer自然来了。</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/code/gpa-zju#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285382</guid>
  <category>post</category>
<category>Art of Code</category>
 </item>
  <item>
    <title>自己做的GRE卡片 for Anki</title>
    <link>https://noting.me/posts/code/gre-anki</link>
    <pubDate>Fri, 08 Jan 2016 15:39:00 GMT</pubDate>
    <description>GRE考完有好久了，期间自己在amazon里自己买了GRE核心词汇考法精析, GRE核心词汇助记与精</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/code/gre-anki'>https://noting.me/posts/code/gre-anki</a></blockquote>
          <p>GRE考完有好久了，期间自己在amazon里自己买了<code>GRE核心词汇考法精析</code>, <code>GRE核心词汇助记与精练</code>, <code>GRE词汇进阶与巩固 (新东方)</code>，然后写了个脚本把内容跑出来然后做了两套Anki卡片。<br>每套卡片都有配音(取自Bing)，有音标，助记(如果有的话)，卡片的色彩、样式可以自定义，直接在<code>卡片</code>-&gt;<code>样式</code>中编辑即可。</p>
<h3>第一套 <a href="http://www.amazon.cn/gp/product/B00JR11EY4">GRE核心词汇考法精析(再要你命3000)</a></h3>
<p>结合了<code>GRE核心词汇助记与精练</code>和网上找到的<code>不择手段背单词</code>里面的助记部分。<br>里面的词汇就是3000的所有词汇，配音全。</p>
<p></p>
<p><a href="https://noting.me/api/v2/objects/file/tl2il3htnl0zjxcxl7.apkg">下载地址</a></p>
<h3>第二套 <a href="http://www.amazon.cn/gp/product/B010D4QEOS">GRE词汇进阶与巩固 (新东方)</a></h3>
<p>结合了<code>GRE词汇进阶与巩固 (新东方)</code>和一个奇奇怪怪的助记。<br>里面的词汇是<code>GRE词汇进阶与巩固 (新东方)</code>的所有词汇，大概一千多个，自己感觉比较靠谱，适合没时间背3000的人。<br>里面的配音是不全的，只有3000的那部分，因为我当时懒得再爬一遍了。</p>
<p></p>
<p><a href="https://noting.me/api/v2/objects/file/m29p2zx218hgy6ulz0.apkg">下载地址</a></p>
<p>最后，祝所有考G的同学都能取得好成绩，祝自己有个好ad……<br>顺便附上一段抓Bing词典里面发音的Python脚本吧。``````</p>
<pre><code class="language-python">    import urllib
    import os
    import string
    import sys
    import re   
    
    def getWord(word):
    	url = 'http://media.engkoo.com:8129/en-us/'+word+'.mp3'
    	# req = urllib2.Request(url)
    	# f = urllib2.urlopen(req)
    	print(word)  
    	localDir = word+'.mp3'
    	try:
    		urllib.urlretrieve(url, localDir)
    		print("\t\t\tsuccess")
    		return True
    	except Exception as e:
    		print(e)
    		print("\t\t\terror")
    		return False
    
    
    def parseVoc(filename):
    	file = open(filename, 'r')
    	count = 0
    	while True:
    		count = count + 1
    		line = file.readline().decode('utf-8')
    		line.strip()
    		line1 = line[0:len(line)-1]
    		if not line:
    			break
    		if len(line1) &gt; 0:
    			# print("..."+line1)
    			print(''+str(count))
    			getWord(line1)
    	print("All success")
    
    parseVoc('word.txt')</code></pre>
          <p style='text-align: right'>
          <a href='https://noting.me/posts/code/gre-anki#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285378</guid>
  <category>post</category>
<category>Art of Code</category>
 </item>
  <item>
    <title>繁花开始落雪</title>
    <link>https://noting.me/posts/life/snowfall-on-blossoms</link>
    <pubDate>Thu, 27 Mar 2014 05:07:00 GMT</pubDate>
    <description>你站在我无法企及的平行世界里，岁月回首。  
欹斜的枝桠上，挂着残破的梦，残梦。  
我就站在你站的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/life/snowfall-on-blossoms'>https://noting.me/posts/life/snowfall-on-blossoms</a></blockquote>
          <blockquote>
<p>你站在我无法企及的平行世界里，岁月回首。<br>欹斜的枝桠上，挂着残破的梦，残梦。<br>我就站在你站的地方，嗅着我虚构的过往，怀念着我虚构的故事。<br>忧伤还是快乐？听着《茗记》的主题曲，这首所有人都问我的 曲 字。<br>也罢，一个 情 字，贯穿了多少故事的始终 &amp;……<br>也许，加了不知道多少溶剂，却依然无法消散岁月的痕迹。<br>我只能悲剧性的，在睖睁的眸中，迷失，迷惘，迷蒙 &amp;……<br>“蝴蝶飞不过沧海，是因为对岸没了期待。”<br>我渡不过沧海，是因为我不清楚对岸是否值得期待？<br>手执着你的承诺，那些难以启齿的柔弱，却使我越来越懦弱。<br>我，只是想隐藏到你着急；只是想消失到你心慌。<br>这些不成熟的做法，却只是为了证明一个根本无法证明的否命题。<br>就像我无法明晰当初若是选择了其他，所有的一切又会是怎样 &amp;……<br>2010年7月这整整一个月，也只是749条短信罢了。<br>从2011年2月14日开始的聊天记录，也只有五十七页罢了。<br>而后，回归一句漏一句的冷漠，或喜或伤...<br>我想，走的深深浅浅的脚印，何时能有一场雪覆盖？<br>我想，走了多多少少的岔路，又何时才能够走向正途？<br>寒水依痕，佛说：“解夏”。<br>岁岁更迭，却道不了一声“别”。<br>《伊利亚特》里说：“当年轻的黎明，垂着玫瑰红的手指。”<br>而我却在想着何时才能像《追风筝的人》的那样讲一句：“为你，千千万万遍。”<br>之前找不到人，后来警醒了，因为……<br>我已不再是那个看到阅读理解说吃甜食可以使人开心就拼命吃大白兔的孩子了，<br>我也不再是那个相信融雪有很奇妙的声音的孩子了。<br>听着忧伤的歌，看着你反反复复上线下线，看来已无言语。<br>不该等着你主动的话语，只该希望你能照顾好自己 &amp;……<br>溃烂成灾的只有我零碎不堪的心绪。<br>也许有一天能懂得，天空在叹息什么？<br>想起了当初看的《秒速5厘米》，看的《追逐繁星的孩子》；<br>想起当初看的《天空之城》，看的《穿越时空的少女》；<br>那些为你准备的故事，在惨淡的笑容中淡然收场，<br>我想说自己很淡定，但汹涌的情绪看不到融融的光线，听不到刺耳的嘶鸣&amp;……<br>当年一起看的电影《80后》，如今电影的情节全然忘记，<br>但依旧记得你一句话语，两个动作，三个表情。<br>怀念那时候的生活，虽然你心里面装的全部都是关于他的日子。<br>我无所谓，不多想，不在意。因为只要你欢乐开心。<br>但是，请每次出去玩的时候早一些回家，家人会担心&amp;……<br>我记得在很久很久之前告诉自己，“长大了”。<br>我记得很久很久之前告诉自己，“从明天开始，做一个幸福的人。”<br>我记得很久很久之前告诉所有人，“那些之前看不起我的人，请拭目以待。”<br>我一直有一个想法，去精神病院看看。就像某人送我的那本《维罗尼卡决定去死》里讲的一样。<br>我想起了很久很久以前王玲玲用力的拍着我的肩膀说“你的肩膀能承担得起吗？”<br>我不知道，因为不知道，所以更加不想知道。<br>我不知道你和我是否还是那个个容易伤感的孩子，<br>但我希望不是，因为，我长大了！<br>再见了，过去。再见了，曾经。再见了，青春 &amp;……<br>外面的世界，繁花开始落雪。<br>-2012-04-03</p>
</blockquote>
<p>哈哈，自黑一把。<br>那种纯情的日子，似乎真的离得很远了呢。<br>我不知道这样的改变是好是坏。<br>只是现在，打着代码，想着其他。<br>早晨六点，看着古老的阳光慢慢地晕染，空气中浸染着跳跃的尘埃。<br>我想说，阳光本身就是不纯洁的，不是吗？<br>就像窗外的樱花，飘落的速度一定不是秒速五厘米，不是吗？<br>只是，何必要刻意栖居于那刻意营造的诗意中呢？<br>越来越感觉，自己变得理性。<br>越来越感觉，痛苦和享受本来就是相辅相成的两件事情。<br>那些难以启齿的悲伤，既然是悲伤，就不要说出来。<br>因为，没有人听。<br>就像前些天没日没夜的码代码赶DDL，<br>就是不愿意辜负队友，不愿意辜负自己。<br>在空无一人的房间里，唯一的乐趣就是在楼梯上，上楼又下楼。<br>一圈一圈，累了，继续写代码。<br>或者，看看窗外的车流，努力辨析每个人的表情，想像他们的心情。<br>那一刻，没有痛苦，只有坦然。<br>好吧，废话好像讲得有点多了。<br>在这个庄重的季节里，我庄重的宣誓：<br>未来的一个月，在1，2节没课的时候，6点起床晨跑，然后读书。<br>做不到的？是小狗？ :)<br>最后，<br>也许，很多年前还坚信着，<br>背对着太阳，影子就是我的世界。<br>而我现在只想说：<br>解夏。</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/life/snowfall-on-blossoms#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285377</guid>
  <category>post</category>
<category>Art of Life</category>
 </item>
  <item>
    <title>向南…</title>
    <link>https://noting.me/posts/life/to-the-south</link>
    <pubDate>Wed, 10 Oct 2012 02:12:00 GMT</pubDate>
    <description>用这么一些话，当作这个博客的开始。  

纯黑的背景下，我的生命像极了不明不暗的点。  
独倚孤桐，</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://noting.me/posts/life/to-the-south'>https://noting.me/posts/life/to-the-south</a></blockquote>
          <p>用这么一些话，当作这个博客的开始。  </p>
<p>纯黑的背景下，我的生命像极了不明不暗的点。<br>独倚孤桐，以回首的姿态笑对路人甲乙略微嘲讽的目光。<br>岁月颤抖，拉凝成丝的阳光共振出刺耳的期待。<br>捂耳埋头的一瞬间，所等待的那个人已翩翩而来，偏偏离去，没有一丝停留或犹豫……<br>茕茕的姿态唤不回消逝的过往，属下的斑驳也找就凑不齐浓墨重彩的未来。<br>想牵你，牵你去看那片枫树海，<br>决然的身影面朝南面少了对北林的等待。<br>是的，窗内的风肆虐，我站在窗外还能听见花开花败。<br>即使摘下了耳机，却依然摘不下所谓的爱。<br>浓黑的瓷杯里到处浓稠的无奈，沉淀后却如同森海的声场一样平淡……<br>我带你，带你去看那片瓦，只要你能摘下环在脖子上的爱；<br>我带你，带你去看那片海，只要你肯露出小拇指来。<br>直尺折断的一刹那，你眼前的空气是否会震颤？<br>擦肩而过的一瞬间你的步履依旧那么流畅。<br>他们都说，是的，都这样说：“蝴蝶飞不过沧海，是因为彼岸已无期待……”</p>

          <p style='text-align: right'>
          <a href='https://noting.me/posts/life/to-the-south#comments'>看完了？说点什么呢</a>
          </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">133901036845285376</guid>
  <category>post</category>
<category>Art of Life</category>
 </item>
  
</channel>
</rss>