<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0"><channel><title>阿炳的小站</title><link>http://undefiner.cn/</link><atom:link href="http://undefiner.cn/feed.xml" rel="self" type="application/rss+xml"/><description>谁知难过 分一丁目赠我</description><generator>Halo v2.20.11</generator><language>zh-cn</language><image><url>/upload/1.jpg</url><title>阿炳的小站</title><link>http://undefiner.cn/</link></image><lastBuildDate>Fri, 14 Aug 2026 03:19:47 GMT</lastBuildDate><item><title><![CDATA[【翻译】SDL2教程 Lesson 01 环境配置与窗口创建]]></title><link>/archives/fan-yi-sdl2jiao-cheng-lesson-01-huan-jing-pei-zhi-yu-chuang-kou-chuang-jian</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E3%80%90%E7%BF%BB%E8%AF%91%E3%80%91SDL2%E6%95%99%E7%A8%8B%20Lesson%2001%20%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE%E4%B8%8E%E7%AA%97%E5%8F%A3%E5%88%9B%E5%BB%BA&amp;url=/archives/fan-yi-sdl2jiao-cheng-lesson-01-huan-jing-pei-zhi-yu-chuang-kou-chuang-jian" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">本文翻译自 <a href="https://thenumb.at/cpp-course/sdl2/01/01.html" target="_blank">https://thenumb.at/cpp-course/sdl2/01/01.html</a> 感谢作者的博文，在我 SDL 学习路上的指导。原文以正体翻译，个人注解则以斜体标出，在不影响阅读的情况下，辅助初学者的理解</p>
</blockquote>
<h2 style="" id="%E5%AF%BC%E8%A8%80">导言</h2>
<p style="">本节课程主要介绍 SDL2（Simple DirectMedia Layer 2<em> 简单直接媒体抽象层</em>）库的使用。它是一个（相对）易于使用的库，可以在不依赖特定操作系统功能的情况下，为程序添加多媒体功能。</p>
<p style="">SDL 提供的功能：</p>
<ul>
 <li>
  <p style="">窗口管理</p></li>
 <li>
  <p style="">基于软件（CPU）和硬件（GPU）的 2D 图形渲染</p></li>
 <li>
  <p style="">输入事件处理机制</p></li>
 <li>
  <p style="">时间管理 （<em>时间函数处理</em>）</p></li>
 <li>
  <p style="">音频处理</p></li>
 <li>
  <p style="">文件 I/O 与库加载</p></li>
 <li>
  <p style="">多线程支持</p></li>
 <li>
  <p style="">用于 3D 图形的 OpenGL API</p></li>
</ul>
<p style="">SDL 还提供了一些扩展库，用于实现更多功能——例如网络支持、更完善的音频系统、图像加载等。</p>
<p style="">本节课程我会引用许多 SDL 提供的函数。课程的重点是教你如何使用 SDL 的功能，而不是讲解每个函数的细枝末节等等。因此，每当提到一个 SDL 对象时，都会附上对应的官方文档链接。如果你对函数参数、返回值、副作用，或者对某个结构有疑问，请查阅对应文档——它是非常有价值的学习材料。</p>
<p style="">SDL官方文档阅读提示：</p>
<ul>
 <li>
  <p style="">如果对某些内容有疑问，请查阅文档。</p></li>
 <li>
  <p style="">示例代码非常有用！</p></li>
 <li>
  <p style="">一定要阅读备注！他们可能展示了意想不到的特性并描述了何时应该释放/保存内存</p></li>
 <li>
  <p style="">每个页面底部的 “相关函数” 部分会告诉你还能使用哪些函数。但这不会在每一课里涵盖所有函数。</p></li>
</ul>
<h2 style="" id="%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE">环境配置</h2>
<p style="">指南：</p>
<ul>
 <li>
  <p style=""><a href="https://thenumb.at/cpp-course/sdl2/01/devSetup.html">Setup for Dev C++</a></p></li>
 <li>
  <p style=""><a href="https://thenumb.at/cpp-course/sdl2/01/vsSetup.html">Setup for Visual Studio</a></p></li>
 <li>
  <p style=""><a href="http://lazyfoo.net/tutorials/SDL/01_hello_SDL/index.php" target="_blank">Other Setup Guides</a></p></li>
</ul>
<p style="">在使用 SDL 的函数或对象时，你必须包含它们的头文件。<code>SDL.h</code> 文件会自动包含几乎所有需要的内容——在大多数情况下，这就足够了。不过，也有一些函数是在特定的头文件中声明的。如果不确定需要包含什么，请查阅文档。</p>
<p style="">如果你的编译器设置正确，你就可以使用尖括号（例如 <code>#include &lt;SDL.h&gt;</code>）。这会告诉编译器去它指定的 include 目录中查找头文件。</p>
<blockquote>
 <p style=""><em>此部分作者引用上述指南的链接来指引配置 </em><code>SDL</code><em> 环境，译者简单讲解一下各个平台配置 SDL的方法，如有需要，则专门开一个教程进行说明</em></p>
 <ul>
  <li>
   <p style="">macOS:<br>
    使用 <code>homebrew</code> 进行安装，运行 <code>brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf</code> 即可</p></li>
 </ul>
 <ul>
  <li>
   <p style="">Windows:<br>
    在 github 的 <a href="https://github.com/libsdl-org/SDL/releases" target="_blank">SDL官方仓库</a> 中，下载对应版本的 sdl2 库，解压后，就可以看到其对应的文件, <code>docs</code> 中是对应的文档， <code>include</code> 中则是相关的头文件，<code>lib</code> 中则是对应的dll动态库。然后再将对应的头文件与动态库，根据不同 IDE 的配置，或者是自己的编译脚本中，即可完成添加</p></li>
 </ul>
</blockquote>
<h2 style="" id="%E5%88%9D%E5%A7%8B%E5%8C%96-sdl">初始化 SDL</h2>
<p style="">在进行其他操作前，你必须先整体初始化 SDL 。即通过调用 <code>SDL_Init()</code> 来完成。如果你想初始化 SDL 的所有部分，可以传入参数 <code>SDL_INIT_EVERYTHING</code>。</p>
<p style="">SDL 也允许你单独初始化库里面的某些子模块。要指定要初始化的部分，可以直接在 <code>SDL_Init()</code> 中传入对应的标志（flags），或者使用 <code>SDL_InitSubSystem()</code> 函数。如果你打算分别初始化各个子系统，那么在最初调用 <code>SDL_Init()</code> 时只需传入 <code>0</code> 即可。</p>
<pre><code class="language-C++">SDL_Init( SDL_INIT_EVERYTHING );</code></pre>
<h2 style="" id="%E5%88%9B%E5%BB%BA%E7%AA%97%E5%8F%A3">创建窗口</h2>
<p style="">你必须创建一个窗口，供程序的多媒体输入输出使用。从头开始编写一个多窗口应用的时候，你必须定义一个 <code>WinMain</code> 函数，来调用操作系统去获取 句柄 （<code>handles</code>）、创建窗口等操作。而 SDL 提供了一个更简单、跨平台的窗口管理 API，让这些步骤大大简化。</p>
<p style="">为了管理窗口，SDL 以便利的方式提供了一个结构体 <code>SDL_Window</code>，以及相关函数，例如 <code>SDL_CreateWindow()</code>。</p>
<p style="">稍微插一句：你可能会注意到，SDL 文档中没有 <code>SDL_Window</code> 结构的详细说明链接。这是因为该结构是<strong>不透明（opaque）</strong>的，也就是说——你的程序无法直接访问或查看 <code>SDL_Window</code> 内部的实际内容。你只需要持有并操作一个指向 <code>SDL_Window</code> 的指针即可。</p>
<p style=""><code>SDL_CreateWindow()</code> 的功能也符合直觉：它接收窗口的<strong>标题、大小、位置、选项</strong>等参数，然后返回一个指向新建 <code>SDL_Window</code> 结构的指针。详细参数说明请参考 SDL 官方文档。</p>
<pre><code class="language-C++">SDL_Window* win = SDL_CreateWindow( "my window", 100, 100, 640, 480, SDL_WINDOW_SHOWN );</code></pre>
<p style="">大多数 SDL 函数在执行失败时会返回一个特定的值，用于表示错误。例如返回的是指针的函数， <code>NULL</code> 就是这个特定的值。因此，你可以很容易地检查操作是否成功。</p>
<p style="">在任何 SDL 的函数内发生了错误之后，<a href="https://wiki.libsdl.org/SDL_GetError">SDL_GetError()</a> 这个函数允许你接收捕获到的一个 <code>string</code> 的错误信息。</p>
<pre><code class="language-C++">if ( !win ) {
	cout &lt;&lt; "Failed to create a window! Error: " &lt;&lt; SDL_GetError() &lt;&lt; endl;
}</code></pre>
<h2 style="" id="%E8%A1%A8%E9%9D%A2">表面</h2>
<p style="">当你创建了一个窗口后，你需要一种方法在其上进行绘制。SDL 将任何可以被绘制的区域——包括已加载的图像——抽象为一个“表面（surface）”。（这适用于软件渲染——在后续章节中，我们将讨论不使用表面的 GPU 渲染。）</p>
<p style="">结构体 <a href="https://wiki.libsdl.org/SDL_Surface">SDL_Surface</a> 以及函数 <a href="https://wiki.libsdl.org/SDL_LoadBMP">SDL_LoadBMP()</a> 和 <a href="https://wiki.libsdl.org/SDL_GetWindowSurface">SDL_GetWindowSurface()</a> 提供了软件渲染（也称为 <strong>blitting</strong><span style="font-size: 17.6px; color: rgb(0, 0, 0)">， <em>位图操作</em></span>）的 API。</p>
<p style="">正如你所料，可以使用<a href="https://wiki.libsdl.org/SDL_GetWindowSurface">SDL_GetWindowSurface()</a>来获取窗口的表面（surface）。在你在这个表面上完成绘制之后，通过调用 <a href="https://wiki.libsdl.org/SDL_UpdateWindowSurface">DL_UpdateWindowSurface()</a><span style="font-size: 17.6px; color: rgb(0, 0, 0)"> </span>就能在窗口中看到绘制结果。</p>
<pre><code class="language-C++">SDL_Surface* winSurface = SDL_GetWindowSurface( win );

// do drawing

SDL_UpdateWindowSurface( win );</code></pre>
<h2 style="" id="%E7%BB%98%E5%88%B6%E4%B8%80%E4%B8%AA%E7%9F%A9%E5%BD%A2">绘制一个矩形</h2>
<p style="">为了测试你的窗口表面是否有正常工作，你可以用一个颜色填充它。最简单的方法是使用 <a href="https://wiki.libsdl.org/SDL_FillRect">SDL_FillRect()</a><span style="font-size: 17.6px; color: rgb(0, 0, 0)"> </span>。如果想填充整个窗口，简单情况下只需传入 <strong>NULL</strong>，而无需用一个 <a href="https://wiki.libsdl.org/SDL_Rect">SDL_Rect</a> 指针。此外 <a href="https://wiki.libsdl.org/SDL_FillRect">SDL_FillRect()</a> 接受一个特定格式的数字来表示颜色。要获得这种格式的颜色值，可以调用 <a href="https://wiki.libsdl.org/SDL_MapRGB">SDL_MapRGB()</a> ，并传入表面的格式以及期望的 <strong>RGB</strong> 数值。</p>
<pre><code class="language-C++">SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface-&gt;format, 255, 90, 120 ));</code></pre>
<h2 style="" id="%E5%85%B3%E9%97%AD">关闭</h2>
<p style="">当你的程序完成了所有操作之后，它必须销毁窗口并释放所有资源。你可能会想到，用 <a href="https://wiki.libsdl.org/SDL_DestroyWindow">SDL_DestroyWindow()</a> 来处理。这个函数会关闭你的窗口，并释放关联的内存资源（包括窗口表面）。</p>
<pre><code class="language-C++">SDL_DestroyWindow( win );
win = NULL;
winSurface = NULL;</code></pre>
<p style="">最后，要彻底关闭整个 SDL，只需调用 <strong>SDL_Quit()</strong>。很好理解这段：</p>
<pre><code class="language-C++">SDL_Quit();</code></pre>
<h2 style="" id="%E9%99%84%E5%BD%95">附录</h2>
<p style="">文章中所用到的代码完整内容如下：</p>
<pre><code class="language-C++">#include &lt;iostream&gt;
#include &lt;SDL.h&gt;

// You shouldn't really use this statement, but it's fine for small programs
using namespace std;

// You must include the command line parameters for your main function to be recognized by SDL
int main(int argc, char** args) {

	// Pointers to our window and surface
	SDL_Surface* winSurface = NULL;
	SDL_Window* window = NULL;

	// Initialize SDL. SDL_Init will return -1 if it fails.
	if ( SDL_Init( SDL_INIT_EVERYTHING ) &lt; 0 ) {
		cout &lt;&lt; "Error initializing SDL: " &lt;&lt; SDL_GetError() &lt;&lt; endl;
		system("pause");
		// End the program
		return 1;
	} 

	// Create our window
	window = SDL_CreateWindow( "Example", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 1280, 720, SDL_WINDOW_SHOWN );

	// Make sure creating the window succeeded
	if ( !window ) {
		cout &lt;&lt; "Error creating window: " &lt;&lt; SDL_GetError()  &lt;&lt; endl;
		system("pause");
		// End the program
		return 1;
	}

	// Get the surface from the window
	winSurface = SDL_GetWindowSurface( window );

	// Make sure getting the surface succeeded
	if ( !winSurface ) {
		cout &lt;&lt; "Error getting surface: " &lt;&lt; SDL_GetError() &lt;&lt; endl;
		system("pause");
		// End the program
		return 1;
	}

	// Fill the window with a white rectangle
	SDL_FillRect( winSurface, NULL, SDL_MapRGB( winSurface-&gt;format, 255, 255, 255 ) );

	// Update the window display
	SDL_UpdateWindowSurface( window );

	// Wait
	system("pause");

	// Destroy the window. This will also destroy the surface
	SDL_DestroyWindow( window );

	// Quit SDL
	SDL_Quit();
	
	// End the program
	return 0;
}</code></pre>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/fan-yi-sdl2jiao-cheng-lesson-01-huan-jing-pei-zhi-yu-chuang-kou-chuang-jian</guid><dc:creator>abing</dc:creator><pubDate>Mon, 20 Oct 2025 15:15:36 GMT</pubDate></item><item><title><![CDATA[芯片制造行业上下游介绍 I]]></title><link>/archives/semiconductor-introduction-I</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E8%8A%AF%E7%89%87%E5%88%B6%E9%80%A0%E8%A1%8C%E4%B8%9A%E4%B8%8A%E4%B8%8B%E6%B8%B8%E4%BB%8B%E7%BB%8D%20I&amp;url=/archives/semiconductor-introduction-I" width="1" height="1" alt="" style="opacity:0;">
<p style="">初入芯片制造行业，想着找找国内资料，无奈中文资料实在太少，因此根据自己的理解与网络上的现有资源，稍微做一点整理。这个系列用于整理记录芯片制造业的上下游各个工序，以及其基本过程。主要分为三部分：芯片设计、晶圆制造以及封装测试。本文主要阐述芯片设计相关内容。</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-pcdl.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="text-align: center">AMD Threadripper Pro 9995WX开盖图<sup>[1]</sup></p>
<p style="">半导体制造其实就是把海量的元件集成到一个芯片（Chip）上，这个Chip上集成的元件数量越多，则代表使用的制程越先进。即我们平常说的x纳米的制程。在上面的图中，这枚价值10w元的AMD Threadripper Pro 9995WX上，有着12个CCD Die，以及中间最大的 I/O Die，这些 Die 从晶圆（Wafer）上取下来，然后安装在上图中蓝绿色的部分——基板上，再辅以各种对应的布线，将对应芯片的信号能够相互传输以及导出并且能够使用外部的电源进行工作。</p>
<p style="">我们时常会听到市场上会说使用了多少纳米多少纳米的制程，一下子会觉得这个是指对应的大小。而实际上，在半导体制造从平面构造转为三维构造之后，我们所说的x纳米就不跟元件的物理特性相关了。实际上我们所说的7nm、8nm指的是相对于上一代的工艺，芯片上的元件数量进一步增加的工艺技术。并非和长度有什么具体的对应关系。</p>
<h1 style="" id="%E8%8A%AF%E7%89%87%E8%AE%BE%E8%AE%A1">芯片设计</h1>
<p style="">每个芯片上面的电路，包括系统架构设计、逻辑设计、电路设计、物理设计等，最终输出光刻掩膜版（光罩）的图形数据。主要分为<span style="font-size: 16.002px; color: rgb(64, 64, 64)">分为 </span><strong>前端设计（Front-End）</strong><span style="font-size: 16.002px; color: rgb(64, 64, 64)"> 和 </span><strong>后端设计（Back-End）</strong><span style="font-size: 16.002px; color: rgb(64, 64, 64)"> 两大阶段。</span></p>
<p style="">简而言之，前端设计就是 <strong>用 RTL 实现芯片的逻辑功能，并保证功能正确性</strong>；而后端设计就是 <strong>把逻辑网表变成可制造的物理版图，并保证性能与可制造 性</strong>。</p>
<h2 style="" id="%E5%89%8D%E7%AB%AF%E8%AE%BE%E8%AE%A1">前端设计</h2>
<h3 style="" id="%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90%E4%B8%8E%E8%A7%84%E6%A0%BC%E5%88%B6%E5%AE%9A"><strong>需求分析与规格制定</strong></h3>
<p style="">对当前的市场进行调研，市场对这个芯片的需求是怎么样子的，如何能切中客户的痛点，并提出预期能够解决的大概方向，预计对应成本控制。然后再制定出对应的芯片规格，其实就类似于我们的软件行业，产品经理需要对客户/用户的需求进行评估，并形成对应的规格文档，<span fontsize="" color="rgb(25, 27, 31)" style="color: rgb(25, 27, 31)">包括芯片需要达到的具体功能和性能方面的要求。</span>这样子才能进行后续的开发。</p>
<h3 style="" id="%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1">架构设计与算法设计</h3>
<p style="">根据对应的规格文档，对一些功能进行算法设计，拿出设计解决方案和具体实现架构，划分模块功能。明确每部分的功能，将芯片每个部分需要实现的工嫩规划清楚，并建立起对应的框架。这一步就会提出对应的Spec（Specification，即技术规格）。</p>
<h3 style="" id="hdl%E7%BC%96%E7%A0%81%E5%AE%9E%E7%8E%B0">HDL编码实现</h3>
<p style=""><span fontsize="" color="rgb(25, 27, 31)" style="color: rgb(25, 27, 31)">使在这一步，要确定芯片的工艺，即平时说的多少纳米多少纳米，工艺决定了对应的芯片大小能容纳的电子器件数量。当然，并非工艺越先进越好，越先进的技术，其良率、寿命、功耗等也不一定完美契合所制造的产品的需求。同时，在这一步也需要用硬件描述语言（VHDL，Verilog HDL，业界公司一般都是使用后者）将模块功能以代码来描述实现，也就是将实际的硬件电路功能通过HDL语言描述出来，形成RTL（寄存器传输级）代码。</span></p>
<blockquote>
 <p style=""><span fontsize="" color="rgb(25, 27, 31)" style="color: rgb(25, 27, 31)">设计输入工具：具有强大的文本编辑功能，多种输入方法（VHDL，Verilog，状态转移图，模块图等），语法模板，语法检查，自动生产代码和文档等功能。如Active-HDL,VisualVHDL/Verilog等。以及用于描述模拟IP的</span><span fontsize="" color="rgb(17, 24, 39)" style="color: rgb(17, 24, 39)">电路图/Spice 电路模型等。</span></p>
</blockquote>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">用RTL实现的各种功能模块，来组成⼀个实现具体功能的IP（</span>Intellectual Property，知识产权核<span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">），SOC芯⽚最终由SOC integration⼯程师把各个IP集成到⼀起。</span></p>
<p style="">IP又分为以下几类</p>
<ol>
 <li>
  <p style="">模拟IP：处理<strong>连续信号</strong>（analog signal）的模块。比如电压、电流、频率等。</p></li>
</ol>
<p style="text-indent: 2em">模拟IP用于处理与现实世界连续信号有关的功能。常用于与外部物理世界打交道的接口部分。对<strong>工艺、温度、电压波动</strong>非常敏感。<strong>仿真复杂、开发周期长。</strong></p>
<ol start="2">
 <li>
  <p style="">数字IP：处理<strong>离散的数字信号</strong>（0 和 1）的模块。比如逻辑运算、寄存器、状态机等。</p></li>
</ol>
<p style="text-indent: 2em">数字IP用于实现数据处理、控制逻辑、计算功能等。基于逻辑门、触发器实现，用 HDL（Verilog/VHDL）描述，可以一定程度上自动化生成。</p>
<p style="">完成了功能的设计之后，就需要进行DFT了。</p>
<p style=""><strong>DFT（Design For Test，面向可测性设计）是芯片设计中非常关键的一环，它确保你设计好的芯片，在制造出来后能有效测试每一个单元是否正常工作</strong>。就是在设计阶段加入一些专用逻辑（不影响功能），用来支持后期制造测试时快速、高覆盖率地验证芯片是否健康，每个部分的功能能不能正常使用。</p>
<h3 style="" id="%E5%8A%9F%E8%83%BD%E9%AA%8C%E8%AF%81">功能验证</h3>
<p style="">完成了设计，就要对其是否实现了预定的功能进行验证。芯片设计一旦流片（Tape-out），<strong>无法修改</strong>，一旦出错，将造成巨大的时间和经济损失。所以我们必须<strong>尽可能在设计阶段发现问题</strong>，验证其功能是否与规格一致。因此，芯片<span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">验证的⼯作量也是占整个芯⽚开发周期的50%-70%，相应的，验证⼯程师与设计⼯程师的数量⼤概在2-3:1。</span></p>
<p style="">从验证的层级上分类，大致可以分为：</p>
<ol>
 <li>
  <p style=""><strong>模块级验证（Block-Level）</strong></p></li>
</ol>
<ul>
 <li>
  <p style=""><strong>验证目标</strong>：单一功能模块是否按照设计规格工作。</p></li>
 <li>
  <p style="">方式：用 Testbench 驱动 DUT（Design Under Test）</p></li>
 <li>
  <p style="">优点：问题容易定位，仿真速度快</p></li>
</ul>
<ol start="2">
 <li>
  <p style=""><strong>子系统级验证（Subsystem-Level）</strong></p></li>
</ol>
<ul>
 <li>
  <p style=""><strong>验证目标</strong>：多个模块组合在一起后的交互和集成是否正确</p></li>
 <li>
  <p style="">需要测试接口协议、数据一致性、同步等问题</p></li>
</ul>
<ol start="3">
 <li>
  <p style=""><strong>系统级验证（System-Level）</strong></p></li>
</ol>
<ul>
 <li>
  <p style=""><strong>验证目标</strong>：整个 SoC 系统是否与 SoC 规格书一致，能否跑操作系统、软件、驱动</p></li>
 <li>
  <p style="">验证环境变得复杂，需要更高的建模抽象，比如虚拟平台或 FPGA 原型验证</p></li>
</ul>
<h3 style="" id="%E9%9D%99%E6%80%81%E6%97%B6%E5%BA%8F%E5%88%86%E6%9E%90">静态时序分析</h3>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">静态时序分析是套⽤特定的时序模型（timing model）,针对特定电路，分析其是否违反designer给定的时序限制（timing constraint），</span>即芯片在电路层级上能正确“跑起来”。</p>
<p style="">数字电路按时钟驱动工作，如果数据没有在时钟上升沿/下降沿之前<strong>及时稳定地到达目的寄存器</strong>，就会发生：</p>
<ul>
 <li>
  <p style=""><strong>建立时间（Setup）违例</strong>：数据太晚到，来不及锁存。</p></li>
 <li>
  <p style=""><strong>保持时间（Hold）违例</strong>：数据太早变，锁存器还没稳定。</p></li>
</ul>
<p style="">这些都会导致芯片逻辑错误、工作异常，甚至随机失效。</p>
<h3 style="" id="asic%E7%BB%BC%E5%90%88"><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">ASIC综合</span></h3>
<p style="">综合（Synthesis）是非常核心的一步，它的作用是把你写的 RTL 代码（Verilog/VHDL） 转换成门级网表（Gate-level Netlist），让设计从抽象的行为描述变成由标准单元库（Standard Cell Library）实现的逻辑电路。<span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">在做综合时要设定约束条件，如电路⾯积、时序要求等⽬标参数。</span></p>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">至此，芯片设计的前端设计工程，基本就完成了。</span></p>
<h2 style="" id="%E5%90%8E%E7%AB%AF%E8%AE%BE%E8%AE%A1">后端设计</h2>
<p style="">后端设计主要是把前端设计画的“饼”给实现。</p>
<h3 style="" id="%E5%BD%A2%E5%BC%8F%E9%AA%8C%E8%AF%81"><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">形式验证</span></h3>
<p style="">形式验证也可以称为等价性验证。主要是比较 <strong>前端 RTL</strong> 和 <strong>后端门级网表</strong>（或布局布线后的网表）是否功能一致。并确保综合、DFT 插入、布局布线等过程中没有引入逻辑错误。<span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">⼀般在逻辑综合，布局布线完成后必须做。</span></p>
<h3 style="" id="%E7%89%A9%E7%90%86%E5%AE%9E%E7%8E%B0">物理实现</h3>
<p style="">物理实现主要分为三个步骤：布局设计、布局、布线。</p>
<h4 style="" id="%E5%B8%83%E5%B1%80%E8%AE%BE%E8%AE%A1%EF%BC%88floorplanning%EF%BC%89">布局设计（Floorplanning）</h4>
<p style="">布局设计主要是确定芯片或模块的整体物理结构，主要由以下几个部分的工作需要实现</p>
<ul>
 <li>
  <p style="">核心区与I/O区域划分</p></li>
 <li>
  <p style="">功能模块位置安排（如CPU核、存储、接口模块）</p></li>
 <li>
  <p style="">电源/地网络初步规划（Power Planning）</p></li>
 <li>
  <p style="">时钟树位置初步考虑</p></li>
</ul>
<p style="">主要是为了确定芯片的基本地图，为后续的布局布线打下基础。</p>
<h4 style="" id="%E5%B8%83%E5%B1%80%EF%BC%88placement%EF%BC%89">布局（Placement）</h4>
<p style="">布局就是在 Floorplan 基础上，将标准单元（Standard Cells）摆放到具体位置。便于后续布线。主要需要考虑以下几个方面的内容：</p>
<ul>
 <li>
  <p style="">初始布局（粗摆）</p></li>
 <li>
  <p style="">详细布局（精确调整单元位置）</p></li>
 <li>
  <p style="">考虑时序、拥塞、功耗优化</p></li>
</ul>
<h4 style="" id="%E5%B8%83%E7%BA%BF">布线</h4>
<p style="">用金属线连接布局好的单元，实现信号传输。</p>
<ul>
 <li>
  <p style="">全局布线（Global Routing）：规划大致走线方向和资源分配</p></li>
 <li>
  <p style="">详细布线（Detail Routing）：精确到每一条导线、过孔（Via）</p></li>
 <li>
  <p style="">考虑信号完整性、时序、寄生效应</p></li>
</ul>
<h3 style="" id="%E6%97%B6%E9%92%9F%E6%A0%91">时钟树</h3>
<p style="">时钟树综合（Clock Tree Synthesis，CTS）是在后端设计中，把芯片的时钟信号从时钟源（PLL、外部输入）分配到全芯片所有触发器（Flip-Flop、Latch）的过程，并且要保证时钟延迟和偏移（Skew）可控且满足时序要求。因为时钟信号在数字芯片中起到全局指挥的作用。它的分布应该是基本上对称地依次分布，以确保<span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">从同⼀个时钟源到达各个寄存器时，时钟延迟差异最⼩。</span></p>
<h3 style="" id="%E5%AF%84%E7%94%9F%E5%8F%82%E6%95%B0%E6%8F%90%E5%8F%96">寄生参数提取</h3>
<p style="">寄生参数提取（Parasitic Extraction, PEX）是在芯片布局布线完成后，从真实的物理版图中计算出金属线和过孔等互连的寄生电阻、电容（有时还包括电感），并生成这些寄生参数的模型文件，供后续的时序分析和仿真使用。</p>
<h3 style="" id="%E7%89%88%E5%9B%BE%E7%89%A9%E7%90%86%E9%AA%8C%E8%AF%81"><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">版图物理验证</span></h3>
<p style="">版图物理验证主要是为了在芯片流片前，确保<strong>物理版图</strong>与<strong>设计意图</strong>一致，并且<strong>满足制造工艺要求</strong>。</p>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">LVS（Layout Vs Schematic）验证：</span>比较<strong>版图提取的网表</strong>与<strong>逻辑/综合后的门级网表</strong>是否完全一致。</p>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">DRC（Design Rule Checking）：</span>检查版图是否符合代工厂的<strong>工艺设计规则</strong>。</p>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">ERC（Electrical Rule Checking）：</span>检查<strong>电气连接上的合理性</strong>。</p>
<p style=""><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">实际的后端流程还包括电路功耗分析，以及随着制造⼯艺不断进步产⽣的DFM（</span><span fontsize="" color="rgb(17, 24, 39)" style="color: rgb(17, 24, 39)">可制造性设计</span><span fontsize="" color="rgb(31, 35, 41)" style="color: rgb(31, 35, 41)">）问题等。</span>版图物理验证就是在流片前做一次“全身体检”，从<strong>逻辑一致性、几何工艺、电气安全性</strong>到<strong>制造可行性</strong>，确保芯片既能做出来，又能正常跑。</p>
<h1 style="" id="%E9%99%84%E5%BD%95%EF%BC%9A%E5%BC%95%E7%94%A8%E5%88%97%E8%A1%A8">附录：引用列表</h1>
<p style="">[1] AMD Threadripper Pro 9995WX开盖图 http://dieshot.com/index.php/2025/07/23/amd-ryzen-threadripper-pro-9995wx/</p>]]></description><guid isPermaLink="false">/archives/semiconductor-introduction-I</guid><dc:creator>abing</dc:creator><category>芯片制造</category><pubDate>Sun, 10 Aug 2025 15:57:37 GMT</pubDate></item><item><title><![CDATA[半导体通讯：SECS/GEM 引入与简介]]></title><link>/archives/semi-secs-gem</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E5%8D%8A%E5%AF%BC%E4%BD%93%E9%80%9A%E8%AE%AF%EF%BC%9ASECS%2FGEM%20%E5%BC%95%E5%85%A5%E4%B8%8E%E7%AE%80%E4%BB%8B&amp;url=/archives/semi-secs-gem" width="1" height="1" alt="" style="opacity:0;">
<p style="">何为 <strong>SECS/GEM</strong> ？全称 <strong>SEMI Equipment Communications Standard </strong>和 <strong>Generic Equipment Model</strong>，即半导体设备通讯标准 和 通用设备模型。广泛地用于半导体设备与 EAP （Equipment Automation Program）之间的通讯。通讯的相关信息，再由 EAP 交给其他系统来进行对应的处理。</p>
<h2 style="" id="secs">SECS</h2>
<p style="">SECS 其实分为两个协议 SECS I 和 SECS II ，<strong>SECS-I</strong> 只是 <strong>物理层</strong> 和 <strong>数据传输 </strong>的协议，定义了如何通过串口传输数据；而 <strong>SECS-II</strong> 则是 <strong>数据交换的语义层</strong>，定义了消息结构、数据格式和通信双方的交互方式。SECS-II 基本是为了让 SECS-I 上的消息具有实际意义和操作性。</p>
<h3 style="" id="secs-i">SECS-I</h3>
<p style="">SECS-I 作为制定通讯传输标准的标准，描述主机和设备是如何进行通信的。基于 <strong>RS-232</strong> 串口通信，速度较慢，通常使用 <strong>9600bps、19200bps</strong> 等传输速率。是一种简单的点对点通信方式，通常是 <strong>设备与 Host</strong> 之间通过单条串口线连接。由于是基于串口，SECS-I 适合于对数据传输速度要求不高的场景，更多应用在设备端和工厂控制系统之间的简单通信。现在其实大部分已经弃用该方式</p>
<h3 style="" id="hsms">HSMS</h3>
<p style="">因为 SECS-I 的速率慢，较为落后。所以为了适应更高速通信的需求，HSMS（High-Speed SECS Message Services）便应运而生了。 HSMS 定义了使用 TCP/IP 作为物理传输媒质时的通信接口。<span style="font-size: 16px; color: rgb(35, 38, 59)">HSMS 使用TCP/IP流支持，提供了可靠的双向同步传输，可以用来作为SECS-I通信以及其他更高级的通信环境的替代。</span></p>
<h3 style="" id="secs-ii">SECS-II</h3>
<p style="">SECS-II 定义了半导体设备与 Host 之间的消息格式、结构、语义和交互逻辑。SECS-II 定义了<strong>标准消息编号</strong>（如 S1F1、S2F41 等）。规定了<strong>每条消息应该包含哪些字段和数据类型。</strong>并定义了<strong>设备和 Host 的行为约定，</strong>还提供了<strong>通用数据结构格式。</strong></p>
<h2 style="" id="gem">GEM</h2>
<p style="">Generic Equipment Model（通用设备模型）主要用于：</p>
<ul>
 <li>
  <p style="">报告设备状态</p></li>
 <li>
  <p style="">接收远程命令</p></li>
 <li>
  <p style="">发送报警信息</p></li>
 <li>
  <p style="">采集设备数据</p></li>
</ul>
<blockquote>
 <p style="line-height: 27px"><span style="font-size: inheritpx"><strong>SEMI E30 GEM</strong> 标准-这是一个“大脑”，它定义了设备的行为（业务规则）、状态机和一些规则，如应使用哪些 SECS-II 报文、在什么情况下以及所产生的活动。它还定义了状态数据收集、跟踪数据收集、警报管理、假脱机、远程指令等功能</span>——https://www.insphere.com.sg/post/introduction-to-secs-gem</p>
</blockquote>
<p style=""><span color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">在开发 300mm 晶圆之前，SEMI SECS-II E5 和 GEM E30 是使 IC 制造商能够通信和控制晶圆加工设备的初始标准，业内一半称之为GEM200，对应200mm的晶圆，随着更大更重的 300 毫米晶圆和更小的芯片拓扑结构的引入，操作员装载或卸载材料变得不切实际，并面临污染风险。由此诞生了新的GEM300标准，GEM300标准包含了更多操作：</span></p>
<p style="text-align: center"><span color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-przu.png&amp;size=m" width="70%" height="auto" style="display: inline-block"></span></p>
<p style="">对上述几个协议有所了解了之后，我们就可以粗略地得出他们之间的关系：</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zwvr.png&amp;size=m" width="30%" height="auto" style="display: inline-block"></p>
<h2 style="" id="%E4%BD%9C%E7%94%A8%E4%B8%8E%E5%BF%85%E8%A6%81%E6%80%A7">作用与必要性</h2>
<p style="">完整地实现一个完备复杂的网络通信协议，对设备的通信来说，冗余的东西是在太多，SECS/GEM在设计上，恰好符合了半导体设备通信的需求。之后的博文中，会逐步介绍对应的协议。</p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/semi-secs-gem</guid><dc:creator>abing</dc:creator><pubDate>Mon, 21 Jul 2025 15:28:48 GMT</pubDate></item><item><title><![CDATA[Shuffling 的算法艺术]]></title><link>/archives/shuffling-de-suan-fa-yi-shu</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=Shuffling%20%E7%9A%84%E7%AE%97%E6%B3%95%E8%89%BA%E6%9C%AF&amp;url=/archives/shuffling-de-suan-fa-yi-shu" width="1" height="1" alt="" style="opacity:0;">
<p style="">牌类游戏，自古到今都有各种变型，从三国杀、炉石到斗地主以及更久远的德州，这些卡牌游戏永远都绕不开一个问题——洗牌。而洗牌的本质，就是让牌的每个排列出现的概率相等。我们现实中要实现基础的随机化洗牌自然容易，当然，魔术师也会使用各种方法，来诱导你相信，他们的洗牌是 “公平” 的。那么对于计算机来说，我们如何保证各类棋牌游戏中，洗牌的公平与随机呢？这就是今天要阐述的课题。</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-mhxj.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<h2 style="" id="%E5%85%AC%E5%B9%B3%E7%9A%84%E6%95%B0%E5%AD%A6%E5%9F%BA%E7%A1%80">公平的数学基础</h2>
<p style="">刚刚我们提到了洗牌如果需要公平，那就得让牌的每个排列出现的概率相等，换句话说，每个排列都得 “可能” 出现相同的次数，这就是我们所说的 “<strong>期望</strong>” 。举个例子，当我们手上有 「9、10、J」 三张牌的时候，我们可能的组合，就有如下几种：</p>
<ul>
 <li>
  <p style="">「9、10、J」，「9、J、10」</p></li>
 <li>
  <p style="">「10、9、J」，「10、J、9」</p></li>
 <li>
  <p style="">「J、10、9」，「J、9、10」</p></li>
</ul>
<p style="">换个理解的方式，我们第一个位置可以有 「3」张卡牌可以选择，但是选择其一，第二次的时候，我们便只有「2」张卡牌可以供选择了，到最后，只剩下「1」张卡牌能够选择，因此总的排列数量就是「3 ✖️ 2 ✖️ 1」一共「6」种。换成4张牌的时候，我们同样可以根据这个方法算出一共有「24」种排列。</p>
<p style="">而我们的目的，就是让这每种排列出现的概率相等。</p>
<h2 style="" id="%E6%9C%B4%E7%B4%A0%E7%9A%84%E6%B4%97%E7%89%8C">朴素的洗牌</h2>
<p style="">按照我们上述朴素的想法，我们可以很快的写出对应的代码：</p>
<pre><code class="language-javascript">function originalFisherYates(arr) {
  const result = [];
  const src = arr.slice();

  while (src.length &gt; 0) {
    const idx = Math.floor(Math.random() * src.length);
    result.push(src.splice(idx, 1)[0]);
  }

  return result;
}</code></pre>
<p style="">上述代码在概率学上，确实做到了各种排列出现的概率一致，但是对于计算机来说——<strong>它太慢了</strong>，注意到<code>src.splice(idx, 1)</code> 这个，从原数组中踢出所随机出来的这个元素，其实时间复杂度是<code>O(n)</code> 的，而我们又要遍历整个原数组一遍，因此对于整体而言，它的时间复杂度，实际上是<code>O(N^2)</code> 。对于动辄几百上千的数据来说，这个时间复杂度，是我们没办法承受的，因此，我们必须要做出改进。</p>
<h2 style="" id="%E6%94%B9%E8%BF%9B%E5%90%8E%E7%9A%84-fisher-yates">改进后的 Fisher-Yates</h2>
<p style="">到了 1964 年，<strong>Durstenfeld</strong> 对它进行了<strong>原地优化</strong>（即就地打乱，无需新数组）：</p>
<pre><code class="language-javascript">function fisherYates(arr) {
  const result = arr.slice();

  for (let i = result.length - 1; i &gt; 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // 0 &lt;= j &lt;= i
    [result[i], result[j]] = [result[j], result[i]];
  }

  return result;
}</code></pre>
<p style="">上述的代码中，我们倒叙遍历数组，每次从<code>[0, i]</code> 中选出一个，与当前位置交换。</p>
<p style="">但是比较绕的是，为什么就地打乱，也可以做到同样的效果呢？他在数学上是正确的？那就让我们来证明一下。</p>
<p style="">第一步，当 <code>i = n - 1</code> 的情况下：</p>
<ul>
 <li>
  <p style="">从下标 <code>j ∈ [0, n − 1]</code> 中随机选一个，交换到末尾；</p></li>
 <li>
  <p style="">被选中放到最后一个位置的元素有 <code>1 / n</code> 的概率</p></li>
</ul>
<p style="">第二步，当 <code>i = n - 2</code> 的情况下：</p>
<ul>
 <li>
  <p style="">从剩下的前 <code>n − 1</code> 个元素中随机选一个，放到倒数第二个位置；</p></li>
 <li>
  <p style="">被选中元素的概率为 <code>1 / n - 1</code>；</p></li>
 <li>
  <p style="">所以前两步组合起来，每个元素排在某个位置的联合概率是 <code>1 / (n - 1) * 1 / n</code></p></li>
</ul>
<p style="">……</p>
<p style="">第 <code>k</code> 步，当 <code>i = n - k</code> 当情况下：</p>
<ul>
 <li>
  <p style="">从 <code>n − k + 1</code> 个还未“固定”的元素中，等概率选一个放在第 <code>n − k</code> 个位置；</p></li>
 <li>
  <p style="">概率是 ​<code>1 / n - k + 1</code>。</p></li>
</ul>
<p style="">……</p>
<p style="">所以到最后，我们只剩下一张牌可以选择，因此，所有的步骤组合起来，每个元素排在某个位置的联合概率（即某个组合出现的概率）为：</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zwgn.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">这样子一来，我们在数学上也证明了其正确性。</p>
<h2 style="" id="%E6%80%BB%E7%BB%93">总结</h2>
<p style="">Fisher-Yates 在改进后，现代 Fisher–Yates 洗牌算法在每一步中都以等概率做选择，且每一步选择独立，最终能等概率地产生所有 <code>n!</code> 个排列。</p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/shuffling-de-suan-fa-yi-shu</guid><dc:creator>abing</dc:creator><pubDate>Mon, 7 Jul 2025 15:14:24 GMT</pubDate></item><item><title><![CDATA[鸟群算法 Boids]]></title><link>/archives/lei-niao-qun-suan-fa-boids</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E9%B8%9F%E7%BE%A4%E7%AE%97%E6%B3%95%20Boids&amp;url=/archives/lei-niao-qun-suan-fa-boids" width="1" height="1" alt="" style="opacity:0;">
<p style="">Boids 鸟群算法，全称 brid-oid object，类鸟对象，最近偶然看到，感觉挺有意思的，简单的三条规则组成的系统，就能够模拟自然中大量动物的群聚行为，确实有其瑰丽所在。</p>
<p style="">从它的三条核心规则开始说起，<strong>克雷格·雷诺兹 Craig Reynolds </strong>从观测群聚动物的过程中发现了群聚动物的行为有以下三个特征：</p>
<ul>
 <li>
  <p style="">分离 (<strong>Separation</strong>): 移动以避免拥挤整个集体 (steer to avoid crowding local flockmates)</p></li>
 <li>
  <p style="">对齐 (<strong>Alignment</strong>): 朝着周围同伴移动的平均方向移动 (<span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">steer towards the average heading of local flockmates</span>)</p></li>
 <li>
  <p style="">聚集 (<strong>Cohesion</strong>): 朝着周围同伴的平均位置（质心）移动 (<span fontsize="" color="rgb(0, 0, 0)" style="color: rgb(0, 0, 0)">steer to move toward the average position of local flockmates)</span></p></li>
</ul>
<p style="">把这三条规则，具象为三条影响方向的向量，我们通过计算其每条向量对原速度的影响，就可以得出我们下一个时刻的速度。接下来，我们来一条条详细讲讲对应的实现以及其的数学原理。</p>
<h2 style="" id="%E8%A1%8C%E5%8A%A8%E8%A7%84%E5%88%99">行动规则</h2>
<h3 style="" id="%E5%88%86%E7%A6%BB">分离</h3>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-yymx.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">分离行为的目的是移动以避免拥挤整个集体，如果群体内的其他个体过于靠近自己（距离小于自己不爽的范围，下文称之为分离半径 SEPARATION_RADIUS），那么自己就要向着它的反方向进行移动</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kgxb.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">上述公式中，<strong><em>separation </em></strong>代表的是分离的速度向量，而 <strong><em>S<sub>i</sub> </em></strong>代表的则是在分离半径之内的邻居，<em>p<sub>i</sub></em> 代表的则是每个个体的移动速度。</p>
<p style="">这样子我们可以很明确知道，距离越近，我们就会越远离对应的个体，反之，距离越远则影响越小。</p>
<h3 style="" id="%E5%AF%B9%E9%BD%90">对齐</h3>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qgtd.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">对齐行为的目的是让自己的速度靠近邻居速度的平均值，所以在我们观测得到的半径内，我们都会朝着观察得到的邻居的平均方向进行靠近</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-szyg.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">上述公式中，<strong>alignment<sub>i </sub></strong>代表的是其平均移动方向向量，其实际上是计算观察半径内所有邻居移动的平均方向，减去当前速度，那么就是自身需要向群体对齐的移动方向。</p>
<h3 style="" id="%E8%81%9A%E9%9B%86">聚集</h3>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kxuq.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">聚合行为的目的是靠近邻居的平均位置，所以，在观测半径内，自身就会朝着这些邻居的平均位置进行靠近</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-cwic.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">上述公式中，我们计算了观测半径内的邻居的质心，再将其减去自身位置，就可以得出自身聚集行为需要移动的对应向量。</p>
<h2 style="" id="%E5%8F%82%E6%95%B0">参数</h2>
<p style="">为控制每种行为对个体的影响，因此为每个行动规则都赋予对应的权值。所以有对应如下三个参数：</p>
<ul>
 <li>
  <p style="">ALIGNMENT_WEIGHT</p></li>
 <li>
  <p style="">COHESION_WEIGHT</p></li>
 <li>
  <p style="">SEPARATION_WEIGHT</p></li>
</ul>
<p style="">同时，还有每个个体的观测半径和分离半径：</p>
<ul>
 <li>
  <p style="">NEIGHBOR_RADIUS</p></li>
 <li>
  <p style="">SEPARATION_RADIUS</p></li>
</ul>
<p style="">最终，为了控制我们的画面，我们还需要规定画布的范围，以及我们所有个体的总数量，并且对每个个体的最大速度加以限制：</p>
<ul>
 <li>
  <p style="">NUM_BOIDS</p></li>
 <li>
  <p style="">WIDTH, HEIGHT</p></li>
 <li>
  <p style="">MAX_SPEED</p></li>
</ul>
<p style="">这样子，通过控制每个参数的行为，我们就可以在宏观上限制整个集体的一些行为。</p>
<h2 style="" id="%E7%BB%93%E6%9E%9C%E5%B1%95%E7%A4%BA">结果展示</h2>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-kvlh.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">可以看到，上述的各个个体行为一开始都较为混乱</p>
<p style="text-align: center"><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-zcsf.png&amp;size=m" width="50%" height="auto" style="display: inline-block"></p>
<p style="">但随着时间的流逝，个体就逐渐被群体所裹挟着前进了。</p>
<h2 style="" id="%E4%BB%A3%E7%A0%81%E5%8F%82%E8%80%83">代码参考</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# 参数设置
NUM_BOIDS = 120
WIDTH, HEIGHT = 100, 100
MAX_SPEED = 30
NEIGHBOR_RADIUS = 10
SEPARATION_RADIUS = 4
ALIGNMENT_WEIGHT = 0.05
COHESION_WEIGHT = 0.01
SEPARATION_WEIGHT = 0.1

# 初始化 boids
positions = np.random.rand(NUM_BOIDS, 2) * [WIDTH, HEIGHT]
velocities = (np.random.rand(NUM_BOIDS, 2) - 0.5) * MAX_SPEED


def limit_speed(v, max_speed):
    speed = np.linalg.norm(v)
    if speed &gt; max_speed:
        return v / speed * max_speed
    return v


def update_boids():
    global positions, velocities
    new_velocities = np.zeros_like(velocities)

    for i in range(NUM_BOIDS):
        pos_i = positions[i]
        vel_i = velocities[i]

        # 初始化三个行为向量
        # 方向对齐
        alignment = np.zeros(2)
        # 群体聚合
        cohesion = np.zeros(2)
        # 保持距离
        separation = np.zeros(2)
        count = 0

        # 计算邻居的影响
        for j in range(NUM_BOIDS):
            if i == j:
                continue
            offset = positions[j] - pos_i
            distance = np.linalg.norm(offset)
            if distance &lt; NEIGHBOR_RADIUS:
                alignment += velocities[j]
                cohesion += positions[j]
                if distance &lt; SEPARATION_RADIUS:
                    separation -= offset / distance
                count += 1

        if count &gt; 0:
            alignment = alignment / count - vel_i
            cohesion = cohesion / count - pos_i

        vel_i += ALIGNMENT_WEIGHT * alignment
        vel_i += COHESION_WEIGHT * cohesion
        vel_i += SEPARATION_WEIGHT * separation
        vel_i = limit_speed(vel_i, MAX_SPEED)
        new_velocities[i] = vel_i

    positions[:] = (positions + new_velocities) % [WIDTH, HEIGHT]
    velocities[:] = new_velocities


fig, ax = plt.subplots()
ax.set_xlim(0, WIDTH)
ax.set_ylim(0, HEIGHT)
ax.set_title("Boids Algorithm Simulation")

quiver = ax.quiver(positions[:, 0], positions[:, 1], velocities[:, 0],
                   velocities[:, 1])


def animate(frame):
    update_boids()
    # 更新
    quiver.set_offsets(positions)
    quiver.set_UVC(velocities[:, 0], velocities[:, 1])
    return quiver,


ani = FuncAnimation(fig, animate, frames=100, interval=100, blit=True)
plt.show()</code></pre>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/lei-niao-qun-suan-fa-boids</guid><dc:creator>abing</dc:creator><pubDate>Wed, 25 Jun 2025 15:20:36 GMT</pubDate></item><item><title><![CDATA[远行与归乡]]></title><link>/archives/yuan-xing-qian-de-si-kao</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E8%BF%9C%E8%A1%8C%E4%B8%8E%E5%BD%92%E4%B9%A1&amp;url=/archives/yuan-xing-qian-de-si-kao" width="1" height="1" alt="" style="opacity:0;">
<p style="">已经是二零二五年二月九号的凌晨，距离要离开家乡，离开福建，去到新的地区——广东，还有不到二十四小时的时间，深夜脑子总是格外灵活。</p>
<blockquote>
 <p style="">作为一个农民的孩子，我羞于用笔墨去形容童年，只能将乡田中的麦穗粒放进嘴里，咀嚼它由冬至夏的过程。</p>
 <p style="margin-left: 192px!important; text-indent: 2em">——余秀华</p>
</blockquote>
<p style="">像以往离开家乡，其实都是迫不得已的理由，学业和幼时父母职业的变更，这一次外出打工，倒是第一次切切实实自己的选择。其实作为农民的孩子，我知道劳苦的农民是不会所谓“深爱”脚底下的这片土地的，我的爷爷奶奶他们种了一辈子的田，也没能真正靠着种田走出我们这个小农村，我感觉得到，父母的分别的眼中和以往的远行已有不同。</p>
<hr>
<p style="">2025.2.23编</p>
<p style="">确实，这次的离别不仅仅是家人分别时的目光有所不同，对我来说也是不同于读书的感觉。离开没多久后，父亲的感冒，母亲的肺炎，游子在外，家人总是最让人牵挂的，虽然说依旧还是有着各种观念的冲突，但是家人总归是心中最柔软的地方。所以最终还是打算回到家中。广州确实是个对打工人不错的城市。</p>]]></description><guid isPermaLink="false">/archives/yuan-xing-qian-de-si-kao</guid><dc:creator>abing</dc:creator><pubDate>Sun, 23 Feb 2025 07:25:32 GMT</pubDate></item><item><title><![CDATA[行动力与风口、供应链、营销]]></title><link>/archives/wei-ming-ming-wen-zhang</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E8%A1%8C%E5%8A%A8%E5%8A%9B%E4%B8%8E%E9%A3%8E%E5%8F%A3%E3%80%81%E4%BE%9B%E5%BA%94%E9%93%BE%E3%80%81%E8%90%A5%E9%94%80&amp;url=/archives/wei-ming-ming-wen-zhang" width="1" height="1" alt="" style="opacity:0;">
<blockquote>
 <p style="">雷军一直都是我蛮钦佩的人物，刚好借着这个机会，也稍稍整理一下他的经历，学习学习。</p>
</blockquote>
<p style="">雷军，从武大到“黄玫瑰”与“免疫90”的成功，我认为这是雷军的开始，可惜，再到三色的汉卡，却迎来了第一次的失败，其实这时候我们就可以略窥一二了，人家其他公司整合出来更成熟的供应链，自然能做到更低的成本，更快抢占市场。再加上三色内部的管理和运营的混乱，没办法将所有的精力都投入到产品的研发中，所以，第一次的创业，草草落场，但是我们还是能从中学到满多东西的，就如题目，行动力、风口、供应链、营销，这四个关键，很可惜，这次缺少了供应链、营销这两个重点。</p>
<p style="">很快，很多大众印象中，金山——这一个大部分人熟知的雷军的起点，迎来了雷军的加入，求伯君因为雷军先前改进的WPS，了解到了这个初出茅庐的雷军，并且邀请他，加入了金山，成为了金山的六号员工，很快，雷军在金山的第一款产品——盘古，也在日夜兼程的开发下，马上问世了。但是迎来的是沉重的打击，这款产品并没有迎来预想的成功，盘古的定位就如同他的名字一样，神话中，盘古身化万物，雷军也希望他能作为一个全功能的办公软件，文字处理，电子表格，词典，名片……但很可惜，这一次什么都做的结果是，什么都没有做好，市场并没有对这一款产品有什么反响，前有盗版，后有微软，再加上之后WPS与Office的互通格式，其实反倒帮助Office快速占据市场，盗版Office也可以打开WPS的文档，财大气粗的微软自然不在乎短期内被盗版的亏损，互通格式反而给了金山沉重一击。</p>
<p style="">之后，雷军陷入了一阵子的低迷期，行动力有了，技术也有了，可是还是没办法迎来理想的成功，这时候他来到了北京中关村来推销软件，这一次，也是日后小米之家的主要市场营销的关键。</p>
<blockquote>
 <p style="">“市场营销，从不是商家主观输出，逼着用户承认自己有多好。而是认真研究潜在用户，发现他们和产品的连接点，只有用户的某个需要被产品满足，他们才会购买。” ——雷军</p>
</blockquote>
<p style="">这个理念其实我们现在来看，不同于以往手机店一进来就推荐各个机型，说xxx的优点，让用户“认识”or“承认”产品多好，小米之家反而是等用户认真查看某个产品了之后，才来询问用户的需求，并且介绍自己的产品是如何契合用户的需求的，这样子认可才会从用户的体验中得到满足，而并非是推销人员一味的语言输入。这一个契机也是关键，雷军逐渐从市场的角度来看待产品，而并非像从前那样子，只是从技术的角度出发。之前读到的《人月神话》中提到的产品经理的地位我想正是这样子的。你对一个产品的认知，必然是全方位的，就如同“外科手术中的主刀医生”一样，他熟知外科手术的各个方面，尽管没有专门做这个步骤的人这么熟悉，但是只有站在不同角度，以不同的目光审视你的产品，才能打磨出真正契合市场的产品。</p>
<p style="">之后，“电脑入门”这一开发成本根本不到盘古零头的产品，正是契合了当时市场的需求，直接爆火。之后，金山影霸也取得了成功。半年的休息后，金山的WPS97成功在当时Office的大市场打了个翻身仗，后续的各个产品，雷军也逐渐摸索营销的各种策略，最终，金山快译，便是最终营销策略——“以极致的性价比与盗版开战”的试验品，从原本百来元，降到了仅二十八元，成功打开了国内使用正版软件的契机。日后，小米手机、红米手机的发家亦是如此。</p>
<p style="">终于，在WPS2000的发布之后，金山迎来了它的高光，终于上市了，雷军也在大家的上市的喜悦中，宣布辞去CEO一职。</p>
<p style="">回顾金山的发展，虽然确实最终上市，但是它并不是千禧年时代发展最迅速的那批互联网公司，反而像一个老前辈的雄厚基础的逐渐累积，雷军也开始反思，到底是因为什么呢？其实正是没有搭上时代最快的那个风口——互联网。</p>
<p style="">之后，顺为资本的创立，也为日后孵化小米生态链埋下了一个种子。但是更为我们熟知的，自然是那时候喊出“1999”的小米，那时候我记得我家里人的手机，最早是各个喊不上名字的杂牌山寨手机与诺基亚的大战，到之后，我母亲用的是三星S4，父亲用的则是酷派，具体型号已经记不清了。那时候，在铺天盖地好几千块的智能手机的市场，1999的小米，真的当得起那一句“屌爆了”，这次，雷军是亲自去跑各个供应商，将供应链一手整合下来，将原本疯狂溢价，手机厂商和各个供应商军阀混战的局面，一下子给当时市场上的山寨机和杂牌机一记重击。当然，这也得益于最初的MIUI，印象中，最早是在社区中推出刷机包，并且不断更迭，而且发版本的速度非常快，每周更新。在当时普遍三千多的市场，这枚重磅炸弹，确实是“屌爆了”。</p>
<p style="">而之后的红米系列（我父亲就是那时候更换了红米，印象最深刻的就是那时候拿他的手机玩球球大作战），再次给市场带来了一次震撼。移动互联网时代正式从分散的各地割据，转到了春秋战国时期。</p>
<p style="">后来，4G牌照的发放，这一次，小米的成功，雷军真正站在了风口之上，中国移动互联网的浪潮，把智能设备的需求推到了巅峰，而主打性价比战略的小米，快速抢占了市场。</p>
<p style="">作为一个史诗，故事到这里本该是巅峰，但是这是现实，先前提到的顺为资本，继续孵化了多家小米生态链，一个接一个，用小米的供应链的思路，逐渐突破各个原本的行业虚高的价格。</p>
<p style="">而近期，雷军又押上了自己所有的荣誉，为小米汽车而战，小米su7，确实是不错的产品，可以看到雷军打磨的用心，期望他的再一次成功。</p>]]></description><guid isPermaLink="false">/archives/wei-ming-ming-wen-zhang</guid><dc:creator>abing</dc:creator><pubDate>Sun, 2 Feb 2025 09:20:46 GMT</pubDate></item><item><title><![CDATA[【数据结构】顺序表 Sequential List]]></title><link>/archives/shun-xu-biao-sequential-list</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E3%80%90%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E3%80%91%E9%A1%BA%E5%BA%8F%E8%A1%A8%20Sequential%20List&amp;url=/archives/shun-xu-biao-sequential-list" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E4%BB%80%E4%B9%88%E6%98%AF%E9%A1%BA%E5%BA%8F%E8%A1%A8%EF%BC%9F"><span fontsize="" color=""><strong>什么是顺序表？</strong></span></h2>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">顺序表是用一段<strong>物理地址连续的存储单元</strong>依次存储数据元素的线性结构，一般情况下采用数组存储。在数组上完成数据的增删查改（棒读）。</span></p>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">顺序表的特点主要集中在它的存储上面，因为它使用的是一段连续的存储空间，所以实际上我们如果要不依靠数据，自己手写一个顺序表的话，实际上我们只需要<strong>指定一个地址作为开始，规定好最大指示的范围，就可以了。</strong></span></p>
<p style="line-height: inherit"><span fontsize="" color="">但是Java并不支持直接操作地址，如果我们要尝试这类操作，可以参考以下的</span><code>C++</code><span fontsize="" color="">代码：</span></p>
<pre><code>#include &lt;cstdio&gt;
using namespace std;
struct LinearList {
 &nbsp; &nbsp;int linearListBeginPosition;
 &nbsp; &nbsp;int maxsize;
 &nbsp; &nbsp;int numberOfThisPosition(int pos) {
        int *p = &amp;linearListBeginPosition;
        p = p + pos;
        return *p;
    }
    void setThisPositionNumber(int pos,int number) {
        int *p = &amp;linearListBeginPosition;
        p = p + pos;
        *p = number;
    }
};
​
int main() {
    LinearList T;
    T.maxsize = 200;
    T.setThisPositionNumber(1,2);
    printf("%d\n",T.numberOfThisPosition(1));
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">上述代码中我们定义了一个结构体（可以理解为类）。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">其中有两个成员变量</span><code>linearListBeginPosition</code><span fontsize="" color="">和</span><code>maxsize</code><span fontsize="" color="">，其中</span><code>linearListBeginPosition</code><span fontsize="" color="">代表的是这个顺序表的开始元素，它的地址就是顺序表的开始地址，对应到我们一般用的数组，其实就是</span><code>a[0]</code><span fontsize="" color="">，而</span><code>maxsize</code><span fontsize="" color="">则表示这个顺序表的最大容量，这时候我们就可以非常直观地看到我们日常使用中的数组是怎么构成的了。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">而</span><code>numberOfThisPosition()</code><span fontsize="" color="">这个函数则是会返回当前位置的值，而</span><code>setThisPositionNumber()</code><span fontsize="" color="">这个函数则是修改当前位置的值。这样子我们就可以基本上实现一个顺序表的基本功能了。</span></p>
<h2 style="" id="%E9%A1%BA%E5%BA%8F%E8%A1%A8%E7%9A%84%E7%89%B9%E6%80%A7"><span fontsize="" color=""><strong>顺序表的特性</strong></span></h2>
<p style="line-height: inherit"><span fontsize="" color="">紧紧接着我们就能由这个定义，我们就能得到顺序表的一个重要特性：</span></p>
<blockquote>
 <ul>
  <li>
   <p style="line-height: inherit"><span fontsize="" color=""><strong>每个存储在顺序表中的元素都由一个唯一位置表示，可以由（顺序表开始地址+唯一位置）找到</strong></span></p></li>
 </ul>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">这是顺序表最为重要的特点，我们在各种编程语言中见到的数组符号（</span><code>[]</code><span fontsize="" color="">就这个玩意），其实较为正式应该称为下标运算符，做的就是上述</span><code>开始地址+唯一位置</code><span fontsize="" color="">这个操作。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">这样子有什么好处呢？</span></p>
<p style="line-height: inherit"><span fontsize="" color="">首先是我们能够直接访问一个位置的元素，不必像一些其他数据结构（如链表），需要从头位置开始依次访问，才能访问到自己需要的那个元素，同样，也能够直接修改这个位置的元素。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">但是坏处就是，我们如果要一次性移动很多个连续的元素，需要的时间则相比较链表等数据结构，实在是太多了。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">其实数组就是一个封装得非常好的 <strong>顺序表</strong> 。我们这时候返回来重新看一下我们最基础学习的数组</span></p>
<pre><code>class LinearList {
 &nbsp; &nbsp;int[] a = new int[100];
 &nbsp; &nbsp;public void test() {
 &nbsp; &nbsp; &nbsp; &nbsp;a[0] = 100;
 &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(a[0]);
 &nbsp; &nbsp; &nbsp; &nbsp;a[100] = 101;
 &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(a[100]);
 &nbsp;  }
 &nbsp; &nbsp;public static void main(String[] args) {
 &nbsp; &nbsp; &nbsp; &nbsp;LinearList test = new LinearList();
 &nbsp; &nbsp; &nbsp; &nbsp;test.test();
 &nbsp;  }
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">这时候我们的代码会报错，因为</span><code>a[100]</code><span fontsize="" color="">这个位置超出我们预设的</span><code>100</code><span fontsize="" color="">的范围，而且由于Java优秀的内存管理机制，它会直接报错，防止引发更大的问题。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">但是我们可以参考一份C++代码，C++是允许访问超出数组预设范围的地址（但是，<strong>这样子做非常危险！这样子做非常危险！这样子做非常危险！</strong>在这里仅仅是为了演示而使用）</span></p>
<pre><code>#include &lt;cstdio&gt;
using namespace std;
int main(){
 &nbsp; &nbsp;int a[100];
 &nbsp; &nbsp;a[0] = 100;
 &nbsp; &nbsp;printf("%d\n",a[0]);
 &nbsp; &nbsp;a[100] = 101;
 &nbsp; &nbsp;printf("%d\n",a[100]);
 &nbsp; &nbsp;return 0;
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">结果也会和我们预期的一样</span></p>
<pre><code>100
101
​
--------------------------------
Process exited after 0.00826 seconds with return value 0</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">所以一般的线性表都会加入 <strong>范围检测</strong> 这个东西，为了防止我们访问超出一开始指定的数据范围的地址（就像Java做的那样），这是防止 <strong>未定义行为、内存破坏、安全漏洞、调试困难</strong> 的重点。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">同时，尽管越界本身并不会直接导致内存泄漏，但会影响到程序的内存管理，从而间接导致内存泄漏。例如，如果越界访问导致指针指向了错误的内存区域，该区域可能被错误地分配并在后续无法访问到，从而引发内存泄漏。所幸，Java已经帮我们尽可能地规范好了这些（即便牺牲了直接访问内存地址这一功能），但是我仍觉得，了解这些，并且能够在编程的时候意识到这些隐形的、可能存在的问题，是程序员应该做到的。</span></p>
<h2 style="" id="%E5%AE%9E%E6%88%98%E8%AE%B2%E8%A7%A3"><span fontsize="" color=""><strong>实战讲解</strong></span></h2>
<p style="line-height: inherit"><span fontsize="" color=""><strong>T1 珠心算测验</strong></span></p>
<blockquote>
 <ul>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目背景：NOIP2014 普及 T1</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目描述：给你一个集合</span><code>a[i]</code><span fontsize="" color="">，问其中有多少个数，恰好等于集合中另外两个（不同的）数之和？</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">数据范围：3\leq n \leq 100,0 \leq a_i \leq 10000</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目地址：</span><a href="https://www.luogu.com.cn/problem/P2141"><span fontsize="" color="">https://www.luogu.com.cn/problem/P2141</span></a></p></li>
 </ul>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">本题并不难，我们要做的只有一件事，只需要确定这个数字能否通过集合内的两个数字相加得到。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">我们采用一个大小为</span><code>20001</code><span fontsize="" color="">的布尔类型的顺序表</span><code>mark[]</code><span fontsize="" color="">，并使用一个二重循环，将所有的两个数的组合都尝试一遍，并且记录到这个顺序表中</span><code>mark[a[i]+a[j]]=true</code><span fontsize="" color="">。再遍历一遍</span><code>a[i]</code><span fontsize="" color="">，如果</span><code>mark[a[i]]</code><span fontsize="" color="">为真，则就代表这个数字为不同两数之和。</span></p>
<pre><code>import java.io.*;
import java.util.*;
​
class Main{
    public static void main(String[] args) throws Exception {
 &nbsp; &nbsp; &nbsp; &nbsp;Scanner cin = new Scanner(System.in);
 &nbsp; &nbsp; &nbsp; &nbsp;int n = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp;int[] a = new int[101];
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 0;i &lt; n;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a[i] = cin.nextInt();
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;boolean[] mark = new boolean[20001];
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 0;i &lt; n;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int j = i + 1;j &lt; n;j++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mark[a[i] + a[j]] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;int ans = 0;
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 0;i &lt; n;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(mark[a[i]]) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ans++;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(ans);
 &nbsp;  }
} </code></pre>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">注：这是竞赛中常用的解题格式，算法竞赛中要求的输出格式中，<strong>不能有多余的内容</strong>，而且并非像</span><code>Leetcode</code><span fontsize="" color="">刷题中直接给我们对应的数组，必须依靠解题者自行读入，因为竞赛中有些题目对于读入的方式也是题目考察的一部分。此处为第一次遇到这类代码，后续笔记中再遇到，不会再特殊注明，自行观察理解即可。</span></p>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">本题并没有太大的难点，但是我们举这道题目的例子是为了引出下一道题目。</span></p>
<p style="line-height: inherit"><span fontsize="" color=""><strong>T2 三数之和</strong></span></p>
<blockquote>
 <ul>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目背景：我脑子里的题</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目描述：给你一个集合</span><code>a[i]</code><span fontsize="" color="">，问其中有多少个数，恰好等于集合中另外三个数（可重复使用）之和？</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">数据范围：3 &lt;= n &lt;= 1000,0 </span>&lt;=<span fontsize="" color=""> a_i </span>&lt;=<span fontsize="" color=""> 10000</span></p></li>
 </ul>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">这题相较于刚刚那题，变成了三个数，而且范围变成了1000，这时候如果我们还是使用类似上面的方法，使用一个三层循环能解决吗？</span></p>
<p style="line-height: inherit"><span fontsize="" color="">我们稍微计算一下计算量，就会知道1000^3差不多是1000000000，一亿的一个计算量，一般我们解算法题，都要将其尽量计算量压在一亿/秒以内，而本题如果使用三层循环时间复杂度就来到了O(n^3)的一个等级，而我们的每个操作又有着相当的常数，所以使用上一题的方法实在是难以通过本题。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">我们稍微总结一下题目的式子：</span><br><code>a[i] + a[j] + a[k] = a[l]</code></p>
<p style="line-height: inherit"><span fontsize="" color="">我们之前在上一题中是直接统计</span><code>a[i] + a[j]</code><span fontsize="" color="">的结果并记录，从而使得我们的时间复杂度只需要O(n^2)，这时候我们就能不能考虑转化一下这个式子，使得式子的左边只剩下两个数呢？当然可以！</span><br><code>a[i] + a[j] = a[l] - a[k]</code></p>
<p style="line-height: inherit"><span fontsize="" color="">这样子我们左边就只需要一个两重循环就可以枚举完了，你会说，右边不需要也是需要两重循环来枚举吗？一起不就甚至是四重循环了吗？其实并不是这样子的，我们可以在先前的循环<strong>之外</strong>另外再用一个两重循环来枚举</span><code>a[l]</code><span fontsize="" color="">和</span><code>a[k]</code><span fontsize="" color="">，这样子时间复杂度就降低到O(n^2)了。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">有个细节是这题的数字是<strong>可重复的</strong>，倘若是不可重复的，我们也可以解决，留作思考题，并不难。<em><s>批注，后续记得添加对应解法</s></em></span></p>
<pre><code>import java.io.*;
import java.util.*;
​
class Main {
 &nbsp; &nbsp;public static void main(String[] args) throws Exception {
 &nbsp; &nbsp; &nbsp; &nbsp;Scanner cin = new Scanner(System.in);
 &nbsp; &nbsp; &nbsp; &nbsp;int n = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp;int[] a = new int[1001];
 &nbsp; &nbsp; &nbsp; &nbsp;for (int i = 0;i &lt; n;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;a[i] = cin.nextInt();
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;boolean[] mark = new boolean[20001];
 &nbsp; &nbsp; &nbsp; &nbsp;for (int i = 0;i &lt; n;i++) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int j = 0;j &lt; n;j++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mark[a[i] + a[j]] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;int ans = 0;
 &nbsp; &nbsp; &nbsp; &nbsp;for (int k = 0;k &lt; n;k++) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for (int l = 0;l &lt; n;l++) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (a[k] - a[l] &lt; 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;continue;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if (mark[a[k] - a[l]]) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ans++;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(ans);
 &nbsp;  }
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color=""><strong>T3 梦中的统计</strong></span></p>
<blockquote>
 <ul>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目背景：洛谷P1554</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目描述：给出两个整数 <em>M</em> 和 <em>N</em>，求在序列 [M,M+1,M+2,…,N−1,N] 中每一个数码（如</span><code>123</code><span fontsize="" color="">可以拆分为</span><code>1,2,3</code><span fontsize="" color="">三个数码）出现了多少次。</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">数据范围：1 \leq M \leq N \leq 2 \times 10^9，N-M \leq 5 \times 10^5</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目地址：</span><a href="https://www.luogu.com.cn/problem/P1554"><span fontsize="" color="">https://www.luogu.com.cn/problem/P1554</span></a></p></li>
 </ul>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">本题的难点主要集中在如何拆分数码，我们这里需要用到两个操作——</span><code>%,/</code><span fontsize="" color="">，我们接触的一般数字都是十进制的，故我们将一个数字</span><code>%10</code><span fontsize="" color="">便可以得到其个位上的数字，而</span><code>/10</code><span fontsize="" color="">又能实现类似二进制中右移的功能，将原本的个位直接抹除，这样子我们就能够通过——获取个位，抹除个位，这个循环，来将整数拆分成数码。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">以下是一个整数拆分为数码并记录在</span><code>nums[]</code><span fontsize="" color="">的函数：</span></p>
<pre><code>int[] nums = new int[10];
public void getDigital(int number) {
 &nbsp; &nbsp;while (number != 0) {
 &nbsp; &nbsp; &nbsp; &nbsp;nums[number % 10]++;
 &nbsp; &nbsp; &nbsp; &nbsp;number /= 10;
 &nbsp;  }
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">接下来就只需要套一层循环，遍历</span><code>[M,N]</code><span fontsize="" color="">即可：</span></p>
<pre><code>import java.io.*;
import java.util.*;
​
class Digital {
 &nbsp; &nbsp;private int[] nums = new int[10];
 &nbsp; &nbsp;public void getDigital(int number) {
 &nbsp; &nbsp; &nbsp; &nbsp;while (number != 0) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;nums[number % 10]++;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;number /= 10;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }
 &nbsp; &nbsp;public void getNums() {
 &nbsp; &nbsp; &nbsp; &nbsp;for (int i = 0;i &lt;= 9;i++) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;System.out.printf("%d ",nums[i]);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }
}
​
class Main {
 &nbsp; &nbsp;public static void main(String[] args) throws Exception {
 &nbsp; &nbsp; &nbsp; &nbsp;Scanner cin = new Scanner(System.in);
 &nbsp; &nbsp; &nbsp; &nbsp;int m = cin.nextInt(),n = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp;Digital digital = new Digital();
 &nbsp; &nbsp; &nbsp; &nbsp;for (int i = m;i &lt;= n;i++) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;digital.getDigital(i);
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;digital.getNums();
 &nbsp;  }
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color=""><strong>T4 MC生存</strong></span></p>
<blockquote>
 <ul>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目背景：洛谷P1789</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目描述：话说有一天 linyorson 在“我的世界”开了一个 n \times n 的方阵，现在他有 m 个火把和 k 个萤石，分别放在 (x_1, y_1) \sim (x_m, y_m) 和 (o_1, p_1) \sim (o_k, p_k) 的位置，没有光并且没放东西的地方会生成怪物。请问在这个方阵中有几个点会生成怪物？</span></p>
   <p style="line-height: inherit"><span fontsize="" color="">P.S. 火把的照亮范围是：</span></p>
   <pre><code>|暗|暗| 光 |暗|暗|
|暗|光| 光 |光|暗|
|光|光|火把|光|光|
|暗|光| 光 |光|暗|
|暗|暗| 光 |暗|暗|</code></pre>
   <p style="line-height: inherit"><span fontsize="" color="">萤石：</span></p>
   <pre><code>|光|光| 光 |光|光|
|光|光| 光 |光|光|
|光|光|萤石|光|光|
|光|光| 光 |光|光|
|光|光| 光 |光|光|</code></pre></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">数据范围：1 \le n \le 100，1 \leq m+k \leq 25，1 \leq m \leq 25，0 \leq k \leq 5。</span></p></li>
  <li>
   <p style="line-height: inherit"><span fontsize="" color="">题目地址：</span><a href="https://www.luogu.com.cn/problem/P1789"><span fontsize="" color="">https://www.luogu.com.cn/problem/P1789</span></a></p></li>
 </ul>
</blockquote>
<p style="line-height: inherit"><span fontsize="" color="">这道题目则是考察我们对二维空间的理解，以及对于方向的一个运用。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">首先我们来考虑火把的情况</span></p>
<p style="line-height: inherit"><span fontsize="" color="">倘若我们现在有个二维数组，并且有个起始位置（例如</span><code>map[x][y]</code><span fontsize="" color="">的情况，</span><code>x</code><span fontsize="" color="">指行，</span><code>y</code><span fontsize="" color="">指列），我们如果想要向上方移动一个位置就相当于列不变，而行减少</span><code>1</code><span fontsize="" color="">，即，将</span><code>map[x][y]</code><span fontsize="" color="">转化成</span><code>map[x-1][y]</code><span fontsize="" color="">，其实也就相当于我们利用了一个二维向量</span><code>(-1,0)</code><span fontsize="" color="">，作用于原本的坐标上。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">所以我们可以把上下左右都抽象成四个二维向量：</span></p>
<ul>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">上：</span><code>(-1,0)</code></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">下：</span><code>(1,0)</code></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">左：</span><code>(0,-1)</code></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">右：</span><code>(0,1)</code></p></li>
</ul>
<p style="line-height: inherit"><span fontsize="" color="">再将其记录下来，我们就可以完成上下左右四个方向的移动了。同理再结合一下，我们就可以获得八个方向的对应向量。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">所以有了这个，我们就可以写出火把的对应照亮的区域的标记过程了</span></p>
<pre><code>int[] dx = new int[]{0,1,0,-1};
int[] dy = new int[]{1,0,-1,0};
int[] dxx = new int[]{1,-1,1,-1};
int[] dyy = new int[]{-1,-1,1,1};
​
map[x][y] = true;
for(int dir = 0;dir &lt; 4;dir++){
 &nbsp; &nbsp;map[Math.max(0,x + dx[dir])][Math.max(0,y + dy[dir])] = true;
 &nbsp; &nbsp;map[Math.max(0,x + 2 * dx[dir])][Math.max(0,y + 2 * dy[dir])] = true;
 &nbsp; &nbsp;map[Math.max(0,x + dxx[dir])][Math.max(0,y + dyy[dir])] = true;
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">当然也可以一个个位置直接通过计算得出，但是这样子太麻烦了。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">而代码中的</span><code>max</code><span fontsize="" color="">函数，则是为了防止相应的空间超出我们声明的范围（即防止变成负数，从而导致程序崩溃）。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">接下来就是萤石的照亮区域，这就简单得多了，我们注意到可以以</span><code>map[x-2][y-2]</code><span fontsize="" color="">的位置作为矩形的左上角，以</span><code>map[x+2][y+2]</code><span fontsize="" color="">的位置作为右下角，遍历这个矩形并且标记就可以了。</span></p>
<pre><code>for(int i = 1;i &lt;= k;i++){
 &nbsp; &nbsp;int x = cin.nextInt(),y = cin.nextInt();
 &nbsp; &nbsp;for(int j = Math.max(0,x - 2);j &lt;= Math.min(n,x + 2);j++)
 &nbsp; &nbsp; &nbsp; &nbsp;for(int kk = Math.max(0,y - 2);kk &lt;= Math.min(n,y + 2);kk++)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[j][kk] = true;
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">最后我们再遍历一遍整个</span><code>map[][]</code><span fontsize="" color="">，并统计答案就可以了。</span></p>
<pre><code class="language-auto">import java.io.*;
import java.util.*;
​
class Main {
 &nbsp; &nbsp;private static boolean[][] map = new boolean[110][110];
 &nbsp; &nbsp;private static int[] dx = new int[]{0,1,0,-1};
 &nbsp; &nbsp;private static int[] dy = new int[]{1,0,-1,0};
 &nbsp; &nbsp;private static int[] dxx = new int[]{1,-1,1,-1};
 &nbsp; &nbsp;private static int[] dyy = new int[]{-1,-1,1,1};
 &nbsp; &nbsp;public static void main(String[] args) throws Exception {
 &nbsp; &nbsp; &nbsp; &nbsp;Scanner cin = new Scanner(System.in);
 &nbsp; &nbsp; &nbsp; &nbsp;int n = cin.nextInt(),m = cin.nextInt(),k = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 1;i &lt;= m;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int x = cin.nextInt(),y = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[x][y] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int dir = 0;dir &lt; 4;dir++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[Math.max(0,x + dx[dir])][Math.max(0,y + dy[dir])] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[Math.max(0,x + 2 * dx[dir])][Math.max(0,y + 2 * dy[dir])] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[Math.max(0,x + dxx[dir])][Math.max(0,y + dyy[dir])] = true;
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 1;i &lt;= k;i++){
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;int x = cin.nextInt(),y = cin.nextInt();
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int j = Math.max(0,x - 2);j &lt;= Math.min(n,x + 2);j++)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int kk = Math.max(0,y - 2);kk &lt;= Math.min(n,y + 2);kk++)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;map[j][kk] = true;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp; &nbsp;int ans = 0;
 &nbsp; &nbsp; &nbsp; &nbsp;for(int i = 1;i &lt;= n;i++)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;for(int j = 1;j &lt;= n;j++)
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;if(!map[i][j])
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;ans++;
 &nbsp; &nbsp; &nbsp; &nbsp;System.out.println(ans);
 &nbsp;  }
}</code></pre>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/shun-xu-biao-sequential-list</guid><dc:creator>abing</dc:creator><category>数据结构</category><pubDate>Fri, 17 Jan 2025 14:17:52 GMT</pubDate></item><item><title><![CDATA[JWT 认证机制讲解与实现]]></title><link>/archives/hu-lian-wang-shou-quan-yu-jwt-de-shi-yong</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=JWT%20%E8%AE%A4%E8%AF%81%E6%9C%BA%E5%88%B6%E8%AE%B2%E8%A7%A3%E4%B8%8E%E5%AE%9E%E7%8E%B0&amp;url=/archives/hu-lian-wang-shou-quan-yu-jwt-de-shi-yong" width="1" height="1" alt="" style="opacity:0;">
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-vaqd.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<h2 style="" id="%E5%BC%95%E5%85%A5"><span fontsize="" color=""><strong>引入</strong></span></h2>
<h3 style="" id="%E4%BB%80%E4%B9%88%E6%98%AFjwt"><span fontsize="" color=""><strong>什么是JWT</strong></span></h3>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">JSON Web Token (JWT)是一个开放标准(RFC 7519) ，它定义了一种紧凑和自包含的方式，</span><code>用于作为 JSON 对象在各方之间安全地传输信息</code><span fontsize="" color="">。此信息可以进行验证和信任，因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。</span></p>
</blockquote>
<h3 style="" id="jwt%E7%9A%84%E4%BD%9C%E7%94%A8"><span fontsize="" color=""><strong>JWT的作用</strong></span></h3>
<ul>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录，后续每个请求都将包含JWT，允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性，因为它的开销很小，并且可以轻松地跨域使用。</span></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言，JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名，例如，用公钥/私钥对，我们可以确定发送人就是它们所说的那个人。另外，由于签名是使用头和有效负载计算的，我们还可以验证内容没有被篡改。</span></p></li>
</ul>
<h2 style="" id="jwt-%E7%9A%84%E7%BB%93%E6%9E%84"><span fontsize="" color=""><strong>JWT 的结构</strong></span></h2>
<h3 style="" id="%E7%BB%93%E6%9E%84%E7%AE%80%E4%BB%8B"><span fontsize="" color=""><strong>结构简介</strong></span></h3>
<pre><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.FeGzJg7_KXcEvRMEje36wdF2vbC90BnC22CPDbkf1Iw</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">JWT由三个部分构成——</span><code>Header</code><span fontsize="" color="">.</span><code>Payload</code><span fontsize="" color="">.</span><code>Signature</code><span fontsize="" color="">。</span></p>
<ul>
 <li>
  <p style="line-height: inherit"><code>Header</code><span fontsize="" color="">，头部，包含</span><code>Token</code><span fontsize="" color="">的类型（JWT）和所使用的签名算法，通常用于告知验证所用的算法，以及标识该项为JWT。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>Payload</code><span fontsize="" color="">，有效载荷，包含有关实体（用户）的数据。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>Signature</code><span fontsize="" color="">，签名，前面的两个用</span><code>Base64Url</code><span fontsize="" color=""> <strong>编码</strong> 后，用</span><code>.</code><span fontsize="" color="">连接在一起，即</span><code>Base64Url(Header)+"."+Base64Url(Payload)</code><span fontsize="" color=""> 后 <strong>再</strong> 经过 <strong>私钥加密</strong> 形成的一个字符串，用于验证令牌的完整性和真实性。</span></p></li>
</ul>
<h3 style="" id="%E7%BB%93%E6%9E%84%E4%BC%98%E5%8A%BF"><span fontsize="" color=""><strong>结构优势</strong></span></h3>
<ul>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">可扩展性</span></p>
  <p style="line-height: inherit"><span fontsize="" color="">JWT的可扩展性主要是因为它的</span><code>Payload</code><span fontsize="" color="">有效载荷部分，并非是钉死的规定的数据，官方只给出了七个建议字段，用户并非一定要按照它的做法来做，相反，我们可以按照自己的需求来编写响应的字段</span></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">安全性</span></p>
  <p style="line-height: inherit"><span fontsize="" color="">JWT的头部包含了加密算法，能让接收方知道如何解密JWT的有效载荷；签名则保证了JWT的完整性和真实性，防止相关信息被篡改或者是伪造。</span></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">无状态性</span></p>
  <p style="line-height: inherit"><span fontsize="" color="">相较于利用传统的</span><code>session</code><span fontsize="" color="">来存储令牌信息，JWT不需要服务器额外存储一个数据来验证用户，只需要每次对令牌进行解签和验证即可，降低了服务器的负担。</span></p></li>
</ul>
<h3 style="" id="header"><span fontsize="" color=""><strong>Header</strong></span></h3>
<p style="line-height: inherit"><code>Header</code><span fontsize="" color="">一般包含两个信息：令牌类型和使用的算法。</span></p>
<ul>
 <li>
  <p style="line-height: inherit"><code>typ</code><span fontsize="" color=""> 令牌类型：用于表示令牌的种类，即JWT本身。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>alg</code><span fontsize="" color=""> 加密算法：用于表示JWT使用的加密算法，即JWT的第三部分</span><code>Signature</code><span fontsize="" color="">使用的加密算法。</span></p></li>
</ul>
<pre><code>{
 &nbsp;"alg": "HS256",
 &nbsp;"typ": "JWT"
}</code></pre>
<h3 style="" id="payload(claims)"><span fontsize="" color=""><strong>Payload(Claims)</strong></span></h3>
<p style="line-height: inherit"><code>Payload</code><span fontsize="" color="">包含了要传输的信息，例如用户的身份、权限，以及JWT的签发时间、过期时间。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">JWT的官方提供了 <em>7</em> 个建议的声明</span></p>
<ol start="NaN">
 <li>
  <p style="line-height: inherit"><code>iss(Issuer)</code><span fontsize="" color="">：令牌发行人，字段为字符串类型。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>sub(Subject)</code><span fontsize="" color="">：令牌的主题，通常是令牌所代表的用户或实体，如：主题为验证权限，则字段可以为</span><code>"Authentication"</code><span fontsize="" color="">。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>aud (Audience)</code><span fontsize="" color="">：令牌的受众，可以是一个或多个字符串或URI，代表可以接收和处理令牌的目标，即用户。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>exp (Expiration Time)</code><span fontsize="" color="">：令牌的过期时间，用UNIX时间戳表示。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>nbf (Not Before)</code><span fontsize="" color="">：令牌的生效时间，以 UNIX 时间戳表示。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>iat (Issued At)</code><span fontsize="" color="">：令牌的签发时间，以 UNIX 时间戳表示。</span></p></li>
 <li>
  <p style="line-height: inherit"><code>jti (JWT ID)</code><span fontsize="" color="">：令牌的唯一标识符，通常用于防止重放攻击。</span></p></li>
</ol>
<p style="line-height: inherit"><span fontsize="" color="">当然，我们也也可以在这个部分中加入自己需要传输的数据，但是，<strong>切记</strong> 不能在这部分中加入用户敏感信息，因为JWT保护的只是这部分数据 <strong>不被修改</strong> ，并非不被人阅读，其实每个人都能够阅读这个部分的信息，只需要用</span><code>base64Url</code><span fontsize="" color="">解码即可。</span></p>
<pre><code>{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}</code></pre>
<h3 style="" id="signature"><span fontsize="" color=""><strong>Signature</strong></span></h3>
<p style="line-height: inherit"><code>Signature</code><span fontsize="" color="">中包含了对上述的</span><code>Header</code><span fontsize="" color="">和</span><code>Payload</code><span fontsize="" color="">的加密，这也是JWT能够无状态和保持信息不被篡改的关键点。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">对比传统的</span><code>session</code><span fontsize="" color="">，我们不需要去对比这个签名是否存在于数据库中，也就避免了去数据库中去搜寻这个是否存在，或者说，JWT把原本需要保存在服务器的</span><code>Signature</code><span fontsize="" color="">，大大方方的存储在了本身中，只要密钥没有泄露，这部分的</span><code>Signature</code><span fontsize="" color="">就保护着JWT的信息不被改变。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">而</span><code>Signature</code><span fontsize="" color="">的生成又与上述两部分强关联，它是先利用</span><code>Base64Url</code><span fontsize="" color="">对上述两个部分进行编码，然后再利用</span><code>Header</code><span fontsize="" color="">中的签名算法+服务器保存的密钥来生成的。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">即</span></p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-vivq.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="line-height: inherit"><span fontsize="" color="">这样子，只要别人不知道密钥，就无法生成这个数字签名，也无法去更改</span><code>Header</code><span fontsize="" color="">和</span><code>Payload</code><span fontsize="" color="">了。</span></p>
<h3 style="" id="%E5%B7%A5%E4%BD%9C%E8%BF%87%E7%A8%8B"><span fontsize="" color=""><strong>工作过程</strong></span></h3>
<p style="line-height: inherit"><span fontsize="" color="">我们以用户登录这个过程来简述一下JWT的工作过程。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">首先前端收集用户的账号密码，并发送到后端，后端先验证用户的密码，如果验证正确，则在响应头中添加</span><code>Authorization</code><span fontsize="" color="">字段，然后格式为</span><code>Bearer JWT</code><span fontsize="" color="">。Bearer表示使用Bearer令牌类型。如：</span><code>Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c</code><span fontsize="" color="">。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">然后前端获取这个响应头，并保存这个响应头，然后在之后的每次的请求，都包含这个JWT，而后端在处理每次的请求之前，都会先检验这个JWT的有效性——即先通过加密算法按照前两部分和保存在服务器的密钥来计算相应的</span><code>Signature</code><span fontsize="" color="">，并且与前端发来的</span><code>Signature</code><span fontsize="" color="">做对比，如果一致，则继续检验</span><code>Payload</code><span fontsize="" color="">部分的过期时间，看是否过期，以及用户权限是否足够请求，如果不够，则返回对应的错误信息。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">这样子后端只需要完成鉴权即可，这也就是JWT的无状态性的最重要的点。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">如果使用</span><code>HS256</code><span fontsize="" color="">来作为加密算法的话，我们的密钥绝对不能够泄露，因为</span><code>HS256</code><span fontsize="" color="">生成签名和验证签名都使用相同的密钥，一旦泄露，则所有人都能够生成签名，来对我们的服务器进行攻击，所以</span><code>HS256</code><span fontsize="" color="">一般用于传统的单体应用。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">而使用</span><code>RS256</code><span fontsize="" color="">等非对称算法来加密的话，公钥则可以让其他人随意获得，因为公钥只能对密钥进行验证，不能生成密钥，所以即便是所有人都能够验证你签名的正确与否，别人也无法伪造出对应的数据。这样子以</span><code>RS256</code><span fontsize="" color="">为加密算法的JWT就可以方便的利用于各个分布式的平台，验证的</span></p>
<p style="line-height: inherit"><span fontsize="" color="">我们接下来的代码编写以</span><code>HS256</code><span fontsize="" color="">来作为加密算法讲解。</span></p>
<h2 style="" id="%E4%BB%A3%E7%A0%81%E7%BC%96%E5%86%99"><span fontsize="" color=""><strong>代码编写</strong></span></h2>
<p style="line-height: inherit"><span fontsize="" color="">当处理加密相关的数据时，为了保证数据的安全性和完整性，我们建议使用现有的加密库来实现，而不是自己编写算法。Go语言中有很多成熟的加密库可供使用，例如标准库中的</span><code>crypto/hmac</code><span fontsize="" color="">和</span><code>crypto/sha256</code><span fontsize="" color="">包，以及第三方库</span><code>github.com/dgrijalva/jwt-go</code><span fontsize="" color="">。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">这次我们以</span><code>github.com/dgrijalva/jwt-go</code><span fontsize="" color="">来介绍创建JWT和解签的操作。</span></p>
<h4 style="" id="%E5%88%9B%E5%BB%BA%E5%AF%86%E9%92%A5"><span fontsize="" color="">创建密钥</span></h4>
<p style="line-height: inherit"><span fontsize="" color="">密钥的创立也是很重要的一个保护数据安全的过程，我们一般采用两种方法来生成密钥。</span></p>
<ul>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">基于伪随机数生成器生成密钥</span></p></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">基于口令的加密（</span><code>Password-based Encryption,PBE</code><span fontsize="" color="">）来生成密钥</span></p></li>
</ul>
<p style="line-height: inherit"><span fontsize="" color="">当然你也可以使用你自己随意指定的一段字符串，前提是你必须为你使用这个字符串作为密钥的后果负责，如果我是一位有耐心的破解者，我会不厌其烦的来尝试所有的弱密码组合，来试出你的密钥，从而大大方方的拿走你的数据，或者是进行随意的篡改。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">这里我们采用</span><code>crypto/rand</code><span fontsize="" color="">这个包来为我们生成伪随机数密钥</span></p>
<pre><code>import (
 &nbsp; &nbsp;"crypto/rand"
 &nbsp; &nbsp;"encoding/base64"
)
​
func generateRandomString(length int) (string, error) {
 &nbsp; &nbsp;bytes := make([]byte, length)
 &nbsp; &nbsp;_, err := rand.Read(bytes)
 &nbsp; &nbsp;if err != nil {
 &nbsp; &nbsp; &nbsp; &nbsp;return "", err
 &nbsp;  }
 &nbsp; &nbsp;return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">这次生成了</span><code>Aekdp1fhSwGeQgdUeRuzJ01Hx1zBivO7</code><span fontsize="" color="">，这一串长度为</span><code>32</code><span fontsize="" color="">的一个字符串来作为我们的密钥。</span></p>
<h4 style="" id="%E4%BB%A4%E7%89%8C%E5%8A%A0%E5%AF%86"><span fontsize="" color="">令牌加密</span></h4>
<p style="line-height: inherit"><span fontsize="" color="">我们采用</span><code>github.com/dgrijalva/jwt-go</code><span fontsize="" color="">来进行加密和解密。</span></p>
<pre><code>import "github.com/dgrijalva/jwt-go"
​
type MyClaims struct {
    Username string `json:"username"`
    jwt.StandardClaims
}
​
var Key = []byte("Aekdp1fhSwGeQgdUeRuzJ01Hx1zBivO7")
​
// MakeClaimsToken Token加密
func MakeClaimsToken(claims MyClaims) (string, error) {
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(Key)
    return tokenString, err
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">上述代码中，我们指定了一个自己的结构体</span><code>MyClaims</code><span fontsize="" color="">，其中包含了一个自己定义的字段</span><code>Username</code><span fontsize="" color="">，并且包含了</span><code>jwt.StandardClaims</code><span fontsize="" color="">，</span><code>Claims</code><span fontsize="" color="">即为我们之前所说的</span><code>Payload</code><span fontsize="" color="">，即为有效载荷信息。接着我们使用</span><code>jwt.NewWithClaims</code><span fontsize="" color="">函数，将JWT初始化，并且指定了加密算法为</span><code>jwt.SigningMethodHS256</code><span fontsize="" color="">，即</span><code>HS256</code><span fontsize="" color="">加密算法。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">接着返回</span><code>tokenString</code><span fontsize="" color="">，即我们所求的JWT。</span></p>
<h4 style="" id="%E4%BB%A4%E7%89%8C%E9%AA%8C%E8%AF%81"><span fontsize="" color="">令牌验证</span></h4>
<pre><code>// ParseClaimsToken Token解签
func ParseClaimsToken(tokenStr string) (MyClaims, error) {
    ParsedClaims := MyClaims{}
    tokenClaims, err := jwt.ParseWithClaims(tokenStr, &amp;ParsedClaims, func(token *jwt.Token) (interface{}, error) {
        return Key, nil
    })
    if err != nil &amp;&amp; !tokenClaims.Valid {
        log.Printf("Invalid Token:%v\n", err)
    }
    return ParsedClaims, err
}
​</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">上述的函数接受一个JWT令牌字符串作为参数，并返回一个包含</span><code>MyClaims</code><span fontsize="" color="">结构体和错误信息。</span></p>
<p style="line-height: inherit"><code>jwt.ParseWithClaims()</code><span fontsize="" color="">函数的第一个参数是要解析的Token，第二个参数则是我们要把解析的数据放入的变量，第三个参数是</span><code>Keyfunc</code><span fontsize="" color="">，将被</span><code>Parse</code><span fontsize="" color="">方法用作回调函数，以提供用于验证的键。函数接收已解析但未验证的令牌。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">如果Token有问题则会返回相应的错误信息</span></p>
<h4 style="" id="%E6%B5%8B%E8%AF%95"><span fontsize="" color="">测试</span></h4>
<pre><code>package main
​
import (
    "JWTdemo/tools"
    "github.com/dgrijalva/jwt-go"
    "log"
    "time"
)
​
func main() {
    token, err := tools.MakeClaimsToken(tools.MyClaims{
        Username: "",
        StandardClaims: jwt.StandardClaims{
            Audience: &nbsp;"test-user",
            ExpiresAt: time.Now().Add(10 * time.Hour).Unix(),
            Id: &nbsp; &nbsp; &nbsp; &nbsp;"1234",
            IssuedAt: &nbsp;time.Now().Unix(),
            Issuer: &nbsp; &nbsp;"BY-King",
            NotBefore: time.Now().Unix(),
            Subject: &nbsp; "test",
        },
    })
    if err != nil {
        log.Println(err)
    } else {
        log.Println(token)
    }
    orgToken, err := tools.ParseClaimsToken(token)
    if err != nil {
        log.Println(err)
    } else {
        log.Printf("%+v\n", orgToken)
    }
}
​</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">上面的代码里面我们初始化了一个</span><code>MyClaims</code><span fontsize="" color="">用来测试，首先是获取对应的令牌并打印，接着以打印的结果再对其进行解签与验证。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">于是我们可以获得以下的信息</span></p>
<pre><code>2023/05/15 02:05:14 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IiIsImF1ZCI6InRlc3QtdXNlciIsImV4cCI6MTY4NDEyMzUxNCwianRpIjoiMTIzNCIsImlhdCI6MTY4NDA4NzUxNCwiaXNzIjoiQlktS2luZyIsIm5iZiI6MTY4NDA4NzUxNCwic3ViIjoidGVzdCJ9.S7XlRwkULVRhrbmBXaX42X4W5ZJanl1zEjJelZgIkFs
2023/05/15 02:05:14 {Username: StandardClaims:{Audience:test-user ExpiresAt:1684123514 Id:1234 IssuedAt:1684087514 Issuer:BY-King NotBefore:1684087514 Subject:test}}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">那如果我们在解签过程中遇到了其他问题怎么办呢？例如，令牌过期、令牌还未生效、权限不对......这些问题</span><code>"github.com/dgrijalva/jwt-go"</code><span fontsize="" color="">也已经帮我们处理好了。</span></p>
<h4 style="" id="%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86"><span fontsize="" color="">错误处理</span></h4>
<p style="line-height: inherit"><span fontsize="" color="">我们略微修改一下刚刚的测试代码</span></p>
<pre><code>package main
​
import (
    "JWTdemo/tools"
    "github.com/dgrijalva/jwt-go"
    "log"
    "time"
)
​
func main() {
    token, err := tools.MakeClaimsToken(tools.MyClaims{
        Username: "",
        StandardClaims: jwt.StandardClaims{
            Audience: &nbsp;"test-user",
            ExpiresAt: time.Now().Unix() - 1,
            Id: &nbsp; &nbsp; &nbsp; &nbsp;"1234",
            IssuedAt: &nbsp;time.Now().Unix() - 3,
            Issuer: &nbsp; &nbsp;"BY-King",
            NotBefore: time.Now().Unix() - 2,
            Subject: &nbsp; "test",
        },
    })
    if err != nil {
        log.Println(err)
    } else {
        log.Println(token)
    }
    orgToken, err := tools.ParseClaimsToken(token)
    if err != nil {
        log.Println(err)
    } else {
        log.Printf("%+v\n", orgToken)
    }
}</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">我们在这次的代码中，特地修改了签发时间，生效时间，以及过期时间，这样子当我们验证令牌的时候，该令牌必然处于过期状态，这次又会出现什么呢？</span></p>
<pre><code>2023/05/15 02:11:50 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IiIsImF1ZCI6InRlc3QtdXNlciIsImV4cCI6MTY4NDA4NzkwOSwianRpIjoiMTIzNCIsImlhdCI6MTY4NDA4NzkwNywiaXNzIjoiQlktS2luZyIsIm5iZiI6MTY4NDA4NzkwOCwic3ViIjoidGVzdCJ9.EsZ9WKs8sHI8To-YmUXw_kWRmsAf9wWM4ZiNTUdXVEo
2023/05/15 02:11:50 Invalid Token:token is expired by 1s</code></pre>
<p style="line-height: inherit"><span fontsize="" color="">是的，会报错Token已经过期了1秒。如果是实际业务的话，我们这时候就要根据业务的不同，来考虑是对用户颁发新的令牌或者是直接拒绝访问。</span></p>
<h2 style="" id="%E5%90%8E%E8%AE%B0"><span fontsize="" color=""><strong>后记</strong></span></h2>
<p style="line-height: inherit"><span fontsize="" color="">我们这次讨论了JWT的基础应用——Authorization（授权），并且学习了JWT的基本格式与算法的使用。</span></p>
<p style="line-height: inherit"><span fontsize="" color="">其实JWT是一种利用自己保护自己的方式，利用了自身的一部分的数字签名来保证自身不被篡改。</span></p>
<p style=""><br></p>]]></description><guid isPermaLink="false">/archives/hu-lian-wang-shou-quan-yu-jwt-de-shi-yong</guid><dc:creator>abing</dc:creator><pubDate>Tue, 14 Jan 2025 12:21:44 GMT</pubDate></item><item><title><![CDATA[【共赏】《山丘》与《越过山丘》]]></title><link>/archives/gong-shang-shan-qiu-yu-yue-guo-shan-qiu</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E3%80%90%E5%85%B1%E8%B5%8F%E3%80%91%E3%80%8A%E5%B1%B1%E4%B8%98%E3%80%8B%E4%B8%8E%E3%80%8A%E8%B6%8A%E8%BF%87%E5%B1%B1%E4%B8%98%E3%80%8B&amp;url=/archives/gong-shang-shan-qiu-yu-yue-guo-shan-qiu" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E5%B1%B1%E4%B8%98">山丘</h2>
<blockquote>
 <hyperlink-card target="_blank" href="https://music.163.com/song?id=26598946&amp;userid=550306391" theme="regular" style="margin-top: 0.75em; margin-bottom: 0;">
  <a href="https://music.163.com/song?id=26598946&amp;userid=550306391" target="_blank">https://music.163.com/song?id=26598946&amp;userid=550306391</a>
 </hyperlink-card>
</blockquote>
<p style="">人们都说，在人生的每个阶段听这首歌，都会有不同的感觉，又说，年少不懂李宗盛。无妨，慢慢听，慢慢品鉴，就像这首歌一样，我们的经历也是越过一道道山丘。</p>
<p style="">人生，跟爬山，确实有着太多的相似的地方了。上山望山，没有功成名就，或许踌躇满志，或许脚步逐渐沉重。但是无论如何，我们每个人都必须取翻越这一道道山丘，以自己的方式。</p>
<p style="">庆幸的是，我还很有时间，也还没有到无人等候的场景，可以把故事慢慢说，也可以像现在这样子，写给屏幕前的你，待到我已越过一个又一个的山丘，或许我也能把我这些年的意念，汇成河流，慢慢诉说。</p>
<p style="">很喜欢 ”不自量力地还手 直至死方休” 这句，可能少年意气？还是也有几分期望中自己想要的江湖侠气？这句给我一种无论怎么样，都能够一直坚持下去的感觉，再加上 ”嘻皮笑脸 面对 人生的难“ ，老李总是能在这种地方，把他自己的力量传递给听歌的人。</p>
<h2 style="" id="%E8%B6%8A%E8%BF%87%E5%B1%B1%E4%B8%98-%E2%80%94%E2%80%94%E8%87%B4-%E6%9D%8E%E5%AE%97%E7%9B%9B%E5%85%88%E7%94%9F">越过山丘 ——致 李宗盛先生</h2>
<blockquote>
 <hyperlink-card target="_blank" href="https://music.163.com/song?id=490602750&amp;userid=550306391" theme="regular" style="margin-top: 0.75em; margin-bottom: 0;">
  <a href="https://music.163.com/song?id=490602750&amp;userid=550306391" target="_blank">https://music.163.com/song?id=490602750&amp;userid=550306391</a>
 </hyperlink-card>
</blockquote>
<p style="">开头的“越过山丘”，无论是调调还是词，都能够让人一下子就想到《山丘》，相比于《山丘》的越过千山，杨宗纬的这首《越过山丘》，倒像是不同人生阶段的具象化，通过时空相聚的不同的自己。</p>
<p style="">这时候，跨过的山丘更加具象化了，是时间，也是这些年，这位歌者的经历的事，大多还是在抒写各个人生阶段的得失吧，虽然大多数是遗憾与妥协，但是人生嘛，或许本就如此？</p>
<p style="">我的十九岁已经过去了，那时候确实是意气风发，想着自己可以做很多事情，但是太多都没有落地了，而结婚和六十岁，现在好像也太过于遥远，人生的速度很快吗？当我们回望的时候确实是这样子的，但是向前看的时候，我们也会觉得，我们还有漫长的需要走的路。</p>
<h2 style="" id="%E6%80%BB%E7%BB%93">总结</h2>
<p style="">新的博文分类，新的风格，这是一个新的开始，分享喜欢的歌曲或者是电影，抑或是有意思的视频，也会有对应的评析，风格多样，我好像也是一个不怎么挑剔的人，当然，至于什么音乐赏析的角度和高度自然是没办法与大师们去相比较的，只是个人偶然听得有所感受，记录所得。也欢迎大家多多推荐~</p>]]></description><guid isPermaLink="false">/archives/gong-shang-shan-qiu-yu-yue-guo-shan-qiu</guid><dc:creator>abing</dc:creator><pubDate>Wed, 11 Dec 2024 18:37:29 GMT</pubDate></item><item><title><![CDATA[落幕与新的旅途]]></title><link>/archives/luo-mu-yu-xin-de-lu-tu</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E8%90%BD%E5%B9%95%E4%B8%8E%E6%96%B0%E7%9A%84%E6%97%85%E9%80%94&amp;url=/archives/luo-mu-yu-xin-de-lu-tu" width="1" height="1" alt="" style="opacity:0;">
<p style="">实习终于是告一段落了，或多或少也有所收获，但是感觉更多的反而是离职的前一个周日，出门去和人泡了泡茶，聊了聊，其实倒也没必要把自己的路子一下子定死，慢慢来，我们或许还有很多的选择。</p>
<p style="">感觉上班其实比较容易禁锢大家的思维，因为上班的时候，人无论再面对着再高远，也会被面前的一个个小的需求拉到现实中，这虽然对于实现需求来说，无可厚非，但是我总觉得在实现之余，我们总要多出一点时间来多想一想，发散发散思维。</p>
<p style="">记得刚入职的时候，很喜欢听李宗盛的《山丘》，今天离职，耳朵里环绕的也是杨宗纬的《越过山丘》，也是蛮巧合的。其中的“无论相遇还是不相遇，都是献给岁月的序曲”，其实我们很多人都已经见了最后一面了，也近年末，一年下来，确实也是经历许多，亲人的离世，低落的情绪，学生到工人的身份转变，上半年到下半年，都有着各自的大事情，也算是逐渐走到了社会上，确确实实有太多的不同，这部分内容，还是留到年度总结在一并书写吧。</p>
<p style="">最近一阵，也近年关了，打算先沉淀一下吧，把手上的事情先完工，也是属于打扫干净屋子再请客。技术方面的话，打算把打算点的分布式的技能也提上日程了。再往后的话，要么就回家创业了，但是现在这个形势，也难说情况怎么样，还是需要更多的调研；要么就是看看春招以及其他感兴趣的方向，想做的事情很多，我们一步步来一点点完成。</p>]]></description><guid isPermaLink="false">/archives/luo-mu-yu-xin-de-lu-tu</guid><dc:creator>abing</dc:creator><pubDate>Tue, 10 Dec 2024 12:30:45 GMT</pubDate></item><item><title><![CDATA[Docker 使用技巧（1）]]></title><link>/archives/docker-gou-jian-xue-xi</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=Docker%20%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%EF%BC%881%EF%BC%89&amp;url=/archives/docker-gou-jian-xue-xi" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="docker%E5%88%86%E9%98%B6%E6%AE%B5%E6%9E%84%E5%BB%BA">Docker分阶段构建</h2>
<p style="">其实之前一直用docker都是无脑直接把对应的代码都拖进去同一个容器然后编译运行，但是这样子会带来一个问题，就是我们这个容器会变得很大，因为它不仅包含了运行所需要的环境，同时也包含了编译所需要的环境。</p>
<p style="">这时候，我们可以敏锐地察觉到，其实运行环境并不需要和编译环境一样，我们实际运行往往只需要更小的环境，能够执行对应的命令操作，以及查看对应的日志就可以了。</p>
<p style="">所以我们将编译这一步隔离出来，我们直接将编译的结果放进对应的容器就可以了</p>
<pre><code>FROM alpine

WORKDIR /build

COPY server .

CMD ["./server"]</code></pre>
<p style="">可以看到，我们将对应的编译结果，拷贝到对应的容器中，然后运行它就可以了</p>
<p style="">这样子一来，我们就解决了运行容器过大的问题。</p>
<p style="">但是，依旧有个问题接踵而至了，比如我们开发的时候，往往是协同开发的，这时候，如果协同开发的服务器中，有人更改了服务器环境中的一些选项，这时候，你再来编译你的项目，就可能出现了一些难以发现的隐藏的问题，等待着哪一天爆发出来。</p>
<p style="">这时候，联想到我们先前采用的方法，我们把编译的这一个过程，也放到对应的容器中去执行，然后再将执行的结果，放入到运行的容器中，而这时候，我们只需要规定好这个编译容器的对应的参数，就可以保证我们每次编译的时候，对应的选项都是确定的。</p>
<pre><code>FROM golang:alpine AS builder

LABEL stage=gobuilder

ENV CGO_ENABLED 0
ENV GOPROXY https://goproxy.cn,direct
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories

RUN apk update --no-cache &amp;&amp; apk add --no-cache tzdata

WORKDIR /build

ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/server ./cmd/cyclopropane/server.go


FROM scratch

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ Asia/Shanghai

WORKDIR /app
COPY --from=builder /app/server /app/server
COPY --from=builder /build/test-conf.yaml /app/test-conf.yaml

CMD ["./server"]</code></pre>
<p style="">其实这种加中间层的方式在计算机行业其实也是蛮常见的，现在的计算机的整个体系，无不是在一点一点增加中间层，可能是为了确保安全，也可能是为了增加更多的功能，当遇到一些问题的时候，我们不妨也尝试着加一点中间层，说不定哪天，我们就开创了一个新的方向呢？</p>
<blockquote>
 <p style="">鸣谢<a href="https://home.cnblogs.com/u/kevinwan/">万俊峰Kevin</a>大佬的博客：<a href="https://www.cnblogs.com/kevinwan/p/16033634.html">构建 Go 应用 docker 镜像的十八种姿势 - 万俊峰Kevin - 博客园 (cnblogs.com)</a></p>
</blockquote>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/docker-gou-jian-xue-xi</guid><dc:creator>abing</dc:creator><category>默认分类</category><pubDate>Thu, 19 Sep 2024 15:57:13 GMT</pubDate></item><item><title><![CDATA[Git 使用技巧 （1）]]></title><link>/archives/git-shi-yong-ji-qiao-1</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=Git%20%E4%BD%BF%E7%94%A8%E6%8A%80%E5%B7%A7%20%EF%BC%881%EF%BC%89&amp;url=/archives/git-shi-yong-ji-qiao-1" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="rebase%E7%9A%84%E5%A6%99%E7%94%A8">Rebase的妙用</h2>
<p style="">当我们一个分支有多次提交，在合并到其他分支的时候，往往会出现当前分支的每次提交都与要合并到的分支出现冲突，所以我们有多少次冲突的提交，就要解决多少次冲突。</p>
<p style="">这显然太过于麻烦了，我们其实想要的基本都是最后一次提交的版本，因此，只需要解决最后一次提交的结果，和中间每次提交中有用的信息。</p>
<p style="">所以这时候我们就可以用 rebase 来帮助我们将这些提交的信息整理到一起。</p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-mehq.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">可以看到，我们现在有四次提交，但是我们需要的只有最后一次，所以这时候，我们就可以使用<code>git rebase -i</code> 来操作了。</p>
<p style="">我们执行<code>git rebase -i HEAD~4</code> 这个命令：</p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-tfog.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">可以看到，这时候出现了一个 <code>vim</code> 编辑框，如果不太熟悉 <code>vim</code> 的话，稍微学一学，键入 <code>i</code> 即可开始编辑</p>
<p style="">它这里的 <code>pick</code> 代表的是选择，我们将其改为 <code>s</code> 也就是底下描述的 <code>squash</code> 看看它的描述我们就知道了，使用该提交，但是结合到之前的提交中。</p>
<p style="">我们将opt3、opt4、opt5这几次提交都标记为 <code>s</code> 然后保存并退出 <code>vim</code> 这时候，我们会进入到另外一个 <code>vim</code> 编辑界面</p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-bcbo.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">这一次，我们要编辑的是我们合并后的提交的信息，因为我们合并了原先的提交信息，自然也要重新修改对应的备注，这时候，一般情况下我们会只保留最重要的信息，直接将其他的都删除即可，但是如果是有特殊的需求，需要更改对应的信息，那就按照自己的要求来编写即可。</p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-qxti.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">我们这里将其修改为对应的信息，然后保存并退出编辑。</p>
<p style="">这时候我们就已经完成了 <code>git rebase</code> ，下一步，我们需要利用强推，将我们的本地分支推送到远程分支上面，因为我们现在分支的信息已经跟远程并不一样了，而且我们的信息是最为准确的，最新的，所以要将远程分支同步为我们的分支，因此要使用强推。</p>
<p style="">这时候查看远端的信息，就可以看到，我们的 <code>git log</code> 已经被修改为只有我们变基之后的了。</p>
<p style=""><img src="http://localhost:8090/apis/api.storage.halo.run/v1alpha1/thumbnails/-/via-uri?uri=%2Fupload%2Fimage-jjny.png&amp;size=m" width="100%" height="100%" style="display: inline-block"></p>
<p style="">这样子我们的这个提交信息就清晰了很多，而且在合并到其他分支的时候，就不用再去处理多次的冲突了。</p>
<h2 style="" id="%E6%9B%B4%E8%BF%9B%E4%B8%80%E6%AD%A5%E7%9A%84%E6%80%9D%E8%80%83">更进一步的思考</h2>
<p style=""><span color="rgb(78, 68, 60)" style="color: rgb(78, 68, 60)">有一种观点认为，仓库的提交历史即是 </span><strong><span>记录实际发生过什么</span></strong><span color="rgb(78, 68, 60)" style="color: rgb(78, 68, 60)">。 它是针对历史的文档，本身就有价值，不能乱改。 从这个角度看来，改变提交历史是一种亵渎，你使用 </span><strong><em><span>谎言</span></em></strong><span color="rgb(78, 68, 60)" style="color: rgb(78, 68, 60)"> 掩盖了实际发生过的事情。</span></p>
<p style=""><span color="rgb(78, 68, 60)" style="color: rgb(78, 68, 60)">如果由合并产生的提交历史是一团糟怎么办？ 既然事实就是如此，那么这些痕迹就应该被保留下来，让后人能够查阅。</span></p>
<p style="">要我说，放他的狗屁，md都错了还不去改它，等着人家看到的时候犯迷糊是吧，既然有这个工具，我们就应该尽力用好它。</p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>
<p style=""></p>]]></description><guid isPermaLink="false">/archives/git-shi-yong-ji-qiao-1</guid><dc:creator>abing</dc:creator><category>默认分类</category><pubDate>Fri, 30 Aug 2024 16:39:27 GMT</pubDate></item><item><title><![CDATA[沉思录 2024/8/25]]></title><link>/archives/chen-si-lu-2024-8-25</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E6%B2%89%E6%80%9D%E5%BD%95%202024%2F8%2F25&amp;url=/archives/chen-si-lu-2024-8-25" width="1" height="1" alt="" style="opacity:0;">
<h2 style="" id="%E8%87%AA%E6%88%91%E8%AE%BE%E9%99%90">自我设限</h2>
<p style="">其实我们有没有可能，一直活在一种自我设限的视角之下？</p>
<p style="">我们现在想事情，往往会从各个角度去思考，这件事情能不能做，能用什么办法做。</p>
<p style="">这其实就已经活在设限之下了，因为我们本身就会难以去考虑到我们没想到的事情。</p>
<p style="">抛开没想到的事情，我们其实总是去限制说这个事情有什么样什么样的难点，我们是否有办法完成，从而一步一步把我们困住，如是，就一直活在了一种自我设限的视角之下。</p>
<p style="">怎么跳出来呢？自然是没办法想到事情直接去做，这样子我们往往会直接栽进坑里，所以不妨换个方向，我们先前是先想好了要做的事情，然后再一点点地去否定，为什么不先想好起点，再一步一步的想到各种能做的事情呢？</p>
<p style="">这个也有点受到最近重看《士兵突击》的影响，许三多能做333个腹部绕杠，其实他一开始的目标会很宏大吗？只是为了拿回流动红旗，但是最终，他做到了。同样场景的还有高城在调走的时候对团长说的“他每做一件小事的时候，他都像救命稻草一样抓着，有一天，我一看，嚯，好家伙，他抱着的已经是颗参天大树了“。</p>
<p style="">所以我们不妨一步步来。</p>
<h2 style="" id="%E4%BA%A7%E5%93%81%E6%80%9D%E7%BB%B4">产品思维</h2>
<p style="">技术只是工具，产品才是最终出来的东西，那我们要考虑的其实就是这个产品最终能带来多少价值了。技术永远都是服务于产品。</p>
<p style="">这个思维其实还可以运用在很多的方面，比如：我们把和外国人沟通、做国外业务，作为目标的产品，那么，英语就是我们的工具。但是工具不只有一种，因此我们考虑成效的时候，可以跳出技术的角度，从产品的角度入手，再来考虑技术的选型，这个思维也能够一定帮助我们更好的来解决问题。</p>]]></description><guid isPermaLink="false">/archives/chen-si-lu-2024-8-25</guid><dc:creator>abing</dc:creator><category>默认分类</category><pubDate>Sat, 24 Aug 2024 21:14:20 GMT</pubDate></item><item><title><![CDATA[《代码整洁之道》 阅读笔记（1）]]></title><link>/archives/dai-ma-zheng-ji-zhi-dao-yue-du-bi-ji</link><description><![CDATA[<img src="http://localhost:8090/plugins/feed/assets/telemetry.gif?title=%E3%80%8A%E4%BB%A3%E7%A0%81%E6%95%B4%E6%B4%81%E4%B9%8B%E9%81%93%E3%80%8B%20%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0%EF%BC%881%EF%BC%89&amp;url=/archives/dai-ma-zheng-ji-zhi-dao-yue-du-bi-ji" width="1" height="1" alt="" style="opacity:0;">
<h2 style="text-align: start; " id="0.-%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2"><strong><span fontsize="" color="">0. 写在前面</span></strong></h2>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">我们永远无法抛弃必要的精准性——所以代码永存。</span></p>
</blockquote>
<p style="text-align: start; ; line-height: inherit"><span fontsize="" color="">在当下人工智能的发展下，我们可以借助 AI 写出不错的代码，可以直接根据对应的需求规约生成对应的程序，于是我们这样子就可以抛弃代码了……吗？</span></p>
<p style="text-align: start; ; line-height: inherit"><span fontsize="" color="">《人月神话》中有句话，“表达是编程的本质”，我觉得可以很好的解释这个问题。代码不仅仅是写给机器执行的，代码呈现出了需求的细节，在更细的层面上，这些细节没办法被忽略或者是抽象出来，必须将需求明确到机器可以执行的细节程度，同时，还需要表达给之后维护的人，因为需求总是在不断更新迭代的。</span></p>
<p style="text-align: start; ; line-height: inherit"><span fontsize="" color="">所以回到书中在第一章中提到的，同时也是笔记开头的引言—— </span><strong><span fontsize="" color="">“我们永远无法抛弃必要的精准性——所以代码永存。”</span></strong></p>
<h2 style="text-align: start; " id="1.-%E5%91%BD%E5%90%8D"><strong><span fontsize="" color="">1. 命名</span></strong></h2>
<p style="text-align: start; ; line-height: inherit"><span fontsize="" color="">代码中的命名随处可见，因此，一个好名字的重要性足以让书把它放在第一位。</span></p>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">伟哥：命名其实就体现了你的思路。</span></p>
</blockquote>
<ul>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">名副其实</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">正确：</span><code>$customerName</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">错误：</span><code>$n</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">分析：变量名应该直接反应其存储的数据的用途</span></p></li>
  </ul></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">避免误导</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">正确：</span><code>$record</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">错误：</span><code>$save</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">分析：变量名不应该让人误解其实际用途</span></p></li>
  </ul></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">有意义的区分</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">正确：</span><code>$userId, $productId</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">错误：</span><code>$id1, $id2</code></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">分析：变量应能够区分不同的数据和功能，而不是让人去猜</span></p></li>
  </ul></li>
 <li>
  <p style="line-height: inherit"><span fontsize="" color="">不要添加没用的语境</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">感觉有点类似 </span><em><span fontsize="" color="">奥卡姆剃刀定律</span></em><span fontsize="" color=""> ：</span><strong><span fontsize="" color="">如无必要，勿增实体</span></strong><span fontsize="" color=""> （</span><em><span fontsize="" color="">Entities should not be multiplied unnecessarily</span></em><span fontsize="" color="">）</span></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">正确：</span></p>
    <pre><code class="language-php">&lt;?php
​
class Cusotmer {
 &nbsp; &nbsp;public $id;
 &nbsp; &nbsp;public $email;
}</code></pre></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">错误：</span></p>
    <pre><code class="language-php">&lt;?php
​
class Cusotmer {
 &nbsp; &nbsp;public $customerId;
 &nbsp; &nbsp;public $customerEmail;
}</code></pre></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">分析：可以看到，无意义的将类名重复一遍的编码方式凭空增加了我们的代码长度，但是对于实际的分辨，其实并没有起到什么作用</span></p></li>
  </ul></li>
</ul>
<h2 style="text-align: start; " id="2.-%E5%87%BD%E6%95%B0"><strong><span fontsize="" color="">2. 函数</span></strong></h2>
<ul>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">短小 &amp; 只做一件事</span></strong></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">这点其实在实际编程中我个人感觉是最难做到的，因为我们在维护代码的时候，总是不可避免地去向已有的代码中插入我们的新东西，这样子就较难以去将每个函数的长度都维持在一个较小的长度，书中建议的是将代码每个函数都最好控制在三四行中。</span></p></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">同时，这也会带来另外一个问题：当你要查看一个过程的时候，你会不可避免地在很多个函数之间跳来跳去，这样子有时候，即便代码结构是清晰的，但是给人的观感仍然不尽人意，所以上述两个要求，他们之间的平衡，我认为才是我们在实际编程中应当去做适当的取舍的</span></p></li>
  </ul>
  <pre><code>// TODO: 插入ADS-1680优化前后的代码</code></pre></li>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">每个函数一个抽象层级</span></strong></p>
  <p style="line-height: inherit"><span fontsize="" color="">他其实最主要体现了 </span><strong><span fontsize="" color="">单一职责原则（Single Responsibility Principle, SRP）</span></strong><span fontsize="" color=""> ，但是具体到实际中，又会根据不同的设计模式产生不同的变化。</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">比如在 MVC 中，我们根据三个核心组件来组织我们的代码</span></p>
    <ul>
     <li>
      <p style="line-height: inherit"><strong><span fontsize="" color="">Model</span></strong><span fontsize="" color="">：数据模型，负责数据存储、检索和业务逻辑处理。</span></p></li>
     <li>
      <p style="line-height: inherit"><strong><span fontsize="" color="">View</span></strong><span fontsize="" color="">：用户界面，负责展示数据（由Model提供）给用户。</span></p></li>
     <li>
      <p style="line-height: inherit"><strong><span fontsize="" color="">Controller</span></strong><span fontsize="" color="">：控制器，负责接收用户的输入并调用Model和View来完成用户的请求。</span></p></li>
    </ul></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">而在 </span><strong><span fontsize="" color="">MVP（Model-View-Presenter）</span></strong><span fontsize="" color=""> 中，Presenter 充当了 View 和 Model 之间的中介，负责处理用户界面逻辑，而Model只负责数据和业务逻辑</span></p></li>
  </ul>
  <p style="line-height: inherit"><span fontsize="" color="">这时候，我们通过对比 </span><strong><span fontsize="" color="">MVC</span></strong><span fontsize="" color=""> 和 </span><strong><span fontsize="" color="">MVP</span></strong><span fontsize="" color=""> 就可以很容易的分辨出每个组件中，他们在实际业务场景下，会对不同的操作有着不同的抽象层级差别。</span></p></li>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">函数参数</span></strong></p>
  <p style="line-height: inherit"><span fontsize="" color="">参数少，意味着我们如果要测试这个函数，所需要模拟的变量就越少，我们会更容易测试自己的函数是否有问题。</span></p>
  <blockquote>
   <p style="line-height: inherit"><a href="https://blog.cleancoder.com/uncle-bob/2014/05/10/WhenToMock.html"><u><span fontsize="" color="">Clean Coder Blog: When to Mock</span></u></a></p>
   <p style="line-height: inherit"><span fontsize="" color="">这篇文章从使用模拟对象（mocks）的时机和方法角度触发，对比了不使用模拟对象和过度使用模拟对象的两种极端情况，作者在写下本书的时候是2010年，这篇博客则是他在2014年写下的，本书中作者推崇的最理想的参数数量是0，但是不可避免的我们时常会引入其他参数来测试我们函数的行为是否符合我们的预期，所以在引入参数的时候，我们同样需要格外小心。</span></p>
  </blockquote></li>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">如何写出这样的函数</span></strong></p>
  <blockquote>
   <p style="line-height: inherit"><strong><span fontsize="" color="">我并不从一开始就按照规则写函数</span></strong></p>
  </blockquote>
  <p style="line-height: inherit"><span fontsize="" color="">好的代码需要打磨，单元测试是作者在书中提到的他常用的方法，一点点通过修改代码和修改测试，来将代码打磨好。</span></p></li>
</ul>
<h2 style="text-align: start; " id="3.-%E6%B3%A8%E9%87%8A"><strong><span fontsize="" color="">3. 注释</span></strong></h2>
<p style="text-align: start; ; line-height: inherit"><span fontsize="" color="">什么也比不上放置良好的注释来得有用，但是作者在文中提到了另外一种可能——</span></p>
<blockquote>
 <p style="line-height: inherit"><span fontsize="" color="">如果编程语言足够有表达力，或者我们长于用这些语言来表达意图，就不那么需要注释——也许根本不需要。</span></p>
</blockquote>
<ul>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">注释不能美化糟糕的代码</span></strong></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">bad :</span></p>
    <pre><code class="language-php">// 递归遍历数组，并将扁平化结果保存到全局变量$result
function processArray($arr) {
 &nbsp; &nbsp;global $result;
 &nbsp; &nbsp;foreach ($arr as $item) {
 &nbsp; &nbsp; &nbsp; &nbsp;if (is_array($item)) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;processArray($item);
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;$result[] = $item;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }
}
​
$input = [1, [2, 3], [4, [5, 6]]];
processArray($input);
​
// 结果存储在全局变量$result中
print_r($result);</code></pre></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">good:</span></p>
    <pre><code class="language-php">// 递归遍历数组并返回扁平化结果
function processArray($arr) {
 &nbsp; &nbsp;$result = [];
 &nbsp; &nbsp;foreach ($arr as $item) {
 &nbsp; &nbsp; &nbsp; &nbsp;if (is_array($item)) {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;$result = array_merge($result, processArray($item)); 
 &nbsp; &nbsp; &nbsp;  } else {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;$result[] = $item;
 &nbsp; &nbsp; &nbsp;  }
 &nbsp;  }
 &nbsp; &nbsp;return $result;
}
​
$input = [1, [2, 3], [4, [5, 6]]];
$result = processArray($input);
​
print_r($result);</code></pre>
    <p style="line-height: inherit"><span fontsize="" color="">可以看到，在第一份代码中，我们采用的是利用全局变量的方法，而第二份代码则采用的是返回值，如果我们每次都要检查函数的结果，则要再次点进这个函数，才能看到对应的变量的名称，而采用返回值，则没有这个困扰，第一份代码的注释并没有起到帮助我们的作用，原因就在于，这第一份代码的可维护性和可读性本身就很差。</span></p></li>
  </ul></li>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">用代码来阐述</span></strong></p>
  <p style="line-height: inherit"><span fontsize="" color="">频繁引入注释有时候只会让代码的表示失去原本的表达力。有时候，我们可以直接利用代码，就表达出我们的意思，这时候就不必引入注释。</span></p>
  <ul>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">bad:</span></p>
    <pre><code class="language-php">// 检查字符串是否为空白、null 或 undefined
if (trim($input) == '' || $input == null || $input == 'undefined') </code></pre></li>
   <li>
    <p style="line-height: inherit"><span fontsize="" color="">good:</span></p>
    <pre><code class="language-php">if (isNullOrEmpty($input))</code></pre></li>
  </ul>
  <p style="line-height: inherit"><span fontsize="" color="">但是这时候也同样需要注意到代码的复用性，如果当前的这部分代码复用性不高，仅仅是某个单点调用的话，则在对应处标明注释，反而能够在这部分的代码中，显得更为直观，后续修改的时候，也能够一目了然。</span></p></li>
 <li>
  <p style="line-height: inherit"><strong><span fontsize="" color="">注释掉的代码</span></strong></p>
  <blockquote>
   <p style="line-height: inherit"><span fontsize="" color="">注释掉的代码堆积在一起，就像破酒瓶底的渣滓一般</span></p>
  </blockquote>
  <p style="line-height: inherit"><span fontsize="" color="">现在的源代码管理系统已经非常完善了，对于旧的，我们现在已经不需要的代码，最好的选择就是把它删除它，因为如果保存，后来的维护者总会想着：</span><strong><span fontsize="" color="">“代码依然放在那里，一定有其原因，而且这段代码很重要，不能删除。”</span></strong><span fontsize="" color="">，最终旧的代码越积累越多，最终会在后来的人阅读代码的时候，绊倒后来的人。</span></p></li>
</ul>
<p style="text-align: start; ; line-height: inherit"></p>
<p style=""><br></p>]]></description><guid isPermaLink="false">/archives/dai-ma-zheng-ji-zhi-dao-yue-du-bi-ji</guid><dc:creator>abing</dc:creator><category>默认分类</category><pubDate>Sun, 11 Aug 2024 15:58:40 GMT</pubDate></item></channel></rss>